From 079734ddae9baa49c358511ec00ea402c4d54663 Mon Sep 17 00:00:00 2001 From: celisej567 Date: Sun, 4 Sep 2022 13:11:00 +0300 Subject: [PATCH] 1 --- sp/src/game/server/BasePropDoor.h | 283 + sp/src/game/server/CRagdollMagnet.cpp | 267 + sp/src/game/server/CRagdollMagnet.h | 58 + sp/src/game/server/CommentarySystem.cpp | 1662 +++ sp/src/game/server/EffectsServer.cpp | 198 + sp/src/game/server/EntityDissolve.cpp | 388 + sp/src/game/server/EntityDissolve.h | 63 + sp/src/game/server/EntityFlame.cpp | 335 + sp/src/game/server/EntityFlame.h | 66 + sp/src/game/server/EntityParticleTrail.cpp | 207 + sp/src/game/server/EntityParticleTrail.h | 52 + sp/src/game/server/EnvBeam.cpp | 826 ++ sp/src/game/server/EnvFade.cpp | 214 + sp/src/game/server/EnvHudHint.cpp | 157 + sp/src/game/server/EnvLaser.cpp | 260 + sp/src/game/server/EnvLaser.h | 58 + sp/src/game/server/EnvMessage.cpp | 307 + sp/src/game/server/EnvMessage.h | 48 + sp/src/game/server/EnvShake.cpp | 413 + sp/src/game/server/EnvSpark.cpp | 195 + sp/src/game/server/EventLog.cpp | 257 + sp/src/game/server/EventLog.h | 46 + sp/src/game/server/GameStats.cpp | 1099 ++ .../server/GameStats_BasicStatsFunctions.cpp | 183 + sp/src/game/server/MaterialModifyControl.cpp | 289 + .../server/PointAngularVelocitySensor.cpp | 529 + sp/src/game/server/RagdollBoogie.cpp | 478 + sp/src/game/server/RagdollBoogie.h | 62 + sp/src/game/server/ServerNetworkProperty.cpp | 305 + sp/src/game/server/ServerNetworkProperty.h | 258 + sp/src/game/server/SkyCamera.cpp | 462 + sp/src/game/server/SkyCamera.h | 103 + sp/src/game/server/TemplateEntities.cpp | 584 + sp/src/game/server/TemplateEntities.h | 36 + sp/src/game/server/WaterLODControl.cpp | 118 + sp/src/game/server/base_gameinterface.cpp | 27 + sp/src/game/server/base_transmit_proxy.cpp | 60 + sp/src/game/server/base_transmit_proxy.h | 41 + sp/src/game/server/basecombatcharacter.cpp | Bin 131072 -> 143801 bytes sp/src/game/server/basecombatcharacter.h | 713 ++ sp/src/game/server/basecombatweapon.cpp | 747 ++ sp/src/game/server/basecombatweapon.h | 30 + sp/src/game/server/baseentity.cpp | 10486 ++++++++++++++++ sp/src/game/server/baseentity.h | 3077 +++++ sp/src/game/server/baseflex.cpp | 2967 +++++ sp/src/game/server/baseflex.h | 324 + sp/src/game/server/basegrenade_concussion.cpp | 131 + sp/src/game/server/basegrenade_contact.cpp | 71 + sp/src/game/server/basegrenade_timed.cpp | 73 + sp/src/game/server/basemultiplayerplayer.cpp | 349 + sp/src/game/server/basemultiplayerplayer.h | 127 + sp/src/game/server/basetempentity.cpp | 126 + sp/src/game/server/basetempentity.h | 65 + sp/src/game/server/basetoggle.h | 69 + sp/src/game/server/baseviewmodel.cpp | 116 + sp/src/game/server/baseviewmodel.h | 17 + sp/src/game/server/bitstring.cpp | 27 + sp/src/game/server/bitstring.h | 28 + sp/src/game/server/bmodels.cpp | 1522 +++ sp/src/game/server/buttons.cpp | 1594 +++ sp/src/game/server/buttons.h | 172 + sp/src/game/server/cbase.cpp | 2199 ++++ sp/src/game/server/cbase.h | 157 + sp/src/game/server/client.cpp | 1575 +++ sp/src/game/server/client.h | 32 + sp/src/game/server/colorcorrection.cpp | 314 + sp/src/game/server/colorcorrectionvolume.cpp | 236 + sp/src/game/server/controlentities.cpp | 146 + sp/src/game/server/cplane.cpp | 71 + sp/src/game/server/cplane.h | 44 + sp/src/game/server/csm.vpc | 9 + sp/src/game/server/damagemodifier.cpp | 85 + sp/src/game/server/damagemodifier.h | 46 + sp/src/game/server/data_collector.cpp | 66 + sp/src/game/server/data_collector.h | 37 + sp/src/game/server/doors.cpp | 1433 +++ sp/src/game/server/doors.h | 162 + sp/src/game/server/dynamiclight.cpp | 203 + sp/src/game/server/effects.cpp | 2628 ++++ sp/src/game/server/effects.h | 68 + sp/src/game/server/enginecallback.h | 147 + sp/src/game/server/entity_tools_server.cpp | 458 + sp/src/game/server/entityapi.h | 33 + sp/src/game/server/entityblocker.cpp | 76 + sp/src/game/server/entityblocker.h | 28 + sp/src/game/server/entityinput.h | 50 + sp/src/game/server/entitylist.cpp | 1716 +++ sp/src/game/server/entitylist.h | 382 + sp/src/game/server/entityoutput.h | 225 + sp/src/game/server/env_cascade_light.cpp | 505 + sp/src/game/server/env_debughistory.cpp | 367 + sp/src/game/server/env_debughistory.h | 35 + sp/src/game/server/env_dof_controller.cpp | 186 + sp/src/game/server/env_dof_controller.h | 56 + sp/src/game/server/env_effectsscript.cpp | 542 + sp/src/game/server/env_entity_maker.cpp | 475 + sp/src/game/server/env_global_light.cpp | 336 + sp/src/game/server/env_instructor_hint.cpp | 229 + sp/src/game/server/env_particlescript.cpp | 193 + .../server/env_player_surface_trigger.cpp | 135 + .../game/server/env_player_surface_trigger.h | 49 + sp/src/game/server/env_projectedtexture.cpp | 791 ++ sp/src/game/server/env_projectedtexture.h | 119 + sp/src/game/server/env_screenoverlay.cpp | 269 + sp/src/game/server/env_texturetoggle.cpp | 63 + sp/src/game/server/env_tonemap_controller.cpp | 372 + sp/src/game/server/env_zoom.cpp | 150 + sp/src/game/server/env_zoom.h | 13 + sp/src/game/server/envmicrophone.cpp | 648 + sp/src/game/server/envmicrophone.h | 103 + sp/src/game/server/envspark.h | 46 + .../game/server/event_tempentity_tester.cpp | 125 + sp/src/game/server/event_tempentity_tester.h | 40 + sp/src/game/server/eventqueue.h | 94 + sp/src/game/server/explode.cpp | 515 + sp/src/game/server/explode.h | 43 + sp/src/game/server/filters.cpp | 2313 ++++ sp/src/game/server/filters.h | 134 + sp/src/game/server/fire.cpp | 1536 +++ sp/src/game/server/fire.h | 52 + sp/src/game/server/fire_smoke.cpp | 191 + sp/src/game/server/fire_smoke.h | 78 + sp/src/game/server/fish.cpp | 831 ++ sp/src/game/server/fish.h | 155 + sp/src/game/server/fogcontroller.cpp | 403 + sp/src/game/server/fogcontroller.h | 101 + sp/src/game/server/forcefeedback.cpp | 155 + .../game/server/fourwheelvehiclephysics.cpp | 1482 +++ sp/src/game/server/fourwheelvehiclephysics.h | 218 + sp/src/game/server/func_areaportal.cpp | 390 + sp/src/game/server/func_areaportalbase.cpp | 71 + sp/src/game/server/func_areaportalbase.h | 120 + sp/src/game/server/func_areaportalwindow.cpp | 128 + sp/src/game/server/func_areaportalwindow.h | 67 + sp/src/game/server/func_break.cpp | 1326 ++ sp/src/game/server/func_break.h | 174 + sp/src/game/server/func_breakablesurf.cpp | 1308 ++ sp/src/game/server/func_breakablesurf.h | 102 + sp/src/game/server/func_dust.cpp | 332 + sp/src/game/server/func_ladder_endpoint.cpp | 68 + sp/src/game/server/func_lod.cpp | 130 + sp/src/game/server/func_movelinear.cpp | 476 + sp/src/game/server/func_movelinear.h | 72 + sp/src/game/server/func_occluder.cpp | 123 + sp/src/game/server/func_reflective_glass.cpp | 58 + sp/src/game/server/func_smokevolume.cpp | 105 + sp/src/game/server/functorutils.h | 450 + sp/src/game/server/game.cpp | 111 + sp/src/game/server/game.h | 41 + sp/src/game/server/game_ui.cpp | 578 + sp/src/game/server/gamedll_replay.cpp | 23 + sp/src/game/server/gamehandle.cpp | 30 + sp/src/game/server/gameinterface.cpp | 3597 ++++++ sp/src/game/server/gameinterface.h | 230 + sp/src/game/server/gametrace_dll.cpp | 33 + sp/src/game/server/gameweaponmanager.cpp | 290 + sp/src/game/server/gameweaponmanager.h | 23 + sp/src/game/server/genericactor.cpp | 634 + sp/src/game/server/genericmonster.cpp | 572 + sp/src/game/server/gib.cpp | 679 + sp/src/game/server/gib.h | 115 + sp/src/game/server/globals.cpp | 27 + sp/src/game/server/globals.h | 20 + sp/src/game/server/globalstate.cpp | 359 + sp/src/game/server/globalstate.h | 109 + sp/src/game/server/globalstate_private.h | 15 + sp/src/game/server/grenadethrown.cpp | 81 + sp/src/game/server/grenadethrown.h | 38 + sp/src/game/server/guntarget.cpp | 255 + sp/src/game/server/h_ai.cpp | 266 + sp/src/game/server/h_cycler.cpp | 518 + sp/src/game/server/h_cycler.h | 41 + sp/src/game/server/h_export.cpp | 48 + sp/src/game/server/hierarchy.cpp | 169 + sp/src/game/server/hierarchy.h | 26 + sp/src/game/server/hl1_CBaseHelicopter.h | 136 + sp/src/game/server/hltvdirector.cpp | 1184 ++ sp/src/game/server/hltvdirector.h | 124 + sp/src/game/server/ilagcompensationmanager.h | 30 + sp/src/game/server/info_camera_link.cpp | 183 + sp/src/game/server/info_camera_link.h | 32 + sp/src/game/server/info_overlay_accessor.cpp | 50 + sp/src/game/server/init_factory.cpp | 25 + sp/src/game/server/init_factory.h | 30 + sp/src/game/server/intermission.cpp | 52 + sp/src/game/server/iscorer.h | 29 + sp/src/game/server/iservervehicle.h | 139 + sp/src/game/server/item_world.cpp | 614 + sp/src/game/server/items.h | 118 + sp/src/game/server/lightglow.cpp | 152 + sp/src/game/server/lights.cpp | 257 + sp/src/game/server/lights.h | 57 + sp/src/game/server/locksounds.h | 37 + sp/src/game/server/logic_achievement.cpp | 182 + sp/src/game/server/logic_measure_movement.cpp | 901 ++ sp/src/game/server/logic_mirror_movement.cpp | 198 + sp/src/game/server/logic_navigation.cpp | 183 + sp/src/game/server/logic_playmovie.cpp | 136 + sp/src/game/server/logic_random_outputs.cpp | 221 + sp/src/game/server/logic_random_outputs.h | 54 + sp/src/game/server/logicauto.cpp | 127 + sp/src/game/server/logicentities.cpp | 7178 +++++++++++ sp/src/game/server/logicrelay.cpp | 480 + sp/src/game/server/logicrelay.h | 125 + sp/src/game/server/mapentities.cpp | 601 + sp/src/game/server/mapentities.h | 47 + sp/src/game/server/maprules.cpp | 826 ++ sp/src/game/server/maprules.h | 15 + sp/src/game/server/message_entity.cpp | 340 + sp/src/game/server/modelentities.cpp | 364 + sp/src/game/server/modelentities.h | 63 + sp/src/game/server/monstermaker.cpp | 1095 ++ sp/src/game/server/monstermaker.h | 184 + sp/src/game/server/movehelper_server.cpp | 416 + sp/src/game/server/movehelper_server.h | 43 + sp/src/game/server/movement.cpp | 658 + sp/src/game/server/movie_display.cpp | 372 + sp/src/game/server/movie_explosion.cpp | 44 + sp/src/game/server/movie_explosion.h | 28 + sp/src/game/server/nav.h | 498 + sp/src/game/server/nav_area.cpp | 5879 +++++++++ sp/src/game/server/nav_area.h | 1065 ++ sp/src/game/server/nav_colors.cpp | 182 + sp/src/game/server/nav_colors.h | 77 + sp/src/game/server/nav_edit.cpp | 3964 ++++++ sp/src/game/server/nav_entities.cpp | 709 ++ sp/src/game/server/nav_entities.h | 130 + sp/src/game/server/nav_file.cpp | 1696 +++ sp/src/game/server/nav_generate.cpp | 4934 ++++++++ sp/src/game/server/nav_ladder.cpp | 654 + sp/src/game/server/nav_ladder.h | 154 + sp/src/game/server/nav_merge.cpp | 350 + sp/src/game/server/nav_mesh.cpp | 3300 +++++ sp/src/game/server/nav_mesh.h | 1346 ++ sp/src/game/server/nav_mesh.vpc | 43 + sp/src/game/server/nav_mesh_factory.cpp | 45 + sp/src/game/server/nav_node.cpp | 488 + sp/src/game/server/nav_node.h | 151 + sp/src/game/server/nav_pathfind.h | 986 ++ sp/src/game/server/nav_simplify.cpp | 285 + sp/src/game/server/ndebugoverlay.cpp | 269 + sp/src/game/server/ndebugoverlay.h | 34 + .../game/server/networkstringtable_gamedll.h | 46 + sp/src/game/server/npc_talker.cpp | 1357 ++ sp/src/game/server/npc_talker.h | 254 + sp/src/game/server/npc_vehicledriver.cpp | 1196 ++ sp/src/game/server/npc_vehicledriver.h | 197 + sp/src/game/server/particle_fire.cpp | 43 + sp/src/game/server/particle_fire.h | 36 + sp/src/game/server/particle_light.cpp | 41 + sp/src/game/server/particle_light.h | 41 + sp/src/game/server/particle_smokegrenade.cpp | 75 + sp/src/game/server/particle_smokegrenade.h | 58 + sp/src/game/server/particle_system.cpp | 276 + sp/src/game/server/particle_system.h | 65 + sp/src/game/server/pathcorner.cpp | 142 + sp/src/game/server/pathtrack.cpp | 592 + sp/src/game/server/pathtrack.h | 158 + sp/src/game/server/phys_controller.cpp | 1078 ++ sp/src/game/server/phys_controller.h | 19 + sp/src/game/server/physconstraint.cpp | 1912 +++ sp/src/game/server/physconstraint.h | 146 + sp/src/game/server/physconstraint_sounds.h | 291 + sp/src/game/server/physgun.cpp | 1523 +++ sp/src/game/server/physics.cpp | 2938 +++++ sp/src/game/server/physics.h | 189 + sp/src/game/server/physics_bone_follower.cpp | 485 + sp/src/game/server/physics_bone_follower.h | 104 + sp/src/game/server/physics_cannister.cpp | 483 + sp/src/game/server/physics_cannister.h | 145 + sp/src/game/server/physics_collisionevent.h | 176 + sp/src/game/server/physics_fx.cpp | 238 + sp/src/game/server/physics_fx.h | 19 + sp/src/game/server/physics_impact_damage.cpp | 766 ++ sp/src/game/server/physics_impact_damage.h | 66 + sp/src/game/server/physics_main.cpp | 2073 +++ sp/src/game/server/physics_npc_solver.cpp | 468 + sp/src/game/server/physics_npc_solver.h | 19 + sp/src/game/server/physics_prop_ragdoll.cpp | 1872 +++ sp/src/game/server/physics_prop_ragdoll.h | 180 + sp/src/game/server/physobj.cpp | 2349 ++++ sp/src/game/server/physobj.h | 249 + sp/src/game/server/plasma.cpp | 93 + sp/src/game/server/plasma.h | 45 + sp/src/game/server/player.cpp | 10148 +++++++++++++++ sp/src/game/server/player.h | 1627 +++ sp/src/game/server/player_command.cpp | 486 + sp/src/game/server/player_command.h | 64 + sp/src/game/server/player_lagcompensation.cpp | 833 ++ sp/src/game/server/player_pickup.cpp | 175 + sp/src/game/server/player_pickup.h | 93 + sp/src/game/server/player_resource.cpp | 130 + sp/src/game/server/player_resource.h | 45 + sp/src/game/server/playerinfomanager.cpp | 113 + sp/src/game/server/playerinfomanager.h | 38 + sp/src/game/server/playerlocaldata.cpp | 303 + sp/src/game/server/playerlocaldata.h | 96 + sp/src/game/server/plugin_check.cpp | 30 + sp/src/game/server/point_camera.cpp | 486 + sp/src/game/server/point_camera.h | 120 + sp/src/game/server/point_devshot_camera.cpp | 254 + sp/src/game/server/point_entity_finder.cpp | 207 + .../server/point_playermoveconstraint.cpp | 161 + sp/src/game/server/point_spotlight.cpp | 567 + sp/src/game/server/point_template.cpp | 614 + sp/src/game/server/point_template.h | 95 + sp/src/game/server/pointanglesensor.cpp | 562 + sp/src/game/server/pointhurt.cpp | 204 + sp/src/game/server/pointteleport.cpp | 234 + sp/src/game/server/postprocesscontroller.cpp | 207 + sp/src/game/server/postprocesscontroller.h | 80 + sp/src/game/server/props.cpp | 6992 +++++++++++ sp/src/game/server/props.h | 488 + sp/src/game/server/pushentity.h | 160 + sp/src/game/server/ragdoll_manager.cpp | 174 + sp/src/game/server/recipientfilter.cpp | 415 + sp/src/game/server/recipientfilter.h | 241 + sp/src/game/server/rope.cpp | 888 ++ sp/src/game/server/rope.h | 226 + sp/src/game/server/saverestore_gamedll.cpp | 254 + sp/src/game/server/sceneentity.cpp | 6355 ++++++++++ sp/src/game/server/sceneentity.h | 64 + .../server/scratchpad_gamedll_helpers.cpp | 95 + .../game/server/scratchpad_gamedll_helpers.h | 40 + sp/src/game/server/scripted.cpp | 2511 ++++ sp/src/game/server/scripted.h | 246 + sp/src/game/server/scriptedtarget.cpp | 362 + sp/src/game/server/scriptedtarget.h | 64 + sp/src/game/server/sendproxy.cpp | 197 + sp/src/game/server/sendproxy.h | 63 + sp/src/game/server/server_base.vpc | 1037 ++ sp/src/game/server/server_episodic.vpc | 303 + sp/src/game/server/server_hl2.vpc | 269 + sp/src/game/server/server_mapbase.vpc | 111 + sp/src/game/server/serverbenchmark_base.cpp | 424 + sp/src/game/server/serverbenchmark_base.h | 65 + sp/src/game/server/shadowcontrol.cpp | 172 + sp/src/game/server/skyboxswapper.cpp | 80 + sp/src/game/server/slideshow_display.cpp | 625 + sp/src/game/server/smoke_trail.cpp | 645 + sp/src/game/server/smoke_trail.h | 210 + sp/src/game/server/smokestack.cpp | 276 + sp/src/game/server/smokestack.h | 85 + sp/src/game/server/sound.cpp | 1587 +++ sp/src/game/server/soundent.cpp | 879 ++ sp/src/game/server/soundent.h | 280 + sp/src/game/server/soundscape.cpp | 598 + sp/src/game/server/soundscape.h | 116 + sp/src/game/server/soundscape_system.cpp | 468 + sp/src/game/server/soundscape_system.h | 65 + sp/src/game/server/spark.h | 15 + sp/src/game/server/spotlightend.cpp | 52 + sp/src/game/server/spotlightend.h | 47 + sp/src/game/server/sprite_perfmonitor.cpp | 95 + sp/src/game/server/stdafx.cpp | 11 + sp/src/game/server/steamjet.cpp | 130 + sp/src/game/server/steamjet.h | 60 + sp/src/game/server/subs.cpp | 354 + sp/src/game/server/sun.cpp | 203 + sp/src/game/server/tactical_mission.cpp | 186 + sp/src/game/server/tactical_mission.h | 109 + sp/src/game/server/tanktrain.cpp | 535 + sp/src/game/server/te.cpp | 526 + sp/src/game/server/te.h | 17 + sp/src/game/server/te_armorricochet.cpp | 142 + sp/src/game/server/te_basebeam.cpp | 66 + sp/src/game/server/te_basebeam.h | 59 + sp/src/game/server/te_beamentpoint.cpp | 154 + sp/src/game/server/te_beaments.cpp | 134 + sp/src/game/server/te_beamfollow.cpp | 112 + sp/src/game/server/te_beamlaser.cpp | 127 + sp/src/game/server/te_beampoints.cpp | 144 + sp/src/game/server/te_beamring.cpp | 135 + sp/src/game/server/te_beamringpoint.cpp | 124 + sp/src/game/server/te_beamspline.cpp | 134 + sp/src/game/server/te_bloodsprite.cpp | 151 + sp/src/game/server/te_bloodstream.cpp | 135 + sp/src/game/server/te_breakmodel.cpp | 146 + sp/src/game/server/te_bspdecal.cpp | 125 + sp/src/game/server/te_bubbles.cpp | 137 + sp/src/game/server/te_bubbletrail.cpp | 140 + sp/src/game/server/te_clientprojectile.cpp | 117 + sp/src/game/server/te_decal.cpp | 131 + sp/src/game/server/te_dynamiclight.cpp | 144 + sp/src/game/server/te_effect_dispatch.cpp | 91 + sp/src/game/server/te_effect_dispatch.h | 23 + sp/src/game/server/te_energysplash.cpp | 112 + sp/src/game/server/te_explosion.cpp | 132 + sp/src/game/server/te_fizz.cpp | 112 + sp/src/game/server/te_footprintdecal.cpp | 91 + sp/src/game/server/te_glassshatter.cpp | 155 + sp/src/game/server/te_glowsprite.cpp | 130 + sp/src/game/server/te_impact.cpp | 97 + .../game/server/te_killplayerattachments.cpp | 92 + sp/src/game/server/te_largefunnel.cpp | 103 + sp/src/game/server/te_muzzleflash.cpp | 99 + sp/src/game/server/te_particlesystem.cpp | 31 + sp/src/game/server/te_particlesystem.h | 33 + sp/src/game/server/te_physicsprop.cpp | 133 + sp/src/game/server/te_playerdecal.cpp | 124 + sp/src/game/server/te_projecteddecal.cpp | 122 + sp/src/game/server/te_showline.cpp | 107 + sp/src/game/server/te_smoke.cpp | 112 + sp/src/game/server/te_sparks.cpp | 101 + sp/src/game/server/te_sprite.cpp | 121 + sp/src/game/server/te_spritespray.cpp | 136 + sp/src/game/server/te_worlddecal.cpp | 121 + sp/src/game/server/team.cpp | 346 + sp/src/game/server/team.h | 101 + sp/src/game/server/team_control_point.cpp | 1077 ++ sp/src/game/server/team_control_point.h | 195 + .../game/server/team_control_point_master.cpp | 1347 ++ .../game/server/team_control_point_master.h | 214 + .../game/server/team_control_point_round.cpp | 414 + sp/src/game/server/team_control_point_round.h | 83 + sp/src/game/server/team_objectiveresource.cpp | 573 + sp/src/game/server/team_objectiveresource.h | 240 + sp/src/game/server/team_spawnpoint.cpp | 133 + sp/src/game/server/team_spawnpoint.h | 56 + sp/src/game/server/team_train_watcher.cpp | 1555 +++ sp/src/game/server/team_train_watcher.h | 230 + sp/src/game/server/tempmonster.cpp | 105 + sp/src/game/server/tesla.cpp | 180 + sp/src/game/server/tesla.h | 62 + sp/src/game/server/test_proxytoggle.cpp | 110 + sp/src/game/server/test_stressentities.cpp | 152 + sp/src/game/server/test_stressentities.h | 56 + sp/src/game/server/testfunctions.cpp | 88 + sp/src/game/server/testtraceline.cpp | 82 + sp/src/game/server/textstatsmgr.cpp | 174 + sp/src/game/server/textstatsmgr.h | 133 + sp/src/game/server/timedeventmgr.cpp | 151 + sp/src/game/server/timedeventmgr.h | 93 + sp/src/game/server/toolframework_server.cpp | 138 + sp/src/game/server/toolframework_server.h | 27 + sp/src/game/server/trains.cpp | 3378 +++++ sp/src/game/server/trains.h | 217 + sp/src/game/server/trigger_area_capture.cpp | 1232 ++ sp/src/game/server/trigger_area_capture.h | 190 + sp/src/game/server/trigger_portal.cpp | 343 + sp/src/game/server/triggers.cpp | 5606 +++++++++ sp/src/game/server/triggers.h | 354 + sp/src/game/server/util.cpp | 3502 ++++++ sp/src/game/server/util.h | 684 + sp/src/game/server/variant_t.cpp | 284 + sp/src/game/server/variant_t.h | 164 + sp/src/game/server/vehicle_base.cpp | 1488 +++ sp/src/game/server/vehicle_base.h | 336 + sp/src/game/server/vehicle_baseserver.cpp | 2747 ++++ sp/src/game/server/vehicle_baseserver.h | 328 + sp/src/game/server/vehicle_choreo_generic.cpp | 995 ++ sp/src/game/server/vehicle_sounds.h | 137 + sp/src/game/server/vguiscreen.cpp | 419 + sp/src/game/server/vguiscreen.h | 88 + sp/src/game/server/vote_controller.cpp | 1100 ++ sp/src/game/server/vote_controller.h | 130 + sp/src/game/server/vscript_server.cpp | 781 ++ sp/src/game/server/vscript_server.h | 47 + sp/src/game/server/vscript_server.nut | 167 + sp/src/game/server/waterbullet.cpp | 107 + sp/src/game/server/waterbullet.h | 31 + sp/src/game/server/wcedit.cpp | 853 ++ sp/src/game/server/wcedit.h | 36 + sp/src/game/server/weapon_cubemap.cpp | 48 + sp/src/game/server/weight_button.cpp | 102 + sp/src/game/server/worker_scientist.h | 1 + sp/src/game/server/world.cpp | 780 ++ sp/src/game/server/world.h | 107 + 468 files changed, 231173 insertions(+) create mode 100644 sp/src/game/server/BasePropDoor.h create mode 100644 sp/src/game/server/CRagdollMagnet.cpp create mode 100644 sp/src/game/server/CRagdollMagnet.h create mode 100644 sp/src/game/server/CommentarySystem.cpp create mode 100644 sp/src/game/server/EffectsServer.cpp create mode 100644 sp/src/game/server/EntityDissolve.cpp create mode 100644 sp/src/game/server/EntityDissolve.h create mode 100644 sp/src/game/server/EntityFlame.cpp create mode 100644 sp/src/game/server/EntityFlame.h create mode 100644 sp/src/game/server/EntityParticleTrail.cpp create mode 100644 sp/src/game/server/EntityParticleTrail.h create mode 100644 sp/src/game/server/EnvBeam.cpp create mode 100644 sp/src/game/server/EnvFade.cpp create mode 100644 sp/src/game/server/EnvHudHint.cpp create mode 100644 sp/src/game/server/EnvLaser.cpp create mode 100644 sp/src/game/server/EnvLaser.h create mode 100644 sp/src/game/server/EnvMessage.cpp create mode 100644 sp/src/game/server/EnvMessage.h create mode 100644 sp/src/game/server/EnvShake.cpp create mode 100644 sp/src/game/server/EnvSpark.cpp create mode 100644 sp/src/game/server/EventLog.cpp create mode 100644 sp/src/game/server/EventLog.h create mode 100644 sp/src/game/server/GameStats.cpp create mode 100644 sp/src/game/server/GameStats_BasicStatsFunctions.cpp create mode 100644 sp/src/game/server/MaterialModifyControl.cpp create mode 100644 sp/src/game/server/PointAngularVelocitySensor.cpp create mode 100644 sp/src/game/server/RagdollBoogie.cpp create mode 100644 sp/src/game/server/RagdollBoogie.h create mode 100644 sp/src/game/server/ServerNetworkProperty.cpp create mode 100644 sp/src/game/server/ServerNetworkProperty.h create mode 100644 sp/src/game/server/SkyCamera.cpp create mode 100644 sp/src/game/server/SkyCamera.h create mode 100644 sp/src/game/server/TemplateEntities.cpp create mode 100644 sp/src/game/server/TemplateEntities.h create mode 100644 sp/src/game/server/WaterLODControl.cpp create mode 100644 sp/src/game/server/base_gameinterface.cpp create mode 100644 sp/src/game/server/base_transmit_proxy.cpp create mode 100644 sp/src/game/server/base_transmit_proxy.h create mode 100644 sp/src/game/server/basecombatcharacter.h create mode 100644 sp/src/game/server/basecombatweapon.cpp create mode 100644 sp/src/game/server/basecombatweapon.h create mode 100644 sp/src/game/server/baseentity.cpp create mode 100644 sp/src/game/server/baseentity.h create mode 100644 sp/src/game/server/baseflex.cpp create mode 100644 sp/src/game/server/baseflex.h create mode 100644 sp/src/game/server/basegrenade_concussion.cpp create mode 100644 sp/src/game/server/basegrenade_contact.cpp create mode 100644 sp/src/game/server/basegrenade_timed.cpp create mode 100644 sp/src/game/server/basemultiplayerplayer.cpp create mode 100644 sp/src/game/server/basemultiplayerplayer.h create mode 100644 sp/src/game/server/basetempentity.cpp create mode 100644 sp/src/game/server/basetempentity.h create mode 100644 sp/src/game/server/basetoggle.h create mode 100644 sp/src/game/server/baseviewmodel.cpp create mode 100644 sp/src/game/server/baseviewmodel.h create mode 100644 sp/src/game/server/bitstring.cpp create mode 100644 sp/src/game/server/bitstring.h create mode 100644 sp/src/game/server/bmodels.cpp create mode 100644 sp/src/game/server/buttons.cpp create mode 100644 sp/src/game/server/buttons.h create mode 100644 sp/src/game/server/cbase.cpp create mode 100644 sp/src/game/server/cbase.h create mode 100644 sp/src/game/server/client.cpp create mode 100644 sp/src/game/server/client.h create mode 100644 sp/src/game/server/colorcorrection.cpp create mode 100644 sp/src/game/server/colorcorrectionvolume.cpp create mode 100644 sp/src/game/server/controlentities.cpp create mode 100644 sp/src/game/server/cplane.cpp create mode 100644 sp/src/game/server/cplane.h create mode 100644 sp/src/game/server/csm.vpc create mode 100644 sp/src/game/server/damagemodifier.cpp create mode 100644 sp/src/game/server/damagemodifier.h create mode 100644 sp/src/game/server/data_collector.cpp create mode 100644 sp/src/game/server/data_collector.h create mode 100644 sp/src/game/server/doors.cpp create mode 100644 sp/src/game/server/doors.h create mode 100644 sp/src/game/server/dynamiclight.cpp create mode 100644 sp/src/game/server/effects.cpp create mode 100644 sp/src/game/server/effects.h create mode 100644 sp/src/game/server/enginecallback.h create mode 100644 sp/src/game/server/entity_tools_server.cpp create mode 100644 sp/src/game/server/entityapi.h create mode 100644 sp/src/game/server/entityblocker.cpp create mode 100644 sp/src/game/server/entityblocker.h create mode 100644 sp/src/game/server/entityinput.h create mode 100644 sp/src/game/server/entitylist.cpp create mode 100644 sp/src/game/server/entitylist.h create mode 100644 sp/src/game/server/entityoutput.h create mode 100644 sp/src/game/server/env_cascade_light.cpp create mode 100644 sp/src/game/server/env_debughistory.cpp create mode 100644 sp/src/game/server/env_debughistory.h create mode 100644 sp/src/game/server/env_dof_controller.cpp create mode 100644 sp/src/game/server/env_dof_controller.h create mode 100644 sp/src/game/server/env_effectsscript.cpp create mode 100644 sp/src/game/server/env_entity_maker.cpp create mode 100644 sp/src/game/server/env_global_light.cpp create mode 100644 sp/src/game/server/env_instructor_hint.cpp create mode 100644 sp/src/game/server/env_particlescript.cpp create mode 100644 sp/src/game/server/env_player_surface_trigger.cpp create mode 100644 sp/src/game/server/env_player_surface_trigger.h create mode 100644 sp/src/game/server/env_projectedtexture.cpp create mode 100644 sp/src/game/server/env_projectedtexture.h create mode 100644 sp/src/game/server/env_screenoverlay.cpp create mode 100644 sp/src/game/server/env_texturetoggle.cpp create mode 100644 sp/src/game/server/env_tonemap_controller.cpp create mode 100644 sp/src/game/server/env_zoom.cpp create mode 100644 sp/src/game/server/env_zoom.h create mode 100644 sp/src/game/server/envmicrophone.cpp create mode 100644 sp/src/game/server/envmicrophone.h create mode 100644 sp/src/game/server/envspark.h create mode 100644 sp/src/game/server/event_tempentity_tester.cpp create mode 100644 sp/src/game/server/event_tempentity_tester.h create mode 100644 sp/src/game/server/eventqueue.h create mode 100644 sp/src/game/server/explode.cpp create mode 100644 sp/src/game/server/explode.h create mode 100644 sp/src/game/server/filters.cpp create mode 100644 sp/src/game/server/filters.h create mode 100644 sp/src/game/server/fire.cpp create mode 100644 sp/src/game/server/fire.h create mode 100644 sp/src/game/server/fire_smoke.cpp create mode 100644 sp/src/game/server/fire_smoke.h create mode 100644 sp/src/game/server/fish.cpp create mode 100644 sp/src/game/server/fish.h create mode 100644 sp/src/game/server/fogcontroller.cpp create mode 100644 sp/src/game/server/fogcontroller.h create mode 100644 sp/src/game/server/forcefeedback.cpp create mode 100644 sp/src/game/server/fourwheelvehiclephysics.cpp create mode 100644 sp/src/game/server/fourwheelvehiclephysics.h create mode 100644 sp/src/game/server/func_areaportal.cpp create mode 100644 sp/src/game/server/func_areaportalbase.cpp create mode 100644 sp/src/game/server/func_areaportalbase.h create mode 100644 sp/src/game/server/func_areaportalwindow.cpp create mode 100644 sp/src/game/server/func_areaportalwindow.h create mode 100644 sp/src/game/server/func_break.cpp create mode 100644 sp/src/game/server/func_break.h create mode 100644 sp/src/game/server/func_breakablesurf.cpp create mode 100644 sp/src/game/server/func_breakablesurf.h create mode 100644 sp/src/game/server/func_dust.cpp create mode 100644 sp/src/game/server/func_ladder_endpoint.cpp create mode 100644 sp/src/game/server/func_lod.cpp create mode 100644 sp/src/game/server/func_movelinear.cpp create mode 100644 sp/src/game/server/func_movelinear.h create mode 100644 sp/src/game/server/func_occluder.cpp create mode 100644 sp/src/game/server/func_reflective_glass.cpp create mode 100644 sp/src/game/server/func_smokevolume.cpp create mode 100644 sp/src/game/server/functorutils.h create mode 100644 sp/src/game/server/game.cpp create mode 100644 sp/src/game/server/game.h create mode 100644 sp/src/game/server/game_ui.cpp create mode 100644 sp/src/game/server/gamedll_replay.cpp create mode 100644 sp/src/game/server/gamehandle.cpp create mode 100644 sp/src/game/server/gameinterface.cpp create mode 100644 sp/src/game/server/gameinterface.h create mode 100644 sp/src/game/server/gametrace_dll.cpp create mode 100644 sp/src/game/server/gameweaponmanager.cpp create mode 100644 sp/src/game/server/gameweaponmanager.h create mode 100644 sp/src/game/server/genericactor.cpp create mode 100644 sp/src/game/server/genericmonster.cpp create mode 100644 sp/src/game/server/gib.cpp create mode 100644 sp/src/game/server/gib.h create mode 100644 sp/src/game/server/globals.cpp create mode 100644 sp/src/game/server/globals.h create mode 100644 sp/src/game/server/globalstate.cpp create mode 100644 sp/src/game/server/globalstate.h create mode 100644 sp/src/game/server/globalstate_private.h create mode 100644 sp/src/game/server/grenadethrown.cpp create mode 100644 sp/src/game/server/grenadethrown.h create mode 100644 sp/src/game/server/guntarget.cpp create mode 100644 sp/src/game/server/h_ai.cpp create mode 100644 sp/src/game/server/h_cycler.cpp create mode 100644 sp/src/game/server/h_cycler.h create mode 100644 sp/src/game/server/h_export.cpp create mode 100644 sp/src/game/server/hierarchy.cpp create mode 100644 sp/src/game/server/hierarchy.h create mode 100644 sp/src/game/server/hl1_CBaseHelicopter.h create mode 100644 sp/src/game/server/hltvdirector.cpp create mode 100644 sp/src/game/server/hltvdirector.h create mode 100644 sp/src/game/server/ilagcompensationmanager.h create mode 100644 sp/src/game/server/info_camera_link.cpp create mode 100644 sp/src/game/server/info_camera_link.h create mode 100644 sp/src/game/server/info_overlay_accessor.cpp create mode 100644 sp/src/game/server/init_factory.cpp create mode 100644 sp/src/game/server/init_factory.h create mode 100644 sp/src/game/server/intermission.cpp create mode 100644 sp/src/game/server/iscorer.h create mode 100644 sp/src/game/server/iservervehicle.h create mode 100644 sp/src/game/server/item_world.cpp create mode 100644 sp/src/game/server/items.h create mode 100644 sp/src/game/server/lightglow.cpp create mode 100644 sp/src/game/server/lights.cpp create mode 100644 sp/src/game/server/lights.h create mode 100644 sp/src/game/server/locksounds.h create mode 100644 sp/src/game/server/logic_achievement.cpp create mode 100644 sp/src/game/server/logic_measure_movement.cpp create mode 100644 sp/src/game/server/logic_mirror_movement.cpp create mode 100644 sp/src/game/server/logic_navigation.cpp create mode 100644 sp/src/game/server/logic_playmovie.cpp create mode 100644 sp/src/game/server/logic_random_outputs.cpp create mode 100644 sp/src/game/server/logic_random_outputs.h create mode 100644 sp/src/game/server/logicauto.cpp create mode 100644 sp/src/game/server/logicentities.cpp create mode 100644 sp/src/game/server/logicrelay.cpp create mode 100644 sp/src/game/server/logicrelay.h create mode 100644 sp/src/game/server/mapentities.cpp create mode 100644 sp/src/game/server/mapentities.h create mode 100644 sp/src/game/server/maprules.cpp create mode 100644 sp/src/game/server/maprules.h create mode 100644 sp/src/game/server/message_entity.cpp create mode 100644 sp/src/game/server/modelentities.cpp create mode 100644 sp/src/game/server/modelentities.h create mode 100644 sp/src/game/server/monstermaker.cpp create mode 100644 sp/src/game/server/monstermaker.h create mode 100644 sp/src/game/server/movehelper_server.cpp create mode 100644 sp/src/game/server/movehelper_server.h create mode 100644 sp/src/game/server/movement.cpp create mode 100644 sp/src/game/server/movie_display.cpp create mode 100644 sp/src/game/server/movie_explosion.cpp create mode 100644 sp/src/game/server/movie_explosion.h create mode 100644 sp/src/game/server/nav.h create mode 100644 sp/src/game/server/nav_area.cpp create mode 100644 sp/src/game/server/nav_area.h create mode 100644 sp/src/game/server/nav_colors.cpp create mode 100644 sp/src/game/server/nav_colors.h create mode 100644 sp/src/game/server/nav_edit.cpp create mode 100644 sp/src/game/server/nav_entities.cpp create mode 100644 sp/src/game/server/nav_entities.h create mode 100644 sp/src/game/server/nav_file.cpp create mode 100644 sp/src/game/server/nav_generate.cpp create mode 100644 sp/src/game/server/nav_ladder.cpp create mode 100644 sp/src/game/server/nav_ladder.h create mode 100644 sp/src/game/server/nav_merge.cpp create mode 100644 sp/src/game/server/nav_mesh.cpp create mode 100644 sp/src/game/server/nav_mesh.h create mode 100644 sp/src/game/server/nav_mesh.vpc create mode 100644 sp/src/game/server/nav_mesh_factory.cpp create mode 100644 sp/src/game/server/nav_node.cpp create mode 100644 sp/src/game/server/nav_node.h create mode 100644 sp/src/game/server/nav_pathfind.h create mode 100644 sp/src/game/server/nav_simplify.cpp create mode 100644 sp/src/game/server/ndebugoverlay.cpp create mode 100644 sp/src/game/server/ndebugoverlay.h create mode 100644 sp/src/game/server/networkstringtable_gamedll.h create mode 100644 sp/src/game/server/npc_talker.cpp create mode 100644 sp/src/game/server/npc_talker.h create mode 100644 sp/src/game/server/npc_vehicledriver.cpp create mode 100644 sp/src/game/server/npc_vehicledriver.h create mode 100644 sp/src/game/server/particle_fire.cpp create mode 100644 sp/src/game/server/particle_fire.h create mode 100644 sp/src/game/server/particle_light.cpp create mode 100644 sp/src/game/server/particle_light.h create mode 100644 sp/src/game/server/particle_smokegrenade.cpp create mode 100644 sp/src/game/server/particle_smokegrenade.h create mode 100644 sp/src/game/server/particle_system.cpp create mode 100644 sp/src/game/server/particle_system.h create mode 100644 sp/src/game/server/pathcorner.cpp create mode 100644 sp/src/game/server/pathtrack.cpp create mode 100644 sp/src/game/server/pathtrack.h create mode 100644 sp/src/game/server/phys_controller.cpp create mode 100644 sp/src/game/server/phys_controller.h create mode 100644 sp/src/game/server/physconstraint.cpp create mode 100644 sp/src/game/server/physconstraint.h create mode 100644 sp/src/game/server/physconstraint_sounds.h create mode 100644 sp/src/game/server/physgun.cpp create mode 100644 sp/src/game/server/physics.cpp create mode 100644 sp/src/game/server/physics.h create mode 100644 sp/src/game/server/physics_bone_follower.cpp create mode 100644 sp/src/game/server/physics_bone_follower.h create mode 100644 sp/src/game/server/physics_cannister.cpp create mode 100644 sp/src/game/server/physics_cannister.h create mode 100644 sp/src/game/server/physics_collisionevent.h create mode 100644 sp/src/game/server/physics_fx.cpp create mode 100644 sp/src/game/server/physics_fx.h create mode 100644 sp/src/game/server/physics_impact_damage.cpp create mode 100644 sp/src/game/server/physics_impact_damage.h create mode 100644 sp/src/game/server/physics_main.cpp create mode 100644 sp/src/game/server/physics_npc_solver.cpp create mode 100644 sp/src/game/server/physics_npc_solver.h create mode 100644 sp/src/game/server/physics_prop_ragdoll.cpp create mode 100644 sp/src/game/server/physics_prop_ragdoll.h create mode 100644 sp/src/game/server/physobj.cpp create mode 100644 sp/src/game/server/physobj.h create mode 100644 sp/src/game/server/plasma.cpp create mode 100644 sp/src/game/server/plasma.h create mode 100644 sp/src/game/server/player.cpp create mode 100644 sp/src/game/server/player.h create mode 100644 sp/src/game/server/player_command.cpp create mode 100644 sp/src/game/server/player_command.h create mode 100644 sp/src/game/server/player_lagcompensation.cpp create mode 100644 sp/src/game/server/player_pickup.cpp create mode 100644 sp/src/game/server/player_pickup.h create mode 100644 sp/src/game/server/player_resource.cpp create mode 100644 sp/src/game/server/player_resource.h create mode 100644 sp/src/game/server/playerinfomanager.cpp create mode 100644 sp/src/game/server/playerinfomanager.h create mode 100644 sp/src/game/server/playerlocaldata.cpp create mode 100644 sp/src/game/server/playerlocaldata.h create mode 100644 sp/src/game/server/plugin_check.cpp create mode 100644 sp/src/game/server/point_camera.cpp create mode 100644 sp/src/game/server/point_camera.h create mode 100644 sp/src/game/server/point_devshot_camera.cpp create mode 100644 sp/src/game/server/point_entity_finder.cpp create mode 100644 sp/src/game/server/point_playermoveconstraint.cpp create mode 100644 sp/src/game/server/point_spotlight.cpp create mode 100644 sp/src/game/server/point_template.cpp create mode 100644 sp/src/game/server/point_template.h create mode 100644 sp/src/game/server/pointanglesensor.cpp create mode 100644 sp/src/game/server/pointhurt.cpp create mode 100644 sp/src/game/server/pointteleport.cpp create mode 100644 sp/src/game/server/postprocesscontroller.cpp create mode 100644 sp/src/game/server/postprocesscontroller.h create mode 100644 sp/src/game/server/props.cpp create mode 100644 sp/src/game/server/props.h create mode 100644 sp/src/game/server/pushentity.h create mode 100644 sp/src/game/server/ragdoll_manager.cpp create mode 100644 sp/src/game/server/recipientfilter.cpp create mode 100644 sp/src/game/server/recipientfilter.h create mode 100644 sp/src/game/server/rope.cpp create mode 100644 sp/src/game/server/rope.h create mode 100644 sp/src/game/server/saverestore_gamedll.cpp create mode 100644 sp/src/game/server/sceneentity.cpp create mode 100644 sp/src/game/server/sceneentity.h create mode 100644 sp/src/game/server/scratchpad_gamedll_helpers.cpp create mode 100644 sp/src/game/server/scratchpad_gamedll_helpers.h create mode 100644 sp/src/game/server/scripted.cpp create mode 100644 sp/src/game/server/scripted.h create mode 100644 sp/src/game/server/scriptedtarget.cpp create mode 100644 sp/src/game/server/scriptedtarget.h create mode 100644 sp/src/game/server/sendproxy.cpp create mode 100644 sp/src/game/server/sendproxy.h create mode 100644 sp/src/game/server/server_base.vpc create mode 100644 sp/src/game/server/server_episodic.vpc create mode 100644 sp/src/game/server/server_hl2.vpc create mode 100644 sp/src/game/server/server_mapbase.vpc create mode 100644 sp/src/game/server/serverbenchmark_base.cpp create mode 100644 sp/src/game/server/serverbenchmark_base.h create mode 100644 sp/src/game/server/shadowcontrol.cpp create mode 100644 sp/src/game/server/skyboxswapper.cpp create mode 100644 sp/src/game/server/slideshow_display.cpp create mode 100644 sp/src/game/server/smoke_trail.cpp create mode 100644 sp/src/game/server/smoke_trail.h create mode 100644 sp/src/game/server/smokestack.cpp create mode 100644 sp/src/game/server/smokestack.h create mode 100644 sp/src/game/server/sound.cpp create mode 100644 sp/src/game/server/soundent.cpp create mode 100644 sp/src/game/server/soundent.h create mode 100644 sp/src/game/server/soundscape.cpp create mode 100644 sp/src/game/server/soundscape.h create mode 100644 sp/src/game/server/soundscape_system.cpp create mode 100644 sp/src/game/server/soundscape_system.h create mode 100644 sp/src/game/server/spark.h create mode 100644 sp/src/game/server/spotlightend.cpp create mode 100644 sp/src/game/server/spotlightend.h create mode 100644 sp/src/game/server/sprite_perfmonitor.cpp create mode 100644 sp/src/game/server/stdafx.cpp create mode 100644 sp/src/game/server/steamjet.cpp create mode 100644 sp/src/game/server/steamjet.h create mode 100644 sp/src/game/server/subs.cpp create mode 100644 sp/src/game/server/sun.cpp create mode 100644 sp/src/game/server/tactical_mission.cpp create mode 100644 sp/src/game/server/tactical_mission.h create mode 100644 sp/src/game/server/tanktrain.cpp create mode 100644 sp/src/game/server/te.cpp create mode 100644 sp/src/game/server/te.h create mode 100644 sp/src/game/server/te_armorricochet.cpp create mode 100644 sp/src/game/server/te_basebeam.cpp create mode 100644 sp/src/game/server/te_basebeam.h create mode 100644 sp/src/game/server/te_beamentpoint.cpp create mode 100644 sp/src/game/server/te_beaments.cpp create mode 100644 sp/src/game/server/te_beamfollow.cpp create mode 100644 sp/src/game/server/te_beamlaser.cpp create mode 100644 sp/src/game/server/te_beampoints.cpp create mode 100644 sp/src/game/server/te_beamring.cpp create mode 100644 sp/src/game/server/te_beamringpoint.cpp create mode 100644 sp/src/game/server/te_beamspline.cpp create mode 100644 sp/src/game/server/te_bloodsprite.cpp create mode 100644 sp/src/game/server/te_bloodstream.cpp create mode 100644 sp/src/game/server/te_breakmodel.cpp create mode 100644 sp/src/game/server/te_bspdecal.cpp create mode 100644 sp/src/game/server/te_bubbles.cpp create mode 100644 sp/src/game/server/te_bubbletrail.cpp create mode 100644 sp/src/game/server/te_clientprojectile.cpp create mode 100644 sp/src/game/server/te_decal.cpp create mode 100644 sp/src/game/server/te_dynamiclight.cpp create mode 100644 sp/src/game/server/te_effect_dispatch.cpp create mode 100644 sp/src/game/server/te_effect_dispatch.h create mode 100644 sp/src/game/server/te_energysplash.cpp create mode 100644 sp/src/game/server/te_explosion.cpp create mode 100644 sp/src/game/server/te_fizz.cpp create mode 100644 sp/src/game/server/te_footprintdecal.cpp create mode 100644 sp/src/game/server/te_glassshatter.cpp create mode 100644 sp/src/game/server/te_glowsprite.cpp create mode 100644 sp/src/game/server/te_impact.cpp create mode 100644 sp/src/game/server/te_killplayerattachments.cpp create mode 100644 sp/src/game/server/te_largefunnel.cpp create mode 100644 sp/src/game/server/te_muzzleflash.cpp create mode 100644 sp/src/game/server/te_particlesystem.cpp create mode 100644 sp/src/game/server/te_particlesystem.h create mode 100644 sp/src/game/server/te_physicsprop.cpp create mode 100644 sp/src/game/server/te_playerdecal.cpp create mode 100644 sp/src/game/server/te_projecteddecal.cpp create mode 100644 sp/src/game/server/te_showline.cpp create mode 100644 sp/src/game/server/te_smoke.cpp create mode 100644 sp/src/game/server/te_sparks.cpp create mode 100644 sp/src/game/server/te_sprite.cpp create mode 100644 sp/src/game/server/te_spritespray.cpp create mode 100644 sp/src/game/server/te_worlddecal.cpp create mode 100644 sp/src/game/server/team.cpp create mode 100644 sp/src/game/server/team.h create mode 100644 sp/src/game/server/team_control_point.cpp create mode 100644 sp/src/game/server/team_control_point.h create mode 100644 sp/src/game/server/team_control_point_master.cpp create mode 100644 sp/src/game/server/team_control_point_master.h create mode 100644 sp/src/game/server/team_control_point_round.cpp create mode 100644 sp/src/game/server/team_control_point_round.h create mode 100644 sp/src/game/server/team_objectiveresource.cpp create mode 100644 sp/src/game/server/team_objectiveresource.h create mode 100644 sp/src/game/server/team_spawnpoint.cpp create mode 100644 sp/src/game/server/team_spawnpoint.h create mode 100644 sp/src/game/server/team_train_watcher.cpp create mode 100644 sp/src/game/server/team_train_watcher.h create mode 100644 sp/src/game/server/tempmonster.cpp create mode 100644 sp/src/game/server/tesla.cpp create mode 100644 sp/src/game/server/tesla.h create mode 100644 sp/src/game/server/test_proxytoggle.cpp create mode 100644 sp/src/game/server/test_stressentities.cpp create mode 100644 sp/src/game/server/test_stressentities.h create mode 100644 sp/src/game/server/testfunctions.cpp create mode 100644 sp/src/game/server/testtraceline.cpp create mode 100644 sp/src/game/server/textstatsmgr.cpp create mode 100644 sp/src/game/server/textstatsmgr.h create mode 100644 sp/src/game/server/timedeventmgr.cpp create mode 100644 sp/src/game/server/timedeventmgr.h create mode 100644 sp/src/game/server/toolframework_server.cpp create mode 100644 sp/src/game/server/toolframework_server.h create mode 100644 sp/src/game/server/trains.cpp create mode 100644 sp/src/game/server/trains.h create mode 100644 sp/src/game/server/trigger_area_capture.cpp create mode 100644 sp/src/game/server/trigger_area_capture.h create mode 100644 sp/src/game/server/trigger_portal.cpp create mode 100644 sp/src/game/server/triggers.cpp create mode 100644 sp/src/game/server/triggers.h create mode 100644 sp/src/game/server/util.cpp create mode 100644 sp/src/game/server/util.h create mode 100644 sp/src/game/server/variant_t.cpp create mode 100644 sp/src/game/server/variant_t.h create mode 100644 sp/src/game/server/vehicle_base.cpp create mode 100644 sp/src/game/server/vehicle_base.h create mode 100644 sp/src/game/server/vehicle_baseserver.cpp create mode 100644 sp/src/game/server/vehicle_baseserver.h create mode 100644 sp/src/game/server/vehicle_choreo_generic.cpp create mode 100644 sp/src/game/server/vehicle_sounds.h create mode 100644 sp/src/game/server/vguiscreen.cpp create mode 100644 sp/src/game/server/vguiscreen.h create mode 100644 sp/src/game/server/vote_controller.cpp create mode 100644 sp/src/game/server/vote_controller.h create mode 100644 sp/src/game/server/vscript_server.cpp create mode 100644 sp/src/game/server/vscript_server.h create mode 100644 sp/src/game/server/vscript_server.nut create mode 100644 sp/src/game/server/waterbullet.cpp create mode 100644 sp/src/game/server/waterbullet.h create mode 100644 sp/src/game/server/wcedit.cpp create mode 100644 sp/src/game/server/wcedit.h create mode 100644 sp/src/game/server/weapon_cubemap.cpp create mode 100644 sp/src/game/server/weight_button.cpp create mode 100644 sp/src/game/server/worker_scientist.h create mode 100644 sp/src/game/server/world.cpp create mode 100644 sp/src/game/server/world.h diff --git a/sp/src/game/server/BasePropDoor.h b/sp/src/game/server/BasePropDoor.h new file mode 100644 index 00000000..d96c75c5 --- /dev/null +++ b/sp/src/game/server/BasePropDoor.h @@ -0,0 +1,283 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A base class for model-based doors. The exact movement required to +// open or close the door is not dictated by this class, only that +// the door has open, closed, opening, and closing states. +// +// Doors must satisfy these requirements: +// +// - Derived classes must support being opened by NPCs. +// - Never autoclose in the face of a player. +// - Never close into an NPC. +// +//=============================================================================// + +#ifndef BASEPROPDOOR_H +#define BASEPROPDOOR_H +#ifdef _WIN32 +#pragma once +#endif + +#include "props.h" +#include "locksounds.h" +#include "entityoutput.h" + +extern ConVar g_debug_doors; + +struct opendata_t +{ + Vector vecStandPos; // Where the NPC should stand. + Vector vecFaceDir; // What direction the NPC should face. + Activity eActivity; // What activity the NPC should play. +}; + + +abstract_class CBasePropDoor : public CDynamicProp +{ +public: + + DECLARE_CLASS( CBasePropDoor, CDynamicProp ); + DECLARE_SERVERCLASS(); + + CBasePropDoor( void ); + + void Spawn(); + void Precache(); + void Activate(); + int ObjectCaps(); + + void HandleAnimEvent( animevent_t *pEvent ); + + // Base class services. + // Do not make the functions in this block virtual!! + // { + inline bool IsDoorOpen(); + inline bool IsDoorAjar(); + inline bool IsDoorOpening(); + inline bool IsDoorClosed(); + inline bool IsDoorClosing(); + inline bool IsDoorLocked(); + inline bool IsDoorBlocked() const; + inline bool IsNPCOpening(CAI_BaseNPC *pNPC); + inline bool IsPlayerOpening(); + inline bool IsOpener(CBaseEntity *pEnt); + + bool NPCOpenDoor(CAI_BaseNPC *pNPC); + bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); + // } + + // Implement these in your leaf class. + // { + virtual bool DoorCanClose( bool bAutoClose ) { return true; } + virtual bool DoorCanOpen( void ) { return true; } + + virtual void GetNPCOpenData(CAI_BaseNPC *pNPC, opendata_t &opendata) = 0; + virtual float GetOpenInterval(void) = 0; + // } + +#ifdef MAPBASE + virtual bool PassesDoorFilter(CBaseEntity *pEntity) { return true; } +#endif + +protected: + + enum DoorState_t + { + DOOR_STATE_CLOSED = 0, + DOOR_STATE_OPENING, + DOOR_STATE_OPEN, + DOOR_STATE_CLOSING, + DOOR_STATE_AJAR, + }; + + // dvs: FIXME: make these private + void DoorClose(); + + CBasePropDoor *GetMaster( void ) { return m_hMaster; } + bool HasSlaves( void ) { return ( m_hDoorList.Count() > 0 ); } + + inline void SetDoorState( DoorState_t eDoorState ); + + float m_flAutoReturnDelay; // How many seconds to wait before automatically closing, -1 never closes automatically. + CUtlVector< CHandle< CBasePropDoor > > m_hDoorList; // List of doors linked to us + + inline CBaseEntity *GetActivator(); + +private: + + // Implement these in your leaf class. + // { + // Called when the door becomes fully open. + virtual void OnDoorOpened() {} + + // Called when the door becomes fully closed. + virtual void OnDoorClosed() {} + + // Called to tell the door to start opening. + virtual void BeginOpening(CBaseEntity *pOpenAwayFrom) = 0; + + // Called to tell the door to start closing. + virtual void BeginClosing( void ) = 0; + + // Called when blocked to tell the door to stop moving. + virtual void DoorStop( void ) = 0; + + // Called when blocked to tell the door to continue moving. + virtual void DoorResume( void ) = 0; + + // Called to send the door instantly to its spawn positions. + virtual void DoorTeleportToSpawnPosition() = 0; + // } + +private: + + // Main entry points for the door base behaviors. + // Do not make the functions in this block virtual!! + // { + bool DoorActivate(); + void DoorOpen( CBaseEntity *pOpenAwayFrom ); + void OpenIfUnlocked(CBaseEntity *pActivator, CBaseEntity *pOpenAwayFrom); + + void DoorOpenMoveDone(); + void DoorCloseMoveDone(); + void DoorAutoCloseThink(); + + void Lock(); + void Unlock(); + + void Use(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value); + void OnUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + inline bool WillAutoReturn() { return m_flAutoReturnDelay != -1; } + + void StartBlocked(CBaseEntity *pOther); + void OnStartBlocked( CBaseEntity *pOther ); + void MasterStartBlocked( CBaseEntity *pOther ); + + void Blocked(CBaseEntity *pOther); + void EndBlocked(void); + void OnEndBlocked( void ); + + void UpdateAreaPortals(bool bOpen); + + // Input handlers + void InputClose(inputdata_t &inputdata); + void InputLock(inputdata_t &inputdata); + void InputOpen(inputdata_t &inputdata); + void InputOpenAwayFrom(inputdata_t &inputdata); + void InputToggle(inputdata_t &inputdata); + void InputUnlock(inputdata_t &inputdata); +#ifdef MAPBASE + void InputAllowPlayerUse(inputdata_t &inputdata); + void InputDisallowPlayerUse(inputdata_t &inputdata); +#endif + + void SetDoorBlocker( CBaseEntity *pBlocker ); + + void SetMaster( CBasePropDoor *pMaster ) { m_hMaster = pMaster; } + + void CalcDoorSounds(); + // } + + int m_nHardwareType; + + DoorState_t m_eDoorState; // Holds whether the door is open, closed, opening, or closing. + + locksound_t m_ls; // The sounds the door plays when being locked, unlocked, etc. + EHANDLE m_hActivator; + + bool m_bLocked; // True if the door is locked. + EHANDLE m_hBlocker; // Entity blocking the door currently + bool m_bFirstBlocked; // Marker for being the first door (in a group) to be blocked (needed for motion control) + + bool m_bForceClosed; // True if this door must close no matter what. + + string_t m_SoundMoving; + string_t m_SoundOpen; + string_t m_SoundClose; + + // dvs: FIXME: can we remove m_flSpeed from CBaseEntity? + //float m_flSpeed; // Rotation speed when opening or closing in degrees per second. + + DECLARE_DATADESC(); + + string_t m_SlaveName; + + CHandle< CBasePropDoor > m_hMaster; + + static void RegisterPrivateActivities(); + + // Outputs + COutputEvent m_OnBlockedClosing; // Triggered when the door becomes blocked while closing. + COutputEvent m_OnBlockedOpening; // Triggered when the door becomes blocked while opening. + COutputEvent m_OnUnblockedClosing; // Triggered when the door becomes unblocked while closing. + COutputEvent m_OnUnblockedOpening; // Triggered when the door becomes unblocked while opening. + COutputEvent m_OnFullyClosed; // Triggered when the door reaches the fully closed position. + COutputEvent m_OnFullyOpen; // Triggered when the door reaches the fully open position. + COutputEvent m_OnClose; // Triggered when the door is told to close. + COutputEvent m_OnOpen; // Triggered when the door is told to open. + COutputEvent m_OnLockedUse; // Triggered when the user tries to open a locked door. +}; + + +void CBasePropDoor::SetDoorState( DoorState_t eDoorState ) +{ + m_eDoorState = eDoorState; +} + +bool CBasePropDoor::IsDoorOpen() +{ + return m_eDoorState == DOOR_STATE_OPEN; +} + +bool CBasePropDoor::IsDoorAjar() +{ + return ( m_eDoorState == DOOR_STATE_AJAR ); +} + +bool CBasePropDoor::IsDoorOpening() +{ + return m_eDoorState == DOOR_STATE_OPENING; +} + +bool CBasePropDoor::IsDoorClosed() +{ + return m_eDoorState == DOOR_STATE_CLOSED; +} + +bool CBasePropDoor::IsDoorClosing() +{ + return m_eDoorState == DOOR_STATE_CLOSING; +} + +bool CBasePropDoor::IsDoorLocked() +{ + return m_bLocked; +} + +CBaseEntity *CBasePropDoor::GetActivator() +{ + return m_hActivator; +} + +bool CBasePropDoor::IsDoorBlocked() const +{ + return ( m_hBlocker != NULL ); +} + +bool CBasePropDoor::IsNPCOpening( CAI_BaseNPC *pNPC ) +{ + return ( pNPC == ( CAI_BaseNPC * )GetActivator() ); +} + +inline bool CBasePropDoor::IsPlayerOpening() +{ + return ( GetActivator() && GetActivator()->IsPlayer() ); +} + +inline bool CBasePropDoor::IsOpener(CBaseEntity *pEnt) +{ + return ( GetActivator() == pEnt ); +} + +#endif // BASEPROPDOOR_H diff --git a/sp/src/game/server/CRagdollMagnet.cpp b/sp/src/game/server/CRagdollMagnet.cpp new file mode 100644 index 00000000..fc17cc9a --- /dev/null +++ b/sp/src/game/server/CRagdollMagnet.cpp @@ -0,0 +1,267 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "game.h" +#include "CRagdollMagnet.h" +#include "cplane.h" + +ConVar ai_debug_ragdoll_magnets( "ai_debug_ragdoll_magnets", "0"); + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( phys_ragdollmagnet, CRagdollMagnet ); +BEGIN_DATADESC( CRagdollMagnet ) + DEFINE_KEYFIELD( m_radius, FIELD_FLOAT, "radius" ), + DEFINE_KEYFIELD( m_force, FIELD_FLOAT, "force" ), + DEFINE_KEYFIELD( m_axis, FIELD_VECTOR, "axis" ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_BoneTarget, FIELD_STRING, "BoneTarget" ), +#endif + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +#ifdef MAPBASE + DEFINE_OUTPUT( m_OnUsed, "OnUsed" ), +#endif + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CRagdollMagnet::InputEnable( inputdata_t &inputdata ) +{ + Enable( true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CRagdollMagnet::InputDisable( inputdata_t &inputdata ) +{ + Enable( false ); +} + +//----------------------------------------------------------------------------- +// Purpose: Find the ragdoll magnet entity that should pull this entity's ragdoll +// Input : *pNPC - the npc that's dying +// Output : CRagdollMagnet - the magnet that's best to use. +// +// NOTES: +// +// The nearest ragdoll magnet pulls me in IF: +// - Present +// - I'm within the magnet's RADIUS +// - LATER: There is clear line of sight from my center to the magnet +// - LATER: I'm not flagged to ignore ragdoll magnets +// - LATER: The magnet is not turned OFF +//----------------------------------------------------------------------------- +CRagdollMagnet *CRagdollMagnet::FindBestMagnet( CBaseEntity *pNPC ) +{ + CRagdollMagnet *pMagnet = NULL; + CRagdollMagnet *pBestMagnet; + + float flClosestDist; + + // Assume we won't find one. + pBestMagnet = NULL; + flClosestDist = FLT_MAX; + + do + { + pMagnet = (CRagdollMagnet *)gEntList.FindEntityByClassname( pMagnet, "phys_ragdollmagnet" ); + + if( pMagnet && pMagnet->IsEnabled() ) + { + if( pMagnet->m_target != NULL_STRING ) + { + // if this magnet has a target, only affect that target! + if( pNPC->GetEntityName() == pMagnet->m_target ) + { + return pMagnet; + } + else + { + continue; + } + } + + float flDist; + flDist = pMagnet->DistToPoint( pNPC->WorldSpaceCenter() ); + + if( flDist < flClosestDist && flDist <= pMagnet->GetRadius() ) + { + // This is the closest magnet that can pull this npc. + flClosestDist = flDist; + pBestMagnet = pMagnet; + } + } + + } while( pMagnet ); + + return pBestMagnet; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the force that we should add to this NPC's ragdoll. +// Input : *pNPC - +// Output : Vector +// +// NOTE: This function assumes pNPC is within this magnet's radius. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +Vector CRagdollMagnet::GetForceVector( CBaseEntity *pNPC, int *pBone ) +#else +Vector CRagdollMagnet::GetForceVector( CBaseEntity *pNPC ) +#endif +{ + Vector vecForceToApply; + +#ifdef MAPBASE + Vector vecNPCPos = pNPC->WorldSpaceCenter(); + + if (pBone) + { + CBaseAnimating *pAnimating = pNPC->GetBaseAnimating(); + Assert( pAnimating != NULL ); + + const char *szBoneTarget = BoneTarget(); + Assert( szBoneTarget != NULL ); + + int iBone = pAnimating->LookupBone( szBoneTarget ); + + if (iBone != -1) + { + matrix3x4_t bonetoworld; + pAnimating->GetBoneTransform( iBone, bonetoworld ); + MatrixPosition( bonetoworld, vecNPCPos ); + *pBone = iBone; + } + } +#endif + + if( IsBarMagnet() ) + { + CPlane axis; + Vector vecForceDir; + Vector vecClosest; + +#ifdef MAPBASE + CalcClosestPointOnLineSegment( vecNPCPos, GetAbsOrigin(), m_axis, vecClosest, NULL ); + + vecForceDir = (vecClosest - vecNPCPos ); + VectorNormalize( vecForceDir ); +#else + CalcClosestPointOnLineSegment( pNPC->WorldSpaceCenter(), GetAbsOrigin(), m_axis, vecClosest, NULL ); + + vecForceDir = (vecClosest - pNPC->WorldSpaceCenter() ); + VectorNormalize( vecForceDir ); +#endif + + vecForceToApply = vecForceDir * m_force; + } + else + { + Vector vecForce; + +#ifdef MAPBASE + vecForce = GetAbsOrigin() - vecNPCPos; +#else + vecForce = GetAbsOrigin() - pNPC->WorldSpaceCenter(); +#endif + VectorNormalize( vecForce ); + + vecForceToApply = vecForce * m_force; + } + + if( ai_debug_ragdoll_magnets.GetBool() ) + { + IPhysicsObject *pPhysObject; + + pPhysObject = pNPC->VPhysicsGetObject(); + + if( pPhysObject ) + { + Msg("Ragdoll magnet adding %f inches/sec to %s\n", m_force/pPhysObject->GetMass(), pNPC->GetClassname() ); + } + } + + return vecForceToApply; +} + +//----------------------------------------------------------------------------- +// Purpose: How far away is this point? This is different for point and bar magnets +// Input : &vecPoint - the point +// Output : float - the dist +//----------------------------------------------------------------------------- +float CRagdollMagnet::DistToPoint( const Vector &vecPoint ) +{ + if( IsBarMagnet() ) + { + // I'm a bar magnet, so the point's distance is really the plane constant. + // A bar magnet is a cylinder who's length is AbsOrigin() to m_axis, and whose + // diameter is m_radius. + + // first we build two planes. The TOP and BOTTOM planes. + // the idea is that vecPoint must be on the right side of both + // planes to be affected by this particular magnet. + // TOP and BOTTOM planes can be visualized as the 'caps' of the cylinder + // that describes the bar magnet, and they point towards each other. + // We're making sure vecPoint is between the caps. + Vector vecAxis; + + vecAxis = GetAxisVector(); + VectorNormalize( vecAxis ); + + CPlane top, bottom; + + bottom.InitializePlane( -vecAxis, m_axis ); + top.InitializePlane( vecAxis, GetAbsOrigin() ); + + if( top.PointInFront( vecPoint ) && bottom.PointInFront( vecPoint ) ) + { + // This point is between the two caps, so calculate the distance + // of vecPoint from the axis of the bar magnet + CPlane axis; + Vector vecUp; + Vector vecRight; + + // Horizontal and Vertical distances. + float hDist, vDist; + + // Need to get a vector that's right-hand to m_axis + VectorVectors( vecAxis, vecRight, vecUp ); + + //CrossProduct( vecAxis, vecUp, vecRight ); + //VectorNormalize( vecRight ); + //VectorNormalize( vecUp ); + + // Set up the plane to measure horizontal dist. + axis.InitializePlane( vecRight, GetAbsOrigin() ); + hDist = fabs( axis.PointDist( vecPoint ) ); + + axis.InitializePlane( vecUp, GetAbsOrigin() ); + vDist = fabs( axis.PointDist( vecPoint ) ); + + return MAX( hDist, vDist ); + } + else + { + return FLT_MAX; + } + } + else + { + // I'm a point magnet. Just return dist + return ( GetAbsOrigin() - vecPoint ).Length(); + } +} diff --git a/sp/src/game/server/CRagdollMagnet.h b/sp/src/game/server/CRagdollMagnet.h new file mode 100644 index 00000000..52d3f57d --- /dev/null +++ b/sp/src/game/server/CRagdollMagnet.h @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Used to influence the initial force for a dying NPC's ragdoll. +// Passive entity. Just represents position in the world, radius, force +// +// $NoKeywords: $ +//=============================================================================// +#pragma once + +#ifndef CRAGDOLLMAGNET_H +#define CRAGDOLLMAGNET_H + +#define SF_RAGDOLLMAGNET_BAR 0x00000002 // this is a bar magnet. + +class CRagdollMagnet : public CPointEntity +{ +public: + DECLARE_CLASS( CRagdollMagnet, CPointEntity ); + DECLARE_DATADESC(); + +#ifdef MAPBASE + Vector GetForceVector( CBaseEntity *pNPC, int *pBone = NULL ); +#else + Vector GetForceVector( CBaseEntity *pNPC ); +#endif + float GetRadius( void ) { return m_radius; } + Vector GetAxisVector( void ) { return m_axis - GetAbsOrigin(); } + float DistToPoint( const Vector &vecPoint ); + + bool IsEnabled( void ) { return !m_bDisabled; } + + int IsBarMagnet( void ) { return (m_spawnflags & SF_RAGDOLLMAGNET_BAR); } + + static CRagdollMagnet *FindBestMagnet( CBaseEntity *pNPC ); + + void Enable( bool bEnable ) { m_bDisabled = !bEnable; } + + // Inputs + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +#ifdef MAPBASE + const char *BoneTarget() { return STRING(m_BoneTarget); } + + COutputVector m_OnUsed; +#endif + +private: + bool m_bDisabled; + float m_radius; + float m_force; + Vector m_axis; +#ifdef MAPBASE + string_t m_BoneTarget; +#endif +}; + +#endif //CRAGDOLLMAGNET_H \ No newline at end of file diff --git a/sp/src/game/server/CommentarySystem.cpp b/sp/src/game/server/CommentarySystem.cpp new file mode 100644 index 00000000..4d010151 --- /dev/null +++ b/sp/src/game/server/CommentarySystem.cpp @@ -0,0 +1,1662 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: The system for handling director's commentary style production info in-game. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +#ifndef _XBOX +#include "tier0/icommandline.h" +#include "igamesystem.h" +#include "filesystem.h" +#include +#include "in_buttons.h" +#include "engine/IEngineSound.h" +#include "soundenvelope.h" +#include "utldict.h" +#include "isaverestore.h" +#include "eventqueue.h" +#include "saverestore_utlvector.h" +#include "gamestats.h" +#include "ai_basenpc.h" +#include "Sprite.h" +#ifdef MAPBASE +#include "mapbase/SystemConvarMod.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static bool g_bTracingVsCommentaryNodes = false; +static const char *s_pCommentaryUpdateViewThink = "CommentaryUpdateViewThink"; + +#define COMMENTARY_SPAWNED_SEMAPHORE "commentary_semaphore" + +extern ConVar commentary; +ConVar commentary_available("commentary_available", "0", FCVAR_NONE, "Automatically set by the game when a commentary file is available for the current map." ); + +enum teleport_stages_t +{ + TELEPORT_NONE, + TELEPORT_FADEOUT, + TELEPORT_TELEPORT, + TELEPORT_FADEIN, +}; + +#ifndef MAPBASE // This has been moved to mapbase/SystemConvarMod.h +// Convar restoration save/restore +#define MAX_MODIFIED_CONVAR_STRING 128 +struct modifiedconvars_t +{ + DECLARE_SIMPLE_DATADESC(); + + char pszConvar[MAX_MODIFIED_CONVAR_STRING]; + char pszCurrentValue[MAX_MODIFIED_CONVAR_STRING]; + char pszOrgValue[MAX_MODIFIED_CONVAR_STRING]; +}; +#endif + +bool g_bInCommentaryMode = false; +bool IsInCommentaryMode( void ) +{ + return g_bInCommentaryMode; +} + +//----------------------------------------------------------------------------- +// Purpose: An entity that marks a spot for a piece of commentary +//----------------------------------------------------------------------------- +class CPointCommentaryNode : public CBaseAnimating +{ + DECLARE_CLASS( CPointCommentaryNode, CBaseAnimating ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + void Spawn( void ); + void Precache( void ); + void Activate( void ); + void SpinThink( void ); + void StartCommentary( void ); + void FinishCommentary( bool bBlendOut = true ); + void CleanupPostCommentary( void ); + void UpdateViewThink( void ); + void UpdateViewPostThink( void ); + bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); + bool HasViewTarget( void ) { return (m_hViewTarget != NULL || m_hViewPosition.Get() != NULL); } + bool PreventsMovement( void ); + bool CannotBeStopped( void ) { return (m_bUnstoppable || m_bPreventChangesWhileMoving); } + int UpdateTransmitState( void ); + void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + void SetDisabled( bool bDisabled ); + void SetNodeNumber( int iCount ) { m_iNodeNumber = iCount; } + + // Called to tell the node when it's moved under/not-under the player's crosshair + void SetUnderCrosshair( bool bUnderCrosshair ); + + // Called when the player attempts to activate the node + void PlayerActivated( void ); + void StopPlaying( void ); + void AbortPlaying( void ); + void TeleportTo( CBasePlayer *pPlayer ); + bool CanTeleportTo( void ); + + // Inputs + void InputStartCommentary( inputdata_t &inputdata ); + void InputStartUnstoppableCommentary( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +private: + string_t m_iszPreCommands; + string_t m_iszPostCommands; + CNetworkVar( string_t, m_iszCommentaryFile ); + CNetworkVar( string_t, m_iszCommentaryFileNoHDR ); + string_t m_iszViewTarget; + EHANDLE m_hViewTarget; + EHANDLE m_hViewTargetAngles; // Entity used to blend view angles to look at the target + string_t m_iszViewPosition; + CNetworkVar( EHANDLE, m_hViewPosition ); + EHANDLE m_hViewPositionMover; // Entity used to blend the view to the viewposition entity + bool m_bPreventMovement; + bool m_bUnderCrosshair; + bool m_bUnstoppable; + float m_flFinishedTime; + Vector m_vecFinishOrigin; + QAngle m_vecOriginalAngles; + QAngle m_vecFinishAngles; + bool m_bPreventChangesWhileMoving; + bool m_bDisabled; + Vector m_vecTeleportOrigin; + + COutputEvent m_pOnCommentaryStarted; + COutputEvent m_pOnCommentaryStopped; + + CNetworkVar( bool, m_bActive ); + CNetworkVar( float, m_flStartTime ); + CNetworkVar( string_t, m_iszSpeakers ); + CNetworkVar( int, m_iNodeNumber ); + CNetworkVar( int, m_iNodeNumberMax ); +}; + +BEGIN_DATADESC( CPointCommentaryNode ) + DEFINE_KEYFIELD( m_iszPreCommands, FIELD_STRING, "precommands" ), + DEFINE_KEYFIELD( m_iszPostCommands, FIELD_STRING, "postcommands" ), + DEFINE_KEYFIELD( m_iszCommentaryFile, FIELD_STRING, "commentaryfile" ), + DEFINE_KEYFIELD( m_iszCommentaryFileNoHDR, FIELD_STRING, "commentaryfile_nohdr" ), + DEFINE_KEYFIELD( m_iszViewTarget, FIELD_STRING, "viewtarget" ), + DEFINE_FIELD( m_hViewTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_hViewTargetAngles, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_iszViewPosition, FIELD_STRING, "viewposition" ), + DEFINE_FIELD( m_hViewPosition, FIELD_EHANDLE ), + DEFINE_FIELD( m_hViewPositionMover, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_bPreventMovement, FIELD_BOOLEAN, "prevent_movement" ), + DEFINE_FIELD( m_bUnderCrosshair, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bUnstoppable, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flFinishedTime, FIELD_TIME ), + DEFINE_FIELD( m_vecFinishOrigin, FIELD_VECTOR ), + DEFINE_FIELD( m_vecOriginalAngles, FIELD_VECTOR ), + DEFINE_FIELD( m_vecFinishAngles, FIELD_VECTOR ), + DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flStartTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_iszSpeakers, FIELD_STRING, "speakers" ), + DEFINE_FIELD( m_iNodeNumber, FIELD_INTEGER ), + DEFINE_FIELD( m_iNodeNumberMax, FIELD_INTEGER ), + DEFINE_FIELD( m_bPreventChangesWhileMoving, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "start_disabled" ), + DEFINE_KEYFIELD( m_vecTeleportOrigin, FIELD_VECTOR, "teleport_origin" ), + + // Outputs + DEFINE_OUTPUT( m_pOnCommentaryStarted, "OnCommentaryStarted" ), + DEFINE_OUTPUT( m_pOnCommentaryStopped, "OnCommentaryStopped" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "StartCommentary", InputStartCommentary ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartUnstoppableCommentary", InputStartUnstoppableCommentary ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + // Functions + DEFINE_THINKFUNC( SpinThink ), + DEFINE_THINKFUNC( UpdateViewThink ), + DEFINE_THINKFUNC( UpdateViewPostThink ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPointCommentaryNode, DT_PointCommentaryNode ) + SendPropBool( SENDINFO(m_bActive) ), + SendPropStringT( SENDINFO(m_iszCommentaryFile) ), + SendPropStringT( SENDINFO(m_iszCommentaryFileNoHDR) ), + SendPropTime( SENDINFO(m_flStartTime) ), + SendPropStringT( SENDINFO(m_iszSpeakers) ), + SendPropInt( SENDINFO(m_iNodeNumber), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_iNodeNumberMax), 8, SPROP_UNSIGNED ), + SendPropEHandle( SENDINFO(m_hViewPosition) ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( point_commentary_node, CPointCommentaryNode ); + +//----------------------------------------------------------------------------- +// Laser Dot +//----------------------------------------------------------------------------- +class CCommentaryViewPosition : public CSprite +{ + DECLARE_CLASS( CCommentaryViewPosition, CSprite ); +public: + virtual void Spawn( void ) + { + Precache(); + SetModelName( MAKE_STRING("sprites/redglow1.vmt") ); + + BaseClass::Spawn(); + + SetMoveType( MOVETYPE_NONE ); + AddSolidFlags( FSOLID_NOT_SOLID ); + AddEffects( EF_NOSHADOW ); + UTIL_SetSize( this, vec3_origin, vec3_origin ); + } + + virtual void Precache( void ) + { + PrecacheModel( "sprites/redglow1.vmt" ); + } +}; + +LINK_ENTITY_TO_CLASS( point_commentary_viewpoint, CCommentaryViewPosition ); + +//----------------------------------------------------------------------------- +// Purpose: In multiplayer, always return player 1 +//----------------------------------------------------------------------------- +CBasePlayer *GetCommentaryPlayer( void ) +{ + CBasePlayer *pPlayer; + + if ( gpGlobals->maxClients <= 1 ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + else + { + // only respond to the first player + pPlayer = UTIL_PlayerByIndex(1); + } + + return pPlayer; +} + +//=========================================================================================================== +// COMMENTARY GAME SYSTEM +//=========================================================================================================== +void CV_GlobalChange_Commentary( IConVar *var, const char *pOldString, float flOldValue ); + +//----------------------------------------------------------------------------- +// Purpose: Game system to kickstart the director's commentary +//----------------------------------------------------------------------------- +class CCommentarySystem : public CAutoGameSystemPerFrame +{ +public: + DECLARE_DATADESC(); + + CCommentarySystem() : CAutoGameSystemPerFrame( "CCommentarySystem" ) + { + m_iCommentaryNodeCount = 0; + } + + virtual void LevelInitPreEntity() + { + m_hCurrentNode = NULL; + m_bCommentaryConvarsChanging = false; + m_iClearPressedButtons = 0; + + // If the map started via the map_commentary cmd, start in commentary + g_bInCommentaryMode = (engine->IsInCommentaryMode() != 0); + + CalculateCommentaryState(); + } + + void CalculateCommentaryState( void ) + { + // Set the available cvar if we can find commentary data for this level + char szFullName[512]; + Q_snprintf(szFullName,sizeof(szFullName), "maps/%s_commentary.txt", STRING( gpGlobals->mapname) ); + if ( filesystem->FileExists( szFullName ) ) + { + commentary_available.SetValue( true ); + + // If the user wanted commentary, kick it on + if ( commentary.GetBool() ) + { + g_bInCommentaryMode = true; + } + } + else + { + g_bInCommentaryMode = false; + commentary_available.SetValue( false ); + } + } + + virtual void LevelShutdownPreEntity() + { + ShutDownCommentary(); + } + + void ParseEntKVBlock( CBaseEntity *pNode, KeyValues *pkvNode ) + { + KeyValues *pkvNodeData = pkvNode->GetFirstSubKey(); + while ( pkvNodeData ) + { + // Handle the connections block + if ( !Q_strcmp(pkvNodeData->GetName(), "connections") ) + { + ParseEntKVBlock( pNode, pkvNodeData ); + } + else + { + #define COMMENTARY_STRING_LENGTH_MAX 1024 + + const char *pszValue = pkvNodeData->GetString(); + Assert( Q_strlen(pszValue) < COMMENTARY_STRING_LENGTH_MAX ); + if ( Q_strnchr(pszValue, '^', COMMENTARY_STRING_LENGTH_MAX) ) + { + // We want to support quotes in our strings so that we can specify multiple parameters in + // an output inside our commentary files. We convert ^s to "s here. + char szTmp[COMMENTARY_STRING_LENGTH_MAX]; + Q_strncpy( szTmp, pszValue, COMMENTARY_STRING_LENGTH_MAX ); + int len = Q_strlen( szTmp ); + for ( int i = 0; i < len; i++ ) + { + if ( szTmp[i] == '^' ) + { + szTmp[i] = '"'; + } + } + + pNode->KeyValue( pkvNodeData->GetName(), szTmp ); + } + else + { + pNode->KeyValue( pkvNodeData->GetName(), pszValue ); + } + } + + pkvNodeData = pkvNodeData->GetNextKey(); + } + } + + virtual void LevelInitPostEntity( void ) + { + if ( !IsInCommentaryMode() ) + return; + + // Don't spawn commentary entities when loading a savegame + if ( gpGlobals->eLoadType == MapLoad_LoadGame || gpGlobals->eLoadType == MapLoad_Background ) + return; + + m_bCommentaryEnabledMidGame = false; + InitCommentary(); + + IGameEvent *event = gameeventmanager->CreateEvent( "playing_commentary" ); + gameeventmanager->FireEventClientSide( event ); + } + + CPointCommentaryNode *GetNodeUnderCrosshair() + { + CBasePlayer *pPlayer = GetCommentaryPlayer(); + if ( !pPlayer ) + return NULL; + + // See if the player's looking at a commentary node + trace_t tr; + Vector vecSrc = pPlayer->EyePosition(); + Vector vecForward = pPlayer->GetAutoaimVector( AUTOAIM_SCALE_DIRECT_ONLY ); + + g_bTracingVsCommentaryNodes = true; + UTIL_TraceLine( vecSrc, vecSrc + vecForward * MAX_TRACE_LENGTH, MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); + g_bTracingVsCommentaryNodes = false; + + if ( !tr.m_pEnt ) + return NULL; + + return dynamic_cast(tr.m_pEnt); + } + + void PrePlayerRunCommand( CBasePlayer *pPlayer, CUserCmd *pUserCmds ) + { + if ( !IsInCommentaryMode() ) + return; + + if ( pPlayer->IsFakeClient() ) + return; + + CPointCommentaryNode *pCurrentNode = GetNodeUnderCrosshair(); + + // Changed nodes? + if ( m_hCurrentNode != pCurrentNode ) + { + // Stop animating the old one + if ( m_hCurrentNode.Get() ) + { + m_hCurrentNode->SetUnderCrosshair( false ); + } + + // Start animating the new one + if ( pCurrentNode ) + { + pCurrentNode->SetUnderCrosshair( true ); + } + + m_hCurrentNode = pCurrentNode; + } + + // Check for commentary node activations + if ( pPlayer ) + { + // Has the player pressed down an attack button? + int buttonsChanged = m_afPlayersLastButtons ^ pUserCmds->buttons; + int buttonsPressed = buttonsChanged & pUserCmds->buttons; + m_afPlayersLastButtons = pUserCmds->buttons; + + if ( !(pUserCmds->buttons & COMMENTARY_BUTTONS) ) + { + m_iClearPressedButtons &= ~COMMENTARY_BUTTONS; + } + + // Detect press events to start/stop commentary nodes + if (buttonsPressed & COMMENTARY_BUTTONS) + { + if ( buttonsPressed & IN_ATTACK2 ) + { + if ( !(GetActiveNode() && GetActiveNode()->CannotBeStopped()) ) + { + JumpToNextNode( pPlayer ); + pUserCmds->buttons &= ~COMMENTARY_BUTTONS; + m_iClearPressedButtons |= (buttonsPressed & COMMENTARY_BUTTONS); + } + } + else + { + // Looking at a node? + if ( m_hCurrentNode ) + { + // Ignore input while an unstoppable node is playing + if ( !GetActiveNode() || !GetActiveNode()->CannotBeStopped() ) + { + // If we have an active node already, stop it + if ( GetActiveNode() && GetActiveNode() != m_hCurrentNode ) + { + GetActiveNode()->StopPlaying(); + } + + m_hCurrentNode->PlayerActivated(); + } + + // Prevent weapon firing when toggling nodes + pUserCmds->buttons &= ~COMMENTARY_BUTTONS; + m_iClearPressedButtons |= (buttonsPressed & COMMENTARY_BUTTONS); + } + else if ( GetActiveNode() && GetActiveNode()->HasViewTarget() ) + { + if ( !GetActiveNode()->CannotBeStopped() ) + { + GetActiveNode()->StopPlaying(); + } + + // Prevent weapon firing when toggling nodes + pUserCmds->buttons &= ~COMMENTARY_BUTTONS; + m_iClearPressedButtons |= (buttonsPressed & COMMENTARY_BUTTONS); + } + } + } + + if ( GetActiveNode() && GetActiveNode()->PreventsMovement() ) + { + pUserCmds->buttons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP | IN_DUCK ); + pUserCmds->upmove = 0; + pUserCmds->sidemove = 0; + pUserCmds->forwardmove = 0; + } + + // When we swallow button down events, we have to keep clearing that button + // until the player releases the button. Otherwise, the frame after we swallow + // it, the code detects the button down and goes ahead as normal. + pUserCmds->buttons &= ~m_iClearPressedButtons; + } + + if ( m_iTeleportStage != TELEPORT_NONE ) + { + if ( m_flNextTeleportTime <= gpGlobals->curtime ) + { + if ( m_iTeleportStage == TELEPORT_FADEOUT ) + { + m_iTeleportStage = TELEPORT_TELEPORT; + m_flNextTeleportTime = gpGlobals->curtime + 0.35; + + color32_s clr = { 0,0,0,255 }; + UTIL_ScreenFade( pPlayer, clr, 0.3, 0, FFADE_OUT | FFADE_PURGE | FFADE_STAYOUT ); + } + else if ( m_iTeleportStage == TELEPORT_TELEPORT ) + { + if ( m_hLastCommentaryNode ) + { + m_hLastCommentaryNode->TeleportTo( pPlayer ); + } + + m_iTeleportStage = TELEPORT_FADEIN; + m_flNextTeleportTime = gpGlobals->curtime + 0.6; + } + else if ( m_iTeleportStage == TELEPORT_FADEIN ) + { + m_iTeleportStage = TELEPORT_NONE; + m_flNextTeleportTime = gpGlobals->curtime + 0.25; + + color32_s clr = { 0,0,0,255 }; + UTIL_ScreenFade( pPlayer, clr, 0.3, 0, FFADE_IN | FFADE_PURGE ); + } + } + } + } + + CPointCommentaryNode *GetActiveNode( void ) + { + return m_hActiveCommentaryNode; + } + + void SetActiveNode( CPointCommentaryNode *pNode ) + { + m_hActiveCommentaryNode = pNode; + if ( pNode ) + { + m_hLastCommentaryNode = pNode; + } + } + + int GetCommentaryNodeCount( void ) + { + return m_iCommentaryNodeCount; + } + + bool CommentaryConvarsChanging( void ) + { + return m_bCommentaryConvarsChanging; + } + + void SetCommentaryConvarsChanging( bool bChanging ) + { + m_bCommentaryConvarsChanging = bChanging; + } + + void ConvarChanged( IConVar *pConVar, const char *pOldString, float flOldValue ) + { + ConVarRef var( pConVar ); + + // A convar has been changed by a commentary node. We need to store + // the old state. If the engine shuts down, we need to restore any + // convars that the commentary changed to their previous values. + for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) + { + // If we find it, just update the current value + if ( !Q_strncmp( var.GetName(), m_ModifiedConvars[i].pszConvar, MAX_MODIFIED_CONVAR_STRING ) ) + { + Q_strncpy( m_ModifiedConvars[i].pszCurrentValue, var.GetString(), MAX_MODIFIED_CONVAR_STRING ); + //Msg(" Updating Convar %s: value %s (org %s)\n", m_ModifiedConvars[i].pszConvar, m_ModifiedConvars[i].pszCurrentValue, m_ModifiedConvars[i].pszOrgValue ); + return; + } + } + + // We didn't find it in our list, so add it + modifiedconvars_t newConvar; + Q_strncpy( newConvar.pszConvar, var.GetName(), MAX_MODIFIED_CONVAR_STRING ); + Q_strncpy( newConvar.pszCurrentValue, var.GetString(), MAX_MODIFIED_CONVAR_STRING ); + Q_strncpy( newConvar.pszOrgValue, pOldString, MAX_MODIFIED_CONVAR_STRING ); + m_ModifiedConvars.AddToTail( newConvar ); + + /* + Msg(" Commentary changed '%s' to '%s' (was '%s')\n", var->GetName(), var->GetString(), pOldString ); + Msg(" Convars stored: %d\n", m_ModifiedConvars.Count() ); + for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) + { + Msg(" Convar %d: %s, value %s (org %s)\n", i, m_ModifiedConvars[i].pszConvar, m_ModifiedConvars[i].pszCurrentValue, m_ModifiedConvars[i].pszOrgValue ); + } + */ + } + + void InitCommentary( void ) + { + // Install the global cvar callback + cvar->InstallGlobalChangeCallback( CV_GlobalChange_Commentary ); + + m_flNextTeleportTime = 0; + m_iTeleportStage = TELEPORT_NONE; + m_hLastCommentaryNode = NULL; + + // If we find the commentary semaphore, the commentary entities already exist. + // This occurs when you transition back to a map that has saved commentary nodes in it. + if ( gEntList.FindEntityByName( NULL, COMMENTARY_SPAWNED_SEMAPHORE ) ) + return; + + // Spawn the commentary semaphore entity + CBaseEntity *pSemaphore = CreateEntityByName( "info_target" ); + pSemaphore->SetName( MAKE_STRING(COMMENTARY_SPAWNED_SEMAPHORE) ); + + bool oldLock = engine->LockNetworkStringTables( false ); + + // Find the commentary file + char szFullName[512]; + Q_snprintf(szFullName,sizeof(szFullName), "maps/%s_commentary.txt", STRING( gpGlobals->mapname )); + KeyValues *pkvFile = new KeyValues( "Commentary" ); + if ( pkvFile->LoadFromFile( filesystem, szFullName, "MOD" ) ) + { + Msg( "Commentary: Loading commentary data from %s. \n", szFullName ); + + // Load each commentary block, and spawn the entities + KeyValues *pkvNode = pkvFile->GetFirstSubKey(); + while ( pkvNode ) + { + // Get node name + const char *pNodeName = pkvNode->GetName(); + + // Skip the trackinfo + if ( !Q_strncmp( pNodeName, "trackinfo", 9 ) ) + { + pkvNode = pkvNode->GetNextKey(); + continue; + } + + KeyValues *pClassname = pkvNode->FindKey( "classname" ); + if ( pClassname ) + { + // Use the classname instead + pNodeName = pClassname->GetString(); + } + + // Spawn the commentary entity + CBaseEntity *pNode = CreateEntityByName( pNodeName ); + if ( pNode ) + { + ParseEntKVBlock( pNode, pkvNode ); + DispatchSpawn( pNode ); + + EHANDLE hHandle; + hHandle = pNode; + m_hSpawnedEntities.AddToTail( hHandle ); + + CPointCommentaryNode *pCommNode = dynamic_cast(pNode); + if ( pCommNode ) + { + m_iCommentaryNodeCount++; + pCommNode->SetNodeNumber( m_iCommentaryNodeCount ); + } + } + else + { + Warning("Commentary: Failed to spawn commentary entity, type: '%s'\n", pNodeName ); + } + + // Move to next entity + pkvNode = pkvNode->GetNextKey(); + } + + // Then activate all the entities + for ( int i = 0; i < m_hSpawnedEntities.Count(); i++ ) + { + m_hSpawnedEntities[i]->Activate(); + } + } + else + { + Msg( "Commentary: Could not find commentary data file '%s'. \n", szFullName ); + } + +#ifdef MAPBASE // VDC Memory Leak Fixes + pkvFile->deleteThis(); +#endif + + engine->LockNetworkStringTables( oldLock ); + } + + void ShutDownCommentary( void ) + { + if ( GetActiveNode() ) + { + GetActiveNode()->AbortPlaying(); + } + + // Destroy all the entities created by commentary + for ( int i = m_hSpawnedEntities.Count()-1; i >= 0; i-- ) + { + if ( m_hSpawnedEntities[i] ) + { + UTIL_Remove( m_hSpawnedEntities[i] ); + } + } + m_hSpawnedEntities.Purge(); + m_iCommentaryNodeCount = 0; + + // Remove the commentary semaphore + CBaseEntity *pSemaphore = gEntList.FindEntityByName( NULL, COMMENTARY_SPAWNED_SEMAPHORE ); + if ( pSemaphore ) + { + UTIL_Remove( pSemaphore ); + } + + // Remove our global convar callback + cvar->RemoveGlobalChangeCallback( CV_GlobalChange_Commentary ); + + // Reset any convars that have been changed by the commentary + for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) + { + ConVar *pConVar = (ConVar *)cvar->FindVar( m_ModifiedConvars[i].pszConvar ); + if ( pConVar ) + { + pConVar->SetValue( m_ModifiedConvars[i].pszOrgValue ); + } + } + m_ModifiedConvars.Purge(); + + m_hCurrentNode = NULL; + m_hActiveCommentaryNode = NULL; + m_hLastCommentaryNode = NULL; + m_flNextTeleportTime = 0; + m_iTeleportStage = TELEPORT_NONE; + } + + void SetCommentaryMode( bool bCommentaryMode ) + { + g_bInCommentaryMode = bCommentaryMode; + CalculateCommentaryState(); + + // If we're turning on commentary, create all the entities. + if ( IsInCommentaryMode() ) + { + m_bCommentaryEnabledMidGame = true; + InitCommentary(); + } + else + { + ShutDownCommentary(); + } + } + + void OnRestore( void ) + { + cvar->RemoveGlobalChangeCallback( CV_GlobalChange_Commentary ); + + if ( !IsInCommentaryMode() ) + return; + + // Set any convars that have already been changed by the commentary before the save + for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) + { + ConVar *pConVar = (ConVar *)cvar->FindVar( m_ModifiedConvars[i].pszConvar ); + if ( pConVar ) + { + //Msg(" Restoring Convar %s: value %s (org %s)\n", m_ModifiedConvars[i].pszConvar, m_ModifiedConvars[i].pszCurrentValue, m_ModifiedConvars[i].pszOrgValue ); + pConVar->SetValue( m_ModifiedConvars[i].pszCurrentValue ); + } + } + + // Install the global cvar callback + cvar->InstallGlobalChangeCallback( CV_GlobalChange_Commentary ); + } + + bool CommentaryWasEnabledMidGame( void ) + { + return m_bCommentaryEnabledMidGame; + } + + void JumpToNextNode( CBasePlayer *pPlayer ) + { + if ( m_flNextTeleportTime > gpGlobals->curtime || m_iTeleportStage != TELEPORT_NONE ) + return; + + CBaseEntity *pEnt = m_hLastCommentaryNode; + while ( ( pEnt = gEntList.FindEntityByClassname( pEnt, "point_commentary_node" ) ) != m_hLastCommentaryNode ) + { + CPointCommentaryNode *pNode = dynamic_cast( pEnt ); + if ( pNode && pNode->CanTeleportTo() ) + { + m_iTeleportStage = TELEPORT_FADEOUT; + m_hLastCommentaryNode = pNode; + m_flNextTeleportTime = gpGlobals->curtime; + + // Stop any active nodes + if ( m_hActiveCommentaryNode ) + { + m_hActiveCommentaryNode->StopPlaying(); + } + break; + } + } + } + +private: + int m_afPlayersLastButtons; + int m_iCommentaryNodeCount; + bool m_bCommentaryConvarsChanging; + int m_iClearPressedButtons; + bool m_bCommentaryEnabledMidGame; + float m_flNextTeleportTime; + int m_iTeleportStage; + + CUtlVector< modifiedconvars_t > m_ModifiedConvars; + CUtlVector m_hSpawnedEntities; + CHandle m_hCurrentNode; + CHandle m_hActiveCommentaryNode; + CHandle m_hLastCommentaryNode; +}; + +CCommentarySystem g_CommentarySystem; + +void CommentarySystem_PePlayerRunCommand( CBasePlayer *player, CUserCmd *ucmd ) +{ + g_CommentarySystem.PrePlayerRunCommand( player, ucmd ); +} + +BEGIN_DATADESC_NO_BASE( CCommentarySystem ) + //int m_afPlayersLastButtons; DON'T SAVE + //bool m_bCommentaryConvarsChanging; DON'T SAVE + //int m_iClearPressedButtons; DON'T SAVE + + DEFINE_FIELD( m_bCommentaryEnabledMidGame, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flNextTeleportTime, FIELD_TIME ), + DEFINE_FIELD( m_iTeleportStage, FIELD_INTEGER ), + + DEFINE_UTLVECTOR( m_ModifiedConvars, FIELD_EMBEDDED ), + DEFINE_UTLVECTOR( m_hSpawnedEntities, FIELD_EHANDLE ), + DEFINE_FIELD( m_hCurrentNode, FIELD_EHANDLE ), + DEFINE_FIELD( m_hActiveCommentaryNode, FIELD_EHANDLE ), + DEFINE_FIELD( m_hLastCommentaryNode, FIELD_EHANDLE ), + DEFINE_FIELD( m_iCommentaryNodeCount, FIELD_INTEGER ), +END_DATADESC() + +#ifndef MAPBASE // This has been moved to mapbase/SystemConvarMod.h +BEGIN_SIMPLE_DATADESC( modifiedconvars_t ) + DEFINE_ARRAY( pszConvar, FIELD_CHARACTER, MAX_MODIFIED_CONVAR_STRING ), + DEFINE_ARRAY( pszCurrentValue, FIELD_CHARACTER, MAX_MODIFIED_CONVAR_STRING ), + DEFINE_ARRAY( pszOrgValue, FIELD_CHARACTER, MAX_MODIFIED_CONVAR_STRING ), +END_DATADESC() +#endif + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CC_CommentaryChanged( IConVar *pConVar, const char *pOldString, float flOldValue ) +{ + ConVarRef var( pConVar ); + if ( var.GetBool() != g_bInCommentaryMode ) + { + g_CommentarySystem.SetCommentaryMode( var.GetBool() ); + } +} +ConVar commentary("commentary", "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX, "Desired commentary mode state.", CC_CommentaryChanged ); + +//----------------------------------------------------------------------------- +// Purpose: We need to revert back any convar changes that are made by the +// commentary system during commentary. This code stores convar changes +// made by the commentary system, and reverts them when finished. +//----------------------------------------------------------------------------- +void CV_GlobalChange_Commentary( IConVar *var, const char *pOldString, float flOldValue ) +{ + if ( !g_CommentarySystem.CommentaryConvarsChanging() ) + { + // A convar has changed, but not due to commentary nodes. Ignore it. + return; + } + + g_CommentarySystem.ConvarChanged( var, pOldString, flOldValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CC_CommentaryNotChanging( void ) +{ + g_CommentarySystem.SetCommentaryConvarsChanging( false ); +} +static ConCommand commentary_cvarsnotchanging("commentary_cvarsnotchanging", CC_CommentaryNotChanging, 0 ); + +bool IsListeningToCommentary( void ) +{ + return ( g_CommentarySystem.GetActiveNode() != NULL ); +} + +//=========================================================================================================== +// COMMENTARY NODES +//=========================================================================================================== + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::Spawn( void ) +{ + // No model specified? + char *szModel = (char *)STRING( GetModelName() ); + if (!szModel || !*szModel) + { + szModel = "models/extras/info_speech.mdl"; + SetModelName( AllocPooledString(szModel) ); + } + + Precache(); + SetModel( szModel ); + UTIL_SetSize( this, -Vector(16,16,16), Vector(16,16,16) ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_CUSTOMRAYTEST | FSOLID_CUSTOMBOXTEST ); + AddEffects( EF_NOSHADOW ); + + // Setup for animation + ResetSequence( LookupSequence("idle") ); + SetThink( &CPointCommentaryNode::SpinThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + m_iNodeNumber = 0; + m_iNodeNumberMax = 0; + + SetDisabled( m_bDisabled ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::Activate( void ) +{ + m_iNodeNumberMax = g_CommentarySystem.GetCommentaryNodeCount(); + + if ( m_iszViewTarget != NULL_STRING ) + { + m_hViewTarget = gEntList.FindEntityByName( NULL, m_iszViewTarget ); + if ( !m_hViewTarget ) + { + Warning("%s: %s could not find viewtarget %s.\n", GetClassname(), GetDebugName(), STRING(m_iszViewTarget) ); + } + } + + if ( m_iszViewPosition != NULL_STRING ) + { + m_hViewPosition = gEntList.FindEntityByName( NULL, m_iszViewPosition ); + if ( !m_hViewPosition.Get() ) + { + Warning("%s: %s could not find viewposition %s.\n", GetClassname(), GetDebugName(), STRING(m_iszViewPosition) ); + } + } + + BaseClass::Activate(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::Precache() +{ + PrecacheModel( STRING( GetModelName() ) ); + + if ( m_iszCommentaryFile.Get() != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_iszCommentaryFile.Get() ) ); + } + else + { + Warning("%s: %s has no commentary file.\n", GetClassname(), GetDebugName() ); + } + + if ( m_iszCommentaryFileNoHDR.Get() != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_iszCommentaryFileNoHDR.Get() ) ); + } + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Called to tell the node when it's moved under/not-under the player's crosshair +//----------------------------------------------------------------------------- +void CPointCommentaryNode::SetUnderCrosshair( bool bUnderCrosshair ) +{ + if ( bUnderCrosshair ) + { + // Start animating + m_bUnderCrosshair = true; + + if ( !m_bActive ) + { + m_flAnimTime = gpGlobals->curtime; + } + } + else + { + // Stop animating + m_bUnderCrosshair = false; + } +} + +//------------------------------------------------------------------------------ +// Purpose: Prevent collisions of everything except the trace from the commentary system +//------------------------------------------------------------------------------ +bool CPointCommentaryNode::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ + if ( !g_bTracingVsCommentaryNodes ) + return false; + if ( m_bDisabled ) + return false; + + return BaseClass::TestCollision( ray, mask, trace ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointCommentaryNode::SpinThink( void ) +{ + // Rotate if we're active, or under the crosshair. Don't rotate if we're + // under the crosshair, but we've already been listened to. + if ( m_bActive || (m_bUnderCrosshair && m_nSkin == 0) ) + { + if ( m_bActive ) + { + m_flPlaybackRate = 3.0; + } + else + { + m_flPlaybackRate = 1.0; + } + StudioFrameAdvance(); + DispatchAnimEvents(this); + } + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointCommentaryNode::PlayerActivated( void ) +{ + gamestats->Event_Commentary(); + + if ( m_bActive ) + { + StopPlaying(); + } + else + { + StartCommentary(); + g_CommentarySystem.SetActiveNode( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::StopPlaying( void ) +{ + if ( m_bActive ) + { + FinishCommentary(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Stop playing the node, but snap completely out of the node. +// Used when players shut down commentary while we're in the middle +// of playing a node, so we can't smoothly blend out (since the +// commentary entities need to be removed). +//----------------------------------------------------------------------------- +void CPointCommentaryNode::AbortPlaying( void ) +{ + if ( m_bActive ) + { + FinishCommentary( false ); + } + else if ( m_bPreventChangesWhileMoving ) + { + // We're a node that's not active, but is in the process of transitioning the view. Finish movement. + CleanupPostCommentary(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPointCommentaryNode::CanTeleportTo( void ) +{ + //return ( m_vecTeleportOrigin != vec3_origin ); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::TeleportTo( CBasePlayer *pPlayer ) +{ + Vector vecTarget = m_vecTeleportOrigin; + if ( m_vecTeleportOrigin == vec3_origin ) + { + vecTarget = GetAbsOrigin(); + } + + trace_t trace; + UTIL_TraceHull( vecTarget, vecTarget + Vector( 0, 0, -500 ), pPlayer->WorldAlignMins(), pPlayer->WorldAlignMaxs(), MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &trace ); + + pPlayer->Teleport( &trace.endpos, NULL, &vec3_origin ); + + Vector vecToNode = GetAbsOrigin() - pPlayer->EyePosition(); + VectorNormalize( vecToNode ); + QAngle vecAngle; + VectorAngles( vecToNode, Vector(0,0,1), vecAngle ); + + pPlayer->SnapEyeAngles( vecAngle ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointCommentaryNode::StartCommentary( void ) +{ + CBasePlayer *pPlayer = GetCommentaryPlayer(); + + if ( !pPlayer ) + return; + + m_bActive = true; + + m_flAnimTime = gpGlobals->curtime; + m_flPrevAnimTime = gpGlobals->curtime; + + // Switch to the greyed out skin + m_nSkin = 1; + + m_pOnCommentaryStarted.FireOutput( this, this ); + + // Fire off our precommands + if ( m_iszPreCommands != NULL_STRING ) + { + g_CommentarySystem.SetCommentaryConvarsChanging( true ); + engine->ClientCommand( pPlayer->edict(), STRING(m_iszPreCommands) ); + engine->ClientCommand( pPlayer->edict(), "commentary_cvarsnotchanging\n" ); + } + + // Start the commentary + m_flStartTime = gpGlobals->curtime; + + // If we have a view target, start blending towards it + if ( m_hViewTarget || m_hViewPosition.Get() ) + { + m_vecOriginalAngles = pPlayer->EyeAngles(); + SetContextThink( &CPointCommentaryNode::UpdateViewThink, gpGlobals->curtime, s_pCommentaryUpdateViewThink ); + } + + //SetContextThink( &CPointCommentaryNode::FinishCommentary, gpGlobals->curtime + flDuration, s_pFinishCommentaryThink ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CC_CommentaryFinishNode( void ) +{ + // We were told by the client DLL that our commentary has finished + if ( g_CommentarySystem.GetActiveNode() ) + { + g_CommentarySystem.GetActiveNode()->StopPlaying(); + } +} +static ConCommand commentary_finishnode("commentary_finishnode", CC_CommentaryFinishNode, 0 ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::UpdateViewThink( void ) +{ + if ( !m_bActive ) + return; + CBasePlayer *pPlayer = GetCommentaryPlayer(); + if ( !pPlayer ) + return; + + // Swing the view towards the target + if ( m_hViewTarget ) + { + if ( !m_hViewTargetAngles && !m_hViewPositionMover ) + { + // Make an invisible entity to attach view angles to + m_hViewTargetAngles = CreateEntityByName( "point_commentary_viewpoint" ); + m_hViewTargetAngles->SetAbsOrigin( pPlayer->EyePosition() ); + m_hViewTargetAngles->SetAbsAngles( pPlayer->EyeAngles() ); + pPlayer->SetViewEntity( m_hViewTargetAngles ); + + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->Holster(); + } + } + + QAngle angGoal; + QAngle angCurrent; + if ( m_hViewPositionMover ) + { + angCurrent = m_hViewPositionMover->GetAbsAngles(); + VectorAngles( m_hViewTarget->WorldSpaceCenter() - m_hViewPositionMover->GetAbsOrigin(), angGoal ); + } + else if ( m_hViewTargetAngles ) + { + angCurrent = m_hViewTargetAngles->GetAbsAngles(); + m_hViewTargetAngles->SetAbsOrigin( pPlayer->EyePosition() ); + VectorAngles( m_hViewTarget->WorldSpaceCenter() - m_hViewTargetAngles->GetAbsOrigin(), angGoal ); + } + else + { + angCurrent = pPlayer->EyeAngles(); + VectorAngles( m_hViewTarget->WorldSpaceCenter() - pPlayer->EyePosition(), angGoal ); + } + + // Accelerate towards the target goal angles + float dx = AngleDiff( angGoal.x, angCurrent.x ); + float dy = AngleDiff( angGoal.y, angCurrent.y ); + float mod = 1.0 - ExponentialDecay( 0.5, 0.3, gpGlobals->frametime ); + float dxmod = dx * mod; + float dymod = dy * mod; + + angCurrent.x = AngleNormalize( angCurrent.x + dxmod ); + angCurrent.y = AngleNormalize( angCurrent.y + dymod ); + + if ( m_hViewPositionMover ) + { + m_hViewPositionMover->SetAbsAngles( angCurrent ); + } + else if ( m_hViewTargetAngles ) + { + m_hViewTargetAngles->SetAbsAngles( angCurrent ); + pPlayer->SnapEyeAngles( angCurrent ); + } + else + { + pPlayer->SnapEyeAngles( angCurrent ); + } + + SetNextThink( gpGlobals->curtime, s_pCommentaryUpdateViewThink ); + } + + if ( m_hViewPosition.Get() ) + { + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->Holster(); + } + + if ( !m_hViewPositionMover ) + { + // Make an invisible info target entity for us to attach the view to, + // and move it to the desired view position. + m_hViewPositionMover = CreateEntityByName( "point_commentary_viewpoint" ); + m_hViewPositionMover->SetAbsAngles( pPlayer->EyeAngles() ); + pPlayer->SetViewEntity( m_hViewPositionMover ); + } + + // Blend to the target position over time. + float flCurTime = (gpGlobals->curtime - m_flStartTime); + float flBlendPerc = clamp( flCurTime * 0.5f, 0.f, 1.f ); + + // Figure out the current view position + Vector vecCurEye; + VectorLerp( pPlayer->EyePosition(), m_hViewPosition.Get()->GetAbsOrigin(), flBlendPerc, vecCurEye ); + m_hViewPositionMover->SetAbsOrigin( vecCurEye ); + + SetNextThink( gpGlobals->curtime, s_pCommentaryUpdateViewThink ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::UpdateViewPostThink( void ) +{ + CBasePlayer *pPlayer = GetCommentaryPlayer(); + if ( !pPlayer ) + return; + + if ( m_hViewPosition.Get() && m_hViewPositionMover ) + { + // Blend back to the player's position over time. + float flCurTime = (gpGlobals->curtime - m_flFinishedTime); + float flTimeToBlend = MIN( 2.0, m_flFinishedTime - m_flStartTime ); + float flBlendPerc = 1.0f - clamp( flCurTime / flTimeToBlend, 0.f, 1.f ); + + //Msg("OUT: CurTime %.2f, BlendTime: %.2f, Blend: %.3f\n", flCurTime, flTimeToBlend, flBlendPerc ); + + // Only do this while we're still moving + if ( flBlendPerc > 0 ) + { + // Figure out the current view position + Vector vecPlayerPos = pPlayer->EyePosition(); + Vector vecToPosition = (m_vecFinishOrigin - vecPlayerPos); + Vector vecCurEye = pPlayer->EyePosition() + (vecToPosition * flBlendPerc); + m_hViewPositionMover->SetAbsOrigin( vecCurEye ); + + if ( m_hViewTarget ) + { + Quaternion quatFinish; + Quaternion quatOriginal; + Quaternion quatCurrent; + AngleQuaternion( m_vecOriginalAngles, quatOriginal ); + AngleQuaternion( m_vecFinishAngles, quatFinish ); + QuaternionSlerp( quatFinish, quatOriginal, 1.0 - flBlendPerc, quatCurrent ); + QAngle angCurrent; + QuaternionAngles( quatCurrent, angCurrent ); + m_hViewPositionMover->SetAbsAngles( angCurrent ); + } + + SetNextThink( gpGlobals->curtime, s_pCommentaryUpdateViewThink ); + return; + } + + pPlayer->SnapEyeAngles( m_hViewPositionMover->GetAbsAngles() ); + } + + // We're done + CleanupPostCommentary(); + + m_bPreventChangesWhileMoving = false; +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointCommentaryNode::FinishCommentary( bool bBlendOut ) +{ + CBasePlayer *pPlayer = GetCommentaryPlayer(); + if ( !pPlayer ) + return; + + // Fire off our postcommands + if ( m_iszPostCommands != NULL_STRING ) + { + g_CommentarySystem.SetCommentaryConvarsChanging( true ); + engine->ClientCommand( pPlayer->edict(), STRING(m_iszPostCommands) ); + engine->ClientCommand( pPlayer->edict(), "commentary_cvarsnotchanging\n" ); + } + + // Stop the commentary + m_flFinishedTime = gpGlobals->curtime; + + if ( bBlendOut && m_hViewPositionMover ) + { + m_bActive = false; + m_flPlaybackRate = 1.0; + m_vecFinishOrigin = m_hViewPositionMover->GetAbsOrigin(); + m_vecFinishAngles = m_hViewPositionMover->GetAbsAngles(); + + m_bPreventChangesWhileMoving = true; + + // We've moved away from the player's position. Move back to it before ending + SetContextThink( &CPointCommentaryNode::UpdateViewPostThink, gpGlobals->curtime, s_pCommentaryUpdateViewThink ); + return; + } + + CleanupPostCommentary(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::CleanupPostCommentary( void ) +{ + CBasePlayer *pPlayer = GetCommentaryPlayer(); + if ( !pPlayer ) + return; + + if ( ( m_hViewPositionMover || m_hViewTargetAngles ) && pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->Deploy(); + } + + if ( m_hViewTargetAngles && pPlayer->GetViewEntity() == m_hViewTargetAngles ) + { + pPlayer->SetViewEntity( NULL ); + } + UTIL_Remove( m_hViewTargetAngles ); + + if ( m_hViewPositionMover && pPlayer->GetViewEntity() == m_hViewPositionMover ) + { + pPlayer->SetViewEntity( NULL ); + } + UTIL_Remove( m_hViewPositionMover ); + + m_bActive = false; + m_flPlaybackRate = 1.0; + m_bUnstoppable = false; + m_flFinishedTime = 0; + SetContextThink( NULL, 0, s_pCommentaryUpdateViewThink ); + + // Clear out any events waiting on our start commentary + g_EventQueue.CancelEvents( this ); + + m_pOnCommentaryStopped.FireOutput( this, this ); + + g_CommentarySystem.SetActiveNode( NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::InputStartCommentary( inputdata_t &inputdata ) +{ + if ( !m_bActive ) + { + if ( g_CommentarySystem.GetActiveNode() ) + { + g_CommentarySystem.GetActiveNode()->StopPlaying(); + } + + PlayerActivated(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::InputStartUnstoppableCommentary( inputdata_t &inputdata ) +{ + if ( !m_bActive ) + { + m_bUnstoppable = true; + InputStartCommentary( inputdata ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::InputEnable( inputdata_t &inputdata ) +{ + SetDisabled( false ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::InputDisable( inputdata_t &inputdata ) +{ + SetDisabled( true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::SetDisabled( bool bDisabled ) +{ + m_bDisabled = bDisabled; + + if ( m_bDisabled ) + { + AddEffects( EF_NODRAW ); + } + else + { + RemoveEffects( EF_NODRAW ); + } +} + +//----------------------------------------------------------------------------- +// Purpose Force our lighting landmark to be transmitted +//----------------------------------------------------------------------------- +int CPointCommentaryNode::UpdateTransmitState( void ) +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCommentaryNode::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + // Are we already marked for transmission? + if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) + return; + + BaseClass::SetTransmit( pInfo, bAlways ); + + // Force our camera view position entity to be sent + if ( m_hViewTarget ) + { + m_hViewTarget->SetTransmit( pInfo, bAlways ); + } + if ( m_hViewTargetAngles ) + { + m_hViewTargetAngles->SetTransmit( pInfo, bAlways ); + } + if ( m_hViewPosition.Get() ) + { + m_hViewPosition.Get()->SetTransmit( pInfo, bAlways ); + } + if ( m_hViewPositionMover ) + { + m_hViewPositionMover->SetTransmit( pInfo, bAlways ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPointCommentaryNode::PreventsMovement( void ) +{ + // If we're moving the player's view at all, prevent movement + if ( m_hViewPosition.Get() ) + return true; + + return m_bPreventMovement; +} + +//----------------------------------------------------------------------------- +// COMMENTARY SAVE / RESTORE +//----------------------------------------------------------------------------- +static short COMMENTARY_SAVE_RESTORE_VERSION = 2; + +class CCommentary_SaveRestoreBlockHandler : public CDefSaveRestoreBlockHandler +{ +public: + const char *GetBlockName() + { + return "Commentary"; + } + + //--------------------------------- + + void Save( ISave *pSave ) + { + pSave->WriteBool( &g_bInCommentaryMode ); + if ( IsInCommentaryMode() ) + { + pSave->WriteAll( &g_CommentarySystem, g_CommentarySystem.GetDataDescMap() ); + pSave->WriteInt( &CAI_BaseNPC::m_nDebugBits ); + } + } + + //--------------------------------- + + void WriteSaveHeaders( ISave *pSave ) + { + pSave->WriteShort( &COMMENTARY_SAVE_RESTORE_VERSION ); + } + + //--------------------------------- + + void ReadRestoreHeaders( IRestore *pRestore ) + { + // No reason why any future version shouldn't try to retain backward compatability. The default here is to not do so. + short version; + pRestore->ReadShort( &version ); + m_fDoLoad = ( version == COMMENTARY_SAVE_RESTORE_VERSION ); + } + + //--------------------------------- + + void Restore( IRestore *pRestore, bool createPlayers ) + { + if ( m_fDoLoad ) + { + pRestore->ReadBool( &g_bInCommentaryMode ); + if ( g_bInCommentaryMode ) + { + pRestore->ReadAll( &g_CommentarySystem, g_CommentarySystem.GetDataDescMap() ); + CAI_BaseNPC::m_nDebugBits = pRestore->ReadInt(); + } + + // Force the commentary convar to match the saved game state + commentary.SetValue( g_bInCommentaryMode ); + } + } + +private: + bool m_fDoLoad; +}; + +//----------------------------------------------------------------------------- + +CCommentary_SaveRestoreBlockHandler g_Commentary_SaveRestoreBlockHandler; + +//------------------------------------- + +ISaveRestoreBlockHandler *GetCommentarySaveRestoreBlockHandler() +{ + return &g_Commentary_SaveRestoreBlockHandler; +} + +//----------------------------------------------------------------------------- +// Purpose: Commentary specific logic_auto replacement. +// Fires outputs based upon how commentary mode has been activated. +//----------------------------------------------------------------------------- +class CCommentaryAuto : public CBaseEntity +{ + DECLARE_CLASS( CCommentaryAuto, CBaseEntity ); +public: + DECLARE_DATADESC(); + + void Spawn(void); + void Think(void); + + void InputMultiplayerSpawned( inputdata_t &inputdata ); + +private: + // fired if commentary started due to new map + COutputEvent m_OnCommentaryNewGame; + + // fired if commentary was turned on in the middle of a map + COutputEvent m_OnCommentaryMidGame; + + // fired when the player spawns in a multiplayer game + COutputEvent m_OnCommentaryMultiplayerSpawn; +}; + +LINK_ENTITY_TO_CLASS(commentary_auto, CCommentaryAuto); + +BEGIN_DATADESC( CCommentaryAuto ) + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "MultiplayerSpawned", InputMultiplayerSpawned ), + + // Outputs + DEFINE_OUTPUT(m_OnCommentaryNewGame, "OnCommentaryNewGame"), + DEFINE_OUTPUT(m_OnCommentaryMidGame, "OnCommentaryMidGame"), + DEFINE_OUTPUT(m_OnCommentaryMultiplayerSpawn, "OnCommentaryMultiplayerSpawn"), +END_DATADESC() + +//------------------------------------------------------------------------------ +// Purpose : Fire my outputs here if I fire on map reload +//------------------------------------------------------------------------------ +void CCommentaryAuto::Spawn(void) +{ + BaseClass::Spawn(); + SetNextThink( gpGlobals->curtime + 0.1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCommentaryAuto::Think(void) +{ + if ( g_CommentarySystem.CommentaryWasEnabledMidGame() ) + { + m_OnCommentaryMidGame.FireOutput(NULL, this); + } + else + { + m_OnCommentaryNewGame.FireOutput(NULL, this); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCommentaryAuto::InputMultiplayerSpawned( inputdata_t &inputdata ) +{ + m_OnCommentaryMultiplayerSpawn.FireOutput( NULL, this ); +} + +#else + +bool IsInCommentaryMode( void ) +{ + return false; +} + +#endif diff --git a/sp/src/game/server/EffectsServer.cpp b/sp/src/game/server/EffectsServer.cpp new file mode 100644 index 00000000..7f0dd0f3 --- /dev/null +++ b/sp/src/game/server/EffectsServer.cpp @@ -0,0 +1,198 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Utility code. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "te.h" +#include "shake.h" +#include "decals.h" +#include "IEffects.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud +extern short g_sModelIndexBloodDrop; // (in combatweapon.cpp) holds the sprite index for the initial blood +extern short g_sModelIndexBloodSpray; // (in combatweapon.cpp) holds the sprite index for splattered blood + + +//----------------------------------------------------------------------------- +// Client-server neutral effects interface +//----------------------------------------------------------------------------- +class CEffectsServer : public IEffects +{ +public: + CEffectsServer(); + virtual ~CEffectsServer(); + + // Members of the IEffect interface + virtual void Beam( const Vector &Start, const Vector &End, int nModelIndex, + int nHaloIndex, unsigned char frameStart, unsigned char frameRate, + float flLife, unsigned char width, unsigned char endWidth, unsigned char fadeLength, + unsigned char noise, unsigned char red, unsigned char green, + unsigned char blue, unsigned char brightness, unsigned char speed); + virtual void Smoke( const Vector &origin, int mModel, float flScale, float flFramerate ); + virtual void Sparks( const Vector &position, int nMagnitude = 1, int nTrailLength = 1, const Vector *pvecDir = NULL ); + virtual void Dust( const Vector &pos, const Vector &dir, float size, float speed ); + virtual void MuzzleFlash( const Vector &origin, const QAngle &angles, float scale, int type ); + virtual void MetalSparks( const Vector &position, const Vector &direction ); + virtual void EnergySplash( const Vector &position, const Vector &direction, bool bExplosive = false ); + virtual void Ricochet( const Vector &position, const Vector &direction ); + + // FIXME: Should these methods remain in this interface? Or go in some + // other client-server neutral interface? + virtual float Time(); + virtual bool IsServer(); + virtual void SuppressEffectsSounds( bool bSuppress ) { Assert(0); } + +private: + //----------------------------------------------------------------------------- + // Purpose: Returning true means don't even call TE func + // Input : filter - + // *suppress_host - + // Output : static bool + //----------------------------------------------------------------------------- + bool SuppressTE( CRecipientFilter& filter ) + { + if ( GetSuppressHost() ) + { + if ( !filter.IgnorePredictionCull() ) + { + filter.RemoveRecipient( (CBasePlayer *)GetSuppressHost() ); + } + + if ( !filter.GetRecipientCount() ) + { + // Suppress it + return true; + } + } + + // There's at least one recipient + return false; + } +}; + + +//----------------------------------------------------------------------------- +// Client-server neutral effects interface accessor +//----------------------------------------------------------------------------- +static CEffectsServer s_EffectServer; +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CEffectsServer, IEffects, IEFFECTS_INTERFACE_VERSION, s_EffectServer); +IEffects *g_pEffects = &s_EffectServer; + + +//----------------------------------------------------------------------------- +// constructor, destructor +//----------------------------------------------------------------------------- +CEffectsServer::CEffectsServer() +{ +} + +CEffectsServer::~CEffectsServer() +{ +} + + +//----------------------------------------------------------------------------- +// Generates a beam +//----------------------------------------------------------------------------- +void CEffectsServer::Beam( const Vector &vecStart, const Vector &vecEnd, int nModelIndex, + int nHaloIndex, unsigned char frameStart, unsigned char frameRate, + float flLife, unsigned char width, unsigned char endWidth, unsigned char fadeLength, + unsigned char noise, unsigned char red, unsigned char green, + unsigned char blue, unsigned char brightness, unsigned char speed) +{ + CBroadcastRecipientFilter filter; + if ( !SuppressTE( filter ) ) + { + te->BeamPoints( filter, 0.0, + &vecStart, &vecEnd, nModelIndex, nHaloIndex, frameStart, frameRate, flLife, + width, endWidth, fadeLength, noise, red, green, blue, brightness, speed ); + } +} + + +//----------------------------------------------------------------------------- +// Generates various tempent effects +//----------------------------------------------------------------------------- +void CEffectsServer::Smoke( const Vector &origin, int mModel, float flScale, float flFramerate ) +{ + CPVSFilter filter( origin ); + if ( !SuppressTE( filter ) ) + { + te->Smoke( filter, 0.0, &origin, mModel, flScale * 0.1f, flFramerate ); + } +} + +void CEffectsServer::Sparks( const Vector &position, int nMagnitude, int nTrailLength, const Vector *pvecDir ) +{ + CPVSFilter filter( position ); + if ( !SuppressTE( filter ) ) + { + te->Sparks( filter, 0.0, &position, nMagnitude, nTrailLength, pvecDir ); + } +} + +void CEffectsServer::Dust( const Vector &pos, const Vector &dir, float size, float speed ) +{ + CPVSFilter filter( pos ); + if ( !SuppressTE( filter ) ) + { + te->Dust( filter, 0.0, pos, dir, size, speed ); + } +} + +void CEffectsServer::MuzzleFlash( const Vector &origin, const QAngle &angles, float scale, int type ) +{ + CPVSFilter filter( origin ); + if ( !SuppressTE( filter ) ) + { + te->MuzzleFlash( filter, 0.0f, origin, angles, scale, type ); + } +} + +void CEffectsServer::MetalSparks( const Vector &position, const Vector &direction ) +{ + CPVSFilter filter( position ); + if ( !SuppressTE( filter ) ) + { + te->MetalSparks( filter, 0.0, &position, &direction ); + } +} + +void CEffectsServer::EnergySplash( const Vector &position, const Vector &direction, bool bExplosive ) +{ + CPVSFilter filter( position ); + if ( !SuppressTE( filter ) ) + { + te->EnergySplash( filter, 0.0, &position, &direction, bExplosive ); + } +} + +void CEffectsServer::Ricochet( const Vector &position, const Vector &direction ) +{ + CPVSFilter filter( position ); + if ( !SuppressTE( filter ) ) + { + te->ArmorRicochet( filter, 0.0, &position, &direction ); + } +} + + +//----------------------------------------------------------------------------- +// FIXME: Should these methods remain in this interface? Or go in some +// other client-server neutral interface? +//----------------------------------------------------------------------------- +float CEffectsServer::Time() +{ + return gpGlobals->curtime; +} + +bool CEffectsServer::IsServer() +{ + return true; +} diff --git a/sp/src/game/server/EntityDissolve.cpp b/sp/src/game/server/EntityDissolve.cpp new file mode 100644 index 00000000..7979e852 --- /dev/null +++ b/sp/src/game/server/EntityDissolve.cpp @@ -0,0 +1,388 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Dissolve entity to be attached to target entity. Serves two purposes: +// +// 1) An entity that can be placed by a level designer and triggered +// to ignite a target entity. +// +// 2) An entity that can be created at runtime to ignite a target entity. +// +//=============================================================================// + +#include "cbase.h" +#include "EntityDissolve.h" +#include "baseanimating.h" +#include "physics_prop_ragdoll.h" +#include "ai_basenpc.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static const char *s_pElectroThinkContext = "ElectroThinkContext"; + + +//----------------------------------------------------------------------------- +// Lifetime +//----------------------------------------------------------------------------- +#define DISSOLVE_FADE_IN_START_TIME 0.0f +#define DISSOLVE_FADE_IN_END_TIME 1.0f +#define DISSOLVE_FADE_OUT_MODEL_START_TIME 1.9f +#define DISSOLVE_FADE_OUT_MODEL_END_TIME 2.0f +#define DISSOLVE_FADE_OUT_START_TIME 2.0f +#define DISSOLVE_FADE_OUT_END_TIME 2.0f + +//----------------------------------------------------------------------------- +// Model +//----------------------------------------------------------------------------- +#define DISSOLVE_SPRITE_NAME "sprites/blueglow1.vmt" + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CEntityDissolve ) + + DEFINE_FIELD( m_flStartTime, FIELD_TIME ), + DEFINE_FIELD( m_flFadeInStart, FIELD_FLOAT ), + DEFINE_FIELD( m_flFadeInLength, FIELD_FLOAT ), + DEFINE_FIELD( m_flFadeOutModelStart, FIELD_FLOAT ), + DEFINE_FIELD( m_flFadeOutModelLength, FIELD_FLOAT ), + DEFINE_FIELD( m_flFadeOutStart, FIELD_FLOAT ), + DEFINE_FIELD( m_flFadeOutLength, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_nDissolveType, FIELD_INTEGER, "dissolvetype" ), + DEFINE_FIELD( m_vDissolverOrigin, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_nMagnitude, FIELD_INTEGER, "magnitude" ), + + DEFINE_FUNCTION( DissolveThink ), + DEFINE_FUNCTION( ElectrocuteThink ), + + DEFINE_INPUTFUNC( FIELD_STRING, "Dissolve", InputDissolve ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Networking +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST( CEntityDissolve, DT_EntityDissolve ) + SendPropTime( SENDINFO( m_flStartTime ) ), + SendPropFloat( SENDINFO( m_flFadeInStart ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_flFadeInLength ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_flFadeOutModelStart ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_flFadeOutModelLength ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_flFadeOutStart ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_flFadeOutLength ), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_nDissolveType ), ENTITY_DISSOLVE_BITS, SPROP_UNSIGNED ), + SendPropVector (SENDINFO(m_vDissolverOrigin), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_nMagnitude ), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_entity_dissolver, CEntityDissolve ); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEntityDissolve::CEntityDissolve( void ) +{ + m_flStartTime = 0.0f; + m_nMagnitude = 250; +} + +CEntityDissolve::~CEntityDissolve( void ) +{ +} + + +//----------------------------------------------------------------------------- +// Precache +//----------------------------------------------------------------------------- +void CEntityDissolve::Precache() +{ + if ( NULL_STRING == GetModelName() ) + { + PrecacheModel( DISSOLVE_SPRITE_NAME ); + } + else + { + PrecacheModel( STRING( GetModelName() ) ); + } +} + + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CEntityDissolve::Spawn() +{ + BaseClass::Spawn(); + Precache(); + UTIL_SetModel( this, STRING( GetModelName() ) ); + + if ( (m_nDissolveType == ENTITY_DISSOLVE_ELECTRICAL) || (m_nDissolveType == ENTITY_DISSOLVE_ELECTRICAL_LIGHT) ) + { + if ( dynamic_cast< CRagdollProp* >( GetMoveParent() ) ) + { + SetContextThink( &CEntityDissolve::ElectrocuteThink, gpGlobals->curtime + 0.01f, s_pElectroThinkContext ); + } + } + + // Setup our times + m_flFadeInStart = DISSOLVE_FADE_IN_START_TIME; + m_flFadeInLength = DISSOLVE_FADE_IN_END_TIME - DISSOLVE_FADE_IN_START_TIME; + + m_flFadeOutModelStart = DISSOLVE_FADE_OUT_MODEL_START_TIME; + m_flFadeOutModelLength = DISSOLVE_FADE_OUT_MODEL_END_TIME - DISSOLVE_FADE_OUT_MODEL_START_TIME; + + m_flFadeOutStart = DISSOLVE_FADE_OUT_START_TIME; + m_flFadeOutLength = DISSOLVE_FADE_OUT_END_TIME - DISSOLVE_FADE_OUT_START_TIME; + + if ( m_nDissolveType == ENTITY_DISSOLVE_CORE ) + { + m_flFadeInStart = 0.0f; + m_flFadeOutStart = CORE_DISSOLVE_FADE_START; + m_flFadeOutModelStart = CORE_DISSOLVE_MODEL_FADE_START; + m_flFadeOutModelLength = CORE_DISSOLVE_MODEL_FADE_LENGTH; + m_flFadeInLength = CORE_DISSOLVE_FADEIN_LENGTH; + } + + m_nRenderMode = kRenderTransColor; + SetRenderColor( 255, 255, 255, 255 ); + m_nRenderFX = kRenderFxNone; + + SetThink( &CEntityDissolve::DissolveThink ); + if ( gpGlobals->curtime > m_flStartTime ) + { + // Necessary for server-side ragdolls + DissolveThink(); + } + else + { + SetNextThink( gpGlobals->curtime + 0.01f ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : inputdata - +//----------------------------------------------------------------------------- +void CEntityDissolve::InputDissolve( inputdata_t &inputdata ) +{ + string_t strTarget = inputdata.value.StringID(); + + if (strTarget == NULL_STRING) + { + strTarget = m_target; + } + + CBaseEntity *pTarget = NULL; + while ((pTarget = gEntList.FindEntityGeneric(pTarget, STRING(strTarget), this, inputdata.pActivator)) != NULL) + { + CBaseAnimating *pBaseAnim = pTarget->GetBaseAnimating(); + if (pBaseAnim) + { + pBaseAnim->Dissolve( NULL, gpGlobals->curtime, false, m_nDissolveType, GetAbsOrigin(), m_nMagnitude ); + } + } +} + + + +//----------------------------------------------------------------------------- +// Purpose: Creates a flame and attaches it to a target entity. +// Input : pTarget - +//----------------------------------------------------------------------------- +CEntityDissolve *CEntityDissolve::Create( CBaseEntity *pTarget, const char *pMaterialName, + float flStartTime, int nDissolveType, bool *pRagdollCreated ) +{ + if ( pRagdollCreated ) + { + *pRagdollCreated = false; + } + + if ( !pMaterialName ) + { + pMaterialName = DISSOLVE_SPRITE_NAME; + } + + if ( pTarget->IsPlayer() ) + { + // Simply immediately kill the player. + CBasePlayer *pPlayer = assert_cast< CBasePlayer* >( pTarget ); + pPlayer->SetArmorValue( 0 ); + CTakeDamageInfo info( pPlayer, pPlayer, pPlayer->GetHealth(), DMG_GENERIC | DMG_REMOVENORAGDOLL | DMG_PREVENT_PHYSICS_FORCE ); + pPlayer->TakeDamage( info ); + return NULL; + } + + CEntityDissolve *pDissolve = (CEntityDissolve *) CreateEntityByName( "env_entity_dissolver" ); + + if ( pDissolve == NULL ) + return NULL; + + pDissolve->m_nDissolveType = nDissolveType; + if ( (nDissolveType == ENTITY_DISSOLVE_ELECTRICAL) || (nDissolveType == ENTITY_DISSOLVE_ELECTRICAL_LIGHT) ) + { + if ( pTarget->IsNPC() && pTarget->MyNPCPointer()->CanBecomeRagdoll() ) + { + CTakeDamageInfo info; + CBaseEntity *pRagdoll = CreateServerRagdoll( pTarget->MyNPCPointer(), 0, info, COLLISION_GROUP_DEBRIS, true ); + pRagdoll->SetCollisionBounds( pTarget->CollisionProp()->OBBMins(), pTarget->CollisionProp()->OBBMaxs() ); + + // Necessary to cause it to do the appropriate death cleanup + if ( pTarget->m_lifeState == LIFE_ALIVE ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + CTakeDamageInfo ragdollInfo( pPlayer, pPlayer, 10000.0, DMG_SHOCK | DMG_REMOVENORAGDOLL | DMG_PREVENT_PHYSICS_FORCE ); + pTarget->TakeDamage( ragdollInfo ); + } + + if ( pRagdollCreated ) + { + *pRagdollCreated = true; + } + + UTIL_Remove( pTarget ); + pTarget = pRagdoll; + } + } + + pDissolve->SetModelName( AllocPooledString(pMaterialName) ); + pDissolve->AttachToEntity( pTarget ); + pDissolve->SetStartTime( flStartTime ); + pDissolve->Spawn(); + + // Send to the client even though we don't have a model + pDissolve->AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + + // Play any appropriate noises when we start to dissolve + if ( (nDissolveType == ENTITY_DISSOLVE_ELECTRICAL) || (nDissolveType == ENTITY_DISSOLVE_ELECTRICAL_LIGHT) ) + { + pTarget->DispatchResponse( "TLK_ELECTROCUTESCREAM" ); + } + else + { + pTarget->DispatchResponse( "TLK_DISSOLVESCREAM" ); + } + return pDissolve; +} + + +//----------------------------------------------------------------------------- +// What type of dissolve? +//----------------------------------------------------------------------------- +CEntityDissolve *CEntityDissolve::Create( CBaseEntity *pTarget, CBaseEntity *pSource ) +{ + // Look for other boogies on the ragdoll + kill them + for ( CBaseEntity *pChild = pSource->FirstMoveChild(); pChild; pChild = pChild->NextMovePeer() ) + { + CEntityDissolve *pDissolve = dynamic_cast(pChild); + if ( !pDissolve ) + continue; + + return Create( pTarget, STRING( pDissolve->GetModelName() ), pDissolve->m_flStartTime, pDissolve->m_nDissolveType ); + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Attaches the flame to an entity and moves with it +// Input : pTarget - target entity to attach to +//----------------------------------------------------------------------------- +void CEntityDissolve::AttachToEntity( CBaseEntity *pTarget ) +{ + // So our dissolver follows the entity around on the server. + SetParent( pTarget ); + SetLocalOrigin( vec3_origin ); + SetLocalAngles( vec3_angle ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : lifetime - +//----------------------------------------------------------------------------- +void CEntityDissolve::SetStartTime( float flStartTime ) +{ + m_flStartTime = flStartTime; +} + +//----------------------------------------------------------------------------- +// Purpose: Burn targets around us +//----------------------------------------------------------------------------- +void CEntityDissolve::DissolveThink( void ) +{ + CBaseAnimating *pTarget = ( GetMoveParent() ) ? GetMoveParent()->GetBaseAnimating() : NULL; + + if ( GetModelName() == NULL_STRING && pTarget == NULL ) + return; + + if ( pTarget == NULL ) + { + UTIL_Remove( this ); + return; + } + + // Turn them into debris + pTarget->SetCollisionGroup( COLLISION_GROUP_DISSOLVING ); + + if ( pTarget && pTarget->GetFlags() & FL_TRANSRAGDOLL ) + { + SetRenderColorA( 0 ); + } + + float dt = gpGlobals->curtime - m_flStartTime; + + if ( dt < m_flFadeInStart ) + { + SetNextThink( m_flStartTime + m_flFadeInStart ); + return; + } + + // If we're done fading, then kill our target entity and us + if ( dt >= m_flFadeOutStart + m_flFadeOutLength ) + { + // Necessary to cause it to do the appropriate death cleanup + // Yeah, the player may have nothing to do with it, but + // passing NULL to TakeDamage causes bad things to happen + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + int iNoPhysicsDamage = g_pGameRules->Damage_GetNoPhysicsForce(); + CTakeDamageInfo info( pPlayer, pPlayer, 10000.0, DMG_GENERIC | DMG_REMOVENORAGDOLL | iNoPhysicsDamage ); + pTarget->TakeDamage( info ); + + if ( pTarget != pPlayer ) + { + UTIL_Remove( pTarget ); + } + + UTIL_Remove( this ); + + return; + } + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Burn targets around us +//----------------------------------------------------------------------------- +void CEntityDissolve::ElectrocuteThink( void ) +{ + CRagdollProp *pRagdoll = dynamic_cast< CRagdollProp* >( GetMoveParent() ); + if ( !pRagdoll ) + return; + + ragdoll_t *pRagdollPhys = pRagdoll->GetRagdoll( ); + for ( int j = 0; j < pRagdollPhys->listCount; ++j ) + { + Vector vecForce; + vecForce = RandomVector( -2400.0f, 2400.0f ); + pRagdollPhys->list[j].pObject->ApplyForceCenter( vecForce ); + } + + SetContextThink( &CEntityDissolve::ElectrocuteThink, gpGlobals->curtime + random->RandomFloat( 0.1, 0.2f ), + s_pElectroThinkContext ); +} diff --git a/sp/src/game/server/EntityDissolve.h b/sp/src/game/server/EntityDissolve.h new file mode 100644 index 00000000..ae9e8195 --- /dev/null +++ b/sp/src/game/server/EntityDissolve.h @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef ENTITYDISSOLVE_H +#define ENTITYDISSOLVE_H + +#ifdef _WIN32 +#pragma once +#endif + +class CEntityDissolve : public CBaseEntity +{ +public: + DECLARE_SERVERCLASS(); + DECLARE_CLASS( CEntityDissolve, CBaseEntity ); + + CEntityDissolve( void ); + ~CEntityDissolve( void ); + + static CEntityDissolve *Create( CBaseEntity *pTarget, const char *pMaterialName, + float flStartTime, int nDissolveType = 0, bool *pRagdollCreated = NULL ); + static CEntityDissolve *Create( CBaseEntity *pTarget, CBaseEntity *pSource ); + + void Precache(); + void Spawn(); + void AttachToEntity( CBaseEntity *pTarget ); + void SetStartTime( float flStartTime ); + void SetDissolverOrigin( Vector vOrigin ) { m_vDissolverOrigin = vOrigin; } + void SetMagnitude( int iMagnitude ){ m_nMagnitude = iMagnitude; } + void SetDissolveType( int iType ) { m_nDissolveType = iType; } + + Vector GetDissolverOrigin( void ) + { + Vector vReturn = m_vDissolverOrigin; + return vReturn; + } + int GetMagnitude( void ) { return m_nMagnitude; } + int GetDissolveType( void ) { return m_nDissolveType; } + + DECLARE_DATADESC(); + + CNetworkVar( float, m_flStartTime ); + CNetworkVar( float, m_flFadeInStart ); + CNetworkVar( float, m_flFadeInLength ); + CNetworkVar( float, m_flFadeOutModelStart ); + CNetworkVar( float, m_flFadeOutModelLength ); + CNetworkVar( float, m_flFadeOutStart ); + CNetworkVar( float, m_flFadeOutLength ); + +protected: + void InputDissolve( inputdata_t &inputdata ); + void DissolveThink( void ); + void ElectrocuteThink( void ); + + CNetworkVar( int, m_nDissolveType ); + CNetworkVector( m_vDissolverOrigin ); + CNetworkVar( int, m_nMagnitude ); +}; + +#endif // ENTITYDISSOLVE_H diff --git a/sp/src/game/server/EntityFlame.cpp b/sp/src/game/server/EntityFlame.cpp new file mode 100644 index 00000000..d3a1be10 --- /dev/null +++ b/sp/src/game/server/EntityFlame.cpp @@ -0,0 +1,335 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Flame entity to be attached to target entity. Serves two purposes: +// +// 1) An entity that can be placed by a level designer and triggered +// to ignite a target entity. +// +// 2) An entity that can be created at runtime to ignite a target entity. +// +//=============================================================================// + +#include "cbase.h" +#include "EntityFlame.h" +#include "ai_basenpc.h" +#include "fire.h" +#include "shareddefs.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +BEGIN_DATADESC( CEntityFlame ) + + DEFINE_KEYFIELD( m_flLifetime, FIELD_FLOAT, "lifetime" ), + + DEFINE_FIELD( m_flSize, FIELD_FLOAT ), + DEFINE_FIELD( m_hEntAttached, FIELD_EHANDLE ), + DEFINE_FIELD( m_bUseHitboxes, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iNumHitboxFires, FIELD_INTEGER ), + DEFINE_FIELD( m_flHitboxFireScale, FIELD_FLOAT ), + // DEFINE_FIELD( m_bPlayingSound, FIELD_BOOLEAN ), + + DEFINE_FUNCTION( FlameThink ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Ignite", InputIgnite ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST( CEntityFlame, DT_EntityFlame ) + SendPropEHandle( SENDINFO( m_hEntAttached ) ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( entityflame, CEntityFlame ); +LINK_ENTITY_TO_CLASS( env_entity_igniter, CEntityFlame ); +PRECACHE_REGISTER(entityflame); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEntityFlame::CEntityFlame( void ) +{ + m_flSize = 0.0f; + m_iNumHitboxFires = 10; + m_flHitboxFireScale = 1.0f; + m_flLifetime = 0.0f; + m_bPlayingSound = false; +} + +void CEntityFlame::UpdateOnRemove() +{ + // Sometimes the entity I'm burning gets destroyed by other means, + // which kills me. Make sure to stop the burning sound. + if ( m_bPlayingSound ) + { + EmitSound( "General.StopBurning" ); + m_bPlayingSound = false; + } + + BaseClass::UpdateOnRemove(); +} + +void CEntityFlame::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "General.StopBurning" ); + PrecacheScriptSound( "General.BurningFlesh" ); + PrecacheScriptSound( "General.BurningObject" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : inputdata - +//----------------------------------------------------------------------------- +void CEntityFlame::InputIgnite( inputdata_t &inputdata ) +{ + if (m_target != NULL_STRING) + { + CBaseEntity *pTarget = NULL; + while ((pTarget = gEntList.FindEntityGeneric(pTarget, STRING(m_target), this, inputdata.pActivator)) != NULL) + { + // Combat characters know how to catch themselves on fire. + CBaseCombatCharacter *pBCC = pTarget->MyCombatCharacterPointer(); + if (pBCC) + { + // DVS TODO: consider promoting Ignite to CBaseEntity and doing everything here + pBCC->Ignite(m_flLifetime); + } + // Everything else, we handle here. + else + { + CEntityFlame *pFlame = CEntityFlame::Create(pTarget); + if (pFlame) + { + pFlame->SetLifetime(m_flLifetime); + } + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Creates a flame and attaches it to a target entity. +// Input : pTarget - +//----------------------------------------------------------------------------- +CEntityFlame *CEntityFlame::Create( CBaseEntity *pTarget, bool useHitboxes ) +{ + CEntityFlame *pFlame = (CEntityFlame *) CreateEntityByName( "entityflame" ); + + if ( pFlame == NULL ) + return NULL; + + float xSize = pTarget->CollisionProp()->OBBMaxs().x - pTarget->CollisionProp()->OBBMins().x; + float ySize = pTarget->CollisionProp()->OBBMaxs().y - pTarget->CollisionProp()->OBBMins().y; + + float size = ( xSize + ySize ) * 0.5f; + + if ( size < 16.0f ) + { + size = 16.0f; + } + + UTIL_SetOrigin( pFlame, pTarget->GetAbsOrigin() ); + + pFlame->m_flSize = size; + pFlame->SetThink( &CEntityFlame::FlameThink ); + pFlame->SetNextThink( gpGlobals->curtime + 0.1f ); + + pFlame->AttachToEntity( pTarget ); + pFlame->SetLifetime( 2.0f ); + + //Send to the client even though we don't have a model + pFlame->AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + + pFlame->SetUseHitboxes( useHitboxes ); + + return pFlame; +} + + +//----------------------------------------------------------------------------- +// Purpose: Attaches the flame to an entity and moves with it +// Input : pTarget - target entity to attach to +//----------------------------------------------------------------------------- +void CEntityFlame::AttachToEntity( CBaseEntity *pTarget ) +{ + // For networking to the client. + m_hEntAttached = pTarget; + + if( pTarget->IsNPC() ) + { + EmitSound( "General.BurningFlesh" ); + } + else + { + EmitSound( "General.BurningObject" ); + } + + m_bPlayingSound = true; + + // So our heat emitter follows the entity around on the server. + SetParent( pTarget ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : lifetime - +//----------------------------------------------------------------------------- +void CEntityFlame::SetLifetime( float lifetime ) +{ + m_flLifetime = gpGlobals->curtime + lifetime; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : use - +//----------------------------------------------------------------------------- +void CEntityFlame::SetUseHitboxes( bool use ) +{ + m_bUseHitboxes = use; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : iNumHitBoxFires - +//----------------------------------------------------------------------------- +void CEntityFlame::SetNumHitboxFires( int iNumHitboxFires ) +{ + m_iNumHitboxFires = iNumHitboxFires; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : flHitboxFireScale - +//----------------------------------------------------------------------------- +void CEntityFlame::SetHitboxFireScale( float flHitboxFireScale ) +{ + m_flHitboxFireScale = flHitboxFireScale; +} + +float CEntityFlame::GetRemainingLife( void ) +{ + return m_flLifetime - gpGlobals->curtime; +} + +int CEntityFlame::GetNumHitboxFires( void ) +{ + return m_iNumHitboxFires; +} + +float CEntityFlame::GetHitboxFireScale( void ) +{ + return m_flHitboxFireScale; +} + +//----------------------------------------------------------------------------- +// Purpose: Burn targets around us +//----------------------------------------------------------------------------- +void CEntityFlame::FlameThink( void ) +{ + // Assure that this function will be ticked again even if we early-out in the if below. + SetNextThink( gpGlobals->curtime + FLAME_DAMAGE_INTERVAL ); + + if ( m_hEntAttached ) + { + if ( m_hEntAttached->GetFlags() & FL_TRANSRAGDOLL ) + { + SetRenderColorA( 0 ); + return; + } + + CAI_BaseNPC *pNPC = m_hEntAttached->MyNPCPointer(); + if ( pNPC && !pNPC->IsAlive() ) + { + UTIL_Remove( this ); + // Notify the NPC that it's no longer burning! + pNPC->Extinguish(); + return; + } + + if( m_hEntAttached->GetWaterLevel() > 0 ) + { + Vector mins, maxs; + + mins = m_hEntAttached->WorldSpaceCenter(); + maxs = mins; + + maxs.z = m_hEntAttached->WorldSpaceCenter().z; + maxs.x += 32; + maxs.y += 32; + + mins.z -= 32; + mins.x -= 32; + mins.y -= 32; + + UTIL_Bubbles( mins, maxs, 12 ); + } + } + else + { + UTIL_Remove( this ); + return; + } + + // See if we're done burning, or our attached ent has vanished + if ( m_flLifetime < gpGlobals->curtime || m_hEntAttached == NULL ) + { + EmitSound( "General.StopBurning" ); + m_bPlayingSound = false; + SetThink( &CEntityFlame::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.5f ); + + // Notify anything we're attached to + if ( m_hEntAttached ) + { + CBaseCombatCharacter *pAttachedCC = m_hEntAttached->MyCombatCharacterPointer(); + + if( pAttachedCC ) + { + // Notify the NPC that it's no longer burning! + pAttachedCC->Extinguish(); + } + } + + return; + } + + if ( m_hEntAttached ) + { + // Do radius damage ignoring the entity I'm attached to. This will harm things around me. + RadiusDamage( CTakeDamageInfo( this, this, 4.0f, DMG_BURN ), GetAbsOrigin(), m_flSize/2, CLASS_NONE, m_hEntAttached ); + + // Directly harm the entity I'm attached to. This is so we can precisely control how much damage the entity + // that is on fire takes without worrying about the flame's position relative to the bodytarget (which is the + // distance that the radius damage code uses to determine how much damage to inflict) + m_hEntAttached->TakeDamage( CTakeDamageInfo( this, this, FLAME_DIRECT_DAMAGE, DMG_BURN | DMG_DIRECT ) ); + + if( !m_hEntAttached->IsNPC() && hl2_episodic.GetBool() ) + { + const float ENTITYFLAME_MOVE_AWAY_DIST = 24.0f; + // Make a sound near my origin, and up a little higher (in case I'm on the ground, so NPC's still hear it) + CSoundEnt::InsertSound( SOUND_MOVE_AWAY, GetAbsOrigin(), ENTITYFLAME_MOVE_AWAY_DIST, 0.1f, this, SOUNDENT_CHANNEL_REPEATED_DANGER ); + CSoundEnt::InsertSound( SOUND_MOVE_AWAY, GetAbsOrigin() + Vector( 0, 0, 48.0f ), ENTITYFLAME_MOVE_AWAY_DIST, 0.1f, this, SOUNDENT_CHANNEL_REPEATING ); + } + } + else + { + RadiusDamage( CTakeDamageInfo( this, this, FLAME_RADIUS_DAMAGE, DMG_BURN ), GetAbsOrigin(), m_flSize/2, CLASS_NONE, NULL ); + } + + FireSystem_AddHeatInRadius( GetAbsOrigin(), m_flSize/2, 2.0f ); + +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pEnt - +//----------------------------------------------------------------------------- +void CreateEntityFlame(CBaseEntity *pEnt) +{ + CEntityFlame::Create( pEnt ); +} diff --git a/sp/src/game/server/EntityFlame.h b/sp/src/game/server/EntityFlame.h new file mode 100644 index 00000000..9ea13601 --- /dev/null +++ b/sp/src/game/server/EntityFlame.h @@ -0,0 +1,66 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef ENTITYFLAME_H +#define ENTITYFLAME_H +#ifdef _WIN32 +#pragma once +#endif + +#define FLAME_DAMAGE_INTERVAL 0.2f // How often to deal damage. +#define FLAME_DIRECT_DAMAGE_PER_SEC 5.0f +#define FLAME_RADIUS_DAMAGE_PER_SEC 4.0f + +#define FLAME_DIRECT_DAMAGE ( FLAME_DIRECT_DAMAGE_PER_SEC * FLAME_DAMAGE_INTERVAL ) +#define FLAME_RADIUS_DAMAGE ( FLAME_RADIUS_DAMAGE_PER_SEC * FLAME_DAMAGE_INTERVAL ) + +#define FLAME_MAX_LIFETIME_ON_DEAD_NPCS 10.0f + +class CEntityFlame : public CBaseEntity +{ +public: + DECLARE_SERVERCLASS(); + DECLARE_CLASS( CEntityFlame, CBaseEntity ); + + CEntityFlame( void ); + + static CEntityFlame *Create( CBaseEntity *pTarget, bool useHitboxes = true ); + + void AttachToEntity( CBaseEntity *pTarget ); + void SetLifetime( float lifetime ); + void SetUseHitboxes( bool use ); + void SetNumHitboxFires( int iNumHitBoxFires ); + void SetHitboxFireScale( float flHitboxFireScale ); + + float GetRemainingLife( void ); + int GetNumHitboxFires( void ); + float GetHitboxFireScale( void ); + + virtual void Precache(); + virtual void UpdateOnRemove(); + + void SetSize( float size ) { m_flSize = size; } + + DECLARE_DATADESC(); + +protected: + + void InputIgnite( inputdata_t &inputdata ); + + void FlameThink( void ); + + CNetworkHandle( CBaseEntity, m_hEntAttached ); // The entity that we are burning (attached to). + + CNetworkVar( float, m_flSize ); + CNetworkVar( bool, m_bUseHitboxes ); + CNetworkVar( int, m_iNumHitboxFires ); + CNetworkVar( float, m_flHitboxFireScale ); + + CNetworkVar( float, m_flLifetime ); + bool m_bPlayingSound; +}; + +#endif // ENTITYFLAME_H diff --git a/sp/src/game/server/EntityParticleTrail.cpp b/sp/src/game/server/EntityParticleTrail.cpp new file mode 100644 index 00000000..544dc5d0 --- /dev/null +++ b/sp/src/game/server/EntityParticleTrail.cpp @@ -0,0 +1,207 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Drops particles where the entity was. +// +//=============================================================================// + +#include "cbase.h" +#include "EntityParticleTrail.h" +#include "networkstringtable_gamedll.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Used to retire the entity +//----------------------------------------------------------------------------- +static const char *s_pRetireContext = "RetireContext"; + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CEntityParticleTrail ) + + DEFINE_FIELD( m_iMaterialName, FIELD_MATERIALINDEX ), + DEFINE_EMBEDDED( m_Info ), + DEFINE_FIELD( m_hConstraintEntity, FIELD_EHANDLE ), + + // Think this should be handled by StartTouch/etc. +// DEFINE_FIELD( m_nRefCount, FIELD_INTEGER ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Networking +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST( CEntityParticleTrail, DT_EntityParticleTrail ) + SendPropInt(SENDINFO(m_iMaterialName), MAX_MATERIAL_STRING_BITS, SPROP_UNSIGNED ), + SendPropDataTable( SENDINFO_DT( m_Info ), &REFERENCE_SEND_TABLE( DT_EntityParticleTrailInfo ) ), + SendPropEHandle(SENDINFO(m_hConstraintEntity)), +END_SEND_TABLE() + + +LINK_ENTITY_TO_CLASS( env_particle_trail, CEntityParticleTrail ); + + +//----------------------------------------------------------------------------- +// Purpose: Creates a flame and attaches it to a target entity. +// Input : pTarget - +//----------------------------------------------------------------------------- +CEntityParticleTrail *CEntityParticleTrail::Create( CBaseEntity *pTarget, const EntityParticleTrailInfo_t &info, CBaseEntity *pConstraintEntity ) +{ + int iMaterialName = GetMaterialIndex( STRING(info.m_strMaterialName) ); + + // Look for other particle trails on the entity + copy state to the new entity + CEntityParticleTrail *pTrail; + CBaseEntity *pNext; + for ( CBaseEntity *pChild = pTarget->FirstMoveChild(); pChild; pChild = pNext ) + { + pNext = pChild->NextMovePeer(); + pTrail = dynamic_cast(pChild); + if ( pTrail && (pTrail->m_iMaterialName == iMaterialName) ) + { + // Prevent destruction if it re-enters the field + pTrail->IncrementRefCount(); + return pTrail; + } + } + + pTrail = (CEntityParticleTrail *)CreateEntityByName( "env_particle_trail" ); + if ( pTrail == NULL ) + return NULL; + + pTrail->m_hConstraintEntity = pConstraintEntity; + pTrail->m_iMaterialName = iMaterialName; + pTrail->m_Info.CopyFrom(info); + pTrail->m_nRefCount = 1; + pTrail->AttachToEntity( pTarget ); + pTrail->Spawn(); + return pTrail; +} + + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CEntityParticleTrail::Spawn() +{ + BaseClass::Spawn(); + + /* + SetThink( &CEntityParticleTrail::BoogieThink ); + SetNextThink( gpGlobals->curtime + 0.01f ); + + if ( HasSpawnFlags( SF_RAGDOLL_BOOGIE_ELECTRICAL ) ) + { + SetContextThink( ZapThink, gpGlobals->curtime + random->RandomFloat( 0.1f, 0.3f ), s_pZapContext ); + } + */ +} + + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CEntityParticleTrail::UpdateOnRemove() +{ + g_pNotify->ClearEntity( this ); + + BaseClass::UpdateOnRemove(); +} + + +//----------------------------------------------------------------------------- +// Force our constraint entity to be trasmitted +//----------------------------------------------------------------------------- +void CEntityParticleTrail::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + // Are we already marked for transmission? + if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) + return; + + BaseClass::SetTransmit( pInfo, bAlways ); + + // Force our constraint entity to be sent too. + if ( m_hConstraintEntity ) + { + m_hConstraintEntity->SetTransmit( pInfo, bAlways ); + } +} + + +//----------------------------------------------------------------------------- +// Retire +//----------------------------------------------------------------------------- +void CEntityParticleTrail::IncrementRefCount() +{ + if ( m_nRefCount == 0 ) + { + SetContextThink( NULL, gpGlobals->curtime, s_pRetireContext ); + } + ++m_nRefCount; +} + +void CEntityParticleTrail::DecrementRefCount() +{ + --m_nRefCount; + Assert( m_nRefCount >= 0 ); + if ( m_nRefCount == 0 ) + { + FollowEntity( NULL ); + g_pNotify->ClearEntity( this ); + SetContextThink( &CEntityParticleTrail::SUB_Remove, gpGlobals->curtime + m_Info.m_flLifetime, s_pRetireContext ); + } +} + + +//----------------------------------------------------------------------------- +// Clean up when the entity goes away. +//----------------------------------------------------------------------------- +void CEntityParticleTrail::NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ) +{ + BaseClass::NotifySystemEvent( pNotify, eventType, params ); + Assert( pNotify == GetMoveParent() ); + if ( eventType == NOTIFY_EVENT_DESTROY ) + { + FollowEntity( NULL ); + g_pNotify->ClearEntity( this ); + if ( m_nRefCount != 0 ) + { + m_nRefCount = 0; + SetContextThink( &CEntityParticleTrail::SUB_Remove, gpGlobals->curtime + m_Info.m_flLifetime, s_pRetireContext ); + } + } +} + + +//----------------------------------------------------------------------------- +// Suppression count +//----------------------------------------------------------------------------- +void CEntityParticleTrail::Destroy( CBaseEntity *pTarget, const EntityParticleTrailInfo_t &info ) +{ + int iMaterialName = GetMaterialIndex( STRING(info.m_strMaterialName) ); + + // Look for the particle trail attached to this entity + decrease refcount + CBaseEntity *pNext; + for ( CBaseEntity *pChild = pTarget->FirstMoveChild(); pChild; pChild = pNext ) + { + pNext = pChild->NextMovePeer(); + CEntityParticleTrail *pTrail = dynamic_cast(pChild); + if ( !pTrail || (pTrail->m_iMaterialName != iMaterialName) ) + continue; + + pTrail->DecrementRefCount(); + } +} + + +//----------------------------------------------------------------------------- +// Attach to an entity +//----------------------------------------------------------------------------- +void CEntityParticleTrail::AttachToEntity( CBaseEntity *pTarget ) +{ + FollowEntity( pTarget ); + g_pNotify->AddEntity( this, pTarget ); +} diff --git a/sp/src/game/server/EntityParticleTrail.h b/sp/src/game/server/EntityParticleTrail.h new file mode 100644 index 00000000..d22e0d4b --- /dev/null +++ b/sp/src/game/server/EntityParticleTrail.h @@ -0,0 +1,52 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef ENTITYPARTICLETRAIL_H +#define ENTITYPARTICLETRAIL_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "baseparticleentity.h" +#include "entityparticletrail_shared.h" + + +//----------------------------------------------------------------------------- +// Spawns particles after the entity +//----------------------------------------------------------------------------- +class CEntityParticleTrail : public CBaseParticleEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CEntityParticleTrail, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + +public: + static CEntityParticleTrail *Create( CBaseEntity *pTarget, const EntityParticleTrailInfo_t &info, CBaseEntity *pConstraint ); + static void Destroy( CBaseEntity *pTarget, const EntityParticleTrailInfo_t &info ); + + void Spawn(); + virtual void UpdateOnRemove(); + + // Force our constraint entity to be trasmitted + virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + + // Clean up when the entity goes away. + virtual void NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ); + +private: + void AttachToEntity( CBaseEntity *pTarget ); + void IncrementRefCount(); + void DecrementRefCount(); + + CNetworkVar( int, m_iMaterialName ); + CNetworkVarEmbedded( EntityParticleTrailInfo_t, m_Info ); + CNetworkHandle( CBaseEntity, m_hConstraintEntity ); + + int m_nRefCount; +}; + +#endif // ENTITYPARTICLETRAIL_H diff --git a/sp/src/game/server/EnvBeam.cpp b/sp/src/game/server/EnvBeam.cpp new file mode 100644 index 00000000..c74df00d --- /dev/null +++ b/sp/src/game/server/EnvBeam.cpp @@ -0,0 +1,826 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements visual effects entities: sprites, beams, bubbles, etc. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "beam_shared.h" +#include "ndebugoverlay.h" +#include "filters.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Keeps us from doing strcmps in the tracefilter. +string_t g_iszPhysicsPropClassname; + +enum Touch_t +{ + touch_none = 0, + touch_player_only, + touch_npc_only, + touch_player_or_npc, + touch_player_or_npc_or_physicsprop, +}; + +class CEnvBeam : public CBeam +{ +public: + DECLARE_CLASS( CEnvBeam, CBeam ); + + void Spawn( void ); + void Precache( void ); + void Activate( void ); + + void StrikeThink( void ); + void UpdateThink( void ); + void RandomArea( void ); + void RandomPoint( const Vector &vecSrc ); + void Zap( const Vector &vecSrc, const Vector &vecDest ); + + void Strike( void ); + + bool PassesTouchFilters(CBaseEntity *pOther); + + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputStrikeOnce( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputAmplitude( inputdata_t &inputdata ); + void InputSetStartEntity( inputdata_t &inputdata ) { m_iszStartEntity = inputdata.value.StringID(); BeamUpdateVars(); } + void InputSetEndEntity( inputdata_t &inputdata ) { m_iszEndEntity = inputdata.value.StringID(); BeamUpdateVars(); } +#endif + + void TurnOn( void ); + void TurnOff( void ); + void Toggle( void ); + + const char *GetDecalName( void ){ return STRING( m_iszDecal );} + + inline bool ServerSide( void ) + { + if ( m_life == 0 && !HasSpawnFlags(SF_BEAM_RING) ) + return true; + + return false; + } + + DECLARE_DATADESC(); + + void BeamUpdateVars( void ); + + int m_active; + int m_spriteTexture; + + string_t m_iszStartEntity; + string_t m_iszEndEntity; + float m_life; + float m_boltWidth; + float m_noiseAmplitude; + int m_speed; + float m_restrike; + string_t m_iszSpriteName; + int m_frameStart; + + float m_radius; + + Touch_t m_TouchType; + string_t m_iFilterName; + EHANDLE m_hFilter; + + string_t m_iszDecal; + + COutputEvent m_OnTouchedByEntity; +}; + +LINK_ENTITY_TO_CLASS( env_beam, CEnvBeam ); + +BEGIN_DATADESC( CEnvBeam ) + + DEFINE_FIELD( m_active, FIELD_INTEGER ), + DEFINE_FIELD( m_spriteTexture, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_iszStartEntity, FIELD_STRING, "LightningStart" ), + DEFINE_KEYFIELD( m_iszEndEntity, FIELD_STRING, "LightningEnd" ), + DEFINE_KEYFIELD( m_life, FIELD_FLOAT, "life" ), + DEFINE_KEYFIELD( m_boltWidth, FIELD_FLOAT, "BoltWidth" ), + DEFINE_KEYFIELD( m_noiseAmplitude, FIELD_FLOAT, "NoiseAmplitude" ), + DEFINE_KEYFIELD( m_speed, FIELD_INTEGER, "TextureScroll" ), + DEFINE_KEYFIELD( m_restrike, FIELD_FLOAT, "StrikeTime" ), + DEFINE_KEYFIELD( m_iszSpriteName, FIELD_STRING, "texture" ), + DEFINE_KEYFIELD( m_frameStart, FIELD_INTEGER, "framestart" ), + DEFINE_KEYFIELD( m_radius, FIELD_FLOAT, "Radius" ), + DEFINE_KEYFIELD( m_TouchType, FIELD_INTEGER, "TouchType" ), + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + DEFINE_KEYFIELD( m_iszDecal, FIELD_STRING, "decalname" ), + + DEFINE_FIELD( m_hFilter, FIELD_EHANDLE ), + + // Function Pointers + DEFINE_FUNCTION( StrikeThink ), + DEFINE_FUNCTION( UpdateThink ), + + // Input functions + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "StrikeOnce", InputStrikeOnce ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_FLOAT, "Amplitude", InputAmplitude ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetStartEntity", InputSetStartEntity ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetEndEntity", InputSetEndEntity ), +#endif + + DEFINE_OUTPUT( m_OnTouchedByEntity, "OnTouchedByEntity" ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvBeam::Spawn( void ) +{ + if ( !m_iszSpriteName ) + { + SetThink( &CEnvBeam::SUB_Remove ); + return; + } + + BaseClass::Spawn(); + + m_noiseAmplitude = MIN(MAX_BEAM_NOISEAMPLITUDE, m_noiseAmplitude); + + // Check for tapering + if ( HasSpawnFlags( SF_BEAM_TAPEROUT ) ) + { + SetWidth( m_boltWidth ); + SetEndWidth( 0 ); + } + else + { + SetWidth( m_boltWidth ); + SetEndWidth( GetWidth() ); // Note: EndWidth is not scaled + } + + if ( ServerSide() ) + { + SetThink( &CEnvBeam::UpdateThink ); + SetNextThink( gpGlobals->curtime ); + SetFireTime( gpGlobals->curtime ); + + if ( GetEntityName() != NULL_STRING ) + { + if ( !(m_spawnflags & SF_BEAM_STARTON) ) + { + AddEffects( EF_NODRAW ); + m_active = 0; + SetNextThink( TICK_NEVER_THINK ); + } + else + { + m_active = 1; + } + } + } + else + { + m_active = 0; + if ( !GetEntityName() || FBitSet(m_spawnflags, SF_BEAM_STARTON) ) + { + SetThink( &CEnvBeam::StrikeThink ); + SetNextThink( gpGlobals->curtime + 1.0f ); + } + } + +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvBeam::Precache( void ) +{ + if ( !Q_stristr( STRING(m_iszSpriteName), ".vmt" ) ) + { + // HACK/YWB: This was almost always the laserbeam.spr, so alloc'ing the name a second time with the proper extension isn't going to + // kill us on memrory. + //Warning( "Level Design Error: %s (%i:%s) Sprite name (%s) missing .vmt extension!\n", + // STRING( m_iClassname ), entindex(), GetEntityName(), STRING(m_iszSpriteName) ); + + char fixedname[ 512 ]; + Q_strncpy( fixedname, STRING( m_iszSpriteName ), sizeof( fixedname ) ); + + Q_SetExtension( fixedname, ".vmt", sizeof( fixedname ) ); + + m_iszSpriteName = AllocPooledString( fixedname ); + } + + g_iszPhysicsPropClassname = AllocPooledString( "prop_physics" ); + + m_spriteTexture = PrecacheModel( STRING(m_iszSpriteName) ); + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvBeam::Activate( void ) +{ + // Get a handle to my filter entity if there is one + if (m_iFilterName != NULL_STRING) + { + m_hFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iFilterName )); + } + + BaseClass::Activate(); + + if ( ServerSide() ) + BeamUpdateVars(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to turn the lightning on either continually or for +// interval refiring. +//----------------------------------------------------------------------------- +void CEnvBeam::InputTurnOn( inputdata_t &inputdata ) +{ + if ( !m_active ) + { + TurnOn(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to turn the lightning off. +//----------------------------------------------------------------------------- +void CEnvBeam::InputTurnOff( inputdata_t &inputdata ) +{ + if ( m_active ) + { + TurnOff(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to toggle the lightning on/off. +//----------------------------------------------------------------------------- +void CEnvBeam::InputToggle( inputdata_t &inputdata ) +{ + if ( m_active ) + { + TurnOff(); + } + else + { + TurnOn(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for making the beam strike once. This will not affect +// any interval refiring that might be going on. If the lifetime is set +// to zero (infinite) it will turn on and stay on. +//----------------------------------------------------------------------------- +void CEnvBeam::InputStrikeOnce( inputdata_t &inputdata ) +{ + Strike(); +} + + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler for amplitude +//----------------------------------------------------------------------------- +void CEnvBeam::InputAmplitude( inputdata_t &inputdata ) +{ + m_noiseAmplitude = inputdata.value.Float(); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Turns the lightning on. If it is set for interval refiring, it will +// begin doing so. If it is set to be continually on, it will do so. +//----------------------------------------------------------------------------- +void CEnvBeam::TurnOn( void ) +{ + m_active = 1; + + if ( ServerSide() ) + { + RemoveEffects( EF_NODRAW ); + DoSparks( GetAbsStartPos(), GetAbsEndPos() ); + + SetThink( &CEnvBeam::UpdateThink ); + SetNextThink( gpGlobals->curtime ); + SetFireTime( gpGlobals->curtime ); + } + else + { + SetThink( &CEnvBeam::StrikeThink ); + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvBeam::TurnOff( void ) +{ + m_active = 0; + + if ( ServerSide() ) + { + AddEffects( EF_NODRAW ); + } + + SetNextThink( TICK_NEVER_THINK ); + SetThink( NULL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function for striking at intervals. +//----------------------------------------------------------------------------- +void CEnvBeam::StrikeThink( void ) +{ + if ( m_life != 0 ) + { + if ( m_spawnflags & SF_BEAM_RANDOM ) + SetNextThink( gpGlobals->curtime + m_life + random->RandomFloat( 0, m_restrike ) ); + else + SetNextThink( gpGlobals->curtime + m_life + m_restrike ); + } + m_active = 1; + + if (!m_iszEndEntity) + { + if (!m_iszStartEntity) + { + RandomArea( ); + } + else + { + CBaseEntity *pStart = RandomTargetname( STRING(m_iszStartEntity) ); + if (pStart != NULL) + { + RandomPoint( pStart->GetAbsOrigin() ); + } + else + { + Msg( "env_beam: unknown entity \"%s\"\n", STRING(m_iszStartEntity) ); + } + } + return; + } + + Strike(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Strikes once for its configured lifetime. +//----------------------------------------------------------------------------- +void CEnvBeam::Strike( void ) +{ + CBroadcastRecipientFilter filter; + + CBaseEntity *pStart = RandomTargetname( STRING(m_iszStartEntity) ); + CBaseEntity *pEnd = RandomTargetname( STRING(m_iszEndEntity) ); + + if ( pStart == NULL || pEnd == NULL ) + return; + + m_speed = clamp( (int) m_speed, 0, (int) MAX_BEAM_SCROLLSPEED ); + +#ifdef MAPBASE + bool pointStart = IsStaticPointEntity( pStart ); + bool pointEnd = IsStaticPointEntity( pEnd ); +#else + int pointStart = IsStaticPointEntity( pStart ); + int pointEnd = IsStaticPointEntity( pEnd ); +#endif + + if ( pointStart || pointEnd ) + { +#ifdef MAPBASE + if ( m_spawnflags & SF_BEAM_RING ) + { + te->BeamRing( filter, 0.0, + pStart->entindex(), + pEnd->entindex(), + m_spriteTexture, + 0, // No halo + m_frameStart, + (int)m_flFrameRate, + m_life, + m_boltWidth, + 0, // No spread + m_noiseAmplitude, + m_clrRender->r, m_clrRender->g, m_clrRender->b, m_clrRender->a, + m_speed ); + } + else + { + te->BeamEntPoint( filter, 0.0, + pStart->entindex(), + &pStart->GetAbsOrigin(), + pEnd->entindex(), + &pEnd->GetAbsOrigin(), + m_spriteTexture, + 0, // No halo + m_frameStart, + (int)m_flFrameRate, + m_life, + m_boltWidth, + m_boltWidth, // End width + 0, // No fade + m_noiseAmplitude, + m_clrRender->r, m_clrRender->g, m_clrRender->b, m_clrRender->a, + m_speed ); + } +#else + if ( m_spawnflags & SF_BEAM_RING ) + { + // don't work + return; + } + + te->BeamEntPoint( filter, 0.0, + pointStart ? 0 : pStart->entindex(), + pointStart ? &pStart->GetAbsOrigin() : NULL, + pointEnd ? 0 : pEnd->entindex(), + pointEnd ? &pEnd->GetAbsOrigin() : NULL, + m_spriteTexture, + 0, // No halo + m_frameStart, + (int)m_flFrameRate, + m_life, + m_boltWidth, + m_boltWidth, // End width + 0, // No fade + m_noiseAmplitude, + m_clrRender->r, m_clrRender->g, m_clrRender->b, m_clrRender->a, + m_speed ); +#endif + } + else + { + if ( m_spawnflags & SF_BEAM_RING) + { + te->BeamRing( filter, 0.0, + pStart->entindex(), + pEnd->entindex(), + m_spriteTexture, + 0, // No halo + m_frameStart, + (int)m_flFrameRate, + m_life, + m_boltWidth, + 0, // No spread + m_noiseAmplitude, + m_clrRender->r, + m_clrRender->g, + m_clrRender->b, + m_clrRender->a, + m_speed ); + } + else + { + te->BeamEnts( filter, 0.0, + pStart->entindex(), + pEnd->entindex(), + m_spriteTexture, + 0, // No halo + m_frameStart, + (int)m_flFrameRate, + m_life, + m_boltWidth, + m_boltWidth, // End width + 0, // No fade + m_noiseAmplitude, + m_clrRender->r, + m_clrRender->g, + m_clrRender->b, + m_clrRender->a, + m_speed ); + + } + } + + DoSparks( pStart->GetAbsOrigin(), pEnd->GetAbsOrigin() ); + if ( m_flDamage > 0 ) + { + trace_t tr; + UTIL_TraceLine( pStart->GetAbsOrigin(), pEnd->GetAbsOrigin(), MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr ); + BeamDamageInstant( &tr, m_flDamage ); + } + +} + + +class CTraceFilterPlayersNPCs : public ITraceFilter +{ +public: + bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity ); + if ( pEntity ) + { + if ( pEntity->IsPlayer() || pEntity->MyNPCPointer() ) + return true; + } + + return false; + } + virtual TraceType_t GetTraceType() const + { + return TRACE_ENTITIES_ONLY; + } +}; + +class CTraceFilterPlayersNPCsPhysicsProps : public ITraceFilter +{ +public: + bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity ); + if ( pEntity ) + { + if ( pEntity->IsPlayer() || pEntity->MyNPCPointer() || pEntity->m_iClassname == g_iszPhysicsPropClassname ) + return true; + } + + return false; + } + virtual TraceType_t GetTraceType() const + { + return TRACE_ENTITIES_ONLY; + } +}; + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CEnvBeam::PassesTouchFilters(CBaseEntity *pOther) +{ + bool fPassedSoFar = false; + + // Touched some player or NPC! + if( m_TouchType != touch_npc_only ) + { + if( pOther->IsPlayer() ) + { + fPassedSoFar = true; + } + } + + if( m_TouchType != touch_player_only ) + { + if( pOther->IsNPC() ) + { + fPassedSoFar = true; + } + } + + if( m_TouchType == touch_player_or_npc_or_physicsprop ) + { + if( pOther->m_iClassname == g_iszPhysicsPropClassname ) + { + fPassedSoFar = true; + } + } + + if( fPassedSoFar ) + { + CBaseFilter* pFilter = (CBaseFilter*)(m_hFilter.Get()); + return (!pFilter) ? true : pFilter->PassesFilter( this, pOther ); + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvBeam::UpdateThink( void ) +{ + // Apply damage every 1/10th of a second. + if ( ( m_flDamage > 0 ) && ( gpGlobals->curtime >= m_flFireTime + 0.1 ) ) + { + trace_t tr; + UTIL_TraceLine( GetAbsStartPos(), GetAbsEndPos(), MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr ); + BeamDamage( &tr ); + // BeamDamage calls RelinkBeam, so no need to call it again. + } + else + { + RelinkBeam(); + } + + if( m_TouchType != touch_none ) + { + trace_t tr; + Ray_t ray; + ray.Init( GetAbsStartPos(), GetAbsEndPos() ); + + if( m_TouchType == touch_player_or_npc_or_physicsprop ) + { + CTraceFilterPlayersNPCsPhysicsProps traceFilter; + enginetrace->TraceRay( ray, MASK_SHOT, &traceFilter, &tr ); + } + else + { + CTraceFilterPlayersNPCs traceFilter; + enginetrace->TraceRay( ray, MASK_SHOT, &traceFilter, &tr ); + } + + if( tr.fraction != 1.0 && PassesTouchFilters( tr.m_pEnt ) ) + { + m_OnTouchedByEntity.FireOutput( tr.m_pEnt, this, 0 ); + return; + } + } + + SetNextThink( gpGlobals->curtime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &vecSrc - +// &vecDest - +//----------------------------------------------------------------------------- +void CEnvBeam::Zap( const Vector &vecSrc, const Vector &vecDest ) +{ + CBroadcastRecipientFilter filter; + + te->BeamPoints( filter, 0.0, + &vecSrc, + &vecDest, + m_spriteTexture, + 0, // No halo + m_frameStart, + (int)m_flFrameRate, + m_life, + m_boltWidth, + m_boltWidth, // End width + 0, // No fade + m_noiseAmplitude, + m_clrRender->r, + m_clrRender->g, + m_clrRender->b, + m_clrRender->a, + m_speed ); + + DoSparks( vecSrc, vecDest ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvBeam::RandomArea( void ) +{ + int iLoops = 0; + + for (iLoops = 0; iLoops < 10; iLoops++) + { + Vector vecSrc = GetAbsOrigin(); + + Vector vecDir1 = Vector( random->RandomFloat( -1.0, 1.0 ), random->RandomFloat( -1.0, 1.0 ),random->RandomFloat( -1.0, 1.0 ) ); + VectorNormalize( vecDir1 ); + trace_t tr1; + UTIL_TraceLine( vecSrc, vecSrc + vecDir1 * m_radius, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr1 ); + + if (tr1.fraction == 1.0) + continue; + + Vector vecDir2; + do { + vecDir2 = Vector( random->RandomFloat( -1.0, 1.0 ), random->RandomFloat( -1.0, 1.0 ),random->RandomFloat( -1.0, 1.0 ) ); + } while (DotProduct(vecDir1, vecDir2 ) > 0); + VectorNormalize( vecDir2 ); + trace_t tr2; + UTIL_TraceLine( vecSrc, vecSrc + vecDir2 * m_radius, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr2 ); + + if (tr2.fraction == 1.0) + continue; + + if ((tr1.endpos - tr2.endpos).Length() < m_radius * 0.1) + continue; + + UTIL_TraceLine( tr1.endpos, tr2.endpos, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr2 ); + + if (tr2.fraction != 1.0) + continue; + + Zap( tr1.endpos, tr2.endpos ); + + break; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : vecSrc - +//----------------------------------------------------------------------------- +void CEnvBeam::RandomPoint( const Vector &vecSrc ) +{ + int iLoops = 0; + + for (iLoops = 0; iLoops < 10; iLoops++) + { + Vector vecDir1 = Vector( random->RandomFloat( -1.0, 1.0 ), random->RandomFloat( -1.0, 1.0 ),random->RandomFloat( -1.0, 1.0 ) ); + VectorNormalize( vecDir1 ); + trace_t tr1; + UTIL_TraceLine( vecSrc, vecSrc + vecDir1 * m_radius, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr1 ); + + if ((tr1.endpos - vecSrc).Length() < m_radius * 0.1) + continue; + + if (tr1.fraction == 1.0) + continue; + + Zap( vecSrc, tr1.endpos ); + break; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvBeam::BeamUpdateVars( void ) +{ + CBaseEntity *pStart = gEntList.FindEntityByName( NULL, m_iszStartEntity ); + CBaseEntity *pEnd = gEntList.FindEntityByName( NULL, m_iszEndEntity ); + + if (( pStart == NULL ) || ( pEnd == NULL )) + { + return; + } + + m_nNumBeamEnts = 2; + + m_speed = clamp( (int) m_speed, 0, (int) MAX_BEAM_SCROLLSPEED ); + + // NOTE: If the end entity is the beam itself (and the start entity + // isn't *also* the beam itself, we've got problems. This is a problem + // because SetAbsStartPos actually sets the entity's origin. + if ( ( pEnd == this ) && ( pStart != this ) ) + { + DevMsg("env_beams cannot have the end entity be the beam itself\n" + "unless the start entity is also the beam itself!\n" ); + Assert(0); + } + + SetModelName( m_iszSpriteName ); + SetTexture( m_spriteTexture ); + + SetType( BEAM_ENTPOINT ); + + if ( IsStaticPointEntity( pStart ) ) + { + SetAbsStartPos( pStart->GetAbsOrigin() ); + } + else + { + SetStartEntity( pStart ); + } + + if ( IsStaticPointEntity( pEnd ) ) + { + SetAbsEndPos( pEnd->GetAbsOrigin() ); + } + else + { + SetEndEntity( pEnd ); + } + + RelinkBeam(); + + SetWidth( MIN(MAX_BEAM_WIDTH, m_boltWidth) ); + SetNoise( MIN(MAX_BEAM_NOISEAMPLITUDE, m_noiseAmplitude) ); + SetFrame( m_frameStart ); + SetScrollRate( m_speed ); + if ( m_spawnflags & SF_BEAM_SHADEIN ) + { + SetBeamFlags( FBEAM_SHADEIN ); + } + else if ( m_spawnflags & SF_BEAM_SHADEOUT ) + { + SetBeamFlags( FBEAM_SHADEOUT ); + } +} diff --git a/sp/src/game/server/EnvFade.cpp b/sp/src/game/server/EnvFade.cpp new file mode 100644 index 00000000..8b6c58f2 --- /dev/null +++ b/sp/src/game/server/EnvFade.cpp @@ -0,0 +1,214 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements visual effects entities: sprites, beams, bubbles, etc. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "shake.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CEnvFade : public CLogicalEntity +{ +private: + + float m_Duration; + float m_HoldTime; + + COutputEvent m_OnBeginFade; + + DECLARE_DATADESC(); + +public: + DECLARE_CLASS( CEnvFade, CLogicalEntity ); + + virtual void Spawn( void ); + + inline float Duration( void ) { return m_Duration; } + inline float HoldTime( void ) { return m_HoldTime; } + + inline void SetDuration( float duration ) { m_Duration = duration; } + inline void SetHoldTime( float hold ) { m_HoldTime = hold; } + + int DrawDebugTextOverlays(void); + + // Inputs + void InputFade( inputdata_t &inputdata ); +}; + +LINK_ENTITY_TO_CLASS( env_fade, CEnvFade ); + +BEGIN_DATADESC( CEnvFade ) + + DEFINE_KEYFIELD( m_Duration, FIELD_FLOAT, "duration" ), + DEFINE_KEYFIELD( m_HoldTime, FIELD_FLOAT, "holdtime" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Fade", InputFade ), + + DEFINE_OUTPUT( m_OnBeginFade, "OnBeginFade"), + +END_DATADESC() + + + +#define SF_FADE_IN 0x0001 // Fade in, not out +#define SF_FADE_MODULATE 0x0002 // Modulate, don't blend +#define SF_FADE_ONLYONE 0x0004 +#define SF_FADE_STAYOUT 0x0008 +#ifdef MAPBASE +#define SF_FADE_DONT_PURGE 0x0016 +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvFade::Spawn( void ) +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that does the screen fade. +//----------------------------------------------------------------------------- +void CEnvFade::InputFade( inputdata_t &inputdata ) +{ + int fadeFlags = 0; + + if ( m_spawnflags & SF_FADE_IN ) + { + fadeFlags |= FFADE_IN; + } + else + { + fadeFlags |= FFADE_OUT; + } + + if ( m_spawnflags & SF_FADE_MODULATE ) + { + fadeFlags |= FFADE_MODULATE; + } + + if ( m_spawnflags & SF_FADE_STAYOUT ) + { + fadeFlags |= FFADE_STAYOUT; + } + +#ifdef MAPBASE + if ( !HasSpawnFlags(SF_FADE_DONT_PURGE) ) + { + fadeFlags |= FFADE_PURGE; + } +#endif + + if ( m_spawnflags & SF_FADE_ONLYONE ) + { + if ( inputdata.pActivator && inputdata.pActivator->IsNetClient() ) + { + UTIL_ScreenFade( inputdata.pActivator, m_clrRender, Duration(), HoldTime(), fadeFlags ); + } + } + else + { +#ifdef MAPBASE + UTIL_ScreenFadeAll( m_clrRender, Duration(), HoldTime(), fadeFlags ); +#else + UTIL_ScreenFadeAll( m_clrRender, Duration(), HoldTime(), fadeFlags|FFADE_PURGE ); +#endif + } + + m_OnBeginFade.FireOutput( inputdata.pActivator, this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Fetches the arguments from the command line for the fadein and fadeout +// console commands. +// Input : flTime - Returns the fade time in seconds (the time to fade in or out) +// clrFade - Returns the color to fade to or from. +//----------------------------------------------------------------------------- +static void GetFadeParms( const CCommand &args, float &flTime, color32 &clrFade) +{ + flTime = 2.0f; + + if ( args.ArgC() > 1 ) + { + flTime = atof( args[1] ); + } + + clrFade.r = 0; + clrFade.g = 0; + clrFade.b = 0; + clrFade.a = 255; + + if ( args.ArgC() > 4 ) + { + clrFade.r = atoi( args[2] ); + clrFade.g = atoi( args[3] ); + clrFade.b = atoi( args[4] ); + + if ( args.ArgC() == 5 ) + { + clrFade.a = atoi( args[5] ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Console command to fade out to a given color. +//----------------------------------------------------------------------------- +static void CC_FadeOut( const CCommand &args ) +{ + float flTime; + color32 clrFade; + GetFadeParms( args, flTime, clrFade ); + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + UTIL_ScreenFade( pPlayer, clrFade, flTime, 0, FFADE_OUT | FFADE_PURGE | FFADE_STAYOUT ); +} +static ConCommand fadeout("fadeout", CC_FadeOut, "fadeout {time r g b}: Fades the screen to black or to the specified color over the given number of seconds.", FCVAR_CHEAT ); + + +//----------------------------------------------------------------------------- +// Purpose: Console command to fade in from a given color. +//----------------------------------------------------------------------------- +static void CC_FadeIn( const CCommand &args ) +{ + float flTime; + color32 clrFade; + GetFadeParms( args, flTime, clrFade ); + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + UTIL_ScreenFade( pPlayer, clrFade, flTime, 0, FFADE_IN | FFADE_PURGE ); +} + +static ConCommand fadein("fadein", CC_FadeIn, "fadein {time r g b}: Fades the screen in from black or from the specified color over the given number of seconds.", FCVAR_CHEAT ); + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CEnvFade::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print duration + Q_snprintf(tempstr,sizeof(tempstr)," duration: %f", m_Duration); + EntityText(text_offset,tempstr,0); + text_offset++; + + // print hold time + Q_snprintf(tempstr,sizeof(tempstr)," hold time: %f", m_HoldTime); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} \ No newline at end of file diff --git a/sp/src/game/server/EnvHudHint.cpp b/sp/src/game/server/EnvHudHint.cpp new file mode 100644 index 00000000..2cb38eab --- /dev/null +++ b/sp/src/game/server/EnvHudHint.cpp @@ -0,0 +1,157 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements visual effects entities: sprites, beams, bubbles, etc. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "engine/IEngineSound.h" +#include "baseentity.h" +#include "entityoutput.h" +#include "recipientfilter.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_HUDHINT_ALLPLAYERS 0x0001 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CEnvHudHint : public CPointEntity +{ +public: + DECLARE_CLASS( CEnvHudHint, CPointEntity ); + + void Spawn( void ); + void Precache( void ); + +private: + inline bool AllPlayers( void ) { return (m_spawnflags & SF_HUDHINT_ALLPLAYERS) != 0; } + + void InputShowHudHint( inputdata_t &inputdata ); + void InputHideHudHint( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetHudHint( inputdata_t &inputdata ); +#endif + string_t m_iszMessage; + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( env_hudhint, CEnvHudHint ); + +BEGIN_DATADESC( CEnvHudHint ) + + DEFINE_KEYFIELD( m_iszMessage, FIELD_STRING, "message" ), + DEFINE_INPUTFUNC( FIELD_VOID, "ShowHudHint", InputShowHudHint ), + DEFINE_INPUTFUNC( FIELD_VOID, "HideHudHint", InputHideHudHint ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetHudHint", InputSetHudHint ), +#endif + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvHudHint::Spawn( void ) +{ + Precache(); + + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvHudHint::Precache( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for showing the message and/or playing the sound. +//----------------------------------------------------------------------------- +void CEnvHudHint::InputShowHudHint( inputdata_t &inputdata ) +{ + if ( AllPlayers() ) + { + CReliableBroadcastRecipientFilter user; + UserMessageBegin( user, "KeyHintText" ); + WRITE_BYTE( 1 ); // one message + WRITE_STRING( STRING(m_iszMessage) ); + MessageEnd(); + } + else + { + CBaseEntity *pPlayer = NULL; + if ( inputdata.pActivator && inputdata.pActivator->IsPlayer() ) + { + pPlayer = inputdata.pActivator; + } + else + { + pPlayer = UTIL_GetLocalPlayer(); + } + + if ( !pPlayer || !pPlayer->IsNetClient() ) + return; + + CSingleUserRecipientFilter user( (CBasePlayer *)pPlayer ); + user.MakeReliable(); + UserMessageBegin( user, "KeyHintText" ); + WRITE_BYTE( 1 ); // one message + WRITE_STRING( STRING(m_iszMessage) ); + MessageEnd(); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvHudHint::InputHideHudHint( inputdata_t &inputdata ) +{ + if ( AllPlayers() ) + { + CReliableBroadcastRecipientFilter user; + UserMessageBegin( user, "KeyHintText" ); + WRITE_BYTE( 1 ); // one message + WRITE_STRING( STRING(NULL_STRING) ); + MessageEnd(); + } + else + { + CBaseEntity *pPlayer = NULL; + + if ( inputdata.pActivator && inputdata.pActivator->IsPlayer() ) + { + pPlayer = inputdata.pActivator; + } + else + { + pPlayer = UTIL_GetLocalPlayer(); + } + + if ( !pPlayer || !pPlayer->IsNetClient() ) + return; + + CSingleUserRecipientFilter user( (CBasePlayer *)pPlayer ); + user.MakeReliable(); + UserMessageBegin( user, "KeyHintText" ); + WRITE_BYTE( 1 ); // one message + WRITE_STRING( STRING(NULL_STRING) ); + MessageEnd(); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvHudHint::InputSetHudHint( inputdata_t &inputdata ) +{ + m_iszMessage = inputdata.value.StringID(); +} +#endif diff --git a/sp/src/game/server/EnvLaser.cpp b/sp/src/game/server/EnvLaser.cpp new file mode 100644 index 00000000..3db94e7c --- /dev/null +++ b/sp/src/game/server/EnvLaser.cpp @@ -0,0 +1,260 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A special kind of beam effect that traces from its start position to +// its end position and stops if it hits anything. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "EnvLaser.h" +#include "Sprite.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( env_laser, CEnvLaser ); + +BEGIN_DATADESC( CEnvLaser ) + + DEFINE_KEYFIELD( m_iszLaserTarget, FIELD_STRING, "LaserTarget" ), + DEFINE_FIELD( m_pSprite, FIELD_CLASSPTR ), + DEFINE_KEYFIELD( m_iszSpriteName, FIELD_STRING, "EndSprite" ), + DEFINE_FIELD( m_firePosition, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_flStartFrame, FIELD_FLOAT, "framestart" ), + + // Function Pointers + DEFINE_FUNCTION( StrikeThink ), + + // Input functions + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + +#ifdef MAPBASE + DEFINE_OUTPUT( m_OnTouchedByEntity, "OnTouchedByEntity" ), +#endif + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::Spawn( void ) +{ + if ( !GetModelName() ) + { + SetThink( &CEnvLaser::SUB_Remove ); + return; + } + + SetSolid( SOLID_NONE ); // Remove model & collisions + SetThink( &CEnvLaser::StrikeThink ); + + SetEndWidth( GetWidth() ); // Note: EndWidth is not scaled + + PointsInit( GetLocalOrigin(), GetLocalOrigin() ); + + Precache( ); + + if ( !m_pSprite && m_iszSpriteName != NULL_STRING ) + { + m_pSprite = CSprite::SpriteCreate( STRING(m_iszSpriteName), GetAbsOrigin(), TRUE ); + } + else + { + m_pSprite = NULL; + } + + if ( m_pSprite ) + { + m_pSprite->SetParent( GetMoveParent() ); + m_pSprite->SetTransparency( kRenderGlow, m_clrRender->r, m_clrRender->g, m_clrRender->b, m_clrRender->a, m_nRenderFX ); + } + + if ( GetEntityName() != NULL_STRING && !(m_spawnflags & SF_BEAM_STARTON) ) + { + TurnOff(); + } + else + { + TurnOn(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::Precache( void ) +{ + SetModelIndex( PrecacheModel( STRING( GetModelName() ) ) ); + if ( m_iszSpriteName != NULL_STRING ) + PrecacheModel( STRING(m_iszSpriteName) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CEnvLaser::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "width")) + { + SetWidth( atof(szValue) ); + } + else if (FStrEq(szKeyName, "NoiseAmplitude")) + { + SetNoise( atoi(szValue) ); + } + else if (FStrEq(szKeyName, "TextureScroll")) + { + SetScrollRate( atoi(szValue) ); + } + else if (FStrEq(szKeyName, "texture")) + { + SetModelName( AllocPooledString(szValue) ); + } + else + { + BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns whether the laser is currently active. +//----------------------------------------------------------------------------- +int CEnvLaser::IsOn( void ) +{ + if ( IsEffectActive( EF_NODRAW ) ) + return 0; + return 1; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::InputTurnOn( inputdata_t &inputdata ) +{ + if (!IsOn()) + { + TurnOn(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::InputTurnOff( inputdata_t &inputdata ) +{ + if (IsOn()) + { + TurnOff(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::InputToggle( inputdata_t &inputdata ) +{ + if ( IsOn() ) + { + TurnOff(); + } + else + { + TurnOn(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::TurnOff( void ) +{ + AddEffects( EF_NODRAW ); + if ( m_pSprite ) + m_pSprite->TurnOff(); + + SetNextThink( TICK_NEVER_THINK ); + SetThink( NULL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::TurnOn( void ) +{ + RemoveEffects( EF_NODRAW ); + if ( m_pSprite ) + m_pSprite->TurnOn(); + + m_flFireTime = gpGlobals->curtime; + + SetThink( &CEnvLaser::StrikeThink ); + + // + // Call StrikeThink here to update the end position, otherwise we will see + // the beam in the wrong place for one frame since we cleared the nodraw flag. + // + StrikeThink(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::FireAtPoint( trace_t &tr ) +{ + SetAbsEndPos( tr.endpos ); + if ( m_pSprite ) + { + UTIL_SetOrigin( m_pSprite, tr.endpos ); + } + + // Apply damage and do sparks every 1/10th of a second. + if ( gpGlobals->curtime >= m_flFireTime + 0.1 ) + { +#ifdef MAPBASE + if ( tr.fraction != 1.0 && tr.m_pEnt && !tr.m_pEnt->IsWorld() ) + { + m_OnTouchedByEntity.FireOutput( tr.m_pEnt, this ); + } +#endif + BeamDamage( &tr ); + DoSparks( GetAbsStartPos(), tr.endpos ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvLaser::StrikeThink( void ) +{ + CBaseEntity *pEnd = RandomTargetname( STRING( m_iszLaserTarget ) ); + + Vector vecFireAt = GetAbsEndPos(); + if ( pEnd ) + { + vecFireAt = pEnd->GetAbsOrigin(); + } + + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), vecFireAt, MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr ); + FireAtPoint( tr ); + SetNextThink( gpGlobals->curtime ); +} + + diff --git a/sp/src/game/server/EnvLaser.h b/sp/src/game/server/EnvLaser.h new file mode 100644 index 00000000..fc740442 --- /dev/null +++ b/sp/src/game/server/EnvLaser.h @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ENVLASER_H +#define ENVLASER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "baseentity.h" +#include "beam_shared.h" +#include "entityoutput.h" + + +class CSprite; + + +class CEnvLaser : public CBeam +{ + DECLARE_CLASS( CEnvLaser, CBeam ); +public: + void Spawn( void ); + void Precache( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + void TurnOn( void ); + void TurnOff( void ); + int IsOn( void ); + + void FireAtPoint( trace_t &point ); + void StrikeThink( void ); + + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetTarget( inputdata_t &inputdata ) { m_iszLaserTarget = inputdata.value.StringID(); } +#endif + + DECLARE_DATADESC(); + + string_t m_iszLaserTarget; // Name of entity or entities to strike at, randomly picked if more than one match. + CSprite *m_pSprite; + string_t m_iszSpriteName; + Vector m_firePosition; + +#ifdef MAPBASE + COutputEvent m_OnTouchedByEntity; +#endif + + float m_flStartFrame; +}; + +#endif // ENVLASER_H diff --git a/sp/src/game/server/EnvMessage.cpp b/sp/src/game/server/EnvMessage.cpp new file mode 100644 index 00000000..6de81783 --- /dev/null +++ b/sp/src/game/server/EnvMessage.cpp @@ -0,0 +1,307 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements visual effects entities: sprites, beams, bubbles, etc. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "EnvMessage.h" +#include "engine/IEngineSound.h" +#include "KeyValues.h" +#include "filesystem.h" +#include "Color.h" +#include "gamestats.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( env_message, CMessage ); + +BEGIN_DATADESC( CMessage ) + + DEFINE_KEYFIELD( m_iszMessage, FIELD_STRING, "message" ), + DEFINE_KEYFIELD( m_sNoise, FIELD_SOUNDNAME, "messagesound" ), + DEFINE_KEYFIELD( m_MessageAttenuation, FIELD_INTEGER, "messageattenuation" ), + DEFINE_KEYFIELD( m_MessageVolume, FIELD_FLOAT, "messagevolume" ), + + DEFINE_FIELD( m_Radius, FIELD_FLOAT ), + + DEFINE_INPUTFUNC( FIELD_VOID, "ShowMessage", InputShowMessage ), + + DEFINE_OUTPUT(m_OnShowMessage, "OnShowMessage"), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessage::Spawn( void ) +{ + Precache(); + + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + switch( m_MessageAttenuation ) + { + case 1: // Medium radius + m_Radius = ATTN_STATIC; + break; + + case 2: // Large radius + m_Radius = ATTN_NORM; + break; + + case 3: //EVERYWHERE + m_Radius = ATTN_NONE; + break; + + default: + case 0: // Small radius + m_Radius = SNDLVL_IDLE; + break; + } + m_MessageAttenuation = 0; + + // Remap volume from [0,10] to [0,1]. + m_MessageVolume *= 0.1; + + // No volume, use normal + if ( m_MessageVolume <= 0 ) + { + m_MessageVolume = 1.0; + } +} + + +void CMessage::Precache( void ) +{ + if ( m_sNoise != NULL_STRING ) + { + PrecacheScriptSound( STRING(m_sNoise) ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for showing the message and/or playing the sound. +//----------------------------------------------------------------------------- +void CMessage::InputShowMessage( inputdata_t &inputdata ) +{ + CBaseEntity *pPlayer = NULL; + + if ( m_spawnflags & SF_MESSAGE_ALL ) + { + UTIL_ShowMessageAll( STRING( m_iszMessage ) ); + } + else + { + if ( inputdata.pActivator && inputdata.pActivator->IsPlayer() ) + { + pPlayer = inputdata.pActivator; + } + else + { + pPlayer = (gpGlobals->maxClients > 1) ? NULL : UTIL_GetLocalPlayer(); + } + + if ( pPlayer && pPlayer->IsPlayer() ) + { + UTIL_ShowMessage( STRING( m_iszMessage ), ToBasePlayer( pPlayer ) ); + } + } + + if ( m_sNoise != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_BODY; + ep.m_pSoundName = (char*)STRING(m_sNoise); + ep.m_flVolume = m_MessageVolume; + ep.m_SoundLevel = ATTN_TO_SNDLVL( m_Radius ); + + EmitSound( filter, entindex(), ep ); + } + + if ( m_spawnflags & SF_MESSAGE_ONCE ) + { + UTIL_Remove( this ); + } + + m_OnShowMessage.FireOutput( inputdata.pActivator, this ); +} + + +void CMessage::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + inputdata_t inputdata; + + inputdata.pActivator = NULL; + inputdata.pCaller = NULL; + + InputShowMessage( inputdata ); +} + + +class CCredits : public CPointEntity +{ +public: + DECLARE_CLASS( CMessage, CPointEntity ); + DECLARE_DATADESC(); + + void Spawn( void ); + void InputRollCredits( inputdata_t &inputdata ); + void InputRollOutroCredits( inputdata_t &inputdata ); + void InputShowLogo( inputdata_t &inputdata ); + void InputSetLogoLength( inputdata_t &inputdata ); + + COutputEvent m_OnCreditsDone; + + virtual void OnRestore(); +private: + + void RollOutroCredits(); + + bool m_bRolledOutroCredits; + float m_flLogoLength; + +#ifdef MAPBASE + // Custom credits.txt, defaults to that + string_t m_iszCreditsFile; +#endif +}; + +LINK_ENTITY_TO_CLASS( env_credits, CCredits ); + +BEGIN_DATADESC( CCredits ) + DEFINE_INPUTFUNC( FIELD_VOID, "RollCredits", InputRollCredits ), + DEFINE_INPUTFUNC( FIELD_VOID, "RollOutroCredits", InputRollOutroCredits ), + DEFINE_INPUTFUNC( FIELD_VOID, "ShowLogo", InputShowLogo ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetLogoLength", InputSetLogoLength ), + DEFINE_OUTPUT( m_OnCreditsDone, "OnCreditsDone"), + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iszCreditsFile, FIELD_STRING, "CreditsFile" ), +#endif + + DEFINE_FIELD( m_bRolledOutroCredits, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flLogoLength, FIELD_FLOAT ) +END_DATADESC() + +void CCredits::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); +} + +static void CreditsDone_f( void ) +{ + CCredits *pCredits = (CCredits*)gEntList.FindEntityByClassname( NULL, "env_credits" ); + + if ( pCredits ) + { + pCredits->m_OnCreditsDone.FireOutput( pCredits, pCredits ); + } +} + +static ConCommand creditsdone("creditsdone", CreditsDone_f ); + +extern ConVar sv_unlockedchapters; + +#ifdef MAPBASE +extern int Mapbase_GetChapterCount(); +#endif + +void CCredits::OnRestore() +{ + BaseClass::OnRestore(); + + if ( m_bRolledOutroCredits ) + { + // Roll them again so that the client .dll will send the "creditsdone" message and we'll + // actually get back to the main menu + RollOutroCredits(); + } +} + +void CCredits::RollOutroCredits() +{ +#ifdef MAPBASE + // Don't set this if we're using Mapbase chapters or if sv_unlockedchapters is already greater than 15 + if (Mapbase_GetChapterCount() <= 0 && sv_unlockedchapters.GetInt() < 15) +#endif + sv_unlockedchapters.SetValue( "15" ); + + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + UserMessageBegin( user, "CreditsMsg" ); + WRITE_BYTE( 3 ); +#ifdef MAPBASE + WRITE_STRING( STRING(m_iszCreditsFile) ); +#endif + MessageEnd(); +} + +void CCredits::InputRollOutroCredits( inputdata_t &inputdata ) +{ + RollOutroCredits(); + + // In case we save restore + m_bRolledOutroCredits = true; + + gamestats->Event_Credits(); +} + +void CCredits::InputShowLogo( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + if ( m_flLogoLength ) + { + UserMessageBegin( user, "LogoTimeMsg" ); + WRITE_FLOAT( m_flLogoLength ); +#ifdef MAPBASE + WRITE_STRING( STRING(m_iszCreditsFile) ); +#endif + MessageEnd(); + } + else + { + UserMessageBegin( user, "CreditsMsg" ); + WRITE_BYTE( 1 ); +#ifdef MAPBASE + WRITE_STRING( STRING(m_iszCreditsFile) ); +#endif + MessageEnd(); + } +} + +void CCredits::InputSetLogoLength( inputdata_t &inputdata ) +{ + m_flLogoLength = inputdata.value.Float(); +} + +void CCredits::InputRollCredits( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + UserMessageBegin( user, "CreditsMsg" ); + WRITE_BYTE( 2 ); +#ifdef MAPBASE + WRITE_STRING( STRING(m_iszCreditsFile) ); +#endif + MessageEnd(); +} diff --git a/sp/src/game/server/EnvMessage.h b/sp/src/game/server/EnvMessage.h new file mode 100644 index 00000000..cca710ea --- /dev/null +++ b/sp/src/game/server/EnvMessage.h @@ -0,0 +1,48 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ENVMESSAGE_H +#define ENVMESSAGE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "baseentity.h" +#include "entityoutput.h" + + +#define SF_MESSAGE_ONCE 0x0001 // Fade in, not out +#define SF_MESSAGE_ALL 0x0002 // Send to all clients + +class CMessage : public CPointEntity +{ +public: + DECLARE_CLASS( CMessage, CPointEntity ); + + void Spawn( void ); + void Precache( void ); + + inline void SetMessage( string_t iszMessage ) { m_iszMessage = iszMessage; } + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + +private: + + void InputShowMessage( inputdata_t &inputdata ); + + string_t m_iszMessage; // Message to display. + float m_MessageVolume; + int m_MessageAttenuation; + float m_Radius; + + DECLARE_DATADESC(); + + string_t m_sNoise; + COutputEvent m_OnShowMessage; +}; + +#endif // ENVMESSAGE_H diff --git a/sp/src/game/server/EnvShake.cpp b/sp/src/game/server/EnvShake.cpp new file mode 100644 index 00000000..14d4a514 --- /dev/null +++ b/sp/src/game/server/EnvShake.cpp @@ -0,0 +1,413 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements a screen shake effect that can also shake physics objects. +// +// NOTE: UTIL_ScreenShake() will only shake players who are on the ground +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "shake.h" +#include "physics_saverestore.h" +#include "rope.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CPhysicsShake : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); + +public: + virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) + { + Vector contact; + if ( !pObject->GetContactPoint( &contact, NULL ) ) + return SIM_NOTHING; + + // fudge the force a bit to make it more dramatic + pObject->CalculateForceOffset( m_force * (1.0f + pObject->GetMass()*0.4f), contact, &linear, &angular ); + + return SIM_LOCAL_FORCE; + } + + Vector m_force; +}; + +BEGIN_SIMPLE_DATADESC( CPhysicsShake ) + DEFINE_FIELD( m_force, FIELD_VECTOR ), +END_DATADESC() + + +class CEnvShake : public CPointEntity +{ +private: + float m_Amplitude; + float m_Frequency; + float m_Duration; + float m_Radius; // radius of 0 means all players + float m_stopTime; + float m_nextShake; + float m_currentAmp; + + Vector m_maxForce; + + IPhysicsMotionController *m_pShakeController; + CPhysicsShake m_shakeCallback; + + DECLARE_DATADESC(); + +public: + DECLARE_CLASS( CEnvShake, CPointEntity ); + + ~CEnvShake( void ); + virtual void Spawn( void ); + virtual void OnRestore( void ); + + inline float Amplitude( void ) { return m_Amplitude; } + inline float Frequency( void ) { return m_Frequency; } + inline float Duration( void ) { return m_Duration; } + float Radius( bool bPlayers = true ); + inline void SetAmplitude( float amplitude ) { m_Amplitude = amplitude; } + inline void SetFrequency( float frequency ) { m_Frequency = frequency; } + inline void SetDuration( float duration ) { m_Duration = duration; } + inline void SetRadius( float radius ) { m_Radius = radius; } + + int DrawDebugTextOverlays(void); + + // Input handlers + void InputStartShake( inputdata_t &inputdata ); + void InputStopShake( inputdata_t &inputdata ); + void InputAmplitude( inputdata_t &inputdata ); + void InputFrequency( inputdata_t &inputdata ); + + // Causes the camera/physics shakes to happen: + void ApplyShake( ShakeCommand_t command ); + void Think( void ); +}; + +LINK_ENTITY_TO_CLASS( env_shake, CEnvShake ); + +BEGIN_DATADESC( CEnvShake ) + + DEFINE_KEYFIELD( m_Amplitude, FIELD_FLOAT, "amplitude" ), + DEFINE_KEYFIELD( m_Frequency, FIELD_FLOAT, "frequency" ), + DEFINE_KEYFIELD( m_Duration, FIELD_FLOAT, "duration" ), + DEFINE_KEYFIELD( m_Radius, FIELD_FLOAT, "radius" ), + DEFINE_FIELD( m_stopTime, FIELD_TIME ), + DEFINE_FIELD( m_nextShake, FIELD_TIME ), + DEFINE_FIELD( m_currentAmp, FIELD_FLOAT ), + DEFINE_FIELD( m_maxForce, FIELD_VECTOR ), + DEFINE_PHYSPTR( m_pShakeController ), + DEFINE_EMBEDDED( m_shakeCallback ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartShake", InputStartShake ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopShake", InputStopShake ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "Amplitude", InputAmplitude ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "Frequency", InputFrequency ), + +END_DATADESC() + + + +#define SF_SHAKE_EVERYONE 0x0001 // Don't check radius +#define SF_SHAKE_INAIR 0x0004 // Shake players in air +#define SF_SHAKE_PHYSICS 0x0008 // Shake physically (not just camera) +#define SF_SHAKE_ROPES 0x0010 // Shake ropes too. +#define SF_SHAKE_NO_VIEW 0x0020 // DON'T shake the view (only ropes and/or physics objects) +#define SF_SHAKE_NO_RUMBLE 0x0040 // DON'T Rumble the XBox Controller + + +//----------------------------------------------------------------------------- +// Purpose: Destructor. +//----------------------------------------------------------------------------- +CEnvShake::~CEnvShake( void ) +{ + if ( m_pShakeController ) + { + physenv->DestroyMotionController( m_pShakeController ); + } +} + + +float CEnvShake::Radius(bool bPlayers) +{ + // The radius for players is zero if SF_SHAKE_EVERYONE is set + if ( bPlayers && HasSpawnFlags(SF_SHAKE_EVERYONE)) + return 0; + return m_Radius; +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets default member values when spawning. +//----------------------------------------------------------------------------- +void CEnvShake::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + if ( GetSpawnFlags() & SF_SHAKE_EVERYONE ) + { + m_Radius = 0; + } + + if ( HasSpawnFlags( SF_SHAKE_NO_VIEW ) && !HasSpawnFlags( SF_SHAKE_PHYSICS ) && !HasSpawnFlags( SF_SHAKE_ROPES ) ) + { + DevWarning( "env_shake %s with \"Don't shake view\" spawnflag set without \"Shake physics\" or \"Shake ropes\" spawnflags set.", GetDebugName() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Restore the motion controller +//----------------------------------------------------------------------------- +void CEnvShake::OnRestore( void ) +{ + BaseClass::OnRestore(); + + if ( m_pShakeController ) + { + m_pShakeController->SetEventHandler( &m_shakeCallback ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvShake::ApplyShake( ShakeCommand_t command ) +{ + if ( !HasSpawnFlags( SF_SHAKE_NO_VIEW ) || !HasSpawnFlags( SF_SHAKE_NO_RUMBLE ) ) + { + bool air = (GetSpawnFlags() & SF_SHAKE_INAIR) ? true : false; + UTIL_ScreenShake( GetAbsOrigin(), Amplitude(), Frequency(), Duration(), Radius(), command, air ); + } + + if ( GetSpawnFlags() & SF_SHAKE_ROPES ) + { + CRopeKeyframe::ShakeRopes( GetAbsOrigin(), Radius(false), Frequency() ); + } + + if ( GetSpawnFlags() & SF_SHAKE_PHYSICS ) + { + if ( !m_pShakeController ) + { + m_pShakeController = physenv->CreateMotionController( &m_shakeCallback ); + } + // do physics shake + switch( command ) + { + case SHAKE_START: + case SHAKE_START_NORUMBLE: + case SHAKE_START_RUMBLEONLY: + { + m_stopTime = gpGlobals->curtime + Duration(); + m_nextShake = 0; + m_pShakeController->ClearObjects(); + SetNextThink( gpGlobals->curtime ); + m_currentAmp = Amplitude(); + CBaseEntity *list[1024]; + float radius = Radius(false); + + // probably checked "Shake Everywhere" do a big radius + if ( !radius ) + { + radius = 512; + } + Vector extents = Vector(radius, radius, radius); + extents.z = MAX(extents.z, 100); + Vector mins = GetAbsOrigin() - extents; + Vector maxs = GetAbsOrigin() + extents; + int count = UTIL_EntitiesInBox( list, 1024, mins, maxs, 0 ); + + for ( int i = 0; i < count; i++ ) + { + // + // Only shake physics entities that players can see. This is one frame out of date + // so it's possible that we could miss objects if a player changed PVS this frame. + // + if ( ( list[i]->GetMoveType() == MOVETYPE_VPHYSICS ) ) + { + IPhysicsObject *pPhys = list[i]->VPhysicsGetObject(); + if ( pPhys && pPhys->IsMoveable() ) + { + m_pShakeController->AttachObject( pPhys, false ); + pPhys->Wake(); + } + } + } + } + break; + case SHAKE_STOP: + m_pShakeController->ClearObjects(); + break; + case SHAKE_AMPLITUDE: + m_currentAmp = Amplitude(); + case SHAKE_FREQUENCY: + m_pShakeController->WakeObjects(); + break; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that starts the screen shake. +//----------------------------------------------------------------------------- +void CEnvShake::InputStartShake( inputdata_t &inputdata ) +{ + if ( HasSpawnFlags( SF_SHAKE_NO_RUMBLE ) ) + { + ApplyShake( SHAKE_START_NORUMBLE ); + } + else if ( HasSpawnFlags( SF_SHAKE_NO_VIEW ) ) + { + ApplyShake( SHAKE_START_RUMBLEONLY ); + } + else + { + ApplyShake( SHAKE_START ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that stops the screen shake. +//----------------------------------------------------------------------------- +void CEnvShake::InputStopShake( inputdata_t &inputdata ) +{ + ApplyShake( SHAKE_STOP ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Handles changes to the shake amplitude from an external source. +//----------------------------------------------------------------------------- +void CEnvShake::InputAmplitude( inputdata_t &inputdata ) +{ + SetAmplitude( inputdata.value.Float() ); + ApplyShake( SHAKE_AMPLITUDE ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Handles changes to the shake frequency from an external source. +//----------------------------------------------------------------------------- +void CEnvShake::InputFrequency( inputdata_t &inputdata ) +{ + SetFrequency( inputdata.value.Float() ); + ApplyShake( SHAKE_FREQUENCY ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculates the physics shake values +//----------------------------------------------------------------------------- +void CEnvShake::Think( void ) +{ + int i; + + if ( gpGlobals->curtime > m_nextShake ) + { + // Higher frequency means we recalc the extents more often and perturb the display again + m_nextShake = gpGlobals->curtime + (1.0f / Frequency()); + + // Compute random shake extents (the shake will settle down from this) + for (i = 0; i < 2; i++ ) + { + m_maxForce[i] = random->RandomFloat( -1, 1 ); + } + // make the force it point mostly up + m_maxForce.z = 4; + VectorNormalize( m_maxForce ); + m_maxForce *= m_currentAmp * 400; // amplitude is the acceleration of a 100kg object + } + + float fraction = ( m_stopTime - gpGlobals->curtime ) / Duration(); + + if ( fraction < 0 ) + { + m_pShakeController->ClearObjects(); + return; + } + + float freq = 0; + // Ramp up frequency over duration + if ( fraction ) + { + freq = (Frequency() / fraction); + } + + // square fraction to approach zero more quickly + fraction *= fraction; + + // Sine wave that slowly settles to zero + fraction = fraction * sin( gpGlobals->curtime * freq ); + + // Add to view origin + for ( i = 0; i < 3; i++ ) + { + // store the force in the controller callback + m_shakeCallback.m_force[i] = m_maxForce[i] * fraction; + } + + // Drop amplitude a bit, less for higher frequency shakes + m_currentAmp -= m_currentAmp * ( gpGlobals->frametime / (Duration() * Frequency()) ); + SetNextThink( gpGlobals->curtime ); +} + + +//------------------------------------------------------------------------------ +// Purpose: Console command to cause a screen shake. +//------------------------------------------------------------------------------ +void CC_Shake( void ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if (pPlayer) + { + UTIL_ScreenShake( pPlayer->WorldSpaceCenter(), 25.0, 150.0, 1.0, 750, SHAKE_START ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Returns current text offset from the top +//----------------------------------------------------------------------------- +int CEnvShake::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print amplitude + Q_snprintf(tempstr,sizeof(tempstr)," magnitude: %f", m_Amplitude); + EntityText(text_offset,tempstr,0); + text_offset++; + + // print frequency + Q_snprintf(tempstr,sizeof(tempstr)," frequency: %f", m_Frequency); + EntityText(text_offset,tempstr,0); + text_offset++; + + // print duration + Q_snprintf(tempstr,sizeof(tempstr)," duration: %f", m_Duration); + EntityText(text_offset,tempstr,0); + text_offset++; + + // print radius + Q_snprintf(tempstr,sizeof(tempstr)," radius: %f", m_Radius); + EntityText(text_offset,tempstr,0); + text_offset++; + + } + return text_offset; +} + +static ConCommand shake("shake", CC_Shake, "Shake the screen.", FCVAR_CHEAT ); + + diff --git a/sp/src/game/server/EnvSpark.cpp b/sp/src/game/server/EnvSpark.cpp new file mode 100644 index 00000000..ba4054b6 --- /dev/null +++ b/sp/src/game/server/EnvSpark.cpp @@ -0,0 +1,195 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A point entity that periodically emits sparks and "bzzt" sounds. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "IEffects.h" +#include "engine/IEngineSound.h" +#include "envspark.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Emits sparks from the given location and plays a random spark sound. +// Input : pev - +// location - +//----------------------------------------------------------------------------- +void DoSpark( CBaseEntity *ent, const Vector &location, int nMagnitude, int nTrailLength, bool bPlaySound, const Vector &vecDir ) +{ + g_pEffects->Sparks( location, nMagnitude, nTrailLength, &vecDir ); + + if ( bPlaySound ) + { + ent->EmitSound( "DoSpark" ); + } +} + +const int SF_SPARK_START_ON = 64; +const int SF_SPARK_GLOW = 128; +const int SF_SPARK_SILENT = 256; +const int SF_SPARK_DIRECTIONAL = 512; + +BEGIN_DATADESC( CEnvSpark ) + + DEFINE_KEYFIELD( m_flDelay, FIELD_FLOAT, "MaxDelay" ), + DEFINE_FIELD( m_nGlowSpriteIndex, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_nMagnitude, FIELD_INTEGER, "Magnitude" ), + DEFINE_KEYFIELD( m_nTrailLength, FIELD_INTEGER, "TrailLength" ), + + // Function Pointers + DEFINE_FUNCTION( SparkThink ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartSpark", InputStartSpark ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopSpark", InputStopSpark ), + DEFINE_INPUTFUNC( FIELD_VOID, "ToggleSpark", InputToggleSpark ), + DEFINE_INPUTFUNC( FIELD_VOID, "SparkOnce", InputSparkOnce ), + + DEFINE_OUTPUT( m_OnSpark, "OnSpark" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( env_spark, CEnvSpark ); + + +//----------------------------------------------------------------------------- +// Purpose: Constructor! Exciting, isn't it? +//----------------------------------------------------------------------------- +CEnvSpark::CEnvSpark( void ) +{ + m_nMagnitude = 1; + m_nTrailLength = 1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been handled. +//----------------------------------------------------------------------------- +void CEnvSpark::Spawn(void) +{ + SetThink( NULL ); + SetUse( NULL ); + + if ( FBitSet(m_spawnflags, SF_SPARK_START_ON ) ) + { + SetThink( &CEnvSpark::SparkThink ); // start sparking + } + + SetNextThink( gpGlobals->curtime + 0.1 + random->RandomFloat( 0, 1.5 ) ); + + // Negative delays are not allowed + if ( m_flDelay < 0 ) + { + m_flDelay = 0; + } + +#ifdef HL1_DLL + // Don't allow 0 delays in HL1 Port. Enforce a default + if( m_flDelay == 0 ) + { + m_flDelay = 1.0f; + } +#endif//HL1_DLL + + Precache( ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvSpark::Precache(void) +{ + m_nGlowSpriteIndex = PrecacheModel( "sprites/glow01.vmt" ); + + PrecacheScriptSound( "DoSpark" ); +} + +extern ConVar phys_pushscale; + +//----------------------------------------------------------------------------- +// Purpose: Emits sparks at random intervals. +//----------------------------------------------------------------------------- +void CEnvSpark::SparkThink(void) +{ + SetNextThink( gpGlobals->curtime + 0.1 + random->RandomFloat(0, m_flDelay) ); + + Vector vecDir = vec3_origin; + if ( FBitSet( m_spawnflags, SF_SPARK_DIRECTIONAL ) ) + { + AngleVectors( GetAbsAngles(), &vecDir ); + } + + DoSpark( this, WorldSpaceCenter(), m_nMagnitude, m_nTrailLength, !( m_spawnflags & SF_SPARK_SILENT ), vecDir ); + + m_OnSpark.FireOutput( this, this ); + + if (FBitSet(m_spawnflags, SF_SPARK_GLOW)) + { + CPVSFilter filter( GetAbsOrigin() ); + te->GlowSprite( filter, 0.0, &GetAbsOrigin(), m_nGlowSpriteIndex, 0.2, 1.5, 25 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for starting the sparks. +//----------------------------------------------------------------------------- +void CEnvSpark::InputStartSpark( inputdata_t &inputdata ) +{ + StartSpark(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvSpark::StartSpark( void ) +{ + SetThink( &CEnvSpark::SparkThink ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: Shoot one spark. +//----------------------------------------------------------------------------- +void CEnvSpark::InputSparkOnce( inputdata_t &inputdata ) +{ + SparkThink(); + SetNextThink( TICK_NEVER_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for starting the sparks. +//----------------------------------------------------------------------------- +void CEnvSpark::InputStopSpark( inputdata_t &inputdata ) +{ + StopSpark(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvSpark::StopSpark( void ) +{ + SetThink( NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for toggling the on/off state of the sparks. +//----------------------------------------------------------------------------- +void CEnvSpark::InputToggleSpark( inputdata_t &inputdata ) +{ + if ( GetNextThink() == TICK_NEVER_THINK ) + { + InputStartSpark( inputdata ); + } + else + { + InputStopSpark( inputdata ); + } +} + + diff --git a/sp/src/game/server/EventLog.cpp b/sp/src/game/server/EventLog.cpp new file mode 100644 index 00000000..7c851272 --- /dev/null +++ b/sp/src/game/server/EventLog.cpp @@ -0,0 +1,257 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "EventLog.h" +#include "team.h" +#include "KeyValues.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CEventLog::CEventLog() +{ +} + +CEventLog::~CEventLog() +{ +} + + +void CEventLog::FireGameEvent( IGameEvent *event ) +{ + PrintEvent ( event ); +} + +bool CEventLog::PrintEvent( IGameEvent *event ) +{ + const char * name = event->GetName(); + + if ( Q_strncmp(name, "server_", strlen("server_")) == 0 ) + { + return true; // we don't care about server events (engine does) + } + else if ( Q_strncmp(name, "player_", strlen("player_")) == 0 ) + { + return PrintPlayerEvent( event ); + } + else if ( Q_strncmp(name, "team_", strlen("team_")) == 0 ) + { + return PrintTeamEvent( event ); + } + else if ( Q_strncmp(name, "game_", strlen("game_")) == 0 ) + { + return PrintGameEvent( event ); + } + else + { + return PrintOtherEvent( event ); // bomb_, round_, et al + } +} + +bool CEventLog::PrintGameEvent( IGameEvent *event ) +{ +// const char * name = event->GetName() + Q_strlen("game_"); // remove prefix + + return false; +} + +bool CEventLog::PrintPlayerEvent( IGameEvent *event ) +{ + const char * eventName = event->GetName(); + const int userid = event->GetInt( "userid" ); + + if ( !Q_strncmp( eventName, "player_connect", Q_strlen("player_connect") ) ) // player connect is before the CBasePlayer pointer is setup + { + const char *name = event->GetString( "name" ); + const char *address = event->GetString( "address" ); + const char *networkid = event->GetString("networkid" ); + UTIL_LogPrintf( "\"%s<%i><%s><>\" connected, address \"%s\"\n", name, userid, networkid, address); + return true; + } + else if ( !Q_strncmp( eventName, "player_disconnect", Q_strlen("player_disconnect") ) ) + { + const char *reason = event->GetString("reason" ); + const char *name = event->GetString("name" ); + const char *networkid = event->GetString("networkid" ); + CTeam *team = NULL; + CBasePlayer *pPlayer = UTIL_PlayerByUserId( userid ); + + if ( pPlayer ) + { + team = pPlayer->GetTeam(); + } + + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" disconnected (reason \"%s\")\n", name, userid, networkid, team ? team->GetName() : "", reason ); + return true; + } + + CBasePlayer *pPlayer = UTIL_PlayerByUserId( userid ); + if ( !pPlayer) + { + DevMsg( "CEventLog::PrintPlayerEvent: Failed to find player (userid: %i, event: %s)\n", userid, eventName ); + return false; + } + + if ( !Q_strncmp( eventName, "player_team", Q_strlen("player_team") ) ) + { + const bool bDisconnecting = event->GetBool( "disconnect" ); + + if ( !bDisconnecting ) + { + const int newTeam = event->GetInt( "team" ); + const int oldTeam = event->GetInt( "oldteam" ); + CTeam *team = GetGlobalTeam( newTeam ); + CTeam *oldteam = GetGlobalTeam( oldTeam ); + + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" joined team \"%s\"\n", + pPlayer->GetPlayerName(), + pPlayer->GetUserID(), + pPlayer->GetNetworkIDString(), + oldteam->GetName(), + team->GetName() ); + } + + return true; + } + else if ( !Q_strncmp( eventName, "player_death", Q_strlen("player_death") ) ) + { + const int attackerid = event->GetInt("attacker" ); + +#ifdef HL2MP + const char *weapon = event->GetString( "weapon" ); +#endif + + CBasePlayer *pAttacker = UTIL_PlayerByUserId( attackerid ); + CTeam *team = pPlayer->GetTeam(); + CTeam *attackerTeam = NULL; + + if ( pAttacker ) + { + attackerTeam = pAttacker->GetTeam(); + } + if ( pPlayer == pAttacker && pPlayer ) + { + +#ifdef HL2MP + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" committed suicide with \"%s\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + team ? team->GetName() : "", + weapon + ); +#else + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" committed suicide with \"%s\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + team ? team->GetName() : "", + pAttacker->GetClassname() + ); +#endif + } + else if ( pAttacker ) + { + CTeam *attackerTeam = pAttacker->GetTeam(); + +#ifdef HL2MP + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" killed \"%s<%i><%s><%s>\" with \"%s\"\n", + pAttacker->GetPlayerName(), + attackerid, + pAttacker->GetNetworkIDString(), + attackerTeam ? attackerTeam->GetName() : "", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + team ? team->GetName() : "", + weapon + ); +#else + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" killed \"%s<%i><%s><%s>\"\n", + pAttacker->GetPlayerName(), + attackerid, + pAttacker->GetNetworkIDString(), + attackerTeam ? attackerTeam->GetName() : "", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + team ? team->GetName() : "" + ); +#endif + } + else + { + // killed by the world + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" committed suicide with \"world\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + team ? team->GetName() : "" + ); + } + return true; + } + else if ( !Q_strncmp( eventName, "player_activate", Q_strlen("player_activate") ) ) + { + UTIL_LogPrintf( "\"%s<%i><%s><>\" entered the game\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString() + ); + + return true; + } + else if ( !Q_strncmp( eventName, "player_changename", Q_strlen("player_changename") ) ) + { + const char *newName = event->GetString( "newname" ); + const char *oldName = event->GetString( "oldname" ); + CTeam *team = pPlayer->GetTeam(); + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" changed name to \"%s\"\n", + oldName, + userid, + pPlayer->GetNetworkIDString(), + team ? team->GetName() : "", + newName + ); + return true; + } + +// ignored events +//player_hurt + return false; +} + +bool CEventLog::PrintTeamEvent( IGameEvent *event ) +{ +// const char * name = event->GetName() + Q_strlen("team_"); // remove prefix + + return false; +} + +bool CEventLog::PrintOtherEvent( IGameEvent *event ) +{ + return false; +} + + +bool CEventLog::Init() +{ + ListenForGameEvent( "player_changename" ); + ListenForGameEvent( "player_activate" ); + ListenForGameEvent( "player_death" ); + ListenForGameEvent( "player_team" ); + ListenForGameEvent( "player_disconnect" ); + ListenForGameEvent( "player_connect" ); + + return true; +} + +void CEventLog::Shutdown() +{ + StopListeningForAllEvents(); +} \ No newline at end of file diff --git a/sp/src/game/server/EventLog.h b/sp/src/game/server/EventLog.h new file mode 100644 index 00000000..1bcbd011 --- /dev/null +++ b/sp/src/game/server/EventLog.h @@ -0,0 +1,46 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#if !defined EVENTLOG_H +#define EVENTLOG_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "GameEventListener.h" +#include + +class CEventLog : public CGameEventListener, public CBaseGameSystem +{ + +public: + CEventLog(); + virtual ~CEventLog(); + +public: // IGameEventListener Interface + + virtual void FireGameEvent( IGameEvent * event ); + +public: // CBaseGameSystem overrides + + virtual bool Init(); + virtual void Shutdown(); + +protected: + + virtual bool PrintEvent( IGameEvent * event ); + virtual bool PrintGameEvent( IGameEvent * event ); + virtual bool PrintPlayerEvent( IGameEvent * event ); + virtual bool PrintTeamEvent( IGameEvent * event ); + virtual bool PrintOtherEvent( IGameEvent * event ); +}; + +extern IGameSystem* GameLogSystem(); + +#endif // EVENTLOG_H diff --git a/sp/src/game/server/GameStats.cpp b/sp/src/game/server/GameStats.cpp new file mode 100644 index 00000000..5b7b9610 --- /dev/null +++ b/sp/src/game/server/GameStats.cpp @@ -0,0 +1,1099 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" + + +#include "igamesystem.h" +#include "gamestats.h" +#include "tier1/utlstring.h" +#include "filesystem.h" +#include "tier1/utlbuffer.h" + +#ifndef SWDS +#include "iregistry.h" +#endif + +#include "tier1/utldict.h" +#include "tier0/icommandline.h" +#include "vehicle_base.h" + +#if defined( _X360 ) +#include "xbox/xbox_win32stubs.h" +#endif + +#define GAMESTATS_LOG_FILE "gamestats.log" +#define GAMESTATS_PATHID "MOD" + +/* +#define ONE_DAY_IN_SECONDS 86400 + +// Lower threshold in debug for testing... +#if defined( _DEBUG ) +#define WALKED_AWAY_FROM_KEYBOARD_SECONDS 15.0f // 15 seconds of movement == might be paused +#else +#define WALKED_AWAY_FROM_KEYBOARD_SECONDS 300.0f // 5 minutes of no movement == might be paused +#endif +*/ + + +extern IUploadGameStats *gamestatsuploader; +extern ConVar skill; + +static CBaseGameStats s_GameStats_Singleton; +CBaseGameStats *gamestats = &s_GameStats_Singleton; //start out pointing at the basic version which does nothing by default + +bool UserCmdChanged( const CUserCmd& lhs, const CUserCmd& rhs ); +bool StatsTrackingIsFullyEnabled( void ); + + +//used to drive most of the game stat event handlers as well as track basic stats under the hood of CBaseGameStats +class CBaseGameStats_Driver : public CAutoGameSystemPerFrame +{ +public: + CBaseGameStats_Driver( void ); + + typedef CAutoGameSystemPerFrame BaseClass; + + // IGameSystem overloads + virtual bool Init(); + virtual void Shutdown(); + + // Level init, shutdown + virtual void LevelInitPreEntity(); + virtual void LevelShutdownPreEntity(); + // Called during game save + virtual void OnSave(); + // Called during game restore, after the local player has connected and entities have been fully restored + virtual void OnRestore(); + + virtual void FrameUpdatePostEntityThink(); + + void PossibleMapChange( void ); + + CUtlString m_PrevMapName; //used to track "OnMapChange" events + int m_iLoadedVersion; + char m_szLoadedUserID[ 17 ]; // GUID + + bool m_bEnabled; //false if incapable of uploading or the user doesn't want to enable stat tracking + bool m_bShuttingDown; + bool m_bInLevel; + bool m_bFirstLevel; + time_t m_tLastUpload; + + float m_flLevelStartTime; + + + bool m_bStationary; + float m_flLastMovementTime; + CUserCmd m_LastUserCmd; + bool m_bGamePaused; + float m_flPauseStartTime; +}; +static CBaseGameStats_Driver CBGSDriver; + +CBaseGameStats_Driver::CBaseGameStats_Driver( void ) : + BaseClass( "CGameStats" ), + m_iLoadedVersion( -1 ), + m_bEnabled( false ), + m_bShuttingDown( false ), + m_bInLevel( false ), + m_bFirstLevel( true ), + m_flLevelStartTime( 0.0f ), + m_bStationary( false ), + m_flLastMovementTime( 0.0f ), + m_bGamePaused( false ) +{ + m_szLoadedUserID[0] = 0;; + m_tLastUpload = 0; + m_LastUserCmd.Reset(); +} + +static FileHandle_t g_LogFileHandle = FILESYSTEM_INVALID_HANDLE; + +CBaseGameStats::CBaseGameStats() : + m_bLogging( false ), + m_bLoggingToFile( false ) +{ +} + +bool CBaseGameStats::StatTrackingAllowed( void ) +{ + return CBGSDriver.m_bEnabled; +} + +// Don't care about vcr hooks here... +#undef localtime +#undef asctime +#undef time + +#include + +void CBaseGameStats::StatsLog( char const *fmt, ... ) +{ + if ( !m_bLogging && !m_bLoggingToFile ) + return; + + char buf[ 2048 ]; + va_list argptr; + va_start( argptr, fmt ); + Q_vsnprintf( buf, sizeof( buf ), fmt, argptr ); + va_end( argptr ); + + // Prepend timestamp and spew it + + // Prepend the time. + time_t aclock; + time( &aclock ); + struct tm *newtime = localtime( &aclock ); + + + char timeString[ 128 ]; + Q_strncpy( timeString, asctime( newtime ), sizeof( timeString ) ); + // Get rid of the \n. + char *pEnd = strstr( timeString, "\n" ); + if ( pEnd ) + { + *pEnd = 0; + } + + if ( m_bLogging ) + { + DevMsg( "[GS %s - %7.2f] %s", timeString, gpGlobals->realtime, buf ); + } + + if ( m_bLoggingToFile ) + { + if ( FILESYSTEM_INVALID_HANDLE == g_LogFileHandle ) + { + g_LogFileHandle = filesystem->Open( GAMESTATS_LOG_FILE, "a", GAMESTATS_PATHID ); + } + + if ( FILESYSTEM_INVALID_HANDLE != g_LogFileHandle ) + { + filesystem->FPrintf( g_LogFileHandle, "[GS %s - %7.2f] %s", timeString, gpGlobals->realtime, buf ); + filesystem->Flush( g_LogFileHandle ); + } + } +} + +static char s_szSaveFileName[256] = ""; +static char s_szStatUploadRegistryKeyName[256] = ""; +static char s_szPseudoUniqueID[20] = ""; + +const char *CBaseGameStats::GetStatSaveFileName( void ) +{ + AssertMsg( s_szSaveFileName[0] != '\0', "Don't know what file to save stats to." ); + return s_szSaveFileName; +} + +const char *CBaseGameStats::GetStatUploadRegistryKeyName( void ) +{ + AssertMsg( s_szStatUploadRegistryKeyName[0] != '\0', "Don't know the registry key to use to mark stats uploads." ); + return s_szStatUploadRegistryKeyName; +} + +const char *CBaseGameStats::GetUserPseudoUniqueID( void ) +{ + AssertMsg( s_szPseudoUniqueID[0] != '\0', "Don't have a pseudo unique ID." ); + return s_szPseudoUniqueID; +} + +void CBaseGameStats::Event_Init( void ) +{ + SetHL2UnlockedChapterStatistic(); + SetSteamStatistic( filesystem->IsSteam() ); + SetCyberCafeStatistic( gamestatsuploader->IsCyberCafeUser() ); + ConVarRef pDXLevel( "mat_dxlevel" ); + if( pDXLevel.IsValid() ) + { + SetDXLevelStatistic( pDXLevel.GetInt() ); + } + ++m_BasicStats.m_Summary.m_nCount; + + StatsLog( "CBaseGameStats::Event_Init [%dth session]\n", m_BasicStats.m_Summary.m_nCount ); +} + +void CBaseGameStats::Event_Shutdown( void ) +{ + StatsLog( "CBaseGameStats::Event_Shutdown [%dth session]\n", m_BasicStats.m_Summary.m_nCount ); + + StatsLog( "\n====================================================================\n\n" ); +} + +void CBaseGameStats::Event_MapChange( const char *szOldMapName, const char *szNewMapName ) +{ + StatsLog( "CBaseGameStats::Event_MapChange to [%s]\n", szNewMapName ); +} + +void CBaseGameStats::Event_LevelInit( void ) +{ + StatsLog( "CBaseGameStats::Event_LevelInit [%s]\n", CBGSDriver.m_PrevMapName.String() ); + + BasicGameStatsRecord_t *map = gamestats->m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + ++map->m_nCount; + + // HACK HACK: Punching this hole through only works in single player!!! + if ( gpGlobals->maxClients == 1 ) + { + ConVarRef closecaption( "closecaption" ); + if( closecaption.IsValid() ) + SetCaptionsStatistic( closecaption.GetBool() ); + + SetHDRStatistic( gamestatsuploader->IsHDREnabled() ); + + SetSkillStatistic( skill.GetInt() ); + SetSteamStatistic( filesystem->IsSteam() ); + SetCyberCafeStatistic( gamestatsuploader->IsCyberCafeUser() ); + } +} + +void CBaseGameStats::Event_LevelShutdown( float flElapsed ) +{ + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + Assert( map ); + map->m_nSeconds += (int)flElapsed; + gamestats->m_BasicStats.m_Summary.m_nSeconds += (int)flElapsed; + + StatsLog( "CBaseGameStats::Event_LevelShutdown [%s] %.2f elapsed %d total\n", CBGSDriver.m_PrevMapName.String(), flElapsed, gamestats->m_BasicStats.m_Summary.m_nSeconds ); +} + +void CBaseGameStats::Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + ++m_BasicStats.m_Summary.m_nDeaths; + + if( CBGSDriver.m_bInLevel ) + { + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + ++map->m_nDeaths; + StatsLog( " Player died %dth time in level [%s]!!!\n", map->m_nDeaths, CBGSDriver.m_PrevMapName.String() ); + } + else + { + StatsLog( " Player died, but not in a level!!!\n" ); + Assert( 0 ); + } + + StatsLog( "CBaseGameStats::Event_PlayerKilled [%s] [%dth death]\n", pPlayer->GetPlayerName(), m_BasicStats.m_Summary.m_nDeaths ); +} + +void CBaseGameStats::Event_SaveGame( void ) +{ + StatsLog( "CBaseGameStats::Event_SaveGame [%s]\n", CBGSDriver.m_PrevMapName.String() ); +} + +void CBaseGameStats::Event_LoadGame( void ) +{ + char const *pchSaveFile = engine->GetMostRecentlyLoadedFileName(); + StatsLog( "CBaseGameStats::Event_LoadGame [%s] from %s\n", CBGSDriver.m_PrevMapName.String(), pchSaveFile ); +} + +void CBaseGameStats::Event_Commentary() +{ + if( CBGSDriver.m_bInLevel ) + { + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + ++map->m_nCommentary; + } + + ++m_BasicStats.m_Summary.m_nCommentary; + + StatsLog( "CBaseGameStats::Event_Commentary [%d]\n", m_BasicStats.m_Summary.m_nCommentary ); +} + +void CBaseGameStats::Event_Credits() +{ + StatsLog( "CBaseGameStats::Event_Credits\n" ); + + float elapsed = 0.0f; + if( CBGSDriver.m_bInLevel ) + { + elapsed = gpGlobals->realtime - CBGSDriver.m_flLevelStartTime; + } + + if( elapsed < 0.0f ) + { + Assert( 0 ); + Warning( "EVENT_CREDITS with negative elapsed time (rt %f starttime %f)\n", gpGlobals->realtime, CBGSDriver.m_flLevelStartTime ); + elapsed = 0.0f; + } + + // Only set this one time!!! + if( gamestats->m_BasicStats.m_nSecondsToCompleteGame == 0 ) + { + if( gamestats->UserPlayedAllTheMaps() ) + { + gamestats->m_BasicStats.m_nSecondsToCompleteGame = elapsed + gamestats->m_BasicStats.m_Summary.m_nSeconds; + gamestats->SaveToFileNOW(); + } + } +} + +void CBaseGameStats::Event_CrateSmashed() +{ + StatsLog( "CBaseGameStats::Event_CrateSmashed\n" ); +} + +void CBaseGameStats::Event_Punted( CBaseEntity *pObject ) +{ + StatsLog( "CBaseGameStats::Event_Punted [%s]\n", pObject->GetClassname() ); +} + +void CBaseGameStats::Event_PlayerTraveled( CBasePlayer *pBasePlayer, float distanceInInches, bool bInVehicle, bool bSprinting ) +{ +} + +void CBaseGameStats::Event_FlippedVehicle( CBasePlayer *pDriver, CPropVehicleDriveable *pVehicle ) +{ + StatsLog( "CBaseGameStats::Event_FlippedVehicle [%s] flipped [%s]\n", pDriver->GetPlayerName(), pVehicle->GetClassname() ); +} + +// Called before .sav file is actually loaded (player should still be in previous level, if any) +void CBaseGameStats::Event_PreSaveGameLoaded( char const *pSaveName, bool bInGame ) +{ + StatsLog( "CBaseGameStats::Event_PreSaveGameLoaded [%s] %s\n", pSaveName, bInGame ? "in-game" : "at console" ); +} + +bool CBaseGameStats::SaveToFileNOW( bool bForceSyncWrite /* = false */ ) +{ + if ( !StatsTrackingIsFullyEnabled() ) + return false; + + CUtlBuffer buf; + buf.PutShort( GAMESTATS_FILE_VERSION ); + buf.Put( s_szPseudoUniqueID, 16 ); + + if( ShouldTrackStandardStats() ) + m_BasicStats.SaveToBuffer( buf ); + else + buf.PutInt( GAMESTATS_STANDARD_NOT_SAVED ); + + gamestats->AppendCustomDataToSaveBuffer( buf ); + + char fullpath[ 512 ] = { 0 }; + if ( filesystem->FileExists( GetStatSaveFileName(), GAMESTATS_PATHID ) ) + { + filesystem->RelativePathToFullPath( GetStatSaveFileName(), GAMESTATS_PATHID, fullpath, sizeof( fullpath ) ); + } + else + { + // filename is local to game dir for Steam, so we need to prepend game dir for regular file save + char gamePath[256]; + engine->GetGameDir( gamePath, 256 ); + Q_StripTrailingSlash( gamePath ); + Q_snprintf( fullpath, sizeof( fullpath ), "%s/%s", gamePath, GetStatSaveFileName() ); + Q_strlower( fullpath ); + Q_FixSlashes( fullpath ); + } + + // StatsLog( "SaveToFileNOW '%s'\n", fullpath ); + + if( CBGSDriver.m_bShuttingDown || bForceSyncWrite ) //write synchronously + { + filesystem->WriteFile( fullpath, GAMESTATS_PATHID, buf ); + + StatsLog( "Shut down wrote to '%s'\n", fullpath ); + } + else + { + // Allocate memory for async system to use (and free afterward!!!) + size_t nBufferSize = buf.TellPut(); + void *pMem = malloc(nBufferSize); + CUtlBuffer statsBuffer( pMem, nBufferSize ); + statsBuffer.Put( buf.Base(), nBufferSize ); + + // Write data async + filesystem->AsyncWrite( fullpath, statsBuffer.Base(), statsBuffer.TellPut(), true, false ); + } + + return true; +} + +void CBaseGameStats::Event_PlayerConnected( CBasePlayer *pBasePlayer ) +{ + StatsLog( "CBaseGameStats::Event_PlayerConnected [%s]\n", pBasePlayer->GetPlayerName() ); +} + +void CBaseGameStats::Event_PlayerDisconnected( CBasePlayer *pBasePlayer ) +{ + StatsLog( "CBaseGameStats::Event_PlayerDisconnected\n", pBasePlayer->GetPlayerName() ); +} + +void CBaseGameStats::Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info ) +{ + //StatsLog( "CBaseGameStats::Event_PlayerDamage [%s] took %.2f damage\n", pBasePlayer->GetPlayerName(), info.GetDamage() ); +} + +void CBaseGameStats::Event_PlayerKilledOther( CBasePlayer *pAttacker, CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + StatsLog( "CBaseGameStats::Event_PlayerKilledOther [%s] killed [%s]\n", pAttacker->GetPlayerName(), pVictim->GetClassname() ); +} + +void CBaseGameStats::Event_WeaponFired( CBasePlayer *pShooter, bool bPrimary, char const *pchWeaponName ) +{ + StatsLog( "CBaseGameStats::Event_WeaponFired [%s] %s weapon [%s]\n", pShooter->GetPlayerName(), bPrimary ? "primary" : "secondary", pchWeaponName ); +} + +void CBaseGameStats::Event_WeaponHit( CBasePlayer *pShooter, bool bPrimary, char const *pchWeaponName, const CTakeDamageInfo &info ) +{ + StatsLog( "CBaseGameStats::Event_WeaponHit [%s] %s weapon [%s] damage [%f]\n", pShooter->GetPlayerName(), bPrimary ? "primary" : "secondary", pchWeaponName, info.GetDamage() ); +} + +void CBaseGameStats::Event_PlayerEnteredGodMode( CBasePlayer *pBasePlayer ) +{ + StatsLog( "CBaseGameStats::Event_PlayerEnteredGodMode [%s] entered GOD mode\n", pBasePlayer->GetPlayerName() ); +} + +void CBaseGameStats::Event_PlayerEnteredNoClip( CBasePlayer *pBasePlayer ) +{ + StatsLog( "CBaseGameStats::Event_PlayerEnteredNoClip [%s] entered NOCLIPe\n", pBasePlayer->GetPlayerName() ); +} + +void CBaseGameStats::Event_DecrementPlayerEnteredNoClip( CBasePlayer *pBasePlayer ) +{ + StatsLog( "CBaseGameStats::Event_DecrementPlayerEnteredNoClip [%s] decrementing NOCLIPe\n", pBasePlayer->GetPlayerName() ); +} + +void CBaseGameStats::Event_IncrementCountedStatistic( const Vector& vecAbsOrigin, char const *pchStatisticName, float flIncrementAmount ) +{ + StatsLog( "Incrementing %s by %f at pos (%d, %d, %d)\n", pchStatisticName, flIncrementAmount, (int)vecAbsOrigin.x, (int)vecAbsOrigin.y, (int)vecAbsOrigin.z ); +} + +bool CBaseGameStats::UploadStatsFileNOW( void ) +{ + if( !StatsTrackingIsFullyEnabled() ) + { + StatsLog( "UploadStatsFileNOW: stats tracking not fully enabled, not uploading file\n" ); + return false; + } + + if (!HaveValidData() ) + { + StatsLog( "UploadStatsFileNOW: no valid game data, not uploading file\n" ); + return false; + } + + if ( !filesystem->FileExists( gamestats->GetStatSaveFileName(), GAMESTATS_PATHID ) ) + { + StatsLog( "UploadStatsFileNOW: can't find stats file, not uploading file\n" ); + return false; + } + + int curtime = Plat_FloatTime(); + + CBGSDriver.m_tLastUpload = curtime; + + // Update the registry +#ifndef SWDS + IRegistry *reg = InstanceRegistry( "Steam" ); + Assert( reg ); + reg->WriteInt( GetStatUploadRegistryKeyName(), CBGSDriver.m_tLastUpload ); + ReleaseInstancedRegistry( reg ); +#endif + + CUtlBuffer buf; + filesystem->ReadFile( GetStatSaveFileName(), GAMESTATS_PATHID, buf ); + unsigned int uBlobSize = buf.TellPut(); + if ( uBlobSize == 0 ) + { + StatsLog( "UploadStatsFileNOW: can't read stats file, not uploading file\n" ); + return false; + } + + const void *pvBlobData = ( const void * )buf.Base(); + + if( gamestatsuploader ) + { + bool bRet = gamestatsuploader->UploadGameStats( "", + 1, + uBlobSize, + pvBlobData ); + + StatsLog( "UploadStatsFileNow: UploadGameStats %s\n", bRet ? "succeeded" : "failed" ); + } + + return false; +} + + +void CBaseGameStats::LoadingEvent_PlayerIDDifferentThanLoadedStats( void ) +{ + StatsLog( "CBaseGameStats::LoadingEvent_PlayerIDDifferentThanLoadedStats\n" ); +} + + +bool CBaseGameStats::LoadFromFile( void ) +{ + if ( filesystem->FileExists( gamestats->GetStatSaveFileName(), GAMESTATS_PATHID ) ) + { + char fullpath[ 512 ]; + filesystem->RelativePathToFullPath( gamestats->GetStatSaveFileName(), GAMESTATS_PATHID, fullpath, sizeof( fullpath ) ); + StatsLog( "Loading stats from '%s'\n", fullpath ); + } + + CUtlBuffer buf; + if ( filesystem->ReadFile( gamestats->GetStatSaveFileName(), GAMESTATS_PATHID, buf ) ) + { + bool bRetVal = true; + + int version = buf.GetShort(); + if ( version > GAMESTATS_FILE_VERSION ) + return false; //file is beyond our comprehension + + // Set global parse version + CBGSDriver.m_iLoadedVersion = version; + + buf.Get( CBGSDriver.m_szLoadedUserID, 16 ); + CBGSDriver.m_szLoadedUserID[ sizeof( CBGSDriver.m_szLoadedUserID ) - 1 ] = 0; + + if ( s_szPseudoUniqueID[ 0 ] != 0 ) + { + if ( Q_stricmp( CBGSDriver.m_szLoadedUserID, s_szPseudoUniqueID ) ) + { + //UserID changed, blow away log!!! + filesystem->RemoveFile( gamestats->GetStatSaveFileName(), GAMESTATS_PATHID ); + filesystem->RemoveFile( GAMESTATS_LOG_FILE, GAMESTATS_PATHID ); + Warning( "Userid changed, clearing stats file\n" ); + CBGSDriver.m_szLoadedUserID[0] = '\0'; + CBGSDriver.m_iLoadedVersion = -1; + gamestats->m_BasicStats.Clear(); + gamestats->LoadingEvent_PlayerIDDifferentThanLoadedStats(); + bRetVal = false; + } + + if ( version <= GAMESTATS_FILE_VERSION_OLD5 ) + { + gamestats->m_BasicStats.Clear(); + bRetVal = false; + } + else + { + // Peek ahead in buffer to see if we have the "no default stats" secret flag set. + int iCheckForStandardStatsInFile = *( int * )buf.PeekGet(); + bool bValid = true; + + if ( iCheckForStandardStatsInFile != GAMESTATS_STANDARD_NOT_SAVED ) + { + //the GAMESTATS_STANDARD_NOT_SAVED flag coincides with user completion time, rewind so the gamestats parser can grab it + bValid = gamestats->m_BasicStats.ParseFromBuffer( buf, version ); + } + else + { + // skip over the flag + buf.GetInt(); + } + + if( !bValid ) + { + m_BasicStats.Clear(); + } + + if( ( buf.TellPut() - buf.TellGet() ) != 0 ) //more data left, must be custom data + { + gamestats->LoadCustomDataFromBuffer( buf ); + } + } + } + + return bRetVal; + } + else + { + filesystem->RemoveFile( GAMESTATS_LOG_FILE, GAMESTATS_PATHID ); + } + + return false; +} + +bool CBaseGameStats_Driver::Init() +{ + const char *pGameDir = CommandLine()->ParmValue( "-game", "hl2" ); + + //standardizing is a good thing + char szLoweredGameDir[256]; + Q_strncpy( szLoweredGameDir, pGameDir, sizeof( szLoweredGameDir ) ); + Q_strlower( szLoweredGameDir ); + + gamestats = gamestats->OnInit( gamestats, szLoweredGameDir ); + + //determine constant strings needed for saving and uploading + Q_strncpy( s_szSaveFileName, szLoweredGameDir, sizeof( s_szSaveFileName ) ); + Q_strncat( s_szSaveFileName, "_gamestats.dat", sizeof( s_szSaveFileName ) ); + + Q_strncpy( s_szStatUploadRegistryKeyName, "GameStatsUpload_", sizeof( s_szStatUploadRegistryKeyName ) ); + Q_strncat( s_szStatUploadRegistryKeyName, szLoweredGameDir, sizeof( s_szStatUploadRegistryKeyName ) ); + + gamestats->m_bLoggingToFile = CommandLine()->FindParm( "-gamestatsloggingtofile" ) ? true : false; + if ( gamestats->m_bLoggingToFile ) + { + gamestats->m_bLogging = true; + } + else + { + gamestats->m_bLogging = CommandLine()->FindParm( "-gamestatslogging" ) ? true : false; + } + +#if 0 + // This should only impact us internally!!! + if ( IsPC() && !filesystem->IsSteam() ) + { + Warning( "Forcing gamestats logging to file, remove before shipping!!!\n" ); + gamestats->m_bLoggingToFile = true; + } +#endif + + if ( gamestatsuploader ) + { + m_bEnabled = gamestatsuploader->IsGameStatsLoggingEnabled(); + if ( m_bEnabled ) + { + gamestatsuploader->GetPseudoUniqueId( s_szPseudoUniqueID, sizeof( s_szPseudoUniqueID ) ); + } + } + + if ( StatsTrackingIsFullyEnabled() ) + { + // FIXME: Load m_tLastUpload from registry and save it back out, too +#ifndef SWDS + IRegistry *reg = InstanceRegistry( "Steam" ); + Assert( reg ); + m_tLastUpload = reg->ReadInt( gamestats->GetStatUploadRegistryKeyName(), 0 ); + ReleaseInstancedRegistry( reg ); +#endif + + //load existing stats + gamestats->LoadFromFile(); + } + + if ( s_szPseudoUniqueID[ 0 ] != 0 ) + { + gamestats->Event_Init(); + + if( gamestats->AutoSave_OnInit() ) + gamestats->SaveToFileNOW(); + + if( gamestats->AutoUpload_OnInit() ) + gamestats->UploadStatsFileNOW(); + } + else + { + m_bEnabled = false; //unable to generate a pseudo-unique ID, disable tracking + } + + return true; +} + +void CBaseGameStats_Driver::Shutdown() +{ + m_bShuttingDown = true; + + gamestats->Event_Shutdown(); + + if( gamestats->AutoSave_OnShutdown() ) + gamestats->SaveToFileNOW(); + + if( gamestats->AutoUpload_OnShutdown() ) + gamestats->UploadStatsFileNOW(); + + if ( FILESYSTEM_INVALID_HANDLE != g_LogFileHandle ) + { + filesystem->Close( g_LogFileHandle ); + g_LogFileHandle = FILESYSTEM_INVALID_HANDLE; + } +} + +void CBaseGameStats_Driver::PossibleMapChange( void ) +{ + //detect and copy map changes + if ( Q_stricmp( m_PrevMapName.String(), STRING( gpGlobals->mapname ) ) ) + { + MEM_ALLOC_CREDIT(); + + CUtlString PrevMapBackup = m_PrevMapName; + + m_PrevMapName = STRING( gpGlobals->mapname ); + + gamestats->Event_MapChange( PrevMapBackup.String(), STRING( gpGlobals->mapname ) ); + + if( gamestats->AutoSave_OnMapChange() ) + gamestats->SaveToFileNOW(); + + if( gamestats->AutoUpload_OnMapChange() ) + gamestats->UploadStatsFileNOW(); + } +} + + + +void CBaseGameStats_Driver::LevelInitPreEntity() +{ + m_bInLevel = true; + m_bFirstLevel = false; + + if ( Q_stricmp( s_szPseudoUniqueID, "unknown" ) == 0 ) + { + // "unknown" means this is a dedicated server and we weren't able to generate a unique ID (e.g. Linux server). + // Change the unique ID to be a hash of IP & port. We couldn't do this earlier because IP is not known until level + // init time. + ConVar *hostip = cvar->FindVar( "hostip" ); + ConVar *hostport = cvar->FindVar( "hostport" ); + if ( hostip && hostport ) + { + int crcInput[2]; + crcInput[0] = hostip->GetInt(); + crcInput[1] = hostport->GetInt(); + if ( crcInput[0] && crcInput[1] ) + { + CRC32_t crc = CRC32_ProcessSingleBuffer( crcInput, sizeof( crcInput ) ); + Q_snprintf( s_szPseudoUniqueID, ARRAYSIZE( s_szPseudoUniqueID ), "H:%x", crc ); + } + } + } + + PossibleMapChange(); + + m_flPauseStartTime = 0.0f; + m_flLevelStartTime = gpGlobals->realtime; + + gamestats->Event_LevelInit(); + + if( gamestats->AutoSave_OnLevelInit() ) + gamestats->SaveToFileNOW(); + + if( gamestats->AutoUpload_OnLevelInit() ) + gamestats->UploadStatsFileNOW(); +} + + +void CBaseGameStats_Driver::LevelShutdownPreEntity() +{ + float flElapsed = gpGlobals->realtime - m_flLevelStartTime; + + if ( flElapsed < 0.0f ) + { + Assert( 0 ); + Warning( "EVENT_LEVELSHUTDOWN: with negative elapsed time (rt %f starttime %f)\n", gpGlobals->realtime, m_flLevelStartTime ); + flElapsed = 0.0f; + } + + //Assert( m_bInLevel ); //so, apparently shutdowns can happen before inits + if ( m_bInLevel && ( gpGlobals->eLoadType != MapLoad_Background ) ) + { + gamestats->Event_LevelShutdown( flElapsed ); + + if( gamestats->AutoSave_OnLevelShutdown() ) + gamestats->SaveToFileNOW( true ); + + if( gamestats->AutoUpload_OnLevelShutdown() ) + gamestats->UploadStatsFileNOW(); + + m_bInLevel = false; + } +} + +void CBaseGameStats_Driver::OnSave() +{ + gamestats->Event_SaveGame(); +} + + +void CBaseGameStats_Driver::OnRestore() +{ + PossibleMapChange(); + + gamestats->Event_LoadGame(); +} + + +void CBaseGameStats_Driver::FrameUpdatePostEntityThink() +{ + bool bGamePaused = ( gpGlobals->frametime == 0.0f ); + + if ( !m_bInLevel ) + { + m_flPauseStartTime = 0.0f; + } + else if ( m_bGamePaused != bGamePaused ) + { + if ( bGamePaused ) + { + m_flPauseStartTime = gpGlobals->realtime; + } + else if ( m_flPauseStartTime != 0.0f ) + { + float flPausedTime = gpGlobals->realtime - m_flPauseStartTime; + if ( flPausedTime < 0.0f ) + { + Assert( 0 ); + Warning( "Game paused time showing up negative (rt %f pausestart %f)\n", gpGlobals->realtime, m_flPauseStartTime ); + flPausedTime = 0.0f; + } + + // Remove this from global time counters + + // Msg( "Pause: adding %f to level starttime\n", flPausedTime ); + + m_flLevelStartTime += flPausedTime; + m_flPauseStartTime = 0.0f; + + // Msg( "Paused for %.2f seconds\n", flPausedTime ); + } + m_bGamePaused = bGamePaused; + } +} + +bool UserCmdChanged( const CUserCmd& lhs, const CUserCmd& rhs ) +{ + if ( lhs.viewangles != rhs.viewangles ) + return true; + if ( lhs.forwardmove != rhs.forwardmove ) + return true; + if ( lhs.sidemove != rhs.sidemove ) + return true; + if ( lhs.upmove != rhs.upmove ) + return true; + if ( lhs.buttons != rhs.buttons ) + return true; + if ( lhs.impulse != rhs.impulse ) + return true; + if ( lhs.weaponselect != rhs.weaponselect ) + return true; + if ( lhs.weaponsubtype != rhs.weaponsubtype ) + return true; + if ( lhs.mousedx != rhs.mousedx ) + return true; + if ( lhs.mousedy != rhs.mousedy ) + return true; + return false; +} + +bool StatsTrackingIsFullyEnabled( void ) +{ + return CBGSDriver.m_bEnabled && gamestats->StatTrackingEnabledForMod(); +} + +void CBaseGameStats::Clear( void ) +{ + gamestats->m_BasicStats.Clear(); +} + +void CBaseGameStats::SetSteamStatistic( bool bUsingSteam ) +{ + if( CBGSDriver.m_bFirstLevel ) + { + m_BasicStats.m_Summary.m_bSteam = bUsingSteam; + } + + if( CBGSDriver.m_bInLevel ) + { + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + map->m_bSteam = bUsingSteam; + } + + m_BasicStats.m_bSteam = bUsingSteam; +} + +void CBaseGameStats::SetCyberCafeStatistic( bool bIsCyberCafeUser ) +{ + if( CBGSDriver.m_bFirstLevel ) + { + m_BasicStats.m_Summary.m_bCyberCafe = bIsCyberCafeUser; + } + + if( CBGSDriver.m_bInLevel ) + { + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + map->m_bCyberCafe = bIsCyberCafeUser; + } + + m_BasicStats.m_bCyberCafe = bIsCyberCafeUser; +} + +void CBaseGameStats::SetHDRStatistic( bool bHDREnabled ) +{ + if( bHDREnabled ) + { + if( CBGSDriver.m_bInLevel ) + { + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + ++map->m_nHDR; + } + + if( CBGSDriver.m_bFirstLevel ) + { + ++m_BasicStats.m_Summary.m_nHDR; + } + } +} + +void CBaseGameStats::SetCaptionsStatistic( bool bClosedCaptionsEnabled ) +{ + if( CBGSDriver.m_bInLevel ) + { + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + ++map->m_nCaptions; + } + + if( CBGSDriver.m_bFirstLevel ) + { + ++m_BasicStats.m_Summary.m_nCaptions; + } +} + +void CBaseGameStats::SetSkillStatistic( int iSkillSetting ) +{ + int skill = clamp( iSkillSetting, 1, 3 ) - 1; + + if( CBGSDriver.m_bInLevel ) + { + BasicGameStatsRecord_t *map = m_BasicStats.FindOrAddRecordForMap( CBGSDriver.m_PrevMapName.String() ); + ++map->m_nSkill[ skill ]; + } + + if ( CBGSDriver. m_bFirstLevel ) + { + ++m_BasicStats.m_Summary.m_nSkill[ skill ]; + } +} + +void CBaseGameStats::SetDXLevelStatistic( int iDXLevel ) +{ + m_BasicStats.m_nDXLevel = iDXLevel; +} + +void CBaseGameStats::SetHL2UnlockedChapterStatistic( void ) +{ + // Now grab the hl2/cfg/config.cfg and suss out the sv_unlockedchapters cvar to estimate how far they got in HL2 + char const *relative = "cfg/config.cfg"; + char fullpath[ 512 ]; + char gamedir[256]; + engine->GetGameDir( gamedir, 256 ); + Q_snprintf( fullpath, sizeof( fullpath ), "%s/../hl2/%s", gamedir, relative ); + + if ( filesystem->FileExists( fullpath ) ) + { + FileHandle_t fh = filesystem->Open( fullpath, "rb" ); + if ( FILESYSTEM_INVALID_HANDLE != fh ) + { + // read file into memory + int size = filesystem->Size(fh); + char *configBuffer = new char[ size + 1 ]; + filesystem->Read( configBuffer, size, fh ); + configBuffer[size] = 0; + filesystem->Close( fh ); + + // loop through looking for all the cvars to apply + const char *search = Q_stristr(configBuffer, "sv_unlockedchapters" ); + if ( search ) + { + // read over the token + search = strtok( (char *)search, " \n" ); + search = strtok( NULL, " \n" ); + + if ( search[0]== '\"' ) + ++search; + + // read the value + int iChapter = Q_atoi( search ); + m_BasicStats.m_nHL2ChaptureUnlocked = iChapter; + } + + // free + delete [] configBuffer; + } + } +} + +static void CC_ResetGameStats( const CCommand &args ) +{ + gamestats->Clear(); + gamestats->SaveToFileNOW(); + gamestats->StatsLog( "CC_ResetGameStats : Server cleared game stats\n" ); +} + +static ConCommand resetGameStats("_resetgamestats", CC_ResetGameStats, "Erases current game stats and writes out a blank stats file", 0 ); + + + +class CPointGamestatsCounter : public CPointEntity +{ +public: + DECLARE_CLASS( CPointGamestatsCounter, CPointEntity ); + DECLARE_DATADESC(); + + CPointGamestatsCounter(); + +protected: + + void InputSetName( inputdata_t &inputdata ); + void InputIncrement( inputdata_t &inputdata ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); +private: + + string_t m_strStatisticName; + bool m_bDisabled; +}; + +BEGIN_DATADESC( CPointGamestatsCounter ) + + DEFINE_KEYFIELD( m_strStatisticName, FIELD_STRING, "Name" ), + DEFINE_FIELD( m_bDisabled, FIELD_BOOLEAN ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetName", InputSetName ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "Increment", InputIncrement ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( point_gamestats_counter, CPointGamestatsCounter ) + + +CPointGamestatsCounter::CPointGamestatsCounter() : + m_strStatisticName( NULL_STRING ), + m_bDisabled( false ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Changes name of statistic +//----------------------------------------------------------------------------- +void CPointGamestatsCounter::InputSetName( inputdata_t &inputdata ) +{ + m_strStatisticName = inputdata.value.StringID(); +} + +//----------------------------------------------------------------------------- +// Purpose: Changes name of statistic +//----------------------------------------------------------------------------- +void CPointGamestatsCounter::InputIncrement( inputdata_t &inputdata ) +{ + if ( m_bDisabled ) + return; + + if ( NULL_STRING == m_strStatisticName ) + { + DevMsg( 1, "CPointGamestatsCounter::InputIncrement: No stat name specified for point_gamestats_counter @%f, %f, %f [ent index %d]\n", + GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, entindex() ); + return; + } + + gamestats->Event_IncrementCountedStatistic( GetAbsOrigin(), STRING( m_strStatisticName ), inputdata.value.Float() ); +} + +void CPointGamestatsCounter::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +void CPointGamestatsCounter::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} diff --git a/sp/src/game/server/GameStats_BasicStatsFunctions.cpp b/sp/src/game/server/GameStats_BasicStatsFunctions.cpp new file mode 100644 index 00000000..5656b6b7 --- /dev/null +++ b/sp/src/game/server/GameStats_BasicStatsFunctions.cpp @@ -0,0 +1,183 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "gamestats.h" + +void BasicGameStatsRecord_t::Clear() +{ + m_nCount = 0; + m_nSeconds = 0; + m_nCommentary = 0; + m_nHDR = 0; + m_nCaptions = 0; + m_bSteam = true; + m_bCyberCafe = false; + Q_memset( m_nSkill, 0, sizeof( m_nSkill ) ); + m_nDeaths = 0; +} + +void BasicGameStatsRecord_t::SaveToBuffer( CUtlBuffer &buf ) +{ + buf.PutInt( m_nCount ); + buf.PutInt( m_nSeconds ); + buf.PutInt( m_nCommentary ); + buf.PutInt( m_nHDR ); + buf.PutInt( m_nCaptions ); + for ( int i = 0; i < 3; ++i ) + { + buf.PutInt( m_nSkill[ i ] ); + } + + buf.PutChar( m_bSteam ? 1 : 0 ); + buf.PutChar( m_bCyberCafe ? 1 : 0 ); + buf.PutInt( m_nDeaths ); +} + +bool BasicGameStatsRecord_t::ParseFromBuffer( CUtlBuffer &buf, int iBufferStatsVersion ) +{ + bool bret = true; + m_nCount = buf.GetInt(); + + if ( m_nCount > 100000 || m_nCount < 0 ) + { + bret = false; + } + + m_nSeconds = buf.GetInt(); + // Note, don't put the buf.GetInt() in the macro since it'll get evaluated twice!!! + m_nSeconds = MAX( m_nSeconds, 0 ); + + m_nCommentary = buf.GetInt(); + if ( m_nCommentary < 0 || m_nCommentary > 100000 ) + { + bret = false; + } + + m_nHDR = buf.GetInt(); + if ( m_nHDR < 0 || m_nHDR > 100000 ) + { + bret = false; + } + + m_nCaptions = buf.GetInt(); + if ( m_nCaptions < 0 || m_nCaptions > 100000 ) + { + bret = false; + } + + for ( int i = 0; i < 3; ++i ) + { + m_nSkill[ i ] = buf.GetInt(); + if ( m_nSkill[ i ] < 0 || m_nSkill[ i ] > 100000 ) + { + bret = false; + } + } + + if ( iBufferStatsVersion > GAMESTATS_FILE_VERSION_OLD ) + { + m_bSteam = buf.GetChar() ? true : false; + } + if ( iBufferStatsVersion > GAMESTATS_FILE_VERSION_OLD2 ) + { + m_bCyberCafe = buf.GetChar() ? true : false; + } + if ( iBufferStatsVersion > GAMESTATS_FILE_VERSION_OLD5 ) + { + m_nDeaths = buf.GetInt(); + } + + return bret; +} + +void BasicGameStats_t::Clear() +{ + m_nSecondsToCompleteGame = 0; + m_Summary.Clear(); + m_MapTotals.Purge(); +} + +void BasicGameStats_t::SaveToBuffer( CUtlBuffer& buf ) +{ + buf.PutInt( m_nSecondsToCompleteGame ); + + m_Summary.SaveToBuffer( buf ); + + int c = m_MapTotals.Count(); + buf.PutInt( c ); + for ( int i = m_MapTotals.First(); i != m_MapTotals.InvalidIndex(); i = m_MapTotals.Next( i ) ) + { + char const *name = m_MapTotals.GetElementName( i ); + BasicGameStatsRecord_t &rec = m_MapTotals[ i ]; + + buf.PutString( name ); + rec.SaveToBuffer( buf ); + } + + buf.PutChar( (char)m_nHL2ChaptureUnlocked ); + buf.PutChar( m_bSteam ? 1 : 0 ); + buf.PutChar( m_bCyberCafe ? 1 : 0 ); + buf.PutShort( (short)m_nDXLevel ); +} + +BasicGameStatsRecord_t *BasicGameStats_t::FindOrAddRecordForMap( char const *mapname ) +{ + int idx = m_MapTotals.Find( mapname ); + if ( idx == m_MapTotals.InvalidIndex() ) + { + idx = m_MapTotals.Insert( mapname ); + } + + return &m_MapTotals[ idx ]; +} + +bool BasicGameStats_t::ParseFromBuffer( CUtlBuffer& buf, int iBufferStatsVersion ) +{ + bool bret = true; + + m_nSecondsToCompleteGame = buf.GetInt(); + if ( m_nSecondsToCompleteGame < 0 || m_nSecondsToCompleteGame > 10000000 ) + { + bret = false; + } + + m_Summary.ParseFromBuffer( buf, iBufferStatsVersion ); + int c = buf.GetInt(); + if ( c > 1024 || c < 0 ) + { + bret = false; + } + + for ( int i = 0; i < c; ++i ) + { + char mapname[ 256 ]; + buf.GetString( mapname, sizeof( mapname ) ); + + BasicGameStatsRecord_t *rec = FindOrAddRecordForMap( mapname ); + bool valid= rec->ParseFromBuffer( buf, iBufferStatsVersion ); + if ( !valid ) + { + bret = false; + } + } + + if ( iBufferStatsVersion >= GAMESTATS_FILE_VERSION_OLD2 ) + { + m_nHL2ChaptureUnlocked = (int)buf.GetChar(); + m_bSteam = buf.GetChar() ? true : false; + } + if ( iBufferStatsVersion > GAMESTATS_FILE_VERSION_OLD2 ) + { + m_bCyberCafe = buf.GetChar() ? true : false; + } + if ( iBufferStatsVersion > GAMESTATS_FILE_VERSION_OLD3 ) + { + m_nDXLevel = (int)buf.GetShort(); + } + return bret; +} + diff --git a/sp/src/game/server/MaterialModifyControl.cpp b/sp/src/game/server/MaterialModifyControl.cpp new file mode 100644 index 00000000..05d8714f --- /dev/null +++ b/sp/src/game/server/MaterialModifyControl.cpp @@ -0,0 +1,289 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Material modify control entity. +// +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//------------------------------------------------------------------------------ +// FIXME: This really should inherit from something more lightweight. +//------------------------------------------------------------------------------ + +#define MATERIAL_MODIFY_STRING_SIZE 255 +#define MATERIAL_MODIFY_ANIMATION_UNSET -1 + +// Must match C_MaterialModifyControl.cpp +enum MaterialModifyMode_t +{ + MATERIAL_MODIFY_MODE_NONE = 0, + MATERIAL_MODIFY_MODE_SETVAR = 1, + MATERIAL_MODIFY_MODE_ANIM_SEQUENCE = 2, + MATERIAL_MODIFY_MODE_FLOAT_LERP = 3, +}; + +ConVar debug_materialmodifycontrol( "debug_materialmodifycontrol", "0" ); + +class CMaterialModifyControl : public CBaseEntity +{ +public: + + DECLARE_CLASS( CMaterialModifyControl, CBaseEntity ); + + CMaterialModifyControl(); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + int UpdateTransmitState(); + int ShouldTransmit( const CCheckTransmitInfo *pInfo ); + + void SetMaterialVar( inputdata_t &inputdata ); + void SetMaterialVarToCurrentTime( inputdata_t &inputdata ); + void InputStartAnimSequence( inputdata_t &inputdata ); + void InputStartFloatLerp( inputdata_t &inputdata ); + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + CNetworkString( m_szMaterialName, MATERIAL_MODIFY_STRING_SIZE ); + CNetworkString( m_szMaterialVar, MATERIAL_MODIFY_STRING_SIZE ); + CNetworkString( m_szMaterialVarValue, MATERIAL_MODIFY_STRING_SIZE ); + CNetworkVar( int, m_iFrameStart ); + CNetworkVar( int, m_iFrameEnd ); + CNetworkVar( bool, m_bWrap ); + CNetworkVar( float, m_flFramerate ); + CNetworkVar( bool, m_bNewAnimCommandsSemaphore ); + CNetworkVar( float, m_flFloatLerpStartValue ); + CNetworkVar( float, m_flFloatLerpEndValue ); + CNetworkVar( float, m_flFloatLerpTransitionTime ); + CNetworkVar( int, m_nModifyMode ); +}; + +LINK_ENTITY_TO_CLASS(material_modify_control, CMaterialModifyControl); + +BEGIN_DATADESC( CMaterialModifyControl ) + // Variables. + DEFINE_AUTO_ARRAY( m_szMaterialName, FIELD_CHARACTER ), + DEFINE_AUTO_ARRAY( m_szMaterialVar, FIELD_CHARACTER ), + DEFINE_AUTO_ARRAY( m_szMaterialVarValue, FIELD_CHARACTER ), + DEFINE_FIELD( m_iFrameStart, FIELD_INTEGER ), + DEFINE_FIELD( m_iFrameEnd, FIELD_INTEGER ), + DEFINE_FIELD( m_bWrap, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flFramerate, FIELD_FLOAT ), + DEFINE_FIELD( m_bNewAnimCommandsSemaphore, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flFloatLerpStartValue, FIELD_FLOAT ), + DEFINE_FIELD( m_flFloatLerpEndValue, FIELD_FLOAT ), + DEFINE_FIELD( m_flFloatLerpTransitionTime, FIELD_FLOAT ), + DEFINE_FIELD( m_nModifyMode, FIELD_INTEGER ), + // Inputs. + DEFINE_INPUTFUNC( FIELD_STRING, "SetMaterialVar", SetMaterialVar ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetMaterialVarToCurrentTime", SetMaterialVarToCurrentTime ), + DEFINE_INPUTFUNC( FIELD_STRING, "StartAnimSequence", InputStartAnimSequence ), + DEFINE_INPUTFUNC( FIELD_STRING, "StartFloatLerp", InputStartFloatLerp ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST(CMaterialModifyControl, DT_MaterialModifyControl) + SendPropString( SENDINFO( m_szMaterialName ) ), + SendPropString( SENDINFO( m_szMaterialVar ) ), + SendPropString( SENDINFO( m_szMaterialVarValue ) ), + SendPropInt( SENDINFO(m_iFrameStart), 8 ), + SendPropInt( SENDINFO(m_iFrameEnd), 8 ), + SendPropInt( SENDINFO(m_bWrap), 1, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_flFramerate), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO(m_bNewAnimCommandsSemaphore), 1, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_flFloatLerpStartValue), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flFloatLerpEndValue), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flFloatLerpTransitionTime), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO(m_nModifyMode), 2, SPROP_UNSIGNED ), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CMaterialModifyControl::CMaterialModifyControl() +{ + m_iFrameStart = MATERIAL_MODIFY_ANIMATION_UNSET; + m_iFrameEnd = MATERIAL_MODIFY_ANIMATION_UNSET; + m_nModifyMode = MATERIAL_MODIFY_MODE_NONE; +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CMaterialModifyControl::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +bool CMaterialModifyControl::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "materialName" ) ) + { + Q_strncpy( m_szMaterialName.GetForModify(), szValue, MATERIAL_MODIFY_STRING_SIZE ); + return true; + } + + if ( FStrEq( szKeyName, "materialVar" ) ) + { + Q_strncpy( m_szMaterialVar.GetForModify(), szValue, MATERIAL_MODIFY_STRING_SIZE ); + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +//------------------------------------------------------------------------------ +// Purpose : Send even though we don't have a model. +//------------------------------------------------------------------------------ +int CMaterialModifyControl::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_FULLCHECK ); +} + +//----------------------------------------------------------------------------- +// Send if the parent is being sent: +//----------------------------------------------------------------------------- +int CMaterialModifyControl::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + CBaseEntity *pEnt = GetMoveParent(); + if ( pEnt ) + { + return pEnt->ShouldTransmit( pInfo ); + } + + return FL_EDICT_DONTSEND; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMaterialModifyControl::SetMaterialVar( inputdata_t &inputdata ) +{ + //if( debug_materialmodifycontrol.GetBool() && Q_stristr( GetDebugName(), "alyx" ) ) + //{ + //DevMsg( 1, "CMaterialModifyControl::SetMaterialVar %s %s %s=\"%s\"\n", + //GetDebugName(), m_szMaterialName.Get(), m_szMaterialVar.Get(), inputdata.value.String() ); + //} + Q_strncpy( m_szMaterialVarValue.GetForModify(), inputdata.value.String(), MATERIAL_MODIFY_STRING_SIZE ); + m_nModifyMode = MATERIAL_MODIFY_MODE_SETVAR; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMaterialModifyControl::SetMaterialVarToCurrentTime( inputdata_t &inputdata ) +{ + char temp[32]; + Q_snprintf( temp, 32, "%f", gpGlobals->curtime ); + Q_strncpy( m_szMaterialVarValue.GetForModify(), temp, MATERIAL_MODIFY_STRING_SIZE ); + m_nModifyMode = MATERIAL_MODIFY_MODE_SETVAR; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMaterialModifyControl::InputStartAnimSequence( inputdata_t &inputdata ) +{ + char parseString[255]; + Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); + + // Get the start & end frames + char *pszParam = strtok(parseString," "); + if ( pszParam && pszParam[0] ) + { + int iFrameStart = atoi(pszParam); + + pszParam = strtok(NULL," "); + if ( pszParam && pszParam[0] ) + { + int iFrameEnd = atoi(pszParam); + + pszParam = strtok(NULL," "); + if ( pszParam && pszParam[0] ) + { + float flFramerate = atof(pszParam); + + pszParam = strtok(NULL," "); + if ( pszParam && pszParam[0] ) + { + bool bWrap = atoi(pszParam) != 0; + + // Got all the parameters. Save 'em and return; + m_iFrameStart = iFrameStart; + m_iFrameEnd = iFrameEnd; + m_flFramerate = flFramerate; + m_bWrap = bWrap; + m_nModifyMode = MATERIAL_MODIFY_MODE_ANIM_SEQUENCE; + m_bNewAnimCommandsSemaphore = !m_bNewAnimCommandsSemaphore; + return; + } + } + } + } + + Warning("%s (%s) received StartAnimSequence input without correct parameters. Syntax: \nSetting to -1 uses the last frame of the texture. should be 1 or 0.\n", GetClassname(), GetDebugName() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMaterialModifyControl::InputStartFloatLerp( inputdata_t &inputdata ) +{ + char parseString[255]; + Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); + +// if( debug_materialmodifycontrol.GetBool() )//&& Q_stristr( GetDebugName(), "alyx" ) ) +// { +// DevMsg( 1, "CMaterialModifyControl::InputStartFloatLerp %s %s %s \"%s\"\n", +// GetDebugName(), m_szMaterialName.Get(), m_szMaterialVar.Get(), inputdata.value.String() ); +// } + + // Get the start & end values + char *pszParam = strtok(parseString," "); + if ( pszParam && pszParam[0] ) + { + float flStartValue = atof(pszParam); + + pszParam = strtok(NULL," "); + if ( pszParam && pszParam[0] ) + { + float flEndValue = atof(pszParam); + + pszParam = strtok(NULL," "); + if ( pszParam && pszParam[0] ) + { + float flTransitionTime = atof(pszParam); + + pszParam = strtok(NULL," "); + if ( pszParam && pszParam[0] ) + { + bool bWrap = atoi(pszParam) != 0; + // We don't implement wrap currently. + bWrap = bWrap; + + // Got all the parameters. Save 'em and return; + m_flFloatLerpStartValue = flStartValue; + m_flFloatLerpEndValue = flEndValue; + m_flFloatLerpTransitionTime = flTransitionTime; + m_nModifyMode = MATERIAL_MODIFY_MODE_FLOAT_LERP; + m_bNewAnimCommandsSemaphore = !m_bNewAnimCommandsSemaphore; + return; + } + } + } + } + + Warning("%s (%s) received StartFloatLerp input without correct parameters. Syntax: \n should be 1 or 0.\n", GetClassname(), GetDebugName() ); +} diff --git a/sp/src/game/server/PointAngularVelocitySensor.cpp b/sp/src/game/server/PointAngularVelocitySensor.cpp new file mode 100644 index 00000000..03e6a45c --- /dev/null +++ b/sp/src/game/server/PointAngularVelocitySensor.cpp @@ -0,0 +1,529 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Used to fire events based on the orientation of a given entity. +// +// Looks at its target's anglular velocity every frame and fires outputs +// as the angular velocity passes a given threshold value. +// +//=============================================================================// + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "mathlib/mathlib.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +enum +{ + AVELOCITY_SENSOR_NO_LAST_RESULT = -2 +}; + +ConVar g_debug_angularsensor( "g_debug_angularsensor", "0", FCVAR_CHEAT ); + +class CPointAngularVelocitySensor : public CPointEntity +{ + DECLARE_CLASS( CPointAngularVelocitySensor, CPointEntity ); + +public: + + CPointAngularVelocitySensor(); + void Activate(void); + void Spawn(void); + void Think(void); + +private: + + float SampleAngularVelocity(CBaseEntity *pEntity); + int CompareToThreshold(CBaseEntity *pEntity, float flThreshold, bool bFireVelocityOutput); + void FireCompareOutput(int nCompareResult, CBaseEntity *pActivator); + void DrawDebugLines( void ); + + // Input handlers + void InputTest( inputdata_t &inputdata ); + void InputTestWithInterval( inputdata_t &inputdata ); + + EHANDLE m_hTargetEntity; // Entity whose angles are being monitored. + float m_flThreshold; // The threshold angular velocity that we are looking for. + int m_nLastCompareResult; // The comparison result from our last measurement, expressed as -1, 0, or 1 + int m_nLastFireResult; // The last result for which we fire the output. + + float m_flFireTime; + float m_flFireInterval; + float m_flLastAngVelocity; + + QAngle m_lastOrientation; + + Vector m_vecAxis; + bool m_bUseHelper; + + // Outputs + COutputFloat m_AngularVelocity; + + // Compare the target's angular velocity to the threshold velocity and fire the appropriate output. + // These outputs are filtered by m_flFireInterval to ignore excessive oscillations. + COutputEvent m_OnLessThan; + COutputEvent m_OnLessThanOrEqualTo; + COutputEvent m_OnGreaterThan; + COutputEvent m_OnGreaterThanOrEqualTo; + COutputEvent m_OnEqualTo; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(point_angularvelocitysensor, CPointAngularVelocitySensor); + + +BEGIN_DATADESC( CPointAngularVelocitySensor ) + + // Fields + DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ), + DEFINE_KEYFIELD(m_flThreshold, FIELD_FLOAT, "threshold"), + DEFINE_FIELD(m_nLastCompareResult, FIELD_INTEGER), + DEFINE_FIELD( m_nLastFireResult, FIELD_INTEGER ), + DEFINE_FIELD( m_flFireTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_flFireInterval, FIELD_FLOAT, "fireinterval" ), + DEFINE_FIELD( m_flLastAngVelocity, FIELD_FLOAT ), + DEFINE_FIELD( m_lastOrientation, FIELD_VECTOR ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Test", InputTest), + DEFINE_INPUTFUNC(FIELD_VOID, "TestWithInterval", InputTestWithInterval), + + // Outputs + DEFINE_OUTPUT(m_OnLessThan, "OnLessThan"), + DEFINE_OUTPUT(m_OnLessThanOrEqualTo, "OnLessThanOrEqualTo"), + DEFINE_OUTPUT(m_OnGreaterThan, "OnGreaterThan"), + DEFINE_OUTPUT(m_OnGreaterThanOrEqualTo, "OnGreaterThanOrEqualTo"), + DEFINE_OUTPUT(m_OnEqualTo, "OnEqualTo"), + DEFINE_OUTPUT(m_AngularVelocity, "AngularVelocity"), + + DEFINE_KEYFIELD( m_vecAxis, FIELD_VECTOR, "axis" ), + DEFINE_KEYFIELD( m_bUseHelper, FIELD_BOOLEAN, "usehelper" ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: constructor provides default values +//----------------------------------------------------------------------------- +CPointAngularVelocitySensor::CPointAngularVelocitySensor() +{ + m_flFireInterval = 0.2f; +} + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning after parsing keyvalues. +//----------------------------------------------------------------------------- +void CPointAngularVelocitySensor::Spawn(void) +{ + m_flThreshold = fabs(m_flThreshold); + m_nLastFireResult = AVELOCITY_SENSOR_NO_LAST_RESULT; + m_nLastCompareResult = AVELOCITY_SENSOR_NO_LAST_RESULT; + // m_flFireInterval = 0.2; + m_lastOrientation = vec3_angle; +} + + +//----------------------------------------------------------------------------- +// Purpose: Called after all entities in the map have spawned. +//----------------------------------------------------------------------------- +void CPointAngularVelocitySensor::Activate(void) +{ + BaseClass::Activate(); + + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target ); + + if (m_hTargetEntity) + { + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draws magic lines... +//----------------------------------------------------------------------------- +void CPointAngularVelocitySensor::DrawDebugLines( void ) +{ + if ( m_hTargetEntity ) + { + Vector vForward, vRight, vUp; + AngleVectors( m_hTargetEntity->GetAbsAngles(), &vForward, &vRight, &vUp ); + + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + vForward * 64, 255, 0, 0, false, 0 ); + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + vRight * 64, 0, 255, 0, false, 0 ); + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + vUp * 64, 0, 0, 255, false, 0 ); + } + + if ( m_bUseHelper == true ) + { + QAngle Angles; + Vector vAxisForward, vAxisRight, vAxisUp; + + Vector vLine = m_vecAxis - GetAbsOrigin(); + + VectorNormalize( vLine ); + + VectorAngles( vLine, Angles ); + AngleVectors( Angles, &vAxisForward, &vAxisRight, &vAxisUp ); + + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + vAxisForward * 64, 255, 0, 0, false, 0 ); + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + vAxisRight * 64, 0, 255, 0, false, 0 ); + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + vAxisUp * 64, 0, 0, 255, false, 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the magnitude of the entity's angular velocity. +//----------------------------------------------------------------------------- +float CPointAngularVelocitySensor::SampleAngularVelocity(CBaseEntity *pEntity) +{ + if (pEntity->GetMoveType() == MOVETYPE_VPHYSICS) + { + IPhysicsObject *pPhys = pEntity->VPhysicsGetObject(); + if (pPhys != NULL) + { + Vector vecVelocity; + AngularImpulse vecAngVelocity; + pPhys->GetVelocity(&vecVelocity, &vecAngVelocity); + + QAngle angles; + pPhys->GetPosition( NULL, &angles ); + + float dt = gpGlobals->curtime - GetLastThink(); + if ( dt == 0 ) + dt = 0.1; + + // HACKHACK: We don't expect a real 'delta' orientation here, just enough of an error estimate to tell if this thing + // is trying to move, but failing. + QAngle delta = angles - m_lastOrientation; + + if ( ( delta.Length() / dt ) < ( vecAngVelocity.Length() * 0.01 ) ) + { + return 0.0f; + } + m_lastOrientation = angles; + + if ( m_bUseHelper == false ) + { + return vecAngVelocity.Length(); + } + else + { + Vector vLine = m_vecAxis - GetAbsOrigin(); + VectorNormalize( vLine ); + + Vector vecWorldAngVelocity; + pPhys->LocalToWorldVector( &vecWorldAngVelocity, vecAngVelocity ); + float flDot = DotProduct( vecWorldAngVelocity, vLine ); + + return flDot; + } + } + } + else + { + QAngle vecAngVel = pEntity->GetLocalAngularVelocity(); + float flMax = MAX(fabs(vecAngVel[PITCH]), fabs(vecAngVel[YAW])); + + return MAX(flMax, fabs(vecAngVel[ROLL])); + } + + return 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Compares the given entity's angular velocity to the threshold velocity. +// Input : pEntity - Entity whose angular velocity is being measured. +// flThreshold - +// Output : Returns -1 if less than, 0 if equal to, or 1 if greater than the threshold. +//----------------------------------------------------------------------------- +int CPointAngularVelocitySensor::CompareToThreshold(CBaseEntity *pEntity, float flThreshold, bool bFireVelocityOutput) +{ + if (pEntity == NULL) + { + return 0; + } + + float flAngVelocity = SampleAngularVelocity(pEntity); + + if ( g_debug_angularsensor.GetBool() ) + { + DrawDebugLines(); + } + + if (bFireVelocityOutput && (flAngVelocity != m_flLastAngVelocity)) + { + m_AngularVelocity.Set(flAngVelocity, pEntity, this); + m_flLastAngVelocity = flAngVelocity; + } + + if (flAngVelocity > flThreshold) + { + return 1; + } + + if (flAngVelocity == flThreshold) + { + return 0; + } + + return -1; +} + + +//----------------------------------------------------------------------------- +// Called every frame to sense the angular velocity of the target entity. +// Output is filtered by m_flFireInterval to ignore excessive oscillations. +//----------------------------------------------------------------------------- +void CPointAngularVelocitySensor::Think(void) +{ + if (m_hTargetEntity != NULL) + { + // + // Check to see if the measure entity's angular velocity has been within + // tolerance of the threshold for the given period of time. + // + int nCompare = CompareToThreshold(m_hTargetEntity, m_flThreshold, true); + if (nCompare != m_nLastCompareResult) + { + // If we've oscillated back to where we last fired the output, don't + // fire the same output again. + if (nCompare == m_nLastFireResult) + { + m_flFireTime = 0; + } + else if (m_nLastCompareResult != AVELOCITY_SENSOR_NO_LAST_RESULT) + { + // + // The value has changed -- reset the timer. We'll fire the output if + // it stays at this value until the interval expires. + // + m_flFireTime = gpGlobals->curtime + m_flFireInterval; + } + + m_nLastCompareResult = nCompare; + } + else if ((m_flFireTime != 0) && (gpGlobals->curtime >= m_flFireTime)) + { + // + // The compare result has held steady long enough -- time to + // fire the output. + // + FireCompareOutput(nCompare, this); + m_nLastFireResult = nCompare; + m_flFireTime = 0; + } + + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +// Fires the output after the fire interval if the velocity is stable. +//----------------------------------------------------------------------------- +void CPointAngularVelocitySensor::InputTestWithInterval( inputdata_t &inputdata ) +{ + if (m_hTargetEntity != NULL) + { + m_flFireTime = gpGlobals->curtime + m_flFireInterval; + m_nLastFireResult = AVELOCITY_SENSOR_NO_LAST_RESULT; + m_nLastCompareResult = CompareToThreshold(m_hTargetEntity, m_flThreshold, true); + + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for forcing an instantaneous test of the condition. +//----------------------------------------------------------------------------- +void CPointAngularVelocitySensor::InputTest( inputdata_t &inputdata ) +{ + int nCompareResult = CompareToThreshold(m_hTargetEntity, m_flThreshold, false); + FireCompareOutput(nCompareResult, inputdata.pActivator); +} + + +//----------------------------------------------------------------------------- +// Purpose: Fires the appropriate output based on the given comparison result. +// Input : nCompareResult - +// pActivator - +//----------------------------------------------------------------------------- +void CPointAngularVelocitySensor::FireCompareOutput( int nCompareResult, CBaseEntity *pActivator ) +{ + if (nCompareResult == -1) + { + m_OnLessThan.FireOutput(pActivator, this); + m_OnLessThanOrEqualTo.FireOutput(pActivator, this); + } + else if (nCompareResult == 1) + { + m_OnGreaterThan.FireOutput(pActivator, this); + m_OnGreaterThanOrEqualTo.FireOutput(pActivator, this); + } + else + { + m_OnEqualTo.FireOutput(pActivator, this); + m_OnLessThanOrEqualTo.FireOutput(pActivator, this); + m_OnGreaterThanOrEqualTo.FireOutput(pActivator, this); + } +} + +// ============================================================================ +// +// Simple velocity sensor +// +// ============================================================================ + +class CPointVelocitySensor : public CPointEntity +{ + DECLARE_CLASS( CPointVelocitySensor, CPointEntity ); + +public: + + void Spawn(); + void Activate( void ); + void Think( void ); + +private: + + void SampleVelocity( void ); + + EHANDLE m_hTargetEntity; // Entity whose angles are being monitored. + Vector m_vecAxis; // Axis along which to measure the speed. + bool m_bEnabled; // Whether we're measuring or not + + // Outputs + float m_fPrevVelocity; // stores velocity from last frame, so we only write the output if it has changed + COutputFloat m_Velocity; + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( point_velocitysensor, CPointVelocitySensor ); + +BEGIN_DATADESC( CPointVelocitySensor ) + + // Fields + DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_vecAxis, FIELD_VECTOR, "axis" ), + DEFINE_KEYFIELD( m_bEnabled, FIELD_BOOLEAN, "enabled" ), + DEFINE_FIELD( m_fPrevVelocity, FIELD_FLOAT ), + + // Outputs + DEFINE_OUTPUT( m_Velocity, "Velocity" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CPointVelocitySensor::Spawn() +{ + Vector vLine = m_vecAxis - GetAbsOrigin(); + VectorNormalize( vLine ); + m_vecAxis = vLine; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointVelocitySensor::Activate( void ) +{ + BaseClass::Activate(); + + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target ); + + if ( m_bEnabled && m_hTargetEntity ) + { + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointVelocitySensor::InputEnable( inputdata_t &inputdata ) +{ + // Don't interrupt us if we're already enabled + if ( m_bEnabled ) + return; + + m_bEnabled = true; + + if ( m_hTargetEntity ) + { + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointVelocitySensor::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Called every frame +//----------------------------------------------------------------------------- +void CPointVelocitySensor::Think( void ) +{ + if ( m_hTargetEntity != NULL && m_bEnabled ) + { + SampleVelocity(); + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the magnitude of the entity's angular velocity. +//----------------------------------------------------------------------------- +void CPointVelocitySensor::SampleVelocity( void ) +{ + if ( m_hTargetEntity == NULL ) + return; + + Vector vecVelocity; + + if ( m_hTargetEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + IPhysicsObject *pPhys = m_hTargetEntity->VPhysicsGetObject(); + if ( pPhys != NULL ) + { + pPhys->GetVelocity( &vecVelocity, NULL ); + } + } + else + { + vecVelocity = m_hTargetEntity->GetAbsVelocity(); + } + + /* + float flSpeed = VectorNormalize( vecVelocity ); + float flDot = ( m_vecAxis != vec3_origin ) ? DotProduct( vecVelocity, m_vecAxis ) : 1.0f; + */ + // We want the component of the velocity vector in the direction of the axis, which since the + // axis is normalized is simply their dot product (eg V . A = |V|*|A|*cos(theta) ) + m_fPrevVelocity = ( m_vecAxis != vec3_origin ) ? DotProduct( vecVelocity, m_vecAxis ) : 1.0f; + + // if it's changed since the last frame, poke the output + if ( m_fPrevVelocity != m_Velocity.Get() ) + { + m_Velocity.Set( m_fPrevVelocity, NULL, NULL ); + } +} diff --git a/sp/src/game/server/RagdollBoogie.cpp b/sp/src/game/server/RagdollBoogie.cpp new file mode 100644 index 00000000..0d54418d --- /dev/null +++ b/sp/src/game/server/RagdollBoogie.cpp @@ -0,0 +1,478 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Dissolve entity to be attached to target entity. Serves two purposes: +// +// 1) An entity that can be placed by a level designer and triggered +// to ignite a target entity. +// +// 2) An entity that can be created at runtime to ignite a target entity. +// +//=============================================================================// + +#include "cbase.h" +#include "RagdollBoogie.h" +#include "physics_prop_ragdoll.h" +#include "effect_dispatch_data.h" +#include "te_effect_dispatch.h" +#include "IEffects.h" +#ifdef MAPBASE +#include "saverestore_utlvector.h" +#include "interval.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Make electriciy every so often +//----------------------------------------------------------------------------- +static const char *s_pZapContext = "ZapContext"; + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CRagdollBoogie ) + + DEFINE_FIELD( m_flStartTime, FIELD_TIME ), + DEFINE_FIELD( m_flBoogieLength, FIELD_FLOAT ), + DEFINE_FIELD( m_flMagnitude, FIELD_FLOAT ), + + // Think this should be handled by StartTouch/etc. +// DEFINE_FIELD( m_nSuppressionCount, FIELD_INTEGER ), + +#ifdef MAPBASE + DEFINE_FIELD( m_vecColor, FIELD_VECTOR ), +#endif + + DEFINE_FUNCTION( BoogieThink ), + DEFINE_FUNCTION( ZapThink ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_ragdoll_boogie, CRagdollBoogie ); + + +//----------------------------------------------------------------------------- +// Purpose: Creates a flame and attaches it to a target entity. +// Input : pTarget - +//----------------------------------------------------------------------------- +CRagdollBoogie *CRagdollBoogie::Create( CBaseEntity *pTarget, float flMagnitude, +#ifdef MAPBASE + float flStartTime, float flLengthTime, int nSpawnFlags, const Vector *vecColor ) +#else + float flStartTime, float flLengthTime, int nSpawnFlags ) +#endif +{ + CRagdollProp *pRagdoll = dynamic_cast< CRagdollProp* >( pTarget ); + if ( !pRagdoll ) + return NULL; + + CRagdollBoogie *pBoogie = (CRagdollBoogie *)CreateEntityByName( "env_ragdoll_boogie" ); + if ( pBoogie == NULL ) + return NULL; + + pBoogie->AddSpawnFlags( nSpawnFlags ); + pBoogie->AttachToEntity( pTarget ); + pBoogie->SetBoogieTime( flStartTime, flLengthTime ); + pBoogie->SetMagnitude( flMagnitude ); +#ifdef MAPBASE + if (vecColor != NULL) + pBoogie->SetColor( *vecColor ); +#endif + pBoogie->Spawn(); + return pBoogie; +} + + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CRagdollBoogie::Spawn() +{ + BaseClass::Spawn(); + + SetThink( &CRagdollBoogie::BoogieThink ); + SetNextThink( gpGlobals->curtime + 0.01f ); + + if ( HasSpawnFlags( SF_RAGDOLL_BOOGIE_ELECTRICAL ) ) + { + SetContextThink( &CRagdollBoogie::ZapThink, gpGlobals->curtime + random->RandomFloat( 0.1f, 0.3f ), s_pZapContext ); + } +} + + +//----------------------------------------------------------------------------- +// Zap! +//----------------------------------------------------------------------------- +void CRagdollBoogie::ZapThink() +{ + if ( !GetMoveParent() ) + return; + + CBaseAnimating *pRagdoll = GetMoveParent()->GetBaseAnimating(); + if ( !pRagdoll ) + return; + + // Make electricity on the client + CStudioHdr *pStudioHdr = pRagdoll->GetModelPtr( ); + if (!pStudioHdr) + return; + + mstudiohitboxset_t *set = pStudioHdr->pHitboxSet( pRagdoll->GetHitboxSet() ); + + if ( set->numhitboxes == 0 ) + return; + + if ( m_nSuppressionCount == 0 ) + { + CEffectData data; + + data.m_nEntIndex = GetMoveParent()->entindex(); + data.m_flMagnitude = 4; + data.m_flScale = HasSpawnFlags(SF_RAGDOLL_BOOGIE_ELECTRICAL_NARROW_BEAM) ? 1.0f : 2.0f; +#ifdef MAPBASE + if (!m_vecColor.IsZero()) + { + data.m_bCustomColors = true; + data.m_CustomColors.m_vecColor1 = m_vecColor; + } +#endif + + DispatchEffect( "TeslaHitboxes", data ); + } + +#ifdef HL2_EPISODIC + EmitSound( "RagdollBoogie.Zap" ); +#endif + + SetContextThink( &CRagdollBoogie::ZapThink, gpGlobals->curtime + random->RandomFloat( 0.1f, 0.3f ), s_pZapContext ); +} + + +//----------------------------------------------------------------------------- +// Suppression count +//----------------------------------------------------------------------------- +void CRagdollBoogie::IncrementSuppressionCount( CBaseEntity *pTarget ) +{ + // Look for other boogies on the ragdoll + kill them + for ( CBaseEntity *pChild = pTarget->FirstMoveChild(); pChild; pChild = pChild->NextMovePeer() ) + { + CRagdollBoogie *pBoogie = dynamic_cast(pChild); + if ( !pBoogie ) + continue; + + ++pBoogie->m_nSuppressionCount; + } +} + +void CRagdollBoogie::DecrementSuppressionCount( CBaseEntity *pTarget ) +{ + // Look for other boogies on the ragdoll + kill them + CBaseEntity *pNext; + for ( CBaseEntity *pChild = pTarget->FirstMoveChild(); pChild; pChild = pNext ) + { + pNext = pChild->NextMovePeer(); + CRagdollBoogie *pBoogie = dynamic_cast(pChild); + if ( !pBoogie ) + continue; + + if ( --pBoogie->m_nSuppressionCount <= 0 ) + { + pBoogie->m_nSuppressionCount = 0; + + float dt = gpGlobals->curtime - pBoogie->m_flStartTime; + if ( dt >= pBoogie->m_flBoogieLength ) + { + PhysCallbackRemove( pBoogie->NetworkProp() ); + } + } + } +} + + +//----------------------------------------------------------------------------- +// Attach to an entity +//----------------------------------------------------------------------------- +void CRagdollBoogie::AttachToEntity( CBaseEntity *pTarget ) +{ + m_nSuppressionCount = 0; + + // Look for other boogies on the ragdoll + kill them + CBaseEntity *pNext; + for ( CBaseEntity *pChild = pTarget->FirstMoveChild(); pChild; pChild = pNext ) + { + pNext = pChild->NextMovePeer(); + CRagdollBoogie *pBoogie = dynamic_cast(pChild); + if ( !pBoogie ) + continue; + + m_nSuppressionCount = pBoogie->m_nSuppressionCount; + UTIL_Remove( pChild ); + } + + FollowEntity( pTarget ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : lifetime - +//----------------------------------------------------------------------------- +void CRagdollBoogie::SetBoogieTime( float flStartTime, float flLengthTime ) +{ + m_flStartTime = flStartTime; + m_flBoogieLength = flLengthTime; +} + + +//----------------------------------------------------------------------------- +// Purpose: Burn targets around us +//----------------------------------------------------------------------------- +void CRagdollBoogie::SetMagnitude( float flMagnitude ) +{ + m_flMagnitude = flMagnitude; +} + + +//----------------------------------------------------------------------------- +// Purpose: Burn targets around us +//----------------------------------------------------------------------------- +void CRagdollBoogie::BoogieThink( void ) +{ + CRagdollProp *pRagdoll = dynamic_cast< CRagdollProp* >( GetMoveParent() ); + if ( !pRagdoll ) + { + UTIL_Remove( this ); + return; + } + + float flMagnitude = m_flMagnitude; + if ( m_flBoogieLength != 0 ) + { + float dt = gpGlobals->curtime - m_flStartTime; + if ( dt >= m_flBoogieLength ) + { + // Don't remove while suppressed... this helps if we try to start another boogie + if ( m_nSuppressionCount == 0 ) + { + UTIL_Remove( this ); + } + SetThink( NULL ); + return; + } + + if ( dt < 0 ) + { + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.1, 0.2f ) ); + return; + } + + flMagnitude = SimpleSplineRemapVal( dt, 0.0f, m_flBoogieLength, m_flMagnitude, 0.0f ); + } + +#ifndef _XBOX + if ( m_nSuppressionCount == 0 ) + { + ragdoll_t *pRagdollPhys = pRagdoll->GetRagdoll( ); + for ( int j = 0; j < pRagdollPhys->listCount; ++j ) + { + float flMass = pRagdollPhys->list[j].pObject->GetMass(); + float flForce = m_flMagnitude * flMass; + + Vector vecForce; + vecForce = RandomVector( -flForce, flForce ); + pRagdollPhys->list[j].pObject->ApplyForceCenter( vecForce ); + } + } +#endif // !_XBOX + + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.1, 0.2f ) ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Allows mappers to control ragdoll dancing +//----------------------------------------------------------------------------- +class CPointRagdollBoogie : public CBaseEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CPointRagdollBoogie, CBaseEntity ); + +public: + bool ApplyBoogie(CBaseEntity *pTarget, CBaseEntity *pActivator); + + void InputActivate( inputdata_t &inputdata ); + void InputDeactivate( inputdata_t &inputdata ); + void InputBoogieTarget( inputdata_t &inputdata ); + void InputSetZapColor( inputdata_t &inputdata ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + +private: + float m_flStartTime; + interval_t m_BoogieLength; + float m_flMagnitude; + + Vector m_vecZapColor; + + // This allows us to change or remove active boogies later. + CUtlVector> m_Boogies; +}; + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CPointRagdollBoogie ) + + DEFINE_KEYFIELD( m_flStartTime, FIELD_FLOAT, "StartTime" ), + DEFINE_KEYFIELD( m_BoogieLength, FIELD_INTERVAL, "BoogieLength" ), + DEFINE_KEYFIELD( m_flMagnitude, FIELD_FLOAT, "Magnitude" ), + + DEFINE_KEYFIELD( m_vecZapColor, FIELD_VECTOR, "ZapColor" ), + + // Think this should be handled by StartTouch/etc. +// DEFINE_FIELD( m_nSuppressionCount, FIELD_INTEGER ), + + DEFINE_UTLVECTOR( m_Boogies, FIELD_EHANDLE ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + DEFINE_INPUTFUNC( FIELD_STRING, "BoogieTarget", InputBoogieTarget ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetZapColor", InputSetZapColor ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( point_ragdollboogie, CPointRagdollBoogie ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +bool CPointRagdollBoogie::ApplyBoogie( CBaseEntity *pTarget, CBaseEntity *pActivator ) +{ + if (dynamic_cast(pTarget)) + { + m_Boogies.AddToTail(CRagdollBoogie::Create(pTarget, m_flMagnitude, gpGlobals->curtime + m_flStartTime, RandomInterval(m_BoogieLength), GetSpawnFlags(), &m_vecZapColor)); + } + else if (pTarget->MyCombatCharacterPointer()) + { + // Basically CBaseCombatCharacter::BecomeRagdollBoogie(), but adjusted to our needs + CTakeDamageInfo info(this, pActivator, 1.0f, DMG_GENERIC); + + CBaseEntity *pRagdoll = CreateServerRagdoll(pTarget->MyCombatCharacterPointer(), 0, info, COLLISION_GROUP_INTERACTIVE_DEBRIS, true); + + pRagdoll->SetCollisionBounds(CollisionProp()->OBBMins(), CollisionProp()->OBBMaxs()); + + m_Boogies.AddToTail(CRagdollBoogie::Create(pRagdoll, m_flMagnitude, gpGlobals->curtime + m_flStartTime, RandomInterval(m_BoogieLength), GetSpawnFlags(), &m_vecZapColor)); + + CTakeDamageInfo ragdollInfo(this, pActivator, 10000.0, DMG_GENERIC | DMG_REMOVENORAGDOLL); + ragdollInfo.SetDamagePosition(WorldSpaceCenter()); + ragdollInfo.SetDamageForce(Vector(0, 0, 1)); + ragdollInfo.SetForceFriendlyFire(true); + pTarget->TakeDamage(ragdollInfo); + } + else + { + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPointRagdollBoogie::InputActivate( inputdata_t &inputdata ) +{ + CBaseEntity *pEnt = gEntList.FindEntityByName(NULL, STRING(m_target), this, inputdata.pActivator, inputdata.pCaller); + while (pEnt) + { + ApplyBoogie(pEnt, inputdata.pActivator); + + pEnt = gEntList.FindEntityByName(pEnt, STRING(m_target), this, inputdata.pActivator, inputdata.pCaller); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPointRagdollBoogie::InputDeactivate( inputdata_t &inputdata ) +{ + if (m_Boogies.Count() == 0) + return; + + for (int i = 0; i < m_Boogies.Count(); i++) + { + UTIL_Remove(m_Boogies[i]); + } + + m_Boogies.Purge(); + + //m_Boogies.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPointRagdollBoogie::InputBoogieTarget( inputdata_t &inputdata ) +{ + CBaseEntity *pEnt = gEntList.FindEntityByName(NULL, inputdata.value.String(), this, inputdata.pActivator, inputdata.pCaller); + while (pEnt) + { + if (!ApplyBoogie(pEnt, inputdata.pActivator)) + { + Warning("%s was unable to apply ragdoll boogie to %s, classname %s.\n", GetDebugName(), pEnt->GetDebugName(), pEnt->GetClassname()); + } + + pEnt = gEntList.FindEntityByName(pEnt, inputdata.value.String(), this, inputdata.pActivator, inputdata.pCaller); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPointRagdollBoogie::InputSetZapColor( inputdata_t &inputdata ) +{ + inputdata.value.Vector3D( m_vecZapColor ); + if (!m_vecZapColor.IsZero()) + { + // Turn into ratios of 255 + m_vecZapColor /= 255.0f; + } + + // Apply to existing boogies + for (int i = 0; i < m_Boogies.Count(); i++) + { + if (m_Boogies[i]) + { + m_Boogies[i]->SetColor( m_vecZapColor ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handles key values from the BSP before spawn is called. +//----------------------------------------------------------------------------- +bool CPointRagdollBoogie::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "ZapColor" ) ) + { + UTIL_StringToVector(m_vecZapColor.Base(), szValue); + if (!m_vecZapColor.IsZero()) + { + // Turn into ratios of 255 + m_vecZapColor /= 255.0f; + } + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} +#endif diff --git a/sp/src/game/server/RagdollBoogie.h b/sp/src/game/server/RagdollBoogie.h new file mode 100644 index 00000000..8ca99e81 --- /dev/null +++ b/sp/src/game/server/RagdollBoogie.h @@ -0,0 +1,62 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef RAGDOLLBOOGIE_H +#define RAGDOLLBOOGIE_H + +#ifdef _WIN32 +#pragma once +#endif + + +//----------------------------------------------------------------------------- +// Set this spawnflag before calling Spawn to get electrical effects +//----------------------------------------------------------------------------- +#define SF_RAGDOLL_BOOGIE_ELECTRICAL 0x10000 +#define SF_RAGDOLL_BOOGIE_ELECTRICAL_NARROW_BEAM 0x20000 + + +//----------------------------------------------------------------------------- +// Makes ragdolls DANCE! +//----------------------------------------------------------------------------- +class CRagdollBoogie : public CBaseEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CRagdollBoogie, CBaseEntity ); + +public: +#ifdef MAPBASE + static CRagdollBoogie *Create( CBaseEntity *pTarget, float flMagnitude, float flStartTime, float flLengthTime = 0.0f, int nSpawnFlags = 0, const Vector *vecColor = NULL ); +#else + static CRagdollBoogie *Create( CBaseEntity *pTarget, float flMagnitude, float flStartTime, float flLengthTime = 0.0f, int nSpawnFlags = 0 ); +#endif + static void IncrementSuppressionCount( CBaseEntity *pTarget ); + static void DecrementSuppressionCount( CBaseEntity *pTarget ); + +#ifdef MAPBASE + void SetColor( const Vector &vecColor ) { m_vecColor = vecColor; } +#endif + + void Spawn(); + +private: + void AttachToEntity( CBaseEntity *pTarget ); + void SetBoogieTime( float flStartTime, float flLengthTime ); + void SetMagnitude( float flMagnitude ); + void BoogieThink( void ); + void ZapThink(); + + float m_flStartTime; + float m_flBoogieLength; + float m_flMagnitude; + int m_nSuppressionCount; + +#ifdef MAPBASE + Vector m_vecColor = Vector(1, 1, 1); +#endif +}; + +#endif // RAGDOLLBOOGIE_H diff --git a/sp/src/game/server/ServerNetworkProperty.cpp b/sp/src/game/server/ServerNetworkProperty.cpp new file mode 100644 index 00000000..5bd93c18 --- /dev/null +++ b/sp/src/game/server/ServerNetworkProperty.cpp @@ -0,0 +1,305 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "ServerNetworkProperty.h" +#include "tier0/dbg.h" +#include "gameinterface.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern CTimedEventMgr g_NetworkPropertyEventMgr; + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC_NO_BASE( CServerNetworkProperty ) +// DEFINE_FIELD( m_pOuter, FIELD_CLASSPTR ), +// DEFINE_FIELD( m_pPev, FIELD_CLASSPTR ), +// DEFINE_FIELD( m_PVSInfo, PVSInfo_t ), +// DEFINE_FIELD( m_pServerClass, FIELD_CLASSPTR ), + DEFINE_GLOBAL_FIELD( m_hParent, FIELD_EHANDLE ), +// DEFINE_FIELD( m_TimerEvent, CEventRegister ), +// DEFINE_FIELD( m_bPendingStateChange, FIELD_BOOLEAN ), +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Constructor, destructor +//----------------------------------------------------------------------------- +CServerNetworkProperty::CServerNetworkProperty() +{ + Init( NULL ); +} + + +CServerNetworkProperty::~CServerNetworkProperty() +{ + /* Free our transmit proxy. + if ( m_pTransmitProxy ) + { + m_pTransmitProxy->Release(); + }*/ + + engine->CleanUpEntityClusterList( &m_PVSInfo ); + + // remove the attached edict if it exists + DetachEdict(); +} + + +//----------------------------------------------------------------------------- +// Initialization +//----------------------------------------------------------------------------- +void CServerNetworkProperty::Init( CBaseEntity *pEntity ) +{ + m_pPev = NULL; + m_pOuter = pEntity; + m_pServerClass = NULL; +// m_pTransmitProxy = NULL; + m_bPendingStateChange = false; + m_PVSInfo.m_nClusterCount = 0; + m_TimerEvent.Init( &g_NetworkPropertyEventMgr, this ); +} + + +//----------------------------------------------------------------------------- +// Connects, disconnects edicts +//----------------------------------------------------------------------------- +void CServerNetworkProperty::AttachEdict( edict_t *pRequiredEdict ) +{ + Assert ( !m_pPev ); + + // see if there is an edict allocated for it, otherwise get one from the engine + if ( !pRequiredEdict ) + { + pRequiredEdict = engine->CreateEdict(); + } + + m_pPev = pRequiredEdict; + m_pPev->SetEdict( GetBaseEntity(), true ); +} + +void CServerNetworkProperty::DetachEdict() +{ + if ( m_pPev ) + { + m_pPev->SetEdict( NULL, false ); + engine->RemoveEdict( m_pPev ); + m_pPev = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Entity handles +//----------------------------------------------------------------------------- +IHandleEntity *CServerNetworkProperty::GetEntityHandle( ) +{ + return m_pOuter; +} + +void CServerNetworkProperty::Release() +{ + delete m_pOuter; +} + + +//----------------------------------------------------------------------------- +// Returns the network parent +//----------------------------------------------------------------------------- +CServerNetworkProperty* CServerNetworkProperty::GetNetworkParent() +{ + CBaseEntity *pParent = m_hParent.Get(); + return pParent ? pParent->NetworkProp() : NULL; +} + + +//----------------------------------------------------------------------------- +// Marks for deletion +//----------------------------------------------------------------------------- +void CServerNetworkProperty::MarkForDeletion() +{ + m_pOuter->AddEFlags( EFL_KILLME ); +} + +bool CServerNetworkProperty::IsMarkedForDeletion() const +{ + return ( m_pOuter->GetEFlags() & EFL_KILLME ) != 0; +} + + +//----------------------------------------------------------------------------- +// PVS information +//----------------------------------------------------------------------------- +void CServerNetworkProperty::RecomputePVSInformation() +{ + if ( m_pPev && ( ( m_pPev->m_fStateFlags & FL_EDICT_DIRTY_PVS_INFORMATION ) != 0 ) ) + { + m_pPev->m_fStateFlags &= ~FL_EDICT_DIRTY_PVS_INFORMATION; + engine->BuildEntityClusterList( edict(), &m_PVSInfo ); + } +} + + +//----------------------------------------------------------------------------- +// Serverclass +//----------------------------------------------------------------------------- +ServerClass* CServerNetworkProperty::GetServerClass() +{ + if ( !m_pServerClass ) + m_pServerClass = m_pOuter->GetServerClass(); + return m_pServerClass; +} + +const char* CServerNetworkProperty::GetClassName() const +{ + return STRING(m_pOuter->m_iClassname); +} + + +//----------------------------------------------------------------------------- +// Transmit proxies +/*----------------------------------------------------------------------------- +void CServerNetworkProperty::SetTransmitProxy( CBaseTransmitProxy *pProxy ) +{ + if ( m_pTransmitProxy ) + { + m_pTransmitProxy->Release(); + } + + m_pTransmitProxy = pProxy; + + if ( m_pTransmitProxy ) + { + m_pTransmitProxy->AddRef(); + } +}*/ + +//----------------------------------------------------------------------------- +// PVS rules +//----------------------------------------------------------------------------- +bool CServerNetworkProperty::IsInPVS( const edict_t *pRecipient, const void *pvs, int pvssize ) +{ + RecomputePVSInformation(); + + // ignore if not touching a PV leaf + // negative leaf count is a node number + // If no pvs, add any entity + + Assert( pvs && ( edict() != pRecipient ) ); + + unsigned char *pPVS = ( unsigned char * )pvs; + + if ( m_PVSInfo.m_nClusterCount < 0 ) // too many clusters, use headnode + { + return ( engine->CheckHeadnodeVisible( m_PVSInfo.m_nHeadNode, pPVS, pvssize ) != 0); + } + + for ( int i = m_PVSInfo.m_nClusterCount; --i >= 0; ) + { + if (pPVS[m_PVSInfo.m_pClusters[i] >> 3] & (1 << (m_PVSInfo.m_pClusters[i] & 7) )) + return true; + } + + return false; // not visible +} + + +//----------------------------------------------------------------------------- +// PVS: this function is called a lot, so it avoids function calls +//----------------------------------------------------------------------------- +bool CServerNetworkProperty::IsInPVS( const CCheckTransmitInfo *pInfo ) +{ + // PVS data must be up to date + Assert( !m_pPev || ( ( m_pPev->m_fStateFlags & FL_EDICT_DIRTY_PVS_INFORMATION ) == 0 ) ); + + int i; + + // Early out if the areas are connected + if ( !m_PVSInfo.m_nAreaNum2 ) + { + for ( i=0; i< pInfo->m_AreasNetworked; i++ ) + { + int clientArea = pInfo->m_Areas[i]; + if ( clientArea == m_PVSInfo.m_nAreaNum || engine->CheckAreasConnected( clientArea, m_PVSInfo.m_nAreaNum ) ) + break; + } + } + else + { + // doors can legally straddle two areas, so + // we may need to check another one + for ( i=0; i< pInfo->m_AreasNetworked; i++ ) + { + int clientArea = pInfo->m_Areas[i]; + if ( clientArea == m_PVSInfo.m_nAreaNum || clientArea == m_PVSInfo.m_nAreaNum2 ) + break; + + if ( engine->CheckAreasConnected( clientArea, m_PVSInfo.m_nAreaNum ) ) + break; + + if ( engine->CheckAreasConnected( clientArea, m_PVSInfo.m_nAreaNum2 ) ) + break; + } + } + + if ( i == pInfo->m_AreasNetworked ) + { + // areas not connected + return false; + } + + // ignore if not touching a PV leaf + // negative leaf count is a node number + // If no pvs, add any entity + + Assert( edict() != pInfo->m_pClientEnt ); + + unsigned char *pPVS = ( unsigned char * )pInfo->m_PVS; + + if ( m_PVSInfo.m_nClusterCount < 0 ) // too many clusters, use headnode + { + return (engine->CheckHeadnodeVisible( m_PVSInfo.m_nHeadNode, pPVS, pInfo->m_nPVSSize ) != 0); + } + + for ( i = m_PVSInfo.m_nClusterCount; --i >= 0; ) + { + int nCluster = m_PVSInfo.m_pClusters[i]; + if ( ((int)(pPVS[nCluster >> 3])) & BitVec_BitInByte( nCluster ) ) + return true; + } + + return false; // not visible + +} + + +void CServerNetworkProperty::SetUpdateInterval( float val ) +{ + if ( val == 0 ) + m_TimerEvent.StopUpdates(); + else + m_TimerEvent.SetUpdateInterval( val ); +} + + +void CServerNetworkProperty::FireEvent() +{ + // Our timer went off. If our state has changed in the background, then + // trigger a state change in the edict. + if ( m_bPendingStateChange ) + { + m_pPev->StateChanged(); + m_bPendingStateChange = false; + } +} + + + diff --git a/sp/src/game/server/ServerNetworkProperty.h b/sp/src/game/server/ServerNetworkProperty.h new file mode 100644 index 00000000..cb4c3866 --- /dev/null +++ b/sp/src/game/server/ServerNetworkProperty.h @@ -0,0 +1,258 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +#ifndef SERVERNETWORKPROPERTY_H +#define SERVERNETWORKPROPERTY_H +#ifdef _WIN32 +#pragma once +#endif + +#include "iservernetworkable.h" +#include "server_class.h" +#include "edict.h" +#include "timedeventmgr.h" + +// +// Lightweight base class for networkable data on the server. +// +class CServerNetworkProperty : public IServerNetworkable, public IEventRegisterCallback +{ +public: + DECLARE_CLASS_NOBASE( CServerNetworkProperty ); + DECLARE_DATADESC(); + +public: + CServerNetworkProperty(); + virtual ~CServerNetworkProperty(); + +public: +// IServerNetworkable implementation. + virtual IHandleEntity *GetEntityHandle( ); + virtual edict_t *GetEdict() const; + virtual CBaseNetworkable* GetBaseNetworkable(); + virtual CBaseEntity* GetBaseEntity(); + virtual ServerClass* GetServerClass(); + virtual const char* GetClassName() const; + virtual void Release(); + virtual int AreaNum() const; + virtual PVSInfo_t* GetPVSInfo(); + +public: + // Other public methods + void Init( CBaseEntity *pEntity ); + + void AttachEdict( edict_t *pRequiredEdict = NULL ); + + // Methods to get the entindex + edict + int entindex() const; + edict_t *edict(); + const edict_t *edict() const; + + // Sets the edict pointer (for swapping edicts) + void SetEdict( edict_t *pEdict ); + + // All these functions call through to CNetStateMgr. + // See CNetStateMgr for details about these functions. + void NetworkStateForceUpdate(); + void NetworkStateChanged(); + void NetworkStateChanged( unsigned short offset ); + + // Marks the PVS information dirty + void MarkPVSInformationDirty(); + + // Marks for deletion + void MarkForDeletion(); + bool IsMarkedForDeletion() const; + + // Sets the network parent + void SetNetworkParent( EHANDLE hParent ); + CServerNetworkProperty* GetNetworkParent(); + + // This is useful for entities that don't change frequently or that the client + // doesn't need updates on very often. If you use this mode, the server will only try to + // detect state changes every N seconds, so it will save CPU cycles and bandwidth. + // + // Note: N must be less than AUTOUPDATE_MAX_TIME_LENGTH. + // + // Set back to zero to disable the feature. + // + // This feature works on top of manual mode. + // - If you turn it on and manual mode is off, it will autodetect changes every N seconds. + // - If you turn it on and manual mode is on, then every N seconds it will only say there + // is a change if you've called NetworkStateChanged. + void SetUpdateInterval( float N ); + + // You can use this to override any entity's ShouldTransmit behavior. + // void SetTransmitProxy( CBaseTransmitProxy *pProxy ); + + // This version does a PVS check which also checks for connected areas + bool IsInPVS( const CCheckTransmitInfo *pInfo ); + + // This version doesn't do the area check + bool IsInPVS( const edict_t *pRecipient, const void *pvs, int pvssize ); + + // Called by the timed event manager when it's time to detect a state change. + virtual void FireEvent(); + + // Recomputes PVS information + void RecomputePVSInformation(); + +private: + // Detaches the edict.. should only be called by CBaseNetworkable's destructor. + void DetachEdict(); + CBaseEntity *GetOuter(); + + // Marks the networkable that it will should transmit + void SetTransmit( CCheckTransmitInfo *pInfo ); + +private: + CBaseEntity *m_pOuter; + // CBaseTransmitProxy *m_pTransmitProxy; + edict_t *m_pPev; + PVSInfo_t m_PVSInfo; + ServerClass *m_pServerClass; + + // NOTE: This state is 'owned' by the entity. It's only copied here + // also to help improve cache performance in networking code. + EHANDLE m_hParent; + + // Counters for SetUpdateInterval. + CEventRegister m_TimerEvent; + bool m_bPendingStateChange : 1; + +// friend class CBaseTransmitProxy; +}; + + +//----------------------------------------------------------------------------- +// inline methods // TODOMO does inline work on virtual functions ? +//----------------------------------------------------------------------------- +inline CBaseNetworkable* CServerNetworkProperty::GetBaseNetworkable() +{ + return NULL; +} + +inline CBaseEntity* CServerNetworkProperty::GetBaseEntity() +{ + return m_pOuter; +} + +inline CBaseEntity *CServerNetworkProperty::GetOuter() +{ + return m_pOuter; +} + +inline PVSInfo_t *CServerNetworkProperty::GetPVSInfo() +{ + return &m_PVSInfo; +} + + +//----------------------------------------------------------------------------- +// Marks the PVS information dirty +//----------------------------------------------------------------------------- +inline void CServerNetworkProperty::MarkPVSInformationDirty() +{ + if ( m_pPev ) + { + m_pPev->m_fStateFlags |= FL_EDICT_DIRTY_PVS_INFORMATION; + } +} + + +//----------------------------------------------------------------------------- +// Sets/gets the network parent +//----------------------------------------------------------------------------- +inline void CServerNetworkProperty::SetNetworkParent( EHANDLE hParent ) +{ + m_hParent = hParent; +} + + +//----------------------------------------------------------------------------- +// Methods related to the net state mgr +//----------------------------------------------------------------------------- +inline void CServerNetworkProperty::NetworkStateForceUpdate() +{ + if ( m_pPev ) + m_pPev->StateChanged(); +} + +inline void CServerNetworkProperty::NetworkStateChanged() +{ + // If we're using the timer, then ignore this call. + if ( m_TimerEvent.IsRegistered() ) + { + // If we're waiting for a timer event, then queue the change so it happens + // when the timer goes off. + m_bPendingStateChange = true; + } + else + { + if ( m_pPev ) + m_pPev->StateChanged(); + } +} + +inline void CServerNetworkProperty::NetworkStateChanged( unsigned short varOffset ) +{ + // If we're using the timer, then ignore this call. + if ( m_TimerEvent.IsRegistered() ) + { + // If we're waiting for a timer event, then queue the change so it happens + // when the timer goes off. + m_bPendingStateChange = true; + } + else + { + if ( m_pPev ) + m_pPev->StateChanged( varOffset ); + } +} + +//----------------------------------------------------------------------------- +// Methods to get the entindex + edict +//----------------------------------------------------------------------------- +inline int CServerNetworkProperty::entindex() const +{ + return ENTINDEX( m_pPev ); +} + +inline edict_t* CServerNetworkProperty::GetEdict() const +{ + // This one's virtual, that's why we have to two other versions + return m_pPev; +} + +inline edict_t *CServerNetworkProperty::edict() +{ + return m_pPev; +} + +inline const edict_t *CServerNetworkProperty::edict() const +{ + return m_pPev; +} + + +//----------------------------------------------------------------------------- +// Sets the edict pointer (for swapping edicts) +//----------------------------------------------------------------------------- +inline void CServerNetworkProperty::SetEdict( edict_t *pEdict ) +{ + m_pPev = pEdict; +} + + +inline int CServerNetworkProperty::AreaNum() const +{ + const_cast(this)->RecomputePVSInformation(); + return m_PVSInfo.m_nAreaNum; +} + + +#endif // SERVERNETWORKPROPERTY_H diff --git a/sp/src/game/server/SkyCamera.cpp b/sp/src/game/server/SkyCamera.cpp new file mode 100644 index 00000000..be9716d4 --- /dev/null +++ b/sp/src/game/server/SkyCamera.cpp @@ -0,0 +1,462 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "igamesystem.h" +#include "entitylist.h" +#include "SkyCamera.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// automatically hooks in the system's callbacks +CEntityClassList g_SkyList; +template <> CSkyCamera *CEntityClassList::m_pClassList = NULL; +#ifdef MAPBASE +CHandle g_hActiveSkybox = NULL; +#endif + +//----------------------------------------------------------------------------- +// Retrives the current skycamera +//----------------------------------------------------------------------------- +CSkyCamera* GetCurrentSkyCamera() +{ +#ifdef MAPBASE + if ( g_hActiveSkybox.Get() == NULL ) + { + g_hActiveSkybox = GetSkyCameraList(); + } + return g_hActiveSkybox.Get(); +#else + return g_SkyList.m_pClassList; +#endif +} + +CSkyCamera* GetSkyCameraList() +{ + return g_SkyList.m_pClassList; +} + +//============================================================================= + +LINK_ENTITY_TO_CLASS( sky_camera, CSkyCamera ); + +BEGIN_DATADESC( CSkyCamera ) + + DEFINE_KEYFIELD( m_skyboxData.scale, FIELD_INTEGER, "scale" ), + DEFINE_FIELD( m_skyboxData.origin, FIELD_VECTOR ), + DEFINE_FIELD( m_skyboxData.area, FIELD_INTEGER ), +#ifdef MAPBASE + DEFINE_FIELD( m_skyboxData.angles, FIELD_VECTOR ), + DEFINE_FIELD( m_skyboxData.skycamera, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_skyboxData.skycolor, FIELD_COLOR32, "skycolor" ), + DEFINE_KEYFIELD( m_bUseAnglesForSky, FIELD_BOOLEAN, "use_angles_for_sky" ), +#endif + + // Quiet down classcheck + // DEFINE_FIELD( m_skyboxData, sky3dparams_t ), + + // This is re-set up in the constructor + // DEFINE_FIELD( m_pNext, CSkyCamera ), + + // fog data for 3d skybox + DEFINE_KEYFIELD( m_bUseAngles, FIELD_BOOLEAN, "use_angles" ), + DEFINE_KEYFIELD( m_skyboxData.fog.enable, FIELD_BOOLEAN, "fogenable" ), + DEFINE_KEYFIELD( m_skyboxData.fog.blend, FIELD_BOOLEAN, "fogblend" ), + DEFINE_KEYFIELD( m_skyboxData.fog.dirPrimary, FIELD_VECTOR, "fogdir" ), + DEFINE_KEYFIELD( m_skyboxData.fog.colorPrimary, FIELD_COLOR32, "fogcolor" ), + DEFINE_KEYFIELD( m_skyboxData.fog.colorSecondary, FIELD_COLOR32, "fogcolor2" ), + DEFINE_KEYFIELD( m_skyboxData.fog.start, FIELD_FLOAT, "fogstart" ), + DEFINE_KEYFIELD( m_skyboxData.fog.end, FIELD_FLOAT, "fogend" ), + DEFINE_KEYFIELD( m_skyboxData.fog.maxdensity, FIELD_FLOAT, "fogmaxdensity" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_skyboxData.fog.farz, FIELD_FLOAT, "farz" ), +#endif + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "ForceUpdate", InputForceUpdate ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartUpdating", InputStartUpdating ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopUpdating", InputStopUpdating ), + + DEFINE_INPUTFUNC( FIELD_VOID, "ActivateSkybox", InputActivateSkybox ), + DEFINE_INPUTFUNC( FIELD_VOID, "DeactivateSkybox", InputDeactivateSkybox ), + + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFogStartDist", InputSetFogStartDist ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFogEndDist", InputSetFogEndDist ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFogMaxDensity", InputSetFogMaxDensity ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOnFog", InputTurnOnFog ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOffFog", InputTurnOffFog ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetFogColor", InputSetFogColor ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetFogColorSecondary", InputSetFogColorSecondary ), + DEFINE_INPUTFUNC( FIELD_EHANDLE, "CopyFogController", InputCopyFogController ), + DEFINE_INPUTFUNC( FIELD_EHANDLE, "CopyFogControllerWithScale", InputCopyFogControllerWithScale ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetFarZ", InputSetFarZ ), + + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetSkyColor", InputSetSkyColor ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetScale", InputSetScale ), + + DEFINE_THINKFUNC( UpdateThink ), +#endif + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// List of maps in HL2 that we must apply our skybox fog fixup hack to +//----------------------------------------------------------------------------- +static const char *s_pBogusFogMaps[] = +{ + "d1_canals_01", + "d1_canals_01a", + "d1_canals_02", + "d1_canals_03", + "d1_canals_09", + "d1_canals_10", + "d1_canals_11", + "d1_canals_12", + "d1_canals_13", + "d1_eli_01", + "d1_trainstation_01", + "d1_trainstation_03", + "d1_trainstation_04", + "d1_trainstation_05", + "d1_trainstation_06", + "d3_c17_04", + "d3_c17_11", + "d3_c17_12", + "d3_citadel_01", + NULL +}; + +//----------------------------------------------------------------------------- +// Constructor, destructor +//----------------------------------------------------------------------------- +CSkyCamera::CSkyCamera() +{ + g_SkyList.Insert( this ); + m_skyboxData.fog.maxdensity = 1.0f; +#ifdef MAPBASE + m_skyboxData.skycolor.Init(0, 0, 0, 0); +#endif +} + +CSkyCamera::~CSkyCamera() +{ + g_SkyList.Remove( this ); +} + +void CSkyCamera::Spawn( void ) +{ +#ifdef MAPBASE + if (HasSpawnFlags(SF_SKY_MASTER)) + g_hActiveSkybox = this; + + if (HasSpawnFlags(SF_SKY_START_UPDATING)) + { + SetCameraEntityMode(); + + SetThink( &CSkyCamera::UpdateThink ); + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); + } + else + { + SetCameraPositionMode(); + } +#else + m_skyboxData.origin = GetLocalOrigin(); +#endif + m_skyboxData.area = engine->GetArea( m_skyboxData.origin ); + + Precache(); +} + + +//----------------------------------------------------------------------------- +// Activate! +//----------------------------------------------------------------------------- +void CSkyCamera::Activate( ) +{ + BaseClass::Activate(); + + if ( m_bUseAngles ) + { + AngleVectors( GetAbsAngles(), &m_skyboxData.fog.dirPrimary.GetForModify() ); + m_skyboxData.fog.dirPrimary.GetForModify() *= -1.0f; + } + +#ifdef HL2_DLL + // NOTE! This is a hack. There was a bug in the skybox fog computation + // on the client DLL that caused it to use the average of the primary and + // secondary fog color when blending was enabled. The bug is fixed, but to make + // the maps look the same as before the bug fix without having to download new maps, + // I have to cheat here and slam the primary and secondary colors to be the average of + // the primary and secondary colors. + if ( m_skyboxData.fog.blend ) + { + for ( int i = 0; s_pBogusFogMaps[i]; ++i ) + { + if ( !Q_stricmp( s_pBogusFogMaps[i], STRING(gpGlobals->mapname) ) ) + { + m_skyboxData.fog.colorPrimary.SetR( ( m_skyboxData.fog.colorPrimary.GetR() + m_skyboxData.fog.colorSecondary.GetR() ) * 0.5f ); + m_skyboxData.fog.colorPrimary.SetG( ( m_skyboxData.fog.colorPrimary.GetG() + m_skyboxData.fog.colorSecondary.GetG() ) * 0.5f ); + m_skyboxData.fog.colorPrimary.SetB( ( m_skyboxData.fog.colorPrimary.GetB() + m_skyboxData.fog.colorSecondary.GetB() ) * 0.5f ); + m_skyboxData.fog.colorPrimary.SetA( ( m_skyboxData.fog.colorPrimary.GetA() + m_skyboxData.fog.colorSecondary.GetA() ) * 0.5f ); + m_skyboxData.fog.colorSecondary = m_skyboxData.fog.colorPrimary; + } + } + } +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CSkyCamera::AcceptInput( const char *szInputName, CBaseEntity *pActivator, CBaseEntity *pCaller, variant_t Value, int outputID ) +{ + if (!BaseClass::AcceptInput( szInputName, pActivator, pCaller, Value, outputID )) + return false; + + if (g_hActiveSkybox == this) + { + // Most inputs require an update + DoUpdate( true ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSkyCamera::SetCameraEntityMode() +{ + m_skyboxData.skycamera = this; + + // Ensure the viewrender knows whether this should be using angles + if (m_bUseAnglesForSky) + m_skyboxData.angles.SetX( 1 ); + else + m_skyboxData.angles.SetX( 0 ); +} + +void CSkyCamera::SetCameraPositionMode() +{ + // Must be absolute now that the sky_camera can be parented + m_skyboxData.skycamera = NULL; + m_skyboxData.origin = GetAbsOrigin(); + if (m_bUseAnglesForSky) + m_skyboxData.angles = GetAbsAngles(); +} + +//----------------------------------------------------------------------------- +// Purpose: Update sky position mid-game +//----------------------------------------------------------------------------- +bool CSkyCamera::DoUpdate( bool bUpdateData ) +{ + // Now that sky camera updating uses an entity handle directly transmitted to the client, + // this thinking is only used to update area and other parameters + + // Getting into another area is unlikely, but if it's not expensive, I guess it's okay. + int area = engine->GetArea( GetAbsOrigin() ); + if (m_skyboxData.area != area) + { + m_skyboxData.area = area; + bUpdateData = true; + } + + if ( m_bUseAngles ) + { + Vector fogForward; + AngleVectors( GetAbsAngles(), &fogForward ); + fogForward *= -1.0f; + + if ( m_skyboxData.fog.dirPrimary.Get() != fogForward ) + { + m_skyboxData.fog.dirPrimary = fogForward; + bUpdateData = true; + } + } + + if (bUpdateData) + { + // Updates client data, this completely ignores m_pOldSkyCamera + CBasePlayer *pPlayer = NULL; + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + pPlayer = UTIL_PlayerByIndex(i); + if (pPlayer) + pPlayer->m_Local.m_skybox3d.CopyFrom(m_skyboxData); + } + } + + // Needed for entity interpolation + SetSimulationTime( gpGlobals->curtime ); + + return bUpdateData; +} + +void CSkyCamera::UpdateThink() +{ + if (DoUpdate()) + { + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); + } + else + { + SetNextThink( gpGlobals->curtime + 0.2f ); + } +} + +void CSkyCamera::InputForceUpdate( inputdata_t &inputdata ) +{ + if (m_skyboxData.skycamera == NULL) + { + m_skyboxData.origin = GetAbsOrigin(); + if (m_bUseAnglesForSky) + m_skyboxData.angles = GetAbsAngles(); + } + + DoUpdate( true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSkyCamera::InputStartUpdating( inputdata_t &inputdata ) +{ + if (GetCurrentSkyCamera() == this) + { + SetCameraEntityMode(); + DoUpdate( true ); + + SetThink( &CSkyCamera::UpdateThink ); + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); + } + + // If we become the current sky camera later, remember that we want to update + AddSpawnFlags( SF_SKY_START_UPDATING ); + + // Must update transmit state so we show up on the client + DispatchUpdateTransmitState(); +} + +void CSkyCamera::InputStopUpdating( inputdata_t &inputdata ) +{ + SetThink( NULL ); + SetNextThink( TICK_NEVER_THINK ); + RemoveSpawnFlags( SF_SKY_START_UPDATING ); + DispatchUpdateTransmitState(); + + SetCameraPositionMode(); + DoUpdate( true ); +} + +//----------------------------------------------------------------------------- +// Activate! +//----------------------------------------------------------------------------- +void CSkyCamera::InputActivateSkybox( inputdata_t &inputdata ) +{ + CSkyCamera *pActiveSky = GetCurrentSkyCamera(); + if (pActiveSky && pActiveSky->GetNextThink() != TICK_NEVER_THINK && pActiveSky != this) + { + // Deactivate that skybox + pActiveSky->SetThink( NULL ); + pActiveSky->SetNextThink( TICK_NEVER_THINK ); + } + + g_hActiveSkybox = this; + + if (HasSpawnFlags( SF_SKY_START_UPDATING )) + InputStartUpdating( inputdata ); +} + +//----------------------------------------------------------------------------- +// Deactivate! +//----------------------------------------------------------------------------- +void CSkyCamera::InputDeactivateSkybox( inputdata_t &inputdata ) +{ + if (GetCurrentSkyCamera() == this) + { + g_hActiveSkybox = NULL; + + // ClientData doesn't catch this immediately + CBasePlayer *pPlayer = NULL; + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + pPlayer = UTIL_PlayerByIndex( i ); + if (pPlayer) + pPlayer->m_Local.m_skybox3d.area = 255; + } + } + + SetThink( NULL ); + SetNextThink( TICK_NEVER_THINK ); +} + +//------------------------------------------------------------------------------ +// Purpose: Input handlers for setting fog stuff. +//------------------------------------------------------------------------------ +void CSkyCamera::InputSetFogStartDist( inputdata_t &inputdata ) { m_skyboxData.fog.start = inputdata.value.Float(); } +void CSkyCamera::InputSetFogEndDist( inputdata_t &inputdata ) { m_skyboxData.fog.end = inputdata.value.Float(); } +void CSkyCamera::InputSetFogMaxDensity( inputdata_t &inputdata ) { m_skyboxData.fog.maxdensity = inputdata.value.Float(); } +void CSkyCamera::InputTurnOnFog( inputdata_t &inputdata ) { m_skyboxData.fog.enable = true; } +void CSkyCamera::InputTurnOffFog( inputdata_t &inputdata ) { m_skyboxData.fog.enable = false; } +void CSkyCamera::InputSetFogColor( inputdata_t &inputdata ) { m_skyboxData.fog.colorPrimary = inputdata.value.Color32(); } +void CSkyCamera::InputSetFogColorSecondary( inputdata_t &inputdata ) { m_skyboxData.fog.colorSecondary = inputdata.value.Color32(); } + +void CSkyCamera::InputSetFarZ( inputdata_t &inputdata ) { m_skyboxData.fog.farz = inputdata.value.Int(); } + +void CSkyCamera::InputCopyFogController( inputdata_t &inputdata ) +{ + CFogController *pFogController = dynamic_cast(inputdata.value.Entity().Get()); + if (!pFogController) + return; + + m_skyboxData.fog.dirPrimary = pFogController->m_fog.dirPrimary; + m_skyboxData.fog.colorPrimary = pFogController->m_fog.colorPrimary; + m_skyboxData.fog.colorSecondary = pFogController->m_fog.colorSecondary; + //m_skyboxData.fog.colorPrimaryLerpTo = pFogController->m_fog.colorPrimaryLerpTo; + //m_skyboxData.fog.colorSecondaryLerpTo = pFogController->m_fog.colorSecondaryLerpTo; + m_skyboxData.fog.start = pFogController->m_fog.start; + m_skyboxData.fog.end = pFogController->m_fog.end; + m_skyboxData.fog.farz = pFogController->m_fog.farz; + m_skyboxData.fog.maxdensity = pFogController->m_fog.maxdensity; + + //m_skyboxData.fog.startLerpTo = pFogController->m_fog.startLerpTo; + //m_skyboxData.fog.endLerpTo = pFogController->m_fog.endLerpTo; + //m_skyboxData.fog.lerptime = pFogController->m_fog.lerptime; + //m_skyboxData.fog.duration = pFogController->m_fog.duration; + //m_skyboxData.fog.enable = pFogController->m_fog.enable; + m_skyboxData.fog.blend = pFogController->m_fog.blend; +} + +void CSkyCamera::InputCopyFogControllerWithScale( inputdata_t &inputdata ) +{ + CFogController *pFogController = dynamic_cast(inputdata.value.Entity().Get()); + if (!pFogController) + return; + + m_skyboxData.fog.dirPrimary = pFogController->m_fog.dirPrimary; + m_skyboxData.fog.colorPrimary = pFogController->m_fog.colorPrimary; + m_skyboxData.fog.colorSecondary = pFogController->m_fog.colorSecondary; + //m_skyboxData.fog.colorPrimaryLerpTo = pFogController->m_fog.colorPrimaryLerpTo; + //m_skyboxData.fog.colorSecondaryLerpTo = pFogController->m_fog.colorSecondaryLerpTo; + m_skyboxData.fog.start = pFogController->m_fog.start * m_skyboxData.scale; + m_skyboxData.fog.end = pFogController->m_fog.end * m_skyboxData.scale; + m_skyboxData.fog.farz = pFogController->m_fog.farz != -1 ? (pFogController->m_fog.farz * m_skyboxData.scale) : pFogController->m_fog.farz; + m_skyboxData.fog.maxdensity = pFogController->m_fog.maxdensity; + + //m_skyboxData.fog.startLerpTo = pFogController->m_fog.startLerpTo; + //m_skyboxData.fog.endLerpTo = pFogController->m_fog.endLerpTo; + //m_skyboxData.fog.lerptime = pFogController->m_fog.lerptime; + //m_skyboxData.fog.duration = pFogController->m_fog.duration; + //m_skyboxData.fog.enable = pFogController->m_fog.enable; + m_skyboxData.fog.blend = pFogController->m_fog.blend; +} +#endif diff --git a/sp/src/game/server/SkyCamera.h b/sp/src/game/server/SkyCamera.h new file mode 100644 index 00000000..a1936568 --- /dev/null +++ b/sp/src/game/server/SkyCamera.h @@ -0,0 +1,103 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Resource collection entity +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SKYCAMERA_H +#define SKYCAMERA_H + +#ifdef _WIN32 +#pragma once +#endif + +class CSkyCamera; + +#ifdef MAPBASE +#define SF_SKY_MASTER (1 << 0) +#define SF_SKY_START_UPDATING (1 << 1) + +//============================================================================= +// +// Sky Camera Class +// Now derived directly from CBaseEntity for parenting and angles! (please don't break anything) +// +//============================================================================= +class CSkyCamera : public CBaseEntity +#else +//============================================================================= +// +// Sky Camera Class +// +class CSkyCamera : public CLogicalEntity +#endif +{ +#ifdef MAPBASE + DECLARE_CLASS( CSkyCamera, CBaseEntity ); +#else + DECLARE_CLASS( CSkyCamera, CLogicalEntity ); +#endif + +public: + + DECLARE_DATADESC(); + CSkyCamera(); + ~CSkyCamera(); + virtual void Spawn( void ); + virtual void Activate(); + +#ifdef MAPBASE + bool AcceptInput( const char *szInputName, CBaseEntity *pActivator, CBaseEntity *pCaller, variant_t Value, int outputID ); + + int UpdateTransmitState() { return HasSpawnFlags( SF_SKY_START_UPDATING ) ? SetTransmitState( FL_EDICT_ALWAYS ) : BaseClass::UpdateTransmitState(); } + + void SetCameraEntityMode(); + void SetCameraPositionMode(); + + bool DoUpdate( bool bUpdateData = false ); + void UpdateThink(); + + void InputForceUpdate( inputdata_t &inputdata ); + void InputStartUpdating( inputdata_t &inputdata ); + void InputStopUpdating( inputdata_t &inputdata ); + + void InputActivateSkybox( inputdata_t &inputdata ); + void InputDeactivateSkybox( inputdata_t &inputdata ); + + void InputSetFogStartDist( inputdata_t &data ); + void InputSetFogEndDist( inputdata_t &data ); + void InputTurnOnFog( inputdata_t &data ); + void InputTurnOffFog( inputdata_t &data ); + void InputSetFogColor( inputdata_t &data ); + void InputSetFogColorSecondary( inputdata_t &data ); + void InputSetFogMaxDensity( inputdata_t &inputdata ); + void InputCopyFogController( inputdata_t &inputdata ); + void InputCopyFogControllerWithScale( inputdata_t &inputdata ); + + void InputSetFarZ( inputdata_t &data ); + + void InputSetSkyColor( inputdata_t &inputdata ) { m_skyboxData.skycolor = inputdata.value.Color32(); } + + void InputSetScale( inputdata_t &inputdata ) { m_skyboxData.scale = inputdata.value.Int(); } +#endif + +public: + sky3dparams_t m_skyboxData; + bool m_bUseAngles; +#ifdef MAPBASE + // Uses angles for actual skybox + bool m_bUseAnglesForSky; +#endif + CSkyCamera *m_pNext; +}; + + +//----------------------------------------------------------------------------- +// Retrives the current skycamera +//----------------------------------------------------------------------------- +CSkyCamera* GetCurrentSkyCamera(); +CSkyCamera* GetSkyCameraList(); + + +#endif // SKYCAMERA_H diff --git a/sp/src/game/server/TemplateEntities.cpp b/sp/src/game/server/TemplateEntities.cpp new file mode 100644 index 00000000..b2a54fd1 --- /dev/null +++ b/sp/src/game/server/TemplateEntities.cpp @@ -0,0 +1,584 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Template entities are used by spawners to create copies of entities +// that were configured by the level designer. This allows us to spawn +// entities with arbitrary sets of key/value data and entity I/O +// connections. +// +// Template entities are marked with a special spawnflag which causes +// them not to spawn, but to be saved as a string containing all the +// map data (keyvalues and I/O connections) from the BSP. Template +// entities are looked up by name by the spawner, which copies the +// map data into a local string (that's how the template data is saved +// and restored). Once all the entities in the map have been activated, +// the template database is freed. +// +//=============================================================================// + +#include "cbase.h" +#include "igamesystem.h" +#include "mapentities_shared.h" +#include "point_template.h" +#include "eventqueue.h" +#include "TemplateEntities.h" +#include "utldict.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar template_debug( "template_debug", "0" ); + +// This is appended to key's values that will need to be unique in template instances +const char *ENTITYIO_FIXUP_STRING = "&0000"; + +int MapEntity_GetNumKeysInEntity( const char *pEntData ); + +struct TemplateEntityData_t +{ + const char *pszName; + char *pszMapData; + string_t iszMapData; + int iMapDataLength; + bool bNeedsEntityIOFixup; // If true, this template has entity I/O in its mapdata that needs fixup before spawning. + char *pszFixedMapData; // A single copy of this template that we used to fix up the Entity I/O whenever someone wants a fixed version of this template + + DECLARE_SIMPLE_DATADESC(); +}; + +BEGIN_SIMPLE_DATADESC( TemplateEntityData_t ) + //DEFINE_FIELD( pszName, FIELD_STRING ), // Saved custom, see below + //DEFINE_FIELD( pszMapData, FIELD_STRING ), // Saved custom, see below + DEFINE_FIELD( iszMapData, FIELD_STRING ), + DEFINE_FIELD( iMapDataLength, FIELD_INTEGER ), + DEFINE_FIELD( bNeedsEntityIOFixup, FIELD_BOOLEAN ), + + //DEFINE_FIELD( pszFixedMapData, FIELD_STRING ), // Not saved at all +END_DATADESC() + +struct grouptemplate_t +{ + CEntityMapData *pMapDataParser; + char pszName[MAPKEY_MAXLENGTH]; + int iIndex; + bool bChangeTargetname; +}; + +static CUtlVector g_Templates; + +int g_iCurrentTemplateInstance; + +//----------------------------------------------------------------------------- +// Purpose: Saves the given entity's keyvalue data for later use by a spawner. +// Returns the index into the templates. +//----------------------------------------------------------------------------- +int Templates_Add(CBaseEntity *pEntity, const char *pszMapData, int nLen) +{ + const char *pszName = STRING(pEntity->GetEntityName()); + if ((!pszName) || (!strlen(pszName))) + { + DevWarning(1, "RegisterTemplateEntity: template entity with no name, class %s\n", pEntity->GetClassname()); + return -1; + } + + TemplateEntityData_t *pEntData = (TemplateEntityData_t *)malloc(sizeof(TemplateEntityData_t)); + pEntData->pszName = strdup( pszName ); + + // We may modify the values of the keys in this mapdata chunk later on to fix Entity I/O + // connections. For this reason, we need to ensure we have enough memory to do that. + int iKeys = MapEntity_GetNumKeysInEntity( pszMapData ); + int iExtraSpace = (strlen(ENTITYIO_FIXUP_STRING)+1) * iKeys; + + // Extra 1 because the mapdata passed in isn't null terminated + pEntData->iMapDataLength = nLen + iExtraSpace + 1; + pEntData->pszMapData = (char *)malloc( pEntData->iMapDataLength ); + memcpy(pEntData->pszMapData, pszMapData, nLen + 1); + pEntData->pszMapData[nLen] = '\0'; + + // We don't alloc these suckers right now because that gives us no time to + // tweak them for Entity I/O purposes. + pEntData->iszMapData = NULL_STRING; + pEntData->bNeedsEntityIOFixup = false; + pEntData->pszFixedMapData = NULL; + + return g_Templates.AddToTail(pEntData); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the specified index needs to be fixed up to be unique +// when the template is spawned. +//----------------------------------------------------------------------------- +bool Templates_IndexRequiresEntityIOFixup( int iIndex ) +{ + Assert( iIndex < g_Templates.Count() ); + return g_Templates[iIndex]->bNeedsEntityIOFixup; +} + +//----------------------------------------------------------------------------- +// Purpose: Looks up a template entity by its index in the templates +// Used by point_templates because they often have multiple templates with the same name +//----------------------------------------------------------------------------- +string_t Templates_FindByIndex( int iIndex ) +{ + Assert( iIndex < g_Templates.Count() ); + + // First time through we alloc the mapdata onto the pool. + // It's safe to do it now because this isn't called until post Entity I/O cleanup. + if ( g_Templates[iIndex]->iszMapData == NULL_STRING ) + { + g_Templates[iIndex]->iszMapData = AllocPooledString( g_Templates[iIndex]->pszMapData ); + } + + return g_Templates[iIndex]->iszMapData; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int Templates_GetStringSize( int iIndex ) +{ + Assert( iIndex < g_Templates.Count() ); + return g_Templates[iIndex]->iMapDataLength; +} + +//----------------------------------------------------------------------------- +// Purpose: Looks up a template entity by name, returning the map data blob as +// a null-terminated string containing key/value pairs. +// NOTE: This can't handle multiple templates with the same targetname. +//----------------------------------------------------------------------------- +string_t Templates_FindByTargetName(const char *pszName) +{ + int nCount = g_Templates.Count(); + for (int i = 0; i < nCount; i++) + { + TemplateEntityData_t *pTemplate = g_Templates.Element(i); + if ( !stricmp(pTemplate->pszName, pszName) ) + return Templates_FindByIndex( i ); + } + + return NULL_STRING; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: A new version of name fixup which targets all instances of a name +// in a keyvalue, including output parameters. +//----------------------------------------------------------------------------- +void Templates_NewNameFixup( CUtlVector< grouptemplate_t > &GroupTemplates, int i, int iCount, CEntityMapData *mapData, CUtlDict< int, int > &KeyInstanceCount, char *keyName, char *value ) +{ + do + { + // Ignore targetnames + if ( !stricmp( keyName, "targetname" ) ) + continue; + + // Add to the count for this + int idx = KeyInstanceCount.Find( keyName ); + if ( idx == KeyInstanceCount.InvalidIndex() ) + { + idx = KeyInstanceCount.Insert( keyName, 0 ); + } + KeyInstanceCount[idx]++; + + // Loop through our group templates + for ( int iTName = 0; iTName < iCount; iTName++ ) + { + char *pName = GroupTemplates[iTName].pszName; + if (strstr( value, pName ) == NULL) + continue; + + if ( template_debug.GetInt() ) + { + Msg("Template Connection Found: Key %s (\"%s\") in entity named \"%s\"(%d) matches entity %d's targetname\n", keyName, value, GroupTemplates[i].pszName, i, iTName ); + } + + char newvalue[MAPKEY_MAXLENGTH]; + char fixedup[MAPKEY_MAXLENGTH]; + Q_strncpy( fixedup, pName, MAPKEY_MAXLENGTH ); + Q_strncat( fixedup, ENTITYIO_FIXUP_STRING, sizeof( fixedup ), COPY_ALL_CHARACTERS ); + + // Get the current key instance. (-1 because it's this one we're changing) + int nKeyInstance = KeyInstanceCount[idx] - 1; + + // Add our IO value to the targetname + V_StrSubst( value, pName, fixedup, newvalue, MAPKEY_MAXLENGTH ); + + if ( template_debug.GetInt() ) + { + Msg(" Fixed up value: Key %s with \"%s\" in entity named \"%s\"(%d) has become \"%s\"\n", keyName, value, GroupTemplates[i].pszName, i, newvalue ); + } + + mapData->SetValue( keyName, newvalue, nKeyInstance ); + Q_strncpy( value, newvalue, MAPKEY_MAXLENGTH ); + + // Remember we changed this targetname + GroupTemplates[iTName].bChangeTargetname = true; + + // Set both entity's flags telling them their template needs fixup when it's spawned + g_Templates[ GroupTemplates[i].iIndex ]->bNeedsEntityIOFixup = true; + g_Templates[ GroupTemplates[iTName].iIndex ]->bNeedsEntityIOFixup = true; + } + } + while ( mapData->GetNextKey(keyName, value) ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: A CPointTemplate has asked us to reconnect all the entity I/O links +// inside it's templates. Go through the keys and add look for values +// that match a name within the group's entity names. Append %d to any +// found values, which will later be filled out by a unique identifier +// whenever the template is instanced. +//----------------------------------------------------------------------------- +void Templates_ReconnectIOForGroup( CPointTemplate *pGroup ) +{ + int iCount = pGroup->GetNumTemplates(); + if ( !iCount ) + return; + + // First assemble a list of the targetnames of all the templates in the group. + // We need to store off the original names here, because we're going to change + // them as we go along. + CUtlVector< grouptemplate_t > GroupTemplates; + int i; + for ( i = 0; i < iCount; i++ ) + { + grouptemplate_t newGroupTemplate; + newGroupTemplate.iIndex = pGroup->GetTemplateIndexForTemplate(i); + newGroupTemplate.pMapDataParser = new CEntityMapData( g_Templates[ newGroupTemplate.iIndex ]->pszMapData, g_Templates[ newGroupTemplate.iIndex ]->iMapDataLength ); + Assert( newGroupTemplate.pMapDataParser ); + newGroupTemplate.pMapDataParser->ExtractValue( "targetname", newGroupTemplate.pszName ); + newGroupTemplate.bChangeTargetname = false; + + GroupTemplates.AddToTail( newGroupTemplate ); + } + + if (pGroup->AllowNameFixup()) + { + char keyName[MAPKEY_MAXLENGTH]; + char value[MAPKEY_MAXLENGTH]; + char valueclipped[MAPKEY_MAXLENGTH]; + + // Now go through all the entities in the group and parse their mapdata keyvalues. + // We're looking for any values that match targetnames of any of the group entities. + for ( i = 0; i < iCount; i++ ) + { + // We need to know what instance of each key we're changing. + // Store a table of the count of the keys we've run into. + CUtlDict< int, int > KeyInstanceCount; + CEntityMapData *mapData = GroupTemplates[i].pMapDataParser; + + // Loop through our keys + if ( !mapData->GetFirstKey(keyName, value) ) + continue; + +#ifdef MAPBASE + if ( pGroup->NameFixupExpanded() ) + { + Templates_NewNameFixup( GroupTemplates, i, iCount, mapData, KeyInstanceCount, keyName, value ); + continue; + } +#endif + + do + { + // Ignore targetnames + if ( !stricmp( keyName, "targetname" ) ) + continue; + + // Add to the count for this + int idx = KeyInstanceCount.Find( keyName ); + if ( idx == KeyInstanceCount.InvalidIndex() ) + { + idx = KeyInstanceCount.Insert( keyName, 0 ); + } + KeyInstanceCount[idx]++; + + // Entity I/O values are stored as "Targetname,", so we need to see if there's a ',' in the string + char *sValue = value; + // FIXME: This is very brittle. Any key with a , will not be found. + char *s = strchr( value, ',' ); + if ( s ) + { + // Grab just the targetname of the receiver + Q_strncpy( valueclipped, value, (s - value+1) ); + sValue = valueclipped; + } + + // Loop through our group templates + for ( int iTName = 0; iTName < iCount; iTName++ ) + { + char *pName = GroupTemplates[iTName].pszName; + if ( stricmp( pName, sValue ) ) + continue; + + if ( template_debug.GetInt() ) + { + Msg("Template Connection Found: Key %s (\"%s\") in entity named \"%s\"(%d) matches entity %d's targetname\n", keyName, sValue, GroupTemplates[i].pszName, i, iTName ); + } + + char newvalue[MAPKEY_MAXLENGTH]; + + // Get the current key instance. (-1 because it's this one we're changing) + int nKeyInstance = KeyInstanceCount[idx] - 1; + + // Add our IO value to the targetname + // We need to append it if this isn't an Entity I/O value, or prepend it to the ',' if it is + if ( s ) + { + Q_strncpy( newvalue, valueclipped, MAPKEY_MAXLENGTH ); + Q_strncat( newvalue, ENTITYIO_FIXUP_STRING, sizeof(newvalue), COPY_ALL_CHARACTERS ); + Q_strncat( newvalue, s, sizeof(newvalue), COPY_ALL_CHARACTERS ); + mapData->SetValue( keyName, newvalue, nKeyInstance ); + } + else + { + Q_strncpy( newvalue, sValue, MAPKEY_MAXLENGTH ); + Q_strncat( newvalue, ENTITYIO_FIXUP_STRING, sizeof(newvalue), COPY_ALL_CHARACTERS ); + mapData->SetValue( keyName, newvalue, nKeyInstance ); + } + + // Remember we changed this targetname + GroupTemplates[iTName].bChangeTargetname = true; + + // Set both entity's flags telling them their template needs fixup when it's spawned + g_Templates[ GroupTemplates[i].iIndex ]->bNeedsEntityIOFixup = true; + g_Templates[ GroupTemplates[iTName].iIndex ]->bNeedsEntityIOFixup = true; + } + } + while ( mapData->GetNextKey(keyName, value) ); + } + + // Now change targetnames for all entities that need them changed + for ( i = 0; i < iCount; i++ ) + { + char value[MAPKEY_MAXLENGTH]; + + if ( GroupTemplates[i].bChangeTargetname ) + { + CEntityMapData *mapData = GroupTemplates[i].pMapDataParser; + mapData->ExtractValue( "targetname", value ); + Q_strncat( value, ENTITYIO_FIXUP_STRING, sizeof(value), COPY_ALL_CHARACTERS ); + mapData->SetValue( "targetname", value ); + } + } + } + + // Delete our group parsers + for ( i = 0; i < iCount; i++ ) + { + delete GroupTemplates[i].pMapDataParser; + } + GroupTemplates.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: Someone's about to start instancing a new group of entities. +// Generate a unique identifier for this group. +//----------------------------------------------------------------------------- +void Templates_StartUniqueInstance( void ) +{ + g_iCurrentTemplateInstance++; + + // Make sure there's enough room to fit it into the string + int iMax = pow(10.0f, (int)((strlen(ENTITYIO_FIXUP_STRING) - 1))); // -1 for the & + if ( g_iCurrentTemplateInstance >= iMax ) + { + // We won't hit this. + Assert(0); + // Hopefully there were still be instance number 0 around. + g_iCurrentTemplateInstance = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Someone wants to spawn an instance of a template that requires +// entity IO fixup. Fill out the pMapData with a copy of the template +// with unique key/values where the template requires them. +//----------------------------------------------------------------------------- +char *Templates_GetEntityIOFixedMapData( int iIndex ) +{ +#ifndef MAPBASE // This code also runs when the point_template's script scope is active + Assert( Templates_IndexRequiresEntityIOFixup( iIndex ) ); +#endif + + // First time through? + if ( !g_Templates[iIndex]->pszFixedMapData ) + { + g_Templates[iIndex]->pszFixedMapData = new char[g_Templates[iIndex]->iMapDataLength]; + Q_strncpy( g_Templates[iIndex]->pszFixedMapData, g_Templates[iIndex]->pszMapData, g_Templates[iIndex]->iMapDataLength ); + } + + int iFixupSize = strlen(ENTITYIO_FIXUP_STRING); // don't include \0 when copying in the fixup + char *sOurFixup = new char[iFixupSize+1]; // do alloc room here for the null terminator + Q_snprintf( sOurFixup, iFixupSize+1, "%c%.4d", ENTITYIO_FIXUP_STRING[0], g_iCurrentTemplateInstance ); + + // Now rip through the map data string and replace any instances of the fixup string with our unique identifier + char *c = g_Templates[iIndex]->pszFixedMapData; + do + { + if ( *c == ENTITYIO_FIXUP_STRING[0] ) + { + // Make sure it's our fixup string + bool bValid = true; + for ( int i = 1; i < iFixupSize; i++ ) + { + // Look for any number, because we've already used this string + if ( !(*(c+i) >= '0' && *(c+i) <= '9') ) + { + // Some other string + bValid = false; + break; + } + } + + // Stomp it with our unique string + if ( bValid ) + { + memcpy( c, sOurFixup, iFixupSize ); + c += iFixupSize; + } + } + c++; + } while (*c); + + return g_Templates[iIndex]->pszFixedMapData; +} + +//----------------------------------------------------------------------------- +// Purpose: Frees all the template data. Called on level shutdown. +//----------------------------------------------------------------------------- +void Templates_RemoveAll(void) +{ + int nCount = g_Templates.Count(); + for (int i = 0; i < nCount; i++) + { + TemplateEntityData_t *pTemplate = g_Templates.Element(i); + + free((void *)pTemplate->pszName); + free(pTemplate->pszMapData); + if ( pTemplate->pszFixedMapData ) + { + free(pTemplate->pszFixedMapData); + } + + free(pTemplate); + } + + g_Templates.RemoveAll(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Hooks in the template manager's callbacks. +//----------------------------------------------------------------------------- +class CTemplatesHook : public CAutoGameSystem +{ +public: + CTemplatesHook( char const *name ) : CAutoGameSystem( name ) + { + } + + virtual void LevelShutdownPostEntity( void ) + { + Templates_RemoveAll(); + } +}; + +CTemplatesHook g_TemplateEntityHook( "CTemplatesHook" ); + + +//----------------------------------------------------------------------------- +// TEMPLATE SAVE / RESTORE +//----------------------------------------------------------------------------- +static short TEMPLATE_SAVE_RESTORE_VERSION = 1; + +class CTemplate_SaveRestoreBlockHandler : public CDefSaveRestoreBlockHandler +{ +public: + const char *GetBlockName() + { + return "Templates"; + } + + //--------------------------------- + + void Save( ISave *pSave ) + { + pSave->WriteInt( &g_iCurrentTemplateInstance ); + + short nCount = g_Templates.Count(); + pSave->WriteShort( &nCount ); + for ( int i = 0; i < nCount; i++ ) + { + TemplateEntityData_t *pTemplate = g_Templates[i]; + pSave->WriteAll( pTemplate ); + pSave->WriteString( pTemplate->pszName ); + pSave->WriteString( pTemplate->pszMapData ); + } + } + + //--------------------------------- + + void WriteSaveHeaders( ISave *pSave ) + { + pSave->WriteShort( &TEMPLATE_SAVE_RESTORE_VERSION ); + } + + //--------------------------------- + + void ReadRestoreHeaders( IRestore *pRestore ) + { + // No reason why any future version shouldn't try to retain backward compatability. The default here is to not do so. + short version; + pRestore->ReadShort( &version ); + m_fDoLoad = ( version == TEMPLATE_SAVE_RESTORE_VERSION ); + } + + //--------------------------------- + + void Restore( IRestore *pRestore, bool createPlayers ) + { + if ( m_fDoLoad ) + { + Templates_RemoveAll(); + g_Templates.Purge(); + g_iCurrentTemplateInstance = pRestore->ReadInt(); + + int iTemplates = pRestore->ReadShort(); + while ( iTemplates-- ) + { + TemplateEntityData_t *pNewTemplate = (TemplateEntityData_t *)malloc(sizeof(TemplateEntityData_t)); + pRestore->ReadAll( pNewTemplate ); + + int sizeData = 0;//pRestore->SkipHeader(); + char szName[MAPKEY_MAXLENGTH]; + pRestore->ReadString( szName, MAPKEY_MAXLENGTH, sizeData ); + pNewTemplate->pszName = strdup( szName ); + //sizeData = pRestore->SkipHeader(); + pNewTemplate->pszMapData = (char *)malloc( pNewTemplate->iMapDataLength ); + pRestore->ReadString( pNewTemplate->pszMapData, pNewTemplate->iMapDataLength, sizeData ); + + // Set this to NULL so it'll be created the first time it gets used + pNewTemplate->pszFixedMapData = NULL; + + g_Templates.AddToTail( pNewTemplate ); + } + } + + } + +private: + bool m_fDoLoad; +}; + +//----------------------------------------------------------------------------- + +CTemplate_SaveRestoreBlockHandler g_Template_SaveRestoreBlockHandler; + +//------------------------------------- + +ISaveRestoreBlockHandler *GetTemplateSaveRestoreBlockHandler() +{ + return &g_Template_SaveRestoreBlockHandler; +} diff --git a/sp/src/game/server/TemplateEntities.h b/sp/src/game/server/TemplateEntities.h new file mode 100644 index 00000000..6d0fa1b0 --- /dev/null +++ b/sp/src/game/server/TemplateEntities.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Template entities are used by spawners to create copies of entities +// that were configured by the level designer. This allows us to spawn +// entities with arbitrary sets of key/value data and entity I/O +// connections. +// +//=============================================================================// + +#ifndef TEMPLATEENTITIES_H +#define TEMPLATEENTITIES_H +#ifdef _WIN32 +#pragma once +#endif + +#include "isaverestore.h" + +class CBaseEntity; +class CPointTemplate; + +int Templates_Add(CBaseEntity *pEntity, const char *pszMapData, int nLen); +string_t Templates_FindByIndex( int iIndex ); +int Templates_GetStringSize( int iIndex ); +string_t Templates_FindByTargetName(const char *pszName); +void Templates_ReconnectIOForGroup( CPointTemplate *pGroup ); + +// Some templates have Entity I/O connecting the entities within the template. +// Unique versions of these templates need to be created whenever they're instanced. +void Templates_StartUniqueInstance( void ); +bool Templates_IndexRequiresEntityIOFixup( int iIndex ); +char *Templates_GetEntityIOFixedMapData( int iIndex ); + +// Save / Restore +ISaveRestoreBlockHandler *GetTemplateSaveRestoreBlockHandler( void ); + +#endif // TEMPLATEENTITIES_H diff --git a/sp/src/game/server/WaterLODControl.cpp b/sp/src/game/server/WaterLODControl.cpp new file mode 100644 index 00000000..ebe61665 --- /dev/null +++ b/sp/src/game/server/WaterLODControl.cpp @@ -0,0 +1,118 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Shadow control entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//------------------------------------------------------------------------------ +// FIXME: This really should inherit from something more lightweight +//------------------------------------------------------------------------------ + + +//------------------------------------------------------------------------------ +// Purpose : Water LOD control entity +//------------------------------------------------------------------------------ +class CWaterLODControl : public CBaseEntity +{ +public: + DECLARE_CLASS( CWaterLODControl, CBaseEntity ); + + CWaterLODControl(); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + int UpdateTransmitState(); + void SetCheapWaterStartDistance( inputdata_t &inputdata ); + void SetCheapWaterEndDistance( inputdata_t &inputdata ); + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + CNetworkVar( float, m_flCheapWaterStartDistance ); + CNetworkVar( float, m_flCheapWaterEndDistance ); +}; + +LINK_ENTITY_TO_CLASS(water_lod_control, CWaterLODControl); + +BEGIN_DATADESC( CWaterLODControl ) + + DEFINE_KEYFIELD( m_flCheapWaterStartDistance, FIELD_FLOAT, "cheapwaterstartdistance" ), + DEFINE_KEYFIELD( m_flCheapWaterEndDistance, FIELD_FLOAT, "cheapwaterenddistance" ), + + // Inputs + DEFINE_INPUT( m_flCheapWaterStartDistance, FIELD_FLOAT, "SetCheapWaterStartDistance" ), + DEFINE_INPUT( m_flCheapWaterEndDistance, FIELD_FLOAT, "SetCheapWaterEndDistance" ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CWaterLODControl, DT_WaterLODControl) + SendPropFloat(SENDINFO(m_flCheapWaterStartDistance), 0, SPROP_NOSCALE ), + SendPropFloat(SENDINFO(m_flCheapWaterEndDistance), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + + +CWaterLODControl::CWaterLODControl() +{ + m_flCheapWaterStartDistance = 1000.0f; + m_flCheapWaterEndDistance = 2000.0f; +} + + +//------------------------------------------------------------------------------ +// Purpose : Send even though we don't have a model +//------------------------------------------------------------------------------ +int CWaterLODControl::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +bool CWaterLODControl::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "cheapwaterstartdistance" ) ) + { + m_flCheapWaterStartDistance = atof( szValue ); + return true; + } + + if ( FStrEq( szKeyName, "cheapwaterenddistance" ) ) + { + m_flCheapWaterEndDistance = atof( szValue ); + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CWaterLODControl::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); +} + +//------------------------------------------------------------------------------ +// Input values +//------------------------------------------------------------------------------ +void CWaterLODControl::SetCheapWaterStartDistance( inputdata_t &inputdata ) +{ + m_flCheapWaterStartDistance = atof( inputdata.value.String() ); +} + +void CWaterLODControl::SetCheapWaterEndDistance( inputdata_t &inputdata ) +{ + m_flCheapWaterEndDistance = atof( inputdata.value.String() ); +} diff --git a/sp/src/game/server/base_gameinterface.cpp b/sp/src/game/server/base_gameinterface.cpp new file mode 100644 index 00000000..cc20c89b --- /dev/null +++ b/sp/src/game/server/base_gameinterface.cpp @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "gameinterface.h" +#include "mapentities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void CServerGameClients::GetPlayerLimits( int& minplayers, int& maxplayers, int &defaultMaxPlayers ) const +{ + minplayers = defaultMaxPlayers = 1; + maxplayers = MAX_PLAYERS; +} + + +// -------------------------------------------------------------------------------------------- // +// Mod-specific CServerGameDLL implementation. +// -------------------------------------------------------------------------------------------- // + +void CServerGameDLL::LevelInit_ParseAllEntities( const char *pMapEntities ) +{ +} diff --git a/sp/src/game/server/base_transmit_proxy.cpp b/sp/src/game/server/base_transmit_proxy.cpp new file mode 100644 index 00000000..4832af8d --- /dev/null +++ b/sp/src/game/server/base_transmit_proxy.cpp @@ -0,0 +1,60 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "base_transmit_proxy.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CBaseTransmitProxy::CBaseTransmitProxy( CBaseEntity *pEnt ) +{ + m_hEnt = pEnt; + m_refCount = 0; +} + + +CBaseTransmitProxy::~CBaseTransmitProxy() +{ + // Unlink from our parent entity. + if ( m_hEnt ) + { + m_refCount = 0xFFFF; // Prevent us from deleting ourselves again. + // m_hEnt->NetworkProp()->SetTransmitProxy( NULL ); + } +} + + +int CBaseTransmitProxy::ShouldTransmit( const CCheckTransmitInfo *pInfo, int nPrevShouldTransmitResult ) +{ + // Anyone implementing a transmit proxy should override this since that's the point!! + Assert( false ); + return FL_EDICT_DONTSEND; +} + + +void CBaseTransmitProxy::AddRef() +{ + m_refCount++; +} + + +void CBaseTransmitProxy::Release() +{ + if ( m_refCount == 0xFFFF ) + { + // This means we are inside our destructor already, so we don't want to do anything here. + } + else if ( m_refCount <= 1 ) + { + delete this; + } + else + { + --m_refCount; + } +} + diff --git a/sp/src/game/server/base_transmit_proxy.h b/sp/src/game/server/base_transmit_proxy.h new file mode 100644 index 00000000..d319779f --- /dev/null +++ b/sp/src/game/server/base_transmit_proxy.h @@ -0,0 +1,41 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef BASE_TRANSMIT_PROXY_H +#define BASE_TRANSMIT_PROXY_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "ehandle.h" + + +class CBaseEntity; + + +class CBaseTransmitProxy +{ +public: + + CBaseTransmitProxy( CBaseEntity *pEnt ); + virtual ~CBaseTransmitProxy(); + + // Override this to control the ShouldTransmit behavior of whatever entity the proxy is attached to. + // bPrevShouldTransmitResult is what the proxy's entity's ShouldTransmit() returned. + virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo, int nPrevShouldTransmitResult ); + + + void AddRef(); + void Release(); + +private: + EHANDLE m_hEnt; + unsigned short m_refCount; +}; + + +#endif // BASE_TRANSMIT_PROXY_H diff --git a/sp/src/game/server/basecombatcharacter.cpp b/sp/src/game/server/basecombatcharacter.cpp index 22e69c905868645b28e634f7bae691be84f6bd7e..b740e3f0bb8f1aefec0ff5249b27cd89bd67c610 100644 GIT binary patch delta 38776 zcmd6Q34B!5+4t`tATw-1vOyAZvoB<43kf0n2H7BC7s4c&BqPbpFf$3nh!L%OUGS)< zZiuZ*>%QAs6{z-WU8>?zTeY}WYhA$BR&A|(|L2^0@7zfO`n|vJ{oaosNoMZZpY!a` zbIv{MtSP^EZ%T7+Zi(?vtMqt>e6F6}fVS4rH|WH7pV#Aa1Y90>mR8=^r^!OU=5zX; zzCmYKj%GBazqz^g+}u5@vQNSverVmYJ^R8V@ywp&a(DI(bUC%e&JKs)nbVuN=aU3J zgU#dFEGolN+~ev%q2+h5l5XFxW8VnQr`W5S#WOL0%1r^C@FOQ)Wc$a79g;;T$)1VwUGSJiflJ&_{Y_0Y_j|U01-_FUL4^QXD^WQVgFuF@~p2oR?h*2y70qqjJ24rH#!_9~W?@}JL3>q01HWftJbz(g3>F<)-qO6ft-OIx zoRq**@l!WRn_^Vq{z=RDy^~_Y#&q`9q`U;ZmX@2V4`J47EN`hOZ>{DFC#UjFlaqq| zT{t<_X17;*+-n^^jUSpE&Yz#G@gtL0F#7|`PR?V&fs8Ijrj+pmQz~fIeSRk`7--S0 zwXd#jE3a>$BEH2|Jy{jwWAf~PZvgew8udT?16vxi^Ut%xd5S%ox7w@tkL+dqkM;$@ zsYOlA=G9X(xMyk(-#ayLg5AbnnwrZeOv~j3)9TQA*R(u-_q0X){b@CP{`BIR2JnrU z?wH;Z?CquL3(>?jBZn8xSit*clOEo}-|#U>E>MG5KrfrLzcI3bHqP0Z#?6EnkPeJg0+ zNo<(|wyf2=T>;GxM&atz1|2?^qodF1=O~?Icew*Lo|h5LFHf4tTazOBX-U86JqbDcZJNq1dzsB!Mj^*bj&#~Kw?cRZoK38WEzazOJ$!;6A+jiOQwn3LK zFyQFZ20gAWt+jU`(B;|T#$MZoxt6kcn%!oz`J90PpS#%3ohccU@c=FP8!3yYZqi)Z z=3+e9W#_M_6rO}n{HB!oQCf-Cw5Fi}CA;jqitToNHF7FDQ_C^s$5QW@U?1k&(hB(A zwB(6)8+yDcC7i#RHb2U4b9HO!nzy2|QcF-h$=M{N&qYN{t2`Z~MT6RGl0ne3bJLsn zE9r%NdPdA7JjavVjOE;&5kvE(uX{2UpCUg}Guaj&cJad*u@mgO__rCGF{>?^`TX+C z2L7ka?AdBRX;a(ED;lcPGq8Msm2aP)EC2p&er>qH4r&{k*R{4+H&I32oR!T-&WPey zWfkz>W?gy`K+GS@p2Gv#k^JK9P7FglX>v|YjENu;bu2^KHIavS+AxlI%8*hV?0hP*QQ_i$c*G`8E;-wqTA)Vs|t=FczYnfY<2Pzzm- z7_5cXc6WYojLQnjXpEN&k|MDJ+L4}iZ)>N|$FzL)s@w&F9ee|w0lvE=Y9h@@cu!~H6Z0_>`6;gepQ`K1o2nWB zMQ;VBwaw|nv+h2RBVe-yT>Z2y;4+HeRbH_yd{%?g?+@}jyNw?5N0-guPcO^lUo2|} zryAz#OV*sMb144vl3t#$ybQ#tt&Zc~6({oxmP`C^UXe9cL77^5_Eg$DXvV~V_;sbN z{77j8pH$Xps#?b(p9dxg7+z9N+Qw7m3*=h9DQ{D}acxB{-&+x_VR{lRY&tVF z07uK?o`W6uQ`k<0gPUD@%@!^!cY$# zD@?7B~o$M6c8tDUZLS*ctoP40x6;r zq>Lg07<+0pCjb3(Ye_@VMtxs*>U=kU*J zr}7tT7b-y5>tgA{E}m7FsTR4pE>l)LS`*8ES~n|#063O`6IVJ?mp2D^ud{YTMb~G= z!wyC8mimGP3Tw^vDt4;#iy-6BJJC%AgDl>uKTQE63)pw&ip)53(VEU-XmqWvAzvCG zPp`=3U$2PcQ7eo1>Xk)c2?mt)+J?6N)0Hdv7b{5@&1gvGO`vrEa4oDekB_%E&Sk~? z(S~7c=4!`KdqDF!hKhM*PU^8yN8A^tTh?!AiCgpgBtg8$PMIgjY7Fs}0)TT*mKjUa%6KjH*Y-t#@rw z0?`||n#~qM9Ol4aWN5^fwk%L{^|z$kvAG6Vz#%bpSn7Q1n#J6^s*K;aDqnurR+rL7 z>{}bocdXV zUr=SY^?Cd`r$dj+!vwi!OB80|Ya@edRc|(aKi8(^Vdf!1$3&>0s0qcP@s=@TeJMY0 zZKPUC`3aXXWBppH*1Nt0Fm-ix*V(xZYxa2tdU~}EPoP(WCk6fa`BNK~NmQA%ks2@BSiyI0EK{5R#Kr($ zu&ES}2R7yMTQ-#w)3Vt*Jz(B$QcL((n-=k$Q%A-;-+yWc;! z?qdG!sf*C#;>`uzwRs-DV6&Upk!{Sj@EKbQp>f%QyCYj~*h161a7zNeW6Mf>`g%(} zuWY}H|FJ!D%q$|dF5!(^lO|ICVpbE24va`Da%MDm4B7|NguZFM}q z!x>L_@w{`H(uC=Gb9bSoem?)YdnUK_7?eY8Y=hp)$!8SPco z6>Dnwb^Y~FA^O|v9MHD9Y6sll7&&B_k_Klmrz!3PxJ<w>Ec_4yGu zF&~>JLoWZQUl)oAfh3TVvT&LlgXKP_!!SGf&7Kry7nioOG!X5uU6e+{015M+tXS+k z?*f|ktb=T}4NdaZynWs>5S`AjtOlLf{Wkodu;_?6iG=^KRRtDfTPxD4fV!l?dVI=Y z^9}?wO)H`=PMDNktdhL?qAkoje!%X`j>KYk;k`(hcLA)?(3=Z}An$ zZOiv(zCml5Jm9)arGRtqvY7^>(Qo`x}~D)0HqLh5T}V)CoWsbG)%ho&ZP2 z`W2)Bnm1r4l3P9yjWul;Sc;!4-tuNR{k>vv1|H5DERy@xGPp`H&f5ly@p$hLJeZOT zkP0*Y2;!`*EQY_m1BUtV6+7oc!%)f=c@YNuRvr)G#2a=h4fqE;&mdD<{Ls%*c;?V- z;ywTtUo}|9&m9WWS=04H$q6WsY)En-gJM_);S$s*z22In=?oeALcE?aER*5<|D&v_^ZPiBl^0$%{$yqpHx~= zMSaU&UgdNIdYe3O0>R3X76R=u8E3M(vK7*$n?>rQL4DAQy9W8y_IY+FXEz~>qF;XA z8DZFwYtAU(&z%9I0zF8Un!BsSPPj|n`Mw;T%f|9UyWkVT%5`-Mh8G!O9Uf1g&4#Ez zhqDW`WT=2*_hSrh|UpR9f_l%_Rh>=86 zpv|t7<^?slX(U!+l4qnceY^_Tbe-#}wo-2=88GiG2um!)0rOT1cr*$i$n}V@l}r;y z&tsGLvuB0J(5lMeVHt$EYwIGo8{dlgf6j`ov>T>ocVCq!fJKLbK#2sK4R&&?uamSX zT5G+(MaC=AGvq4UcgORQ-4MHk4L{wj@uznu@=tan#&UK7&pSJbx1F8Lht5vq*Pm^{ zlxNSL2LOI@_FVjKxgeTr=PW%1{To8aIobUBbMjH?#dAnz!x}j!6IMbe z7=>0>-&VW2c}+`uU3Ga?5zqfYav~0`73zt1(DJw22$|+a}(ufRN@=Yjj=qRON*F+wz311)2POHi5C_EfOD~3Cu0^P z7dEJo4_#OrWCG5MXI&f@G8(!9iL~Yn4f)FMifm@r+#xrv=w{4N#Y-02FU>FZ_cmu@}<%T0|-+2 zEtk&@DHq?aW^w$VmxlxL(!iENoA>U?(FSt6JKYVqsG?oCeGMb6$`?pN1NXzurIow@+PTjSXt)UcHMAnMa z9q>0g{M)o_jpseFL~0o2KiWvsz41rO$jZ>+s`GSy>~Kh*AUlH z{LgAl5AzSNsU+q;s!JFb<=3v4UindD-FR; z;RqYQ?uJ}!g$hr+d&2@$jozC@VqJbYj)x2w65Fz~(vjD>fG4$-s8594zb#Ijs z|GRZ>G0(oymSZT>=A31(E_b&lr`8#$a`ZcTpasi5saMyHiR5`yi@>C=xiOAEa-$QI zir*KRDob=w>hd$VT!YKmdBVPMG}yVXm_N9WJT4#YTf!4=QjwBjJKuJr28G6KXdzeA zj8K&Er<)f17b)WfLbJ-77xU9^PLp_k>&@Bxm7CMB7vVhYmT5fhmbtv}mSn#9mbLiF z=I`8+fOTUkeu<8Jv`4$a}h+b9|~_jXNDclqrU zL>uO3-d?*5Era+QbXXcRq?dsKt0;NsdhK^?G1rS2n#J9ulV^g_H?Vb>|966b2!9L+ zc+OotF(E6oOp*B9gLhR}xQ$xAoo~EuAuqip5;+JI}p8hOfLonS1Y#DFIc;Rm)Ck=h2cfi2P0p&`3klSfNhqv~0sy zf9U=~Zhs)w6pu4|VHmeOu#kxPf(MrH0}pKCu@BbB-MR1Q89-^E*3}O#vZM}X_+OOzthp@EauA|UdOL|I1Ayspv<63 z#drYZc>Q6WUf3g9btXdBPz|X}p_W&DE71&AS{?|HFT*DnNjyygLa!!?VAmt-_^XfP zMWM_fayc_a@Wnj-=WF=D&x^Uaw6}l0jK}?=3n;SbamAr6^=UQvISZC7f>T8CQiB2W zL%-;phi7UA(zYY-vpIc&+L}iT`K6B*&Y@}wK57<*&#;)k^JtM2k=P$ogGIyqUsmzK zUzYNRf4P`MBlY#gFO$?qn`k+}=7<@m!t2-cm{N18LjPkqQrW~b`OS|N@Yfzollww@ zZ|NL0qaH8g>mN^2UPW0-zjpCUA5V%Q_G};z=-r4J8j9U0gW6CzD8TaS69oyR$jKc^ zsP{L!Yg|4<>M3ys?wY+nk8j!EoNw+N>Qo|}(q(KpY-45PjxvtQy1L`RrGXu`ugf2(Uq?~L_RBt zM%hh99NznrWPKj1`Cp!bIrTl~vf+PuEpw^(4UDlTv-xGenN;}w=HspKczRqi!fp8X z#`pgyK-H4!%n$x4kxzeG@#E!BYrOsGG=Bcmnf(5zm+>P{>*j9sGY!O{Y{*TK=-b_g z_%g}PjoN&zU|}&o`ToBq0Kpr!5lT$E^_4ZW&MRJBPp*23o)Gir-LEF{3twHzpL~@o zk#y?sllWP`&!Awmz97Y-`QyK*yeGqvjy{|_p#{2;WGj;wk;mFT=v67FU`XMwY4F0s z1MTW|BFP~@=+{tRQ{7&*p}wh>oGX|?rK7KNfQ)8M+S;0w5ZwJ5#d~gjt(t%GS|uEk z6wUHBI(pnr6}>k0-(U)De<&wSk|3;Hj};y_4VRr?%bFTI6uA7iMVH52USmH0i)z?qqQ9ti{H!a_ox>5N%z0F4CL?_nCe9C*tlOQ_) zZ<75`lXJB zPkTQZ+&~x z!_WC}5#|M|6ZhW368YO7&Y;zZJx{WDh0)P}>Evhp1+mt@q)2^Dsfc8duBHSpms_xvbC^)3e9f#-5Bq>E zg)-jg2>4t(wbNI_{m!5HxC(>J`eZgw`D6(`wSQ75-3E7l5@uK?5}H=1Uiax@{?ezl z5u|0Vl}RPEyk?oyR?PE1>qkE~eU<^~7%Xi=X26bOe)uy2VeFAPeDRS|OTLMWfAFxs zYW%h%)z}yt|NO{0iEfQYvuHaofb1ft$=RcaGA}<$Sp-<^Ye!);*~NCo=1eo@LF`w& z&46}|pHCBIQ&}wE@p+=yJe47%7045cD|^04<2U>b@mGkx|HK0RSQ$U|g|2eV_$AI20gGOD%drARYmQ@d%%JW%vR#ivRQ-pJVz{R1Y$(vs!@Nd7MbYJL>VSmphK1)(1WQvwlilKjO;4l9p zQF6fGV>IH|eL9za^;rZj{3k^q-}t(QU;EEAzT#hP{KkK2{J_6-3fcT+9+8Soe7*_( z$)A2H!DHixzKoJ|K5XMhzg#?>YA7_My(#=Ejc@%bQf?&>kT8w>0dIXam;e4Nl7`2= zs-g#fIGV_Nzdi*Ic72`C@A;Y{4&zM3hTf&z_KE`wAk4n~R~(R>#*;o~o5a;K|Gk0# z;on(&(l;v9yXTuL<^?Sxb6?QTG>2;b18mgAjv;40`%etN?>~zSSp&ofGx)WfgXASe zjEra7@YN|STzr$pri-F^Y!-3d+wXzff8Vz@otp3cHiN(Z?PgwhEP(Y2mcnN9UmZ)A z#-9!@#hr>+jIBU-Qc}0kA}5{MsM{Hdc&@MsLrMjr-oL)Xv(u=Jr{c`nY|i}pw(7?A ziiY~8m4i-)mPnL9HETg6?B<%z>6 zhwm2C*Hsjn7R7Qa>T$+or&6gz#sn6J5$Y$fEHOBN)r$iY7-22A?_VddBr)S8wq5~A z^q<7QR)9dH?A4HsqCqvI7_+!dkn*N08q|A?D4ob+ zP2Lwtv% zJy2oRBvvWbOk(o^hI1ydBJl)0m@t_o5h{4Ww?z457EWK@qA%O=MVyCXar}tsv7p5Vcn}SA_cb`( z&}0_od095!-TxvF{Tn(gIKwZ-mTG4YcYdTv) zTGwgQSy7wjtF)t2{XV`SH$(zZk4Lh(<2V$>r;%)#$c$o?zAkZf zdlbtTKZ#;Vy3cS}yb#4!66l_YWKk1glG;U4G)qm9^+R|bfJuGG-wLViVqYYSJXu++ zcG9{d#erxxH$)Ez|vMbq-=3;n$*4`N|fJW8HGV#aJt zz|5Z0?LTpNF1u8MhdQarW?RMYV_2^!jb(k}&RCXb#@Cb>GCd(i;+{Ab%jy)yHpT&g z&x~W4H1ytlVCwJUbORtL!8gJ)Dc;Uy6U7#dr3!lvtNH(KInNiD^^vW!ejfC_I5J(6P*u1cHvV%$_S zpd@w`usPICa|+~=$%5`lVa?)L3ZqO#%sw|2TY_KF!kx-yB*`h#LZz%CvL1tZ;^tKJ z_g*SnFP5b->F1<)w@n@gbO#`-JNsPT{9E4f-th z<*{t>d>&L1oj6C+Cg@}?`=Q#I>TKpf>p#RWoM;v0vFtSbxjUBSkYPd$N@`kTsM9WP zN`x4BJ4fmv09SJy%R$St;#j)y=CX3RMjiGxVPZI{Y}Ht@Bu`X9Lw)*p&I59UOrMxE zz+{Xw!=TMkgWZs?4@TRYt@`g&T@z~J!GB#iP6qP;3{H&OFE3;{V!|S;4)T4ZDJEv3 z*r?+UqI(G|k*bEN0w^vn#lPX`Ast|n$S-86BqBEz%C!=DNzr|MA=>~bfcE}f2;@y# z%C?EimNE~9WksOPRYk0o%Apt*vxI5jR=UEiIKpi4NfA3`JYg7K%vuc(5t^o&JRW=` zLOfVJeycZ&*#?uf*hKjsx8^hwBJZB^O`3jCK5JMVdG$kgg;)k4fNK~?J5ek4w$TCc?BzZq z(Jlie!y>Yl)rt06wm~{C&$=*9yk5)lWT3$0h`QTAB-F83vAm9*Dz2+zP3s8&=E!RQ zO@Fod+(-{ld{SrF`fLrH8Bf9AThA^KX)7?+4^}Yg?4V@6J+!wJaJb^b$>QKjwiHs5 zHq7c6(mMyNWTq~(%!CG3COR5ez6Da2;@cb8bs$jLk7!?sHM$zvBA^_>ZcJ|k+s7MO zhWM}%T%4dRSxI%1(uszJw+W7bN1NCtUH!BGDL5z|qMxE>mX?e`LctQOWk#3?KavT3 zjPyM#D$!^C0D%suN@(8aw=nYhnb7)e3+o||hrS@)S7b!pzjx=95h)g}R{XSmHCqj0 zHWDYr3#-B9ksVnA-Y4_+;knN=4|XW_jD&9TSjp%^Iqi||Cx`z>p2PsuOq?B+B(JlE zKejSNY)#L*$s54=2owPX)wN!yqc6}aPY8%LZ7jFI@RB9;w^M~KnJ`Brpu+_W65Eo1C=!2G||0|#V?Zcg~JKc!v#LjUul90M~}Y(ac~`j6VlF04kHx- z)IDiED-_Guvz1_jz_-iSOIr?@&M$jYdM?E?>mljv;^YldEm*R_P)3h!fC6&y1~x<2 z%J*$RD*V9>EJHe3jeKl{#;9AMS1a5I?Ge?Yg?A%VA8Jq~`NX)Emw$&VfY^#=jg4YY zu>UgMTn0(Y8|v3q7m0swWNElnM%`(nGbD6OM5{og*x9ko@O3n8VY!t3XpoOM*v_IR zsS0rLE}(n>*;4cBQA%B`i_%kB23U$>RyO(|ld?K+5FzB?g1G!tsY%>=&m0VA6GfRF5Uvn!i4*G&_9%_B{1(xM)b+A7@QA?*PuGxn zc(quZ-OJ{~p{LK+C{;^B)A`dDRN)E^R}K`oCy->2M^k$zTs!u_BSxu|ioHd^FfH(M<}cSFHW@GzW1 z0QcE9$ma0jA?V$HHhr=>Y$h)EK)pPOU*wtC;{mm-+`()(l87Wjp9_B5z)&$Q(twjB zN1*ExFSH+r7m$3>i>W_CzwjwdeGS_V_l+z#Zzroh&J8l+$ac_4nhzA?_pvHd{Y~++ zY+AxvKb#J;22;gbr>oe!_^qGO$#<-U1z^LM4YTZ2&V) zycJ+c-wn}=2G}|sqK%W0J_3l01#Fg@7ND6Rw(Mjx$AC1PJTKn`#V>aNieWnui;%Dj zg`(*Y2G}A+;ShXqrw&1zxOIqCn6O)WI_>1Jxaf42W5N!24$M_}D&D>jW9%PfQzuJQ zwSY1+IK%G(rG(+T!z@V-OnB!%gT>oS{Yo6z%_1fe#fYsVgc)ZrTn8Zjg+B^VX}cJ1 zOtA?Em7cwer7Kab@LhI_>xoXdHxx*S7q6YkCh>oCBWjO_?YCLBK$(8QE*Zh0I6LA{r$vVz)u=4WJTb2DoaG{xsb_2YhwR} ztXN{q)fcgOC`VPQop}*cy6w7)*iy0QB12(4coAC`bhBK<;vf)Y8`aK+ljVpX2AsS+ zWwvO!7%uQN7qb*Gd@(~-2KvPYr-(}~K|K14iy3ad0@;vmrJ1V`&tAf^AF;0nM%#_bPg+v=y;jch9E$yu7^4#bWPe4EHsNw=aVZb?h=w zPwwR`Qa}2D{t%#(y;&A6x-K{LA2(FddoE{FNNJYfvQ$&UC{{yzA>P`9sKl|$*(6E(;91=hzQyXee0ED z*61$cBov9ZD_B5^4qLe&cb_6B5GOih3k#1Xq>(>H4E<`(h89w{dZRqm#3~z6YJry_ zLd64|%@=QRMhT2XSIUHejaRY)amkgeo^VuN^^>kC@uD;4BUiFT(n{%&sRAld9dcQU zIkEgI1lotLVx_wNc;G4)sd(#~SAm(uUCo+wX!)*Yy_&5icXbdX^o3?zS6;c2 zeu_knVEvRos`gSpB_p>3`|*$f<2oi-vPxqh00lck`LZ9ec5(fWSiYn}+QHxd2!6vk z*RXUI610iSTA^fYzJ`SpIn%#kF?{W!FJ4Z@Z4sT_w`70e{Rit3SpRhF#Y) z9M~EbS6|PnMzi9jH?W!$b5J@dBNG%w*j^SEPb9rorJ*6;g3|A@)V;k~+JF~ndo!+t zDS(Q=tTXpQ{n)n`n!!`_>4Uv&UO04Fok79Yk!Q}8yHrjV=Lf`lH?p!=8l;l$iqNqv zlP>86b0}B{=1{SZor>OX+DB1o?9bEt*n&j#3t|UrtjC?SI(>(-D3Np%TPPW@MIF|a zFtPn6Oc1yHR>_zwM#n#Wp0YX}ElAZvWgPTPR(m`9R^^&2yp_d`O~B~ARVo*E-^QZM#4RIU z{MfBb9{dwu-wHEr#cj+X?!1l7fw5@{eU+%KZ->zi+4l?O{-Iqd(I??Q?)gg7y&}{J zZI^_&zE8$>!QC1$Q+c=>2s0KFUf^Lm@FMnSQ2VRsSKrTM*yQlfqbqV5E~F1?d2mi#~1R_|8LAbtEy z7Ld%(?qqdHuhTClHhk$P-Nh<&Cf#rsE6AiNSXPNI=8le9vAFv#wnA@u>@HR-YVT&t z38BZd7dPC^HtRLnJ#3R=oxoDjjSN0@W-PcIeMlrmQG#E=a_UhLCt5UlvXGdS=GPpW z7f9ggqXelj1L_c=xr@YmrT29Ey$EXm=w4P*g9fs@?#cmQ75bperMw*D*c(cfx zh->bLs<;1sHb+l@6mQ?pij6}t)CYl{;E?|SMEB+gVD6s(09y*=dh!8QCDR{8?t^Sf zni`J)F=|md3o{U}>p>*oJoX@~m7+}nH5Bk7{vnnu@o>CSj01>vKV;BpV!se2?q4RJlE<(3zjU>-Ka>^~dQeqd^}qCR_w?VW zEXVHOFx=b;U|#VXu$!O%26?gH(66%JvT||uZ&@u8Z1f$NB98u+;i3ZM06h&ee#O&( z_3o!xX^g&x1aC78I(hd~aN2J@4Qpz`Gb}U2ms#`-TR_V*VIo|$bt3utS%$MMz@OOXSS(75pJNpi+%(3(ED@UnU9W6!e)DB#4l=++I9_X4wAu+f5>&YaM>e8y$qib;u)7m&ww!wW#o$6kQaXnPU6 zk^3UL>wA&*Ks@}Sa>RpZ4!?-~Nj${#bI`-0@er#}LIiva|8#2*tdok8#dC*PLaHp0 z<@5_bgzUPmqkz)l@fVA^FJV_#y(I0xJ6{5#CA`dv;VCC9^(sfRq+IXIj55lAIM=+) zQs6u6Z+GLkM5j|f%LmZD{4!e{bj)8$? z9%|(M^y{S8;HqBo8fybYE`1H0^uTMnpXJlnSalp020_u`)Q}MFhd;pK*76GqwE^7p z=g$$V{=jgGLtOX=7O)sdV*MZ4vdPej0XWIJq}y)V>&nGTQGC(*I->2~*Y$Lqt6vAr zKk_;&uy#`Q1`vY3foz=j-q1OH`&o zIBm+3O_ocM8RCyRN3tGTqZNdbJUx*Vy!b7Ki$aD)^IOn$a0_6hos0x>DvZY|%x2EF zDPMG0-WzC4cAOE&lG*+arDYC_%if_TyXdSOxCcqS$ah(*{P4?nnRMXAy~mP|-<$aI zJs3!2i4?!D5^~nO548N@`;xGMk#odTC=hSF4?gtI_o4Vk{~0+;h^GBnA}%TROWgzg zuoeBqnrr@iDZ_vrlGl>QfNrxRIb`PYSd+wPyOFz~F8`R+Ak69LV z9`gyPuJ;p}26)XU5Na=c0(SI|PbiB76i>HC3TQczf`1D3LSH3@rGBPzyjwp5hMoQy zcI>{-SPDcO@j?Q9y}zjiFWvwH0c$$?8KtZPI>RFM2rHBfO!6m#eNij%+%drQ>LYBX zv>kNTWpKD*@x&2UBYi=VKPeic?rBKz&Og{3sab}0D;ke7M+{XmWdlCyT^n-K#Q#e_ zSBoM}K>0kcea`xj-x)d{O;ru94xbqsXfbifQMu{(-IOoCW; zCy4rgumn==iSdu!j<^2-TKe%n*nH}OfMNI)1(oC(?evvpFK>3YKy`F>t-(<{^F~Ls zQ_Y4&{y*6*77jTM%OH%q^Gk(s26Vsrl0_KO#@x+ek?<8@RrnQ)`(8ax<4b3SiQ2Cf zH~KzZ;~)hk-V)Ts-@azJ(Uj0K;$OhyW&dJcQX{M&!B-MADrbn2@x`HkF$&63En~xv zv*O?UTe;r8{x_=@z=1sB{e~rDaO*t9)8DX#FiZ%qOo&%Gy?ve`c>;~bP-N);59AazHWR7%NrF>psR(meNwJ7=`Ie<6xar>mPvmOE=s0 zI&iTq9eA?Ruz2zqP=GTm%l)F>7vUJ@RT1)ot^8~kbKmOT|l|pkEBSayVL2X5lkl@ zeYPgnk=zU$wV?E?pI{Lq^=frgR^#f!T@X0brGJpwZZrgv@hJk6=>{}lD*I0-umHxi zd-QBmo|Kzu32DS;>B;>0M$VU(fUARDo&DHKD#+3jz4F#jykh{BheyyrmsbEK_OTVP z=yNKiQb%jaJ~SF^Y-dilvwsME8Nyue3msKc$#?cJq9yvBecjZ<8E1^`9xwLjaR&0o z4nW*ZGV&8m&W$UN;bZ9bxsZZq8NwZe#|QwTmuG2Mc*ou`@Gra`461?zd#M-jF6LjR4jWeJN5(;BP!aG$TQ7n3q z3Gf#Gg6<~zswPTZY8EhM%ZAy@LxH7bc$4%F*v0c-^>WxNY zhOzKaf!_2~YIjCd%l1*l2{RC?e4<`S=)_utT1K28#qd zX-9%=d{m+}^`nyOfL+z@5lU=o@p(F(T?0NO8buSf!U0(ic?Qy; zU-i%{1R(zdB$v^tBa2q&K#F)5>I0l3Iy?=LO){)&2sG;IY)3L~U|A&{k2OR*jgz@N zU9W7w8cXtn0eALFIRn2hE)ri*pk_xakW@ff8GgubDZ~M#E~o?^4~t{Y$-KZ=9xte8l;cKU^|o9coHete^^DAs}-79Vt>I7)MP`r1OE> z$ZEK1+%tbnqR$0LYfw>WvbfC(0DXpZ@r-&*b2sfS zHJg!u;qFyJOJ)}7hnrVRAlK7XP94>ZhkJ+) z{NR8D=&IKV6;{!=VLz8vc^YN&8fvXhRbZfIbgL@LlMw!~<8*rZakqX~h*<;{s6@0f z4Gl#wXd#7;MX<`vZE9|-rk^65pQhYtvS^^=LXX1q3D-O5VtF{OohIW_jHv?8^*2-K zijem4wY8A8q^>)Efo<=breGl3^b*jd=FT%W zvu6bZL+;V7AAra4`d5{ud?XwSx+FrT-7>24iXo+FSPcWO|8&(E@R9~?9q5qW^l`@) zrFSArOS85y>xa&R97yTr@X%D%Abwm7sKBa0T}5zDu91%3YCs08RwBfx6F4e!m`jxKR^_R0i+{3e{B>PIQF4-a<-hx#OhykYQ`tq)~OXjOmIPA?pl_U&r~lWcgl6 za=#d0H`kyZ&>{@P9swEan9$~tgD;cJvC`Au;m~8u%Q0o6B~XuOz)0sNexxwUu)<7g z05o+E5@`V>kWiUG2KuAPN#x%#BE>PDssYBiJV}@4s%>R$qrWFz%g@sAipzHOE{3&g zhhW|)j1I26NK1iOr27hBpy0(IFmQ+PC^5^pohf^H8m4J6=(EV$uzZ(59u43W31I0p z`2QBdt@6|m@t7iA7ofMO1mUd8N8e5&^`;+Mn|VHZPBk3geICE$B`Ih zQFLM?re2{I)=pt$DGV=5H@Y7!$-p-4+4tygo7 zNCg{K1^A;OVv$c6yti3G8WhsjT-98pHPZ1*NtX`I@9OtLu(&<$Y#&{nOD;A|c|uC< zHhPs3-d(MGM$jb&z;A#jWGzz(+WLIOzyHqWq>%Jb@H0RyC#P?=?8t)S6#g4Aaa?U* zh09@;e~P01`?hfT$oJjC;Pn3QZ6NlqFhK4-%LdBpkChpygAFhb4ju~d=l6e(7*yUz%@})54SFxx>10T(iv$XW8;6Z`*K!(_Q72SIi1X+l7NU!ivt6J;q zb#)r=VNp80zCRX)i63eXpGpkZVq{c>7#VsS8HNUH?ST;irCc4yfOQ`rZ@8bl@EuNN zvBSsN?dg$f7UH2m*E&}h9PAZCH6G|BR;H+>>iuK{1%q@nlKqN|*Agw{y`edPL!ixr zNS1yW1W2gQZqy*Exv4_`w=O7zEgLfnu1A_e3C8mv+tbs5?Yfut`z$zn`8toUkFUF7 zMyi!b{SU6)x>=SzlxqeaV#Av|{G9{1=3J&srtABfp)FUprju=GY@teqHh0uKZwF+| zHvaKX6RXyahn;!2ztAVMGIx-TsGaWgc?>b{(Wj6lt>S!o46_x&BtX`%QA+qqxoUAE z<4*j)2W`h2YMKEgLk;?$S@8cmwC1aB2+y)2^mtJD{{txF<9lwVuCHL?T90&<69-h0 zoqh-9ZT35RaT%N+u|{&(4Ql;PIBBR3kg*32JFo#y4-Qm0`u(_R3!g{8+Y}sd2n$cs zwN$DOmZp1G67V(0>DH}nlZwOw`rjhGgK8>cGBQgz!kVy&qpe2`U9 z_A>%2s{8CT5)EceK-@+G#26YNhQINW^B~ zg*JpT6sbekxXBJLelkXYPVJ}EY^hBL5DX-z_6}znmQQ9skOQ~F(-oVL?eMnkfS~Du z8Raa}oPEwg2VOKQQ}y5urC>W^=!8fHpRa{;-*CEPWFun>zHiJYw2&Y`QVj3CkQhd* zSN(*(Ru``~>u=ZJv8!O-=s^r%#DiD$;vI6nZLq`81*+i|ma+9vtxYNkjg%T-OKLob zi1>)^5Mdcapu*El;cn!p0QHUW>iy+t*XBdEiqB8n3s+H5M+zN+laRBMB?xAkxG_SF zEjaC!sSD)32O-0fmthM`fqdC&bD- z^cP0is2%9VZJF1I(>MOa#-Eq!0ka|a7}E)vtCA?TX16}~N~GiV5XDea^1TwX(aTUb z4d@ZNSWKO&Vs)ksq4VhK5FWkS85qE8O$;+aM=Of(rSEX`Z3C!egfK%Qj50E)U(I4i zl;e8{;wYtT6yaF(P+fA!eo8fo>coHKE30XZpeifOfrCZCBu%;i%FsLUmmf*2NC;U` zgVQlcER!(Yt;=*%PK?9RgJNPFa8s~nEV#`i_fh+xq(;cYPy`+s`aeUIQC)@NuS&lq z{0*swoj0;W#-KER#B&NV+Q=m=<#j~}-C z-5@pk5Gfv`IT(tFHR;@ZFZVR-i@7QNn{LOkgwaB#r_%?q34JS9(AvjRvrIpv>^b-W z;0$oNPb0IBA_)#-88Z0^F?8iY!1_$u#!I?!i<7ScE$dZ71sMs1TpaFet1fSBFK^nQ zW#)oPCEc65D=R_gFzY~?Vdo9Kk*)+orl6&7Qc!SKL@U)(B8x`UVR&85f=h$ClrO4< znW`;YCcm4S_n7WdgY`p)Rwk|F`Z_BLTD4Z3BD1#43RV~5`bt9upWHz@YVQD)_wNLz zX~4Fj2PE!wIteu-NQb^3L={U_c8*(aXNk7d@-Js8-Yuw;)o}p7M0aW6)wJZok-bP| z3wR^-A(;sW6$nyP;R!CtMqLqdV_ZTsWH* zeGa@yvWwOWbr3s6gOWO9#Q$yjG{i+4S?sdWEr_WdCsT&np*R><3!Ptblw7_Xim=VZ XH#t!LxC5at>W{Pl9UpYy;K~06Mq;&2 delta 32740 zcmcJ22VhiH`tKe@U;+a~k`PF{A%QeVp-X5X2_z7bkOZU}C&>gxlFX1y=pa~8DdJr@ zDk@-C1hIDoyJBNSTs!F6)&j17yW;zO-#PcrObF_}|9fbr+O>(=|<{-(au#a~YQ+uxcOyX!KIF+u;b zvYcV~+gvv2RQ_OYt{A;}dqf|)?hH%vG}Sh=*13&-wKc95cjkhA&dnbu3a8mg#AgKG zb~Sk#U4DrWX0Vcn~HNVc=&@k0Czsb$;o0~dB#Rt#m zX>o%)_D3c3_^O-L$c#()DM`RZbc`}5pW>Vw z+||9<-Q-`?>TYcd*%EJaLtPta^*nx;zin%e-`&X6Fn6C4hpG0T5)&6ZO>=8wO@q6- zWr3?1f=1*O9plk1p<`5plBarb$|xUTFc8J;G;!`!|gX+W+8rID3oH?q4;%`2af zS6S#Z|L4>Jz^ZSjq^?$b;BFRn8gL4|H*^|}ySH|l3i_1zCclJ95VXV<%F=nr@H2yRmuPeibgy8i+7`6Hr zw{UjN#OK_u*&V?!r`gyw3tz9{*Zp0K82yND*|=8FZ5Upcb(@ISC%WZ^GbV(Hg|q2! zmWNE^at`JGTbwk-)6OAy{lS^jPR|kD%LB-7=sv<>BW~{RK9UFN*n_4I5ASP^>)F|y z-6O6&DYZeNcDMG(;-2B*$>y&;QuSx*KhPejyJ_|u%-@pC&w3KY!o#7d;CXm>l6g)z z$vS=7r$6;Ie-6*ZwbxFIG*_S2$t*vukNqu<`@MErjQN1RI$Yn2>6JuEX-RfiVQE2i zL0(l}L1ATnY8qyf2ab#0Xx!MVUQHgEZ2s7*6j#C{dzrH$^7&J``E0}#{&X?FCYz5( z5;t<<6V3in8+6YY^OY#olRkAe$M&AgZTgwF_r6-Uc1OFoHTC=~x?EqK)u&vK(aU_S z4-tdzckVk@f2!}RSV33!_MN4#?(N$}ei`1Oi#a)Fj=uD4%$YVC#j$7GQ@s&8k6R2k z7sQp=R}aOherxS32jhpL9ibkRKtiKF^)P)2$@<$@2|dj81gH5_0+EgGe`U8^Vsz(w zrACy#x5UVS4|Fo!EAuOg%c~USZR0>UA)!VpI(%uQ<25Rggk(Y@DT+5Sy_f17!Zh=U zuN*>{m1Pt+dHhBTth1-qSnO){xL|TyoOHjFGmK49c51YFPQShw?5ci+j3;jP;Lhc( z%|367dz8T)&|-bkXxv%h4D+?tGwW>cS#e;6WW-0$-^;_hdzy8w)y{#|9+BQ8+yHMylbPke_cPs?ju*87X}`6Bt0s+??9itV@Zq zz;Za0y<~7NP9c$gGi4-Rd!)v2A3`H5m4ve~H3sGbxYDJKsl!L8FMt-8_?2GhPZ0yK zFwN49G#^ZPfy?g#R;%C!Qs*&R6=^vb_Pn$byuO+?2o|1Hwvt||@+OoNrlv6!z(X{s zPA8M8KEIznNjT-!ktVq2RL z41f|@%zBy@LHIpaEd_i^@-Y5hZ>$-S*;_GaQ07!zTAo=7N`Vq24hrPhEOHU*^Bq|^ zs`Uq1QA2~s)mYu&YVj90*>e#7Jc{^1X=C9BcjcILx~OBhen!HhK8$8&it~e6N*aD~Z~%ggVPr_Q4T~{-!yNnxDlv@U*fuO~ zB!*l;!-kGZh@UWWekH|Kp@Tbz_d{^>M?+WvoHau|0#yQBGoMJga`;~+<>BGo6;Yg( zi14NqR)8RyfZb*jNkLKTLXQ~$w|G+C^wKK(+W4Zvyn_6Sya}L>2D=SG+eN*oqzzH` zjv&88EImG=r~1^jxU{k`zsj(t$DK}Q(a3IgyH2KeWG6DCCn0#iF`tB1P^7ecZ?mt( zJ<(O?*2V*P(0G-8uP?7j=UQYJ%uh!en7Z@nB!vS`Czn!sdh!@FEpDo9b~hsIXGQXP z8#q6k*o^Fi4T__3`h@CAzpL3#)0DOP`6}^?)Dxxxu4aa(#Z%*HKvrYax@s4An&$IO zwkobs@k+_oj*7;tYez+$G8pjOH7WvmjibK7>+(AseorGy0VKg0RtvRmG4jpC(GlS# z?v|Diw_}Rj{)9pq{cwWf_r?5PH^ z{0~M(8z;5VUAbAGZmNfx(A+mp1-svm8=?s{aD0{JZOGe7pOQBmAq+K9Z4%Q@XhqFt=JiKb&&@yvdRK4i)pDpx z5l%jv8X#zfKm^ccL~Xr-G3xDySg3hvRy`-7FkzBKkJ1T4+jaAsCyehF)Y)miH6dX< zE~xOqCd(KFkLDmI3(#VWF~Dj@U;qNAIXHhJ(l2Y6#F`+&1FRzj5$56ih?oGnx?>RN zs!bmXGHApwb5nkd*{7hda%C0~)VS!)vy%6H5MwVb&{`Qc#=wPWq}`f@91kgT2q+TC z?`Z>MgWj_+1Im?KNZF~YFh@()6@@b`N`6xq%O4TEgilPTxQHhoH!)qeJyH;BuA3N6 zK^X2Z^lep&XO^>{aHz>#l48B6nj-o z9a6>Z+mP3e&Pn`j5hsDs3LFqwvR}=ajn!nJNO+VK{A2168(2qaDsT?w6gd@!9|cB0 zQC&VL48F;Ljr25ud!398~!P}C#9GGQ;KL@7uVOYc6k(XDT z#vezy2l0Deg~0)TXe5RH`hq1{!Ln~j=U+p8PvvxlcIT?Rzl8Rl%5mo2NuA8A>cUmW zXQq!C1c*HKsfMSV%V7E5RuCJOEL&N54Xn16#^I>-qCncD;#o6y z7R}_FbxwOXkIxza8w#1Ochw^3DDX5Z)?{YagFCsihw=D}W)C*F)eLv7-`i{~u4;BQ zwII%M*WpT~&kNx+_swqN#@TZyIxm7 z6?pA5FTFiT$ITl-@d+TMfX6U*R7Z5l;V~hsSIdhK1K2q!7vON1x$c47?1$49k_$N7ke=S0q9Ye6($utzJ7_#GAveaio(`=CB$nB?R>HmYRM_&i2=g z4&-<0{!Z6GbA35Q!@7Hpd9=0%z8sGDF6K>FqnK3m1&j3i12`lnO5Q z^d55}AVh3MVbp}J#1YNfF&BEyYdr)J=I`JPli#Ft=xq)^I|fo4aF^8ug%hSvf{#AC zI2l7VRu{Ptz|~D^ZGxrCr1}uY@-}157tcn=>!OGJ@-;yypz2{+%5ScnHaxh}WsEWk+*rtIB@>~G7rs`%VHn8KfE5xS{yGXv zLDxOmN&a8uTu#jicgEkt-iWwpjq+;SxbD9J$8%k#9`@b5(CWJnjtk=@CNHkaZM%j`kor9r6=~D8zjZ3*+NzhqUzHLh*aRc(s_`kH2>U_tS#sdh2 zgErH1myN-M5N}!K;hixFZp*P;QM5C@A} z%6U~eHBFHLr~|M%kDDS)7cUaJn_-C%&lug8xR#-tqh*Q5U%Mbx=1qmDZeP{O(x}^4 zk^g>iRX@Cbxr#y(=W2S*TulkZwAGYkw63PRa&;1gdZcmmhgSs`7I{Twy0`U@4ILOWDel}*jd zFXBE)(5#@Q=EgNtRl02L5GtiP+OP@OS!oV#vi>l;jInTTLzL-Q0VK=M?`tNU--XEv zv5P-9maT)jMXO}mE(Ae{8m|k562SETb#Bzx+RUxkhjj5=syVJXe~Kpe7w6K#UgCLu zEa4b?UKTNsa03H5oF(pC1q4_h^SbjUQxWcDq@gCST^LKGA>X6`F%!-WtaQ*_?^=h7 zczJYs!Cx?BubfW{B(60K2cSg@bM5()EjX86Fe%jN{d!?S_lac{XXI5BSf(QNq5)mB z#9^1fg%pr&yGSL^yDyBhOgVs6;$e%q`Rj#NypeiQCVC}Ym~1Y;h$@ID=|9j$l(4X^ zN}b!~Utk#u$SB{0{ar`iecifw%4B@H&LAa_FHaKDnk`8e7g0%^$U;RYgM`pm;a=1V zQ&MMJfOPv(kVRE=+lp)u# zB<-95DmE4}dy+^pr(ZgRkq8F{r>iz6r6>&W zx|riDq>>qMZw)it%~(QpHI&uYx40=)d}=cpyknae1o7pBXyC!i<6-%-vY<7T&%&~{ zOm(#^GzJ+?xp9aXfe{pon$6h?xmV25T%c(a@jK0!zmIAEhUf3J6o`ttx&QAO6hPyD z6(peGLdAF`UuM@U^4Y~&F4A(@4s5Ncxnd^87It1_-E*2RT`}+^cfz(T0FfQ$x?6{` z>Sd*4k#)-;OQGN2G6@*L^mtSID7yhe>M+Z1i)1ud7t~8S{JGOyD2YoiNkjGe{kABx ze zV*b&q3cx8V;Q`j@I_~ON4i5Q?>*{_~gu_1wGrgh8aW$3sc3$mfSi-kP<*PfIPgc01 zdvG_aq)m7`5W4${uoNFus0Y$DTPYa6cB`rfzP5D)JKL+(!ZU%Da!n#hWYDk}8W-A# z2VKhT*W*KyrKE+h8FK2))$&38b zOZ z)-2i%{I9XcmV@@NH}AP+K9a^@tU~I(OoL&!PGF*I`L#=m7u`CHME}k$8Du}8G3Mbd zg~}56Qll-KQg$0{n{nTk85$~Vy)BM(m$5a&ukNR^DnPs*L*icdseJx{&pLA z>=Cz@(4=YCBn%_C0U-Zr*g~mqEm;#5_>b}dgLkBSe0#^R|cm2j!>-?cTnMQ%N?YJ z58n|}%qqmB;Vw!nFig-gG8ZD+8W|>zy=I0Ea&2c3Z4TN+mT~f~*ih%+xT`|hj(xjE zkO~lW%(c6Fo4tSNY4+buN>aIdqE?7&cH6suUf4Ypw|?W-oIAb;X85v8xSq4;7b;okC@-CY7>$|eqASK*Q(mL$!>3F^5ZZh;i zX#VGJvM!nT)R6?1JYXQpCg#(;+8_a^oXjC3hM|b901gS--@T_6{m0$ghkY-(ih1`^ zfPDqOzI<;VeE#I#QLL-XxIk}eJorAM=HmNMv^DR$Z+O7mMfN zdiLSqKp8uN(#*_9s+1Ny`gnqh9NvvGAAUp?rDL8@wfwv%RD`qhiCNs{VqnEBma z(ylun8`R0*CVPY$#k07ZaD zpM*<=Sb3W2y@SofC#cSq@D$O%_$kt#3!X}{e8U}2Q5gK=Q?Ue$)#J$Ci0;_IsEVho zx8~@lCsQN{c4wnr@iYQ^0BS=~NZli#1FDdV3R%67!*a5#8U~;zc{hYKx>bTUV8_E7==yx_!pxS?D8LxH8FVO+thL> zlDYPUJR}inFk0A1#4uCKF@|~Yg*<>8`Qq&w+$UemI7zzk`-^0&#_nH=S73qH5B3+Z z)oceBLNQb`nE5jHYGIfQUrHf+2a&@<*_`#Q#=g~?z*;Dk^FGA`1+SKSV^tz)}OpQ-QwUYGm(rCY@i$| z0&LwZVD%DRRRRQKY^w9)$ ze89kgn(D zMO#rzdh8umtq1a0p_m8IZq&yoc$-M22IWBOUOtek92Xg7bq9NM2jxJNV*f!Kp0&FO zWLW5u-8ERvH~eE5YayR&Fkd~8c^cofa8}~5|I9m|GHI%Uh_Jw$A%qDFh700w@zg&=LTOGcCa7G7buK$Am<=?hUo{b zZ5ZbrEFr$avHj!VO!$6PF2@QATU*x~Y|lVJbH|mvI@?a@4RSVz-k7Sp4miXWCUepu zgNE2V4Pn@tLt$EAHXI^u?K)Iw$;9`ExCWSab(A^!%?OU2mGndYj~_D3l+2W`F2wf~$=_Lf;?{`ODW>0vX0 zB8SikW(5T-!6Irt_|`NF9p~FAT>d2jD~e3=>)RHE)x3zw2#76XxLC zX@gZC8v+}C`;-x{UXq$MHySlwKXxpqy^|7rcI1S`6~F~Blo~(qPC7^lv9_*+7t&#> z>Vjpc$#FnRGt{R#4;owG_G7J%R|j{z*PqRYY9h-&=ZQH|3zLaGL^Lm< zVx}(d$1~?s-lsgbOkTT5~Z0n9yEz-ECidNpgDOypVDK0zfgE|$3Jo^D1 z4C!{5L^|*AFp4(G49K{vMQmqKh@-=64pVsh`(Y|_PCU||3*c784~ch$XZ`{zU4q9) zavA1Fk3_2?Httqo;bczsJXjNaLFlfRD2^SWM8)}G-%*T~)o=(k927h#Asw7$(|2nP zZ|;ZjcEo+ohs$~HpFhkYg~vZ|H|L|IB=tp;2Vl}I5XX8YI+=?;qFmwmkK#`>Ad8Nv z-6J!my8O+arN;6K-kdV%h0)!FY^fO0%P8*nnggKv%WelnK?=>Dp9K)OK+R4OY6Rf zWrNB54>)6n#4#CiPSpzcc{8H^^%KvgEklOlX9`Q=D zAN%%8+O}c#{6g;sl*Z{I$_BO{0ed2>A-k+D{%RHm{qU=PDsl+EhtB5FkNcWgU#qQ8 z^S&k}cK(M7fG>Vsh1d9_u5GiA!MFjF8tnMTc~B4dThH$!r2_3Jh&k&gDz7+9@8{7uw&JMJk7f@pP^rMO7-w3STA}5u zKdG%jU;RX^>+>@$BG&wzY{7EL&$MQ~>*tv^3B!JwL>mC`58inq<%R|#Ro)(~iQ$=` zX5bK2G1+p*fi-FxfWWiqs0d{BMTL4$j_#|Z&|%*)%s#(#Gh=>@QPmV4g5YkR4WL42BdErxjk;Ice1b|_A{W^sD%!93wJ-B&%dhz$$XmZ=@V8TcBT*ms z8(lm1H<0RzcoAdn{f#zDz(Rl}=F#8C@1Jr!i5CSNIOq=-He~5g+Rp6t`b)g1(_#Ga z$7wl>3BqX&PO~Ekz)rD^$H`0YJgy2W|KxAq9UqHpLw?V(tjB`iv-u+=_nO~Rv6!Q( zTN*|4wcp7u{QNsYZvdDVT-`;}6I~k9%ST?v)jZ$rCo{gndBp}X6#e&!+4P+V9wJ(7 zlxDbb8p2v*LmxR-2vla}Od*mL{=|v;h7xxZ0=uEv*)$XhhrB|F1PVs+kLD$BM^#KT zVP#XY(uT`K;$xrS)Q;@ZL10s&oY6sqmclKc2XBWg=>*#%U+*9?LSorYr-&>SNXn6? zh%ppJH~w4nL6ELQiRFon9@26!s)PbB+LWSX$1;a|y>W7WM=_F}r`*v|KuqL29YtR~ z_KzJ!KG6fisS(54T0M5ia>uCxVWHgR;3LQC8m&vqvpR{S{>r8*7l2)iO?3_KLM);C zQZ4rj(mCZFsRG3od8CujE__R8VQq)lk^%ocDouo;!RXEc@7)1)>NU-bK(UM2Ayenjz>R7{tm)GQ@>sE(kMuyo;bEQ2MNb47u7N zu(P>+oKBUa!$it}zk(Rt(%Lj(gpU1Fn7~5Z31g?p`@0IYV^+S?Risl;MS|FN*OBbg zO&|)DzHVYJdt-3eDgV(;%)!M$P70soz8=D=u}Pm(*x8`m=oF}p$z4v-Hz;Ydl!S%+ zEvFcbCXwC6IOXh#T(i3i_+)u$caa1FlLFAy`??DhuD0|Lv1DjuTn{lo<(ar`ZMfmK zpe((sH-05rBMEjQ+F9L0bRro>Rud(+_7Htf2(C}S$DxYbFuKwJs-xBLK~q|DD1{&| zpo0*cMreZ8qD;`Z%<#KijgXIif+G!iNl$^)MIPuWBDIwGB1J6MJTQPU$%Bot<;^&n z*SyT2t+U_(?xV;0NP(phx%P9>ORkC*VR5t%pDG`~8ZfrWw&iufX z>Vnw>cXlZ1K?x`C20Q-nbHQ0#E&%YtXArL^)?r}VP zNt{4lByWrpLu05fRbk;^>A43e!uXBa29FN~MtLkwBypWN^cIe!$^r3gIl-5^vPFcf zju+9yqyteRK}vWA@YyMEiWgHc58l#cY=Thx&1fGBvq}~v2o$U3MG1m4cKK3*K%ykS zO%Q|NeL*lC*((RP)DVb8PcY)j=_!v&MjTDBxw9X)0JIg@Oo)OKR(a%P0+ zXPYIO9x^jX&0I^?m*+dVlKj7nT!5IG5RuAX(L6*!hI}BF>?S_8i7Ef9*oW04Nd6H? z0&gpMwPK?3L1K&wDD}p_QQE0m0VRr_GCNFklhK)CfYO2SnF5R2tOMGmuFe#*EOB}# zlU1xkmRN{JjakCWki3`0MrL5PsN|NQ$M_tP*xk;aS(jk9Bjq#MqUXs1nNQK*0g5V9 zOhXJ%U%_Uw`n}|u91(G{-Z$rnSwVIdFmxF#P>7Rb2aBA^JgikpwHEzU)x^4L6xsnnIBy=L{Epu=#>$iHzNWrwkwo ztHiV0khIey+;V}N+%%lS=*Nc(y$MZ%{qa5YGg}nMa0I?8S!g41iCRh`M-^Hcl#Qp0 zp>cd!Ppg_TOx$;gd@KtA+3P6efnDf+LOX)6J*aJAaE%f}5&px1+nScASGSz<_EBW} zbTO1wE?1~!$SwKseT#BAB;AlJvXQ)D9$MLR)%w->(9wn}Sb*qn|r^2O1t*n~3nh{wwKu?PxhjuA0xK>~u(CVeE@ zke4d(0ykrU3(>X9sKGh6R^K?MykU%(i&1|cBTDO-j5Z;)x>@FlMrn(J9UNH(TsQcm z9T&>4e>YaF!?df$@$7Gnqoi2VdfmDddC7R8j-6BaHbF+_iQy?s<2LnVJs;@K=Tlf2 zn)1YW481!~8@Zsdm_^_=X=YkJdr;z+wy@{t>lhqN z-jy#n zJZp2igauI>%@i5GEfjSKSumlVO4~M&jdX?R|KHAl0z|P=^c9mt1xOIs*e^euMCqD2 zVtt$(QX~wT7t`=eSoD&uMS|ApKzpIm~_IjKh1?llH zH0a`YErfi#T@C&P$SH6vPR=eCSqi!}B_e`*Y0UW?+zFZ@MY)(l;YwM#7znRq5g%S@ z%w%zTx`Li&!^hAa3}sDKm|ktC1TE#2leyCJ#$--sdQK617DLXM!h*7GiYP}~$N$Oy zOcBYbNQI7vfLYoO`O!r3 zlejXDovADD%xT-zc259Zp8zv(MAlR9#`y$)V3Q&rmMSTpSXiA`Qaqz@lvCbcCQ_iO zCz?v~EsDJClt8HCWn51@QX#U~(STC6TySQLDLq9jcE)J?xd3cZg0>0907PAT_J!^d z#1vnZi?q-nC}A4AeeR@I$<-DaJcj_w?_AM$%F1ahTpOohahO|Ts5_^L;N=TQ?z_{3 zeH=;tig6Vwfrd8US|t?Lumu4s<8y#?1+$7vCt-QG4b9~23X#J3;vbTZ%q$6+Eu{8z zD&LbCm7+gLZj%;G?Gt1nVDQ;EnzgUoSSh;w_gtg`E`g$_lKEAlv4XBCo+)zUASO<4 z29X=wQy^=#I|(+HiGy0Q34;1Mr7>OT4GI&di>q5W6PwygtN5o)2%nk5WMp;cckhA2U>j|d`Q4#bOi)24+@ z;O;Vdw&(|EPmTVNA}eQ$L<+{*ol%F~zt4sjaLTedq9EqaUFQpW@w%#lK*f9TCsRxo*2fibIX}5L}Shr zLs)SX69A4=t~gWh(wDsDOmR93(UCJnB$=WW_>)fz7X#pclnjM#4;Vm~m9T`a0Jq5k zmR1X#*pWM{h1&WEQnG5;t4;EgYSDle6=zWjg4K*@x%VuAZKS+*@nE%xC!oh5r1dZ2(0NL6rG5Vw#E~Zuf|(ooeZ+1Wx&zhjXrhXA3&82aKibY?OH7 zR}07GEgeteR@MGxKDSkOlAF&Ki9Mmlb)FXNyXEKTfE)YH<|;DI*c<%~-1Rkkuzuvn z!;OeUHno+xAuMFpLXJqM)G=ei-N1Q!9T$-K^J{eij|q|d`G-0#;NbH-B;ZVgHs)Ik z{ecE#grB&nf+;)Kvn8mg7qnY|<8|6-B|FR)gA^Td=F{>YJ;*?x+L7Dmiy?6YF>zj} z8%^$kh*anq7_@jPznU)+;67}mtb@k^%xs~zawGt3`2!7z+^|55|Er9v@(8uVT!L|x z0JvSA@uq6xANWJ{`~X8TIB)wec;J)|FBDxdhQ$cfebqrjFuE;6P;()M1Q>8SI)*iJ zd|25?<%A&GizY!&dIpq3ns^Z;l=gk)^0P&Ff)z3U5ewA9sy3x|Nd=IE8pR+MSRk(S z3fcgm5;$l^ob2Wk>LF=zpijhUH8|W1fvoe16t)QK@xiJ?J>V1c%n^q?LQTRKiBxRy zu-Q+85|qWo!5-tQHZey$ny$z5-mkM2t^q zcB^MQwk+`Y{@ggkEEhB6dwz=q+n0)TMj6|%6Xc{;L5~Xy;xJ|?`g5F=QyagW7VMUS zN`EkZY>yyY@xPpiPL@Krf^nG8etGi$j!o6ciY#w zHERQ9AAa7jOgmQ;C`s|2%k{NO&*j4QjpvGS2uDFpc^-N>j}^>yBQjSNe51rpKiCc9DRZCk5r@OwO}>TpGwC=)x5#pFd(h=xDfUu7l>ub z&+E?Fx@%}GL*oar#xi9n%3UZjAny-dC}t)x%lRygQN#Po^s)V*hw8}eizwcc;lIFS zF1<($;hkVha83jXWd8-s%r^6+}(r&SjVe(WU6!Z{a<3QnnPVp&1~G8Euda$b#&+nT>Y;%@ixiBK6I)S;T`&fLI$wWb*2fM;7>2Tt!dwkkyw9^%Q2g z<8qGQAGuu2<{yYb^bLGdq9-Ms~mFySa=(-vWz4nP<_jDe$BZ~!iP zqC7pi%7tgc)-KE>NHyqIFw43uKincFQ6-PNx7WgEQ_wmF2^98H2FVz9gYA|g3ibpS zt;EsDr-#BRD`mk?EG@s%0)F|GA`Y?*VGn(>w!Gy^mhA&qis-=3XY1&Y{P9XV*BWw_ zpr2h)9?QJu#RPfVRU(&9=s9YL7WnZa#6C#7?1wuM;ufP-z0I25o&d-HhH9z81Zqzj zxkPj8i$;F+jAA@K-j*x~#mOdOqYyHiemVdxK}1T&R!&Z7Q@+gHD)@=8GH)xdPcKk! zo3`rp>EE}C=pd=TM)cJ1O}Iu3=cOI1wxM^LLO4-tvv&5&+tGsyliCjjc0$URuhEr@ zpRN%(sYJ_=XR2DnvCiaaTFdP{v5UcXt>~#tZqOkBi@4aldaW44X-&w8^6<4HkryCB zF1MXX#$U%eIPN+zJf~9CqHSU@ zm#;%G1>QltI#DKX+$JX5Lwvi9_w9|^F2<_FI2aW*5`4>cG0*Pt^>#5w`xEGhQ%=Jk z{5VGI#ICvwjBWaZCvp>3PmU2vy#rA}fLU-EK3b5#WB7<;?TukQV;)#JrMO;{umF`_ zul+ELBr!2sUVgnuKy<+l6(iN>w>B4GXxe6+of&wn09N#`7m>Vo0pPUN*dMOv3e3nH()D;g$R1$(4Bn!2C)+Muif__&xbtqJ+Kr?$5Ay4qL>E*D(M?md4YHWX8q!kL zxSM(FtN&&(NqKwSGf3dMi+ufNk<4VY#%|jJLo+KPZVBv^&Ao+-F&l3YWy;BioF_w} z=oSv>5^fc%n0wdWDzaFQ+i)E5!27p~n2Z1$LI28y9v_57Nk|AZ3`hKJqA#*24oybi zCSsJM^G(31IK6Wf6*%>Y>4Hv-fLh-VZbtzb^lH;>;w(`5=xw~4H{*8EkAg;;E6Cjk z^~0!iQ&&+Q+0B;k$n6}=b>GSBImJ5}{}nsQALxdhef7LUj6ya+%pl9`(R&RbvILCf z?9}St!aKwWZ7y%RLxemYiyA1d{0EIV@6=vUbsfHo>tOY}#0c`ToELfW>gwuyTwQtZ+z9G(9rEsr48m$vi44q zrUHPCcZy6BOr`?OU4=SpU^6h-^2VK_05&m%D41WS?hzyGl7p<>!(}##68G&9>1yz= z_ApN(?y?IwGBK+|oSb`?m=~w04>?1?L3>=Sby5yE6>RD+kP_oGe!PV}AhJj!9D(KWRHLqOezCQTNm4<0wHsjF z_dw8osEh}NdY-#H_d!8NvX~q@ALR1LYY%ds(d!|8oAHpygGtWkpZvqo#C-^x?|(?B zM~iS$fOd-A;~(Y_d-lW3v&$Y9L)Ae4oQF*~r~LQBeBhzmBO;wGe9*1D zN5l{&jC~C&uxlO>{ZF`f*CQgiZSTX6@YdNuk8-)F{!zhCkC&G`${ywZM{Sip_Na&_ zCPSjbAJZvF=3`>QXri<|HQ|BX0*ZY~Md+dYVfqp4nyoM&xQ44T=RSI-&11ryf^XEB z_Gsx=fi*mPcwjd1ab6q$`{SY+yzcY_(!Qk?VFxpt9axIYo|XRPuExW+RM&r z^j={-oKDW&D{!(&F5AocAop_G@X=o8Y?pmJNWnfaiQU2aeIk*dQ~u11oSGv)-Y0sZ z`*-_95(ma?-s7JX^z)5qM%~Mw(P|-MT2rL4u8qR zPm19kkl?4^j|4yGDKUnD@;=4V{?St+mzHITaz1sukJJv+Wj!tW**0+c)1p6lw8rWt z9AB+<>*qUw4|2oPf}S|U(_6;B_cH)vUh2cI zF5t1dDyceo659b>8TG8VK*|2TXPMB>=Xk|)o_gE$9Iv?@eoj;|YQvrvIY8LFZnw3$ zaP#w`kd%b#Hvj~O;#vR&s&P4kM;fhoXnIQ~HgrGF3yA2r`|1I*`~@+U2nxIiHEr9! zeL>K3?-?U~jcfYXzi92-;Y8u{7r7RA>VBagOexd$^PZC8{bKNmqaN7LLYeRqZ*OgS zDd0wV?X=U&B0r>jsU3fsQ=an*(vn3lv$kFPvY_L-LD0er7&7Y>G4|xW#}o6Ff~W;@ zWux>D%e9k2YC^!K)7dG=$y2ZLHviADyb?5f+sspSqlcr&f4nB5Rabh1`=9rrmApd- zL|@dvR|J4of`|d$)#(p|TC0snSvsZe{oWa9}&m)7S5jP9ZMpQ27}?I< z#}A3YDu}oCEaBwPA(6^fhLl{ce-p*H!Z*cf7-Zd>Som3Nf6f9$*b+VRrWnfl5}L7q zTH4G|T5{x|h=_(QR!O#&A?q1{a_~O|Jr39+uoV~bx@_)S*5Sg|w?r}}E4sH;_Bf#z z$<6$?t%}6vx5Zh1ygFR@PGBi`OL~WRdFs1Xw3PEMn~+uSicBW^9q;lf#Ut;s?e6rR zpu?yP06+0oUh*E-{Pp8+0Y}dJR#Z3leHQ(5-)Dh%@O{xg4!DqzvOO(sDJ{p(u97`f z7v=bSMRJee(gYdWMIWHdF!}?L#95m*>$c@r4RT^=5Ooi*1v~>y7TBpEZ#yb_%8x$~ zy;Z);R$W;OhJvOGKP+NSHq+R{!lf#{0iPT^Ev9D5u!04t^x`8HNnrlg4F^nZW@efw zK&t7nJi&)NLrrYZs1YBD!Aj*``2uF{%#TE_R*gW(@@~Y&~;CI{zh> zDWV13zZPBwPA=L4gK%f(4*lhv>qNBO@7sT|97KOC5{a3?T|)UOr+v(En72!ZesaUd z!ojIw8|H8$);*$|e5p$ZUN)mvfgK@7KNdH&aSH9*BQ~G4pNfHsDw{tQIkxW(a2Y%< z`&>jpy+8j{#QpVw-}p=n|0@Hd4a)Cdf2wO;AiX-ijd}0;oE74o&xNnQCSIthvInkE zTP$$g(kU1ITf}grNhHA^k%>g$4+t>p3#&YS@fV^HXhJ$@8^rN1L^5kM(FNbDz7#`~ zF)GWRPLb(RItV +#include "weapon_proficiency.h" + +#ifdef _WIN32 +#pragma once +#endif + +#ifdef INVASION_DLL +#include "tf_shareddefs.h" + +#define POWERUP_THINK_CONTEXT "PowerupThink" +#endif + +#include "cbase.h" +#include "baseentity.h" +#include "baseflex.h" +#include "damagemodifier.h" +#include "utllinkedlist.h" +#include "ai_hull.h" +#include "ai_utils.h" +#include "physics_impact_damage.h" + +class CNavArea; +class CScriptedTarget; +typedef CHandle CBaseCombatWeaponHandle; + +// ------------------------------------- +// Capability Bits +// ------------------------------------- + +enum Capability_t +{ + bits_CAP_MOVE_GROUND = 0x00000001, // walk/run + bits_CAP_MOVE_JUMP = 0x00000002, // jump/leap + bits_CAP_MOVE_FLY = 0x00000004, // can fly, move all around + bits_CAP_MOVE_CLIMB = 0x00000008, // climb ladders + bits_CAP_MOVE_SWIM = 0x00000010, // navigate in water // UNDONE - not yet implemented + bits_CAP_MOVE_CRAWL = 0x00000020, // crawl // UNDONE - not yet implemented + bits_CAP_MOVE_SHOOT = 0x00000040, // tries to shoot weapon while moving + bits_CAP_SKIP_NAV_GROUND_CHECK = 0x00000080, // optimization - skips ground tests while computing navigation + bits_CAP_USE = 0x00000100, // open doors/push buttons/pull levers + //bits_CAP_HEAR = 0x00000200, // can hear forced sounds + bits_CAP_AUTO_DOORS = 0x00000400, // can trigger auto doors + bits_CAP_OPEN_DOORS = 0x00000800, // can open manual doors + bits_CAP_TURN_HEAD = 0x00001000, // can turn head, always bone controller 0 + bits_CAP_WEAPON_RANGE_ATTACK1 = 0x00002000, // can do a weapon range attack 1 + bits_CAP_WEAPON_RANGE_ATTACK2 = 0x00004000, // can do a weapon range attack 2 + bits_CAP_WEAPON_MELEE_ATTACK1 = 0x00008000, // can do a weapon melee attack 1 + bits_CAP_WEAPON_MELEE_ATTACK2 = 0x00010000, // can do a weapon melee attack 2 + bits_CAP_INNATE_RANGE_ATTACK1 = 0x00020000, // can do a innate range attack 1 + bits_CAP_INNATE_RANGE_ATTACK2 = 0x00040000, // can do a innate range attack 1 + bits_CAP_INNATE_MELEE_ATTACK1 = 0x00080000, // can do a innate melee attack 1 + bits_CAP_INNATE_MELEE_ATTACK2 = 0x00100000, // can do a innate melee attack 1 + bits_CAP_USE_WEAPONS = 0x00200000, // can use weapons (non-innate attacks) + //bits_CAP_STRAFE = 0x00400000, // strafe ( walk/run sideways) + bits_CAP_ANIMATEDFACE = 0x00800000, // has animated eyes/face + bits_CAP_USE_SHOT_REGULATOR = 0x01000000, // Uses the shot regulator for range attack1 + bits_CAP_FRIENDLY_DMG_IMMUNE = 0x02000000, // don't take damage from npc's that are D_LI + bits_CAP_SQUAD = 0x04000000, // can form squads + bits_CAP_DUCK = 0x08000000, // cover and reload ducking + bits_CAP_NO_HIT_PLAYER = 0x10000000, // don't hit players + bits_CAP_AIM_GUN = 0x20000000, // Use arms to aim gun, not just body + bits_CAP_NO_HIT_SQUADMATES = 0x40000000, // none + bits_CAP_SIMPLE_RADIUS_DAMAGE = 0x80000000, // Do not use robust radius damage model on this character. +}; + +#define bits_CAP_DOORS_GROUP (bits_CAP_AUTO_DOORS | bits_CAP_OPEN_DOORS) +#define bits_CAP_RANGE_ATTACK_GROUP (bits_CAP_WEAPON_RANGE_ATTACK1 | bits_CAP_WEAPON_RANGE_ATTACK2) +#define bits_CAP_MELEE_ATTACK_GROUP (bits_CAP_WEAPON_MELEE_ATTACK1 | bits_CAP_WEAPON_MELEE_ATTACK2) + + +class CBaseCombatWeapon; + +#define BCC_DEFAULT_LOOK_TOWARDS_TOLERANCE 0.9f + +enum Disposition_t +{ + D_ER, // Undefined - error + D_HT, // Hate + D_FR, // Fear + D_LI, // Like + D_NU // Neutral +}; + +const int DEF_RELATIONSHIP_PRIORITY = INT_MIN; + +struct Relationship_t +{ + EHANDLE entity; // Relationship to a particular entity + Class_T classType; // Relationship to a class CLASS_NONE = not class based (Def. in baseentity.h) + Disposition_t disposition; // D_HT (Hate), D_FR (Fear), D_LI (Like), D_NT (Neutral) + int priority; // Relative importance of this relationship (higher numbers mean more important) + + DECLARE_SIMPLE_DATADESC(); +}; + +//----------------------------------------------------------------------------- +// Purpose: This should contain all of the combat entry points / functionality +// that are common between NPCs and players +//----------------------------------------------------------------------------- +class CBaseCombatCharacter : public CBaseFlex +{ + DECLARE_CLASS( CBaseCombatCharacter, CBaseFlex ); + +public: + CBaseCombatCharacter(void); + ~CBaseCombatCharacter(void); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + DECLARE_PREDICTABLE(); +#ifdef MAPBASE_VSCRIPT + DECLARE_ENT_SCRIPTDESC(); +#endif + +public: + + virtual void Spawn( void ); + virtual void Precache(); + + virtual int Restore( IRestore &restore ); + + virtual const impactdamagetable_t &GetPhysicsImpactDamageTable( void ); + + int TakeHealth( float flHealth, int bitsDamageType ); + void CauseDeath( const CTakeDamageInfo &info ); + + virtual bool FVisible ( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); // true iff the parameter can be seen by me. + virtual bool FVisible( const Vector &vecTarget, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ) { return BaseClass::FVisible( vecTarget, traceMask, ppBlocker ); } + static void ResetVisibilityCache( CBaseCombatCharacter *pBCC = NULL ); + +#ifdef MAPBASE + virtual bool ShouldUseVisibilityCache( CBaseEntity *pEntity ); +#endif + +#ifdef PORTAL + virtual bool FVisibleThroughPortal( const CProp_Portal *pPortal, CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); +#endif + + virtual bool FInViewCone( CBaseEntity *pEntity ); + virtual bool FInViewCone( const Vector &vecSpot ); + +#ifdef PORTAL + virtual CProp_Portal* FInViewConeThroughPortal( CBaseEntity *pEntity ); + virtual CProp_Portal* FInViewConeThroughPortal( const Vector &vecSpot ); +#endif + + virtual bool FInAimCone( CBaseEntity *pEntity ); + virtual bool FInAimCone( const Vector &vecSpot ); + + virtual bool ShouldShootMissTarget( CBaseCombatCharacter *pAttacker ); + virtual CBaseEntity *FindMissTarget( void ); + +#ifndef MAPBASE // This function now exists in CBaseEntity + // Do not call HandleInteraction directly, use DispatchInteraction + bool DispatchInteraction( int interactionType, void *data, CBaseCombatCharacter* sourceEnt ) { return ( interactionType > 0 ) ? HandleInteraction( interactionType, data, sourceEnt ) : false; } +#endif + virtual bool HandleInteraction( int interactionType, void *data, CBaseCombatCharacter* sourceEnt ); + + virtual QAngle BodyAngles(); + virtual Vector BodyDirection2D( void ); + virtual Vector BodyDirection3D( void ); + virtual Vector HeadDirection2D( void ) { return BodyDirection2D( ); }; // No head motion so just return body dir + virtual Vector HeadDirection3D( void ) { return BodyDirection2D( ); }; // No head motion so just return body dir + virtual Vector EyeDirection2D( void ) { return HeadDirection2D( ); }; // No eye motion so just return head dir + virtual Vector EyeDirection3D( void ) { return HeadDirection3D( ); }; // No eye motion so just return head dir + + virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + + // ----------------------- + // Fog + // ----------------------- + virtual bool IsHiddenByFog( const Vector &target ) const; ///< return true if given target cant be seen because of fog + virtual bool IsHiddenByFog( CBaseEntity *target ) const; ///< return true if given target cant be seen because of fog + virtual bool IsHiddenByFog( float range ) const; ///< return true if given distance is too far to see through the fog + virtual float GetFogObscuredRatio( const Vector &target ) const;///< return 0-1 ratio where zero is not obscured, and 1 is completely obscured + virtual float GetFogObscuredRatio( CBaseEntity *target ) const; ///< return 0-1 ratio where zero is not obscured, and 1 is completely obscured + virtual float GetFogObscuredRatio( float range ) const; ///< return 0-1 ratio where zero is not obscured, and 1 is completely obscured + + + // ----------------------- + // Vision + // ----------------------- + enum FieldOfViewCheckType { USE_FOV, DISREGARD_FOV }; + + // Visible starts with line of sight, and adds all the extra game checks like fog, smoke, camo... + bool IsAbleToSee( const CBaseEntity *entity, FieldOfViewCheckType checkFOV ); + bool IsAbleToSee( CBaseCombatCharacter *pBCC, FieldOfViewCheckType checkFOV ); + + virtual bool IsLookingTowards( const CBaseEntity *target, float cosTolerance = BCC_DEFAULT_LOOK_TOWARDS_TOLERANCE ) const; // return true if our view direction is pointing at the given target, within the cosine of the angular tolerance. LINE OF SIGHT IS NOT CHECKED. + virtual bool IsLookingTowards( const Vector &target, float cosTolerance = BCC_DEFAULT_LOOK_TOWARDS_TOLERANCE ) const; // return true if our view direction is pointing at the given target, within the cosine of the angular tolerance. LINE OF SIGHT IS NOT CHECKED. + + virtual bool IsInFieldOfView( CBaseEntity *entity ) const; // Calls IsLookingTowards with the current field of view. + virtual bool IsInFieldOfView( const Vector &pos ) const; + + enum LineOfSightCheckType + { + IGNORE_NOTHING, + IGNORE_ACTORS + }; + virtual bool IsLineOfSightClear( CBaseEntity *entity, LineOfSightCheckType checkType = IGNORE_NOTHING ) const;// strictly LOS check with no other considerations + virtual bool IsLineOfSightClear( const Vector &pos, LineOfSightCheckType checkType = IGNORE_NOTHING, CBaseEntity *entityToIgnore = NULL ) const; + + // ----------------------- + // Ammo + // ----------------------- + virtual int GiveAmmo( int iCount, int iAmmoIndex, bool bSuppressSound = false ); + int GiveAmmo( int iCount, const char *szName, bool bSuppressSound = false ); + virtual void RemoveAmmo( int iCount, int iAmmoIndex ); + virtual void RemoveAmmo( int iCount, const char *szName ); + void RemoveAllAmmo( ); + virtual int GetAmmoCount( int iAmmoIndex ) const; + int GetAmmoCount( char *szName ) const; + + virtual Activity NPC_TranslateActivity( Activity baseAct ); + + // ----------------------- + // Weapons + // ----------------------- + CBaseCombatWeapon* Weapon_Create( const char *pWeaponName ); + virtual Activity Weapon_TranslateActivity( Activity baseAct, bool *pRequired = NULL ); + void Weapon_SetActivity( Activity newActivity, float duration ); + virtual void Weapon_FrameUpdate( void ); + virtual void Weapon_HandleAnimEvent( animevent_t *pEvent ); + CBaseCombatWeapon* Weapon_OwnsThisType( const char *pszWeapon, int iSubType = 0 ) const; // True if already owns a weapon of this class + virtual bool Weapon_CanUse( CBaseCombatWeapon *pWeapon ); // True is allowed to use this class of weapon +#ifdef MAPBASE + virtual Activity Weapon_BackupActivity( Activity activity, bool weaponTranslationWasRequired = false, CBaseCombatWeapon *pSpecificWeapon = NULL ); + virtual void Weapon_Equip( CBaseCombatWeapon *pWeapon ); // Adds weapon to player + virtual void Weapon_EquipHolstered( CBaseCombatWeapon *pWeapon ); // Pretty much only useful for NPCs + virtual void Weapon_HandleEquip( CBaseCombatWeapon *pWeapon ); +#else + virtual void Weapon_Equip( CBaseCombatWeapon *pWeapon ); // Adds weapon to player +#endif + virtual bool Weapon_EquipAmmoOnly( CBaseCombatWeapon *pWeapon ); // Adds weapon ammo to player, leaves weapon + bool Weapon_Detach( CBaseCombatWeapon *pWeapon ); // Clear any pointers to the weapon. + virtual void Weapon_Drop( CBaseCombatWeapon *pWeapon, const Vector *pvecTarget = NULL, const Vector *pVelocity = NULL ); + virtual bool Weapon_Switch( CBaseCombatWeapon *pWeapon, int viewmodelindex = 0 ); // Switch to given weapon if has ammo (false if failed) + virtual Vector Weapon_ShootPosition( ); // gun position at current position/orientation + bool Weapon_IsOnGround( CBaseCombatWeapon *pWeapon ); + CBaseEntity* Weapon_FindUsable( const Vector &range ); // search for a usable weapon in this range + virtual bool Weapon_CanSwitchTo(CBaseCombatWeapon *pWeapon); + virtual bool Weapon_SlotOccupied( CBaseCombatWeapon *pWeapon ); + virtual CBaseCombatWeapon *Weapon_GetSlot( int slot ) const; + CBaseCombatWeapon *Weapon_GetWpnForAmmo( int iAmmoIndex ); + + + // For weapon strip + void Weapon_DropAll( bool bDisallowWeaponPickup = false ); + + virtual bool AddPlayerItem( CBaseCombatWeapon *pItem ) { return false; } + virtual bool RemovePlayerItem( CBaseCombatWeapon *pItem ) { return false; } + + virtual bool CanBecomeServerRagdoll( void ) { return true; } + + // ----------------------- + // Damage + // ----------------------- + // Don't override this for characters, override the per-life-state versions below + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + // Override these to control how your character takes damage in different states + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual int OnTakeDamage_Dying( const CTakeDamageInfo &info ); + virtual int OnTakeDamage_Dead( const CTakeDamageInfo &info ); + + virtual float GetAliveDuration( void ) const; // return time we have been alive (only valid when alive) + + virtual void OnFriendDamaged( CBaseCombatCharacter *pSquadmate, CBaseEntity *pAttacker ) {} + virtual void NotifyFriendsOfDamage( CBaseEntity *pAttackerEntity ) {} + virtual bool HasEverBeenInjured( int team = TEAM_ANY ) const; // return true if we have ever been injured by a member of the given team + virtual float GetTimeSinceLastInjury( int team = TEAM_ANY ) const; // return time since we were hurt by a member of the given team + + + virtual void OnPlayerKilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) {} + + // utility function to calc damage force + Vector CalcDamageForceVector( const CTakeDamageInfo &info ); + + virtual int BloodColor(); + virtual Activity GetDeathActivity( void ); + + virtual bool CorpseGib( const CTakeDamageInfo &info ); + virtual void CorpseFade( void ); // Called instead of GibNPC() when gibs are disabled + virtual bool HasHumanGibs( void ); + virtual bool HasAlienGibs( void ); + virtual bool ShouldGib( const CTakeDamageInfo &info ) { return false; } // Always ragdoll, unless specified by the leaf class + + float GetDamageAccumulator() { return m_flDamageAccumulator; } + int GetDamageCount( void ) { return m_iDamageCount; } // # of times NPC has been damaged. used for tracking 1-shot kills. + + // Character killed (only fired once) + virtual void Event_Killed( const CTakeDamageInfo &info ); + + // Killed a character + void InputKilledNPC( inputdata_t &inputdata ); +#ifdef MAPBASE + + void InputGiveWeapon( inputdata_t &inputdata ); + void InputDropWeapon( inputdata_t &inputdata ); + void InputPickupWeaponInstant( inputdata_t &inputdata ); + COutputEvent m_OnWeaponEquip; + COutputEvent m_OnWeaponDrop; + + virtual void InputHolsterWeapon( inputdata_t &inputdata ); + virtual void InputHolsterAndDestroyWeapon( inputdata_t &inputdata ); + virtual void InputUnholsterWeapon( inputdata_t &inputdata ); + void InputSwitchToWeapon( inputdata_t &inputdata ); + + COutputEHANDLE m_OnKilledEnemy; + COutputEHANDLE m_OnKilledPlayer; + virtual void OnKilledNPC( CBaseCombatCharacter *pKilled ); + + virtual CBaseEntity *FindNamedEntity( const char *pszName, IEntityFindFilter *pFilter = NULL ); + + COutputFloat m_OnHealthChanged; +#else + virtual void OnKilledNPC( CBaseCombatCharacter *pKilled ) {}; +#endif + + // Exactly one of these happens immediately after killed (gibbed may happen later when the corpse gibs) + // Character gibbed or faded out (violence controls) (only fired once) + // returns true if gibs were spawned + virtual bool Event_Gibbed( const CTakeDamageInfo &info ); + // Character entered the dying state without being gibbed (only fired once) + virtual void Event_Dying( const CTakeDamageInfo &info ); + virtual void Event_Dying(); + // character died and should become a ragdoll now + // return true if converted to a ragdoll, false to use AI death + virtual bool BecomeRagdoll( const CTakeDamageInfo &info, const Vector &forceVector ); + virtual void FixupBurningServerRagdoll( CBaseEntity *pRagdoll ); + + virtual bool BecomeRagdollBoogie( CBaseEntity *pKiller, const Vector &forceVector, float duration, int flags ); + +#ifdef MAPBASE + // A version of BecomeRagdollBoogie() that allows the color to change and returns the entity itself instead. + // In order to avoid breaking anything, it doesn't change the original function. + virtual CBaseEntity *BecomeRagdollBoogie( CBaseEntity *pKiller, const Vector &forceVector, float duration, int flags, const Vector *vecColor ); +#endif + + CBaseEntity *FindHealthItem( const Vector &vecPosition, const Vector &range ); + + + virtual CBaseEntity *CheckTraceHullAttack( float flDist, const Vector &mins, const Vector &maxs, int iDamage, int iDmgType, float forceScale = 1.0f, bool bDamageAnyNPC = false ); + virtual CBaseEntity *CheckTraceHullAttack( const Vector &vStart, const Vector &vEnd, const Vector &mins, const Vector &maxs, int iDamage, int iDmgType, float flForceScale = 1.0f, bool bDamageAnyNPC = false ); + + virtual CBaseCombatCharacter *MyCombatCharacterPointer( void ) { return this; } + + // VPHYSICS + virtual void VPhysicsShadowCollision( int index, gamevcollisionevent_t *pEvent ); + virtual void VPhysicsUpdate( IPhysicsObject *pPhysics ); + float CalculatePhysicsStressDamage( vphysics_objectstress_t *pStressOut, IPhysicsObject *pPhysics ); + void ApplyStressDamage( IPhysicsObject *pPhysics, bool bRequireLargeObject ); + + virtual void PushawayTouch( CBaseEntity *pOther ) {} + + void SetImpactEnergyScale( float fScale ) { m_impactEnergyScale = fScale; } + + virtual void UpdateOnRemove( void ); + + virtual Disposition_t IRelationType( CBaseEntity *pTarget ); + virtual int IRelationPriority( CBaseEntity *pTarget ); + +#ifdef MAPBASE + void AddRelationship( const char *pszRelationship, CBaseEntity *pActivator ); + void InputSetRelationship( inputdata_t &inputdata ); +#endif + + virtual void SetLightingOriginRelative( CBaseEntity *pLightingOrigin ); + +protected: + Relationship_t *FindEntityRelationship( CBaseEntity *pTarget ); + +public: + + // Vehicle queries + virtual bool IsInAVehicle( void ) const { return false; } + virtual IServerVehicle *GetVehicle( void ) { return NULL; } + virtual CBaseEntity *GetVehicleEntity( void ) { return NULL; } + virtual bool ExitVehicle( void ) { return false; } + + // Blood color (see BLOOD_COLOR_* macros in baseentity.h) + void SetBloodColor( int nBloodColor ); +#ifdef MAPBASE + void InputSetBloodColor( inputdata_t &inputdata ); +#endif + + // Weapons.. + CBaseCombatWeapon* GetActiveWeapon() const; + int WeaponCount() const; + CBaseCombatWeapon* GetWeapon( int i ) const; + bool RemoveWeapon( CBaseCombatWeapon *pWeapon ); + virtual void RemoveAllWeapons(); + WeaponProficiency_t GetCurrentWeaponProficiency() + { +#ifdef MAPBASE + // Mapbase adds proficiency override + return (m_ProficiencyOverride > WEAPON_PROFICIENCY_INVALID) ? m_ProficiencyOverride : m_CurrentWeaponProficiency; +#else + return m_CurrentWeaponProficiency; +#endif + } + void SetCurrentWeaponProficiency( WeaponProficiency_t iProficiency ) { m_CurrentWeaponProficiency = iProficiency; } + virtual WeaponProficiency_t CalcWeaponProficiency( CBaseCombatWeapon *pWeapon ); + virtual Vector GetAttackSpread( CBaseCombatWeapon *pWeapon, CBaseEntity *pTarget = NULL ); + virtual float GetSpreadBias( CBaseCombatWeapon *pWeapon, CBaseEntity *pTarget ); + virtual void DoMuzzleFlash(); + +#ifdef MAPBASE_VSCRIPT + HSCRIPT GetScriptActiveWeapon(); + HSCRIPT GetScriptWeaponIndex( int i ); + HSCRIPT GetScriptWeaponByType( const char *pszWeapon, int iSubType = 0 ); + void GetScriptAllWeapons( HSCRIPT hTable ); + int ScriptGetCurrentWeaponProficiency() { return GetCurrentWeaponProficiency(); } + + void ScriptDropWeapon( HSCRIPT hWeapon ); + void ScriptEquipWeapon( HSCRIPT hWeapon ); + + int ScriptGetAmmoCount( int iType ) const; + void ScriptSetAmmoCount( int iType, int iCount ); + + const Vector& ScriptGetAttackSpread( HSCRIPT hWeapon, HSCRIPT hTarget ); + float ScriptGetSpreadBias( HSCRIPT hWeapon, HSCRIPT hTarget ); + + int ScriptRelationType( HSCRIPT pTarget ); + int ScriptRelationPriority( HSCRIPT pTarget ); + void ScriptSetRelationship( HSCRIPT pTarget, int disposition, int priority ); + + HSCRIPT GetScriptVehicleEntity(); + + bool ScriptInViewCone( const Vector &vecSpot ) { return FInViewCone( vecSpot ); } + bool ScriptEntInViewCone( HSCRIPT pEntity ) { return FInViewCone( ToEnt( pEntity ) ); } + + bool ScriptInAimCone( const Vector &vecSpot ) { return FInAimCone( vecSpot ); } + bool ScriptEntInAimCone( HSCRIPT pEntity ) { return FInAimCone( ToEnt( pEntity ) ); } + + const Vector& ScriptBodyAngles( void ) { static Vector vec; QAngle qa = BodyAngles(); vec.x = qa.x; vec.y = qa.y; vec.z = qa.z; return vec; } +#endif + + // Interactions + static void InitInteractionSystem(); + + // Relationships + static void AllocateDefaultRelationships( ); + static void SetDefaultRelationship( Class_T nClass, Class_T nClassTarget, Disposition_t nDisposition, int nPriority ); +#ifdef MAPBASE + static Disposition_t GetDefaultRelationshipDisposition( Class_T nClassSource, Class_T nClassTarget ); + static int GetDefaultRelationshipPriority( Class_T nClassSource, Class_T nClassTarget ); + int GetDefaultRelationshipPriority( Class_T nClassTarget ); +#endif + Disposition_t GetDefaultRelationshipDisposition( Class_T nClassTarget ); + virtual void AddEntityRelationship( CBaseEntity *pEntity, Disposition_t nDisposition, int nPriority ); + virtual bool RemoveEntityRelationship( CBaseEntity *pEntity ); + virtual void AddClassRelationship( Class_T nClass, Disposition_t nDisposition, int nPriority ); +#ifdef MAPBASE + virtual bool RemoveClassRelationship( Class_T nClass ); +#endif + + virtual void ChangeTeam( int iTeamNum ); + + // Nav hull type + Hull_t GetHullType() const { return m_eHull; } + void SetHullType( Hull_t hullType ) { m_eHull = hullType; } + + // FIXME: The following 3 methods are backdoor hack methods + + // This is a sort of hack back-door only used by physgun! + void SetAmmoCount( int iCount, int iAmmoIndex ); + + // This is a hack to blat out the current active weapon... + // Used by weapon_slam + game_ui + void SetActiveWeapon( CBaseCombatWeapon *pNewWeapon ); + void ClearActiveWeapon() { SetActiveWeapon( NULL ); } + virtual void OnChangeActiveWeapon( CBaseCombatWeapon *pOldWeapon, CBaseCombatWeapon *pNewWeapon ) {} + + // I can't use my current weapon anymore. Switch me to the next best weapon. + bool SwitchToNextBestWeapon(CBaseCombatWeapon *pCurrent); + + // This is a hack to copy the relationship strings used by monstermaker + void SetRelationshipString( string_t theString ) { m_RelationshipString = theString; } + + float GetNextAttack() const { return m_flNextAttack; } + void SetNextAttack( float flWait ) { m_flNextAttack = flWait; } + + bool m_bForceServerRagdoll; + + // Pickup prevention + bool IsAllowedToPickupWeapons( void ) { return !m_bPreventWeaponPickup; } + void SetPreventWeaponPickup( bool bPrevent ) { m_bPreventWeaponPickup = bPrevent; } + bool m_bPreventWeaponPickup; + + virtual CNavArea *GetLastKnownArea( void ) const { return m_lastNavArea; } // return the last nav area the player occupied - NULL if unknown + virtual bool IsAreaTraversable( const CNavArea *area ) const; // return true if we can use the given area + virtual void ClearLastKnownArea( void ); + virtual void UpdateLastKnownArea( void ); // invoke this to update our last known nav area (since there is no think method chained to CBaseCombatCharacter) + virtual void OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ) { } // invoked (by UpdateLastKnownArea) when we enter a new nav area (or it is reset to NULL) + virtual void OnNavAreaRemoved( CNavArea *removedArea ); + + // ----------------------- + // Notification from INextBots. + // ----------------------- + virtual void OnPursuedBy( INextBot * RESTRICT pPursuer ){} // called every frame while pursued by a bot in DirectChase. + +#ifdef GLOWS_ENABLE + // Glows + void AddGlowEffect( void ); + void RemoveGlowEffect( void ); + bool IsGlowEffectActive( void ); +#endif // GLOWS_ENABLE + +#ifdef INVASION_DLL +public: + + + // TF2 Powerups + virtual bool CanBePoweredUp( void ); + bool HasPowerup( int iPowerup ); + virtual bool CanPowerupNow( int iPowerup ); // Return true if I can be powered by this powerup right now + virtual bool CanPowerupEver( int iPowerup ); // Return true if I ever accept this powerup type + + void SetPowerup( int iPowerup, bool bState, float flTime = 0, float flAmount = 0, CBaseEntity *pAttacker = NULL, CDamageModifier *pDamageModifier = NULL ); + virtual bool AttemptToPowerup( int iPowerup, float flTime, float flAmount = 0, CBaseEntity *pAttacker = NULL, CDamageModifier *pDamageModifier = NULL ); + virtual float PowerupDuration( int iPowerup, float flTime ); + virtual void PowerupStart( int iPowerup, float flAmount = 0, CBaseEntity *pAttacker = NULL, CDamageModifier *pDamageModifier = NULL ); + virtual void PowerupEnd( int iPowerup ); + + void PowerupThink( void ); + virtual void PowerupThink( int iPowerup ); + +public: + + CNetworkVar( int, m_iPowerups ); + float m_flPowerupAttemptTimes[ MAX_POWERUPS ]; + float m_flPowerupEndTimes[ MAX_POWERUPS ]; + float m_flFractionalBoost; // POWERUP_BOOST health fraction - specific powerup data + +#endif + +public: + // returns the last body region that took damage + int LastHitGroup() const { return m_LastHitGroup; } +#ifndef MAPBASE // For filter_damage_transfer +protected: +#endif + void SetLastHitGroup( int nHitGroup ) { m_LastHitGroup = nHitGroup; } + +public: + CNetworkVar( float, m_flNextAttack ); // cannot attack again until this time + +#ifdef GLOWS_ENABLE +protected: + CNetworkVar( bool, m_bGlowEnabled ); +#endif // GLOWS_ENABLE + +private: + Hull_t m_eHull; + + void UpdateGlowEffect( void ); + void DestroyGlowEffect( void ); + +protected: + int m_bloodColor; // color of blood particless + + // ------------------- + // combat ability data + // ------------------- + float m_flFieldOfView; // cosine of field of view for this character + Vector m_HackedGunPos; // HACK until we can query end of gun + string_t m_RelationshipString; // Used to load up relationship keyvalues + float m_impactEnergyScale;// scale the amount of energy used to calculate damage this ent takes due to physics + +public: + static int GetInteractionID(); // Returns the next interaction # + +#ifdef MAPBASE + // Mapbase's new method for adding interactions which allows them to be handled with their names, currently for VScript + static void AddInteractionWithString( int &interaction, const char *szName ); +#endif + +protected: + // Visibility-related stuff + bool ComputeLOS( const Vector &vecEyePosition, const Vector &vecTarget ) const; +private: + // For weapon strip + void ThrowDirForWeaponStrip( CBaseCombatWeapon *pWeapon, const Vector &vecForward, Vector *pVecThrowDir ); + void DropWeaponForWeaponStrip( CBaseCombatWeapon *pWeapon, const Vector &vecForward, const QAngle &vecAngles, float flDiameter ); + + friend class CScriptedTarget; // needs to access GetInteractionID() + + static int m_lastInteraction; // Last registered interaction # + static Relationship_t** m_DefaultRelationship; + + // attack/damage + int m_LastHitGroup; // the last body region that took damage + float m_flDamageAccumulator; // so very small amounts of damage do not get lost. + int m_iDamageCount; // # of times NPC has been damaged. used for tracking 1-shot kills. + + // Weapon proficiency gets calculated each time an NPC changes his weapon, and then + // cached off as the CurrentWeaponProficiency. + WeaponProficiency_t m_CurrentWeaponProficiency; + +#ifdef MAPBASE + // Weapon proficiency can be overridden with this. + WeaponProficiency_t m_ProficiencyOverride = WEAPON_PROFICIENCY_INVALID; +#endif + + // --------------- + // Relationships + // --------------- + CUtlVector m_Relationship; // Array of relationships + +protected: + // shared ammo slots + CNetworkArrayForDerived( int, m_iAmmo, MAX_AMMO_SLOTS ); + + // Usable character items + CNetworkArray( CBaseCombatWeaponHandle, m_hMyWeapons, MAX_WEAPONS ); + + CNetworkHandle( CBaseCombatWeapon, m_hActiveWeapon ); + + friend class CCleanupDefaultRelationShips; + + IntervalTimer m_aliveTimer; + + unsigned int m_hasBeenInjured; // bitfield corresponding to team ID that did the injury + + // we do this because MAX_TEAMS is 32, which is wasteful for most games + enum { MAX_DAMAGE_TEAMS = 4 }; + struct DamageHistory + { + int team; // which team hurt us (TEAM_INVALID means slot unused) + IntervalTimer interval; // how long has it been + }; + DamageHistory m_damageHistory[ MAX_DAMAGE_TEAMS ]; + + // last known navigation area of player - NULL if unknown + CNavArea *m_lastNavArea; + CAI_MoveMonitor m_NavAreaUpdateMonitor; + int m_registeredNavTeam; // ugly, but needed to clean up player team counts in nav mesh +}; + + +inline float CBaseCombatCharacter::GetAliveDuration( void ) const +{ + return m_aliveTimer.GetElapsedTime(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +inline int CBaseCombatCharacter::WeaponCount() const +{ + return MAX_WEAPONS; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : i - +//----------------------------------------------------------------------------- +inline CBaseCombatWeapon *CBaseCombatCharacter::GetWeapon( int i ) const +{ + Assert( (i >= 0) && (i < MAX_WEAPONS) ); + return m_hMyWeapons[i].Get(); +} + +#ifdef INVASION_DLL +// Powerup Inlines +inline bool CBaseCombatCharacter::CanBePoweredUp( void ) { return true; } +inline float CBaseCombatCharacter::PowerupDuration( int iPowerup, float flTime ) { return flTime; } +inline void CBaseCombatCharacter::PowerupEnd( int iPowerup ) { return; } +inline void CBaseCombatCharacter::PowerupThink( int iPowerup ) { return; } +#endif + +EXTERN_SEND_TABLE(DT_BaseCombatCharacter); + +void RadiusDamage( const CTakeDamageInfo &info, const Vector &vecSrc, float flRadius, int iClassIgnore, CBaseEntity *pEntityIgnore ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CTraceFilterMelee : public CTraceFilterEntitiesOnly +{ +public: + // It does have a base, but we'll never network anything below here.. + DECLARE_CLASS_NOBASE( CTraceFilterMelee ); + + CTraceFilterMelee( const IHandleEntity *passentity, int collisionGroup, CTakeDamageInfo *dmgInfo, float flForceScale, bool bDamageAnyNPC ) + : m_pPassEnt(passentity), m_collisionGroup(collisionGroup), m_dmgInfo(dmgInfo), m_pHit(NULL), m_flForceScale(flForceScale), m_bDamageAnyNPC(bDamageAnyNPC) + { + } + + virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ); + +public: + const IHandleEntity *m_pPassEnt; + int m_collisionGroup; + CTakeDamageInfo *m_dmgInfo; + CBaseEntity *m_pHit; + float m_flForceScale; + bool m_bDamageAnyNPC; +}; + +#endif // BASECOMBATCHARACTER_H diff --git a/sp/src/game/server/basecombatweapon.cpp b/sp/src/game/server/basecombatweapon.cpp new file mode 100644 index 00000000..1135aa82 --- /dev/null +++ b/sp/src/game/server/basecombatweapon.cpp @@ -0,0 +1,747 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "ai_basenpc.h" +#include "animation.h" +#include "basecombatweapon.h" +#include "player.h" // For gEvilImpulse101 / CBasePlayer +#include "gamerules.h" // For g_pGameRules +#include +#include "ammodef.h" +#include "baseviewmodel.h" +#include "in_buttons.h" +#include "soundent.h" +#include "weapon_parse.h" +#include "game.h" +#include "engine/IEngineSound.h" +#include "sendproxy.h" +#include "tier1/strtools.h" +#include "vphysics/constraints.h" +#include "npcevent.h" +#include "igamesystem.h" +#include "collisionutils.h" +#include "iservervehicle.h" +#include "func_break.h" + +#ifdef HL2MP + #include "hl2mp_gamerules.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern int gEvilImpulse101; // In Player.h + +// ----------------------------------------- +// Sprite Index info +// ----------------------------------------- +short g_sModelIndexLaser; // holds the index for the laser beam +const char *g_pModelNameLaser = "sprites/laserbeam.vmt"; +short g_sModelIndexLaserDot; // holds the index for the laser beam dot +short g_sModelIndexFireball; // holds the index for the fireball +short g_sModelIndexSmoke; // holds the index for the smoke cloud +short g_sModelIndexWExplosion; // holds the index for the underwater explosion +short g_sModelIndexBubbles; // holds the index for the bubbles model +short g_sModelIndexBloodDrop; // holds the sprite index for the initial blood +short g_sModelIndexBloodSpray; // holds the sprite index for splattered blood + + +ConVar weapon_showproficiency( "weapon_showproficiency", "0" ); +extern ConVar ai_debug_shoot_positions; + +//----------------------------------------------------------------------------- +// Purpose: Precache global weapon sounds +//----------------------------------------------------------------------------- +void W_Precache(void) +{ + PrecacheFileWeaponInfoDatabase( filesystem, g_pGameRules->GetEncryptionKey() ); + + + +#ifdef HL1_DLL + g_sModelIndexWExplosion = CBaseEntity::PrecacheModel ("sprites/WXplo1.vmt");// underwater fireball + g_sModelIndexBloodSpray = CBaseEntity::PrecacheModel ("sprites/bloodspray.vmt"); // initial blood + g_sModelIndexBloodDrop = CBaseEntity::PrecacheModel ("sprites/blood.vmt"); // splattered blood + g_sModelIndexLaserDot = CBaseEntity::PrecacheModel("sprites/laserdot.vmt"); +#endif // HL1_DLL + +#ifndef TF_DLL + g_sModelIndexFireball = CBaseEntity::PrecacheModel ("sprites/zerogxplode.vmt");// fireball + + g_sModelIndexSmoke = CBaseEntity::PrecacheModel ("sprites/steam1.vmt");// smoke + g_sModelIndexBubbles = CBaseEntity::PrecacheModel ("sprites/bubble.vmt");//bubbles + g_sModelIndexLaser = CBaseEntity::PrecacheModel( (char *)g_pModelNameLaser ); + + PrecacheParticleSystem( "blood_impact_red_01" ); + PrecacheParticleSystem( "blood_impact_green_01" ); + PrecacheParticleSystem( "blood_impact_yellow_01" ); + + CBaseEntity::PrecacheModel ("effects/bubble.vmt");//bubble trails + + CBaseEntity::PrecacheModel("models/weapons/w_bullet.mdl"); +#endif + + CBaseEntity::PrecacheScriptSound( "BaseCombatWeapon.WeaponDrop" ); + CBaseEntity::PrecacheScriptSound( "BaseCombatWeapon.WeaponMaterialize" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Transmit weapon data +//----------------------------------------------------------------------------- +int CBaseCombatWeapon::UpdateTransmitState( void) +{ + // If the weapon is being carried by a CBaseCombatCharacter, let the combat character do the logic + // about whether or not to transmit it. + if ( GetOwner() ) + { + return SetTransmitState( FL_EDICT_PVSCHECK ); + } + else + { + // If it's just lying around, then use CBaseEntity's visibility test to see if it should be sent. + return BaseClass::UpdateTransmitState(); + } +} + + +void CBaseCombatWeapon::Operator_FrameUpdate( CBaseCombatCharacter *pOperator ) +{ + StudioFrameAdvance( ); // animate + + if ( IsSequenceFinished() ) + { + if ( SequenceLoops() ) + { + // animation does loop, which means we're playing subtle idle. Might need to fidget. + int iSequence = SelectWeightedSequence( GetActivity() ); + if ( iSequence != ACTIVITY_NOT_AVAILABLE ) + { + ResetSequence( iSequence ); // Set to new anim (if it's there) + } + } +#if 0 + else + { + // animation that just ended doesn't loop! That means we just finished a fidget + // and should return to our heaviest weighted idle (the subtle one) + SelectHeaviestSequence( GetActivity() ); + } +#endif + } + + // Animation events are passed back to the weapon's owner/operator + DispatchAnimEvents( pOperator ); + + // Update and dispatch the viewmodel events + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + + if ( pOwner == NULL ) + return; + + CBaseViewModel *vm = pOwner->GetViewModel( m_nViewModelIndex ); + + if ( vm != NULL ) + { + vm->StudioFrameAdvance(); + vm->DispatchAnimEvents( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEvent - +// *pOperator - +//----------------------------------------------------------------------------- +void CBaseCombatWeapon::Operator_HandleAnimEvent( animevent_t *pEvent, CBaseCombatCharacter *pOperator ) +{ + if ( (pEvent->type & AE_TYPE_NEWEVENTSYSTEM) && (pEvent->type & AE_TYPE_SERVER) ) + { + if ( pEvent->event == AE_NPC_WEAPON_FIRE ) + { + bool bSecondary = (atoi( pEvent->options ) != 0); + Operator_ForceNPCFire( pOperator, bSecondary ); + return; + } + else if ( pEvent->event == AE_WPN_PLAYWPNSOUND ) + { + int iSnd = GetWeaponSoundFromString(pEvent->options); + if ( iSnd != -1 ) + { + WeaponSound( (WeaponSound_t)iSnd ); + } + } + } + + DevWarning( 2, "Unhandled animation event %d from %s --> %s\n", pEvent->event, pOperator->GetClassname(), GetClassname() ); +} + +// NOTE: This should never be called when a character is operating the weapon. Animation events should be +// routed through the character, and then back into CharacterAnimEvent() +void CBaseCombatWeapon::HandleAnimEvent( animevent_t *pEvent ) +{ + //If the player is receiving this message, pass it through + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + + if ( pOwner != NULL ) + { + Operator_HandleAnimEvent( pEvent, pOwner ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Make the weapon visible and tangible +//----------------------------------------------------------------------------- +CBaseEntity* CBaseCombatWeapon::Respawn( void ) +{ + // make a copy of this weapon that is invisible and inaccessible to players (no touch function). The weapon spawn/respawn code + // will decide when to make the weapon visible and touchable. + CBaseEntity *pNewWeapon = CBaseEntity::Create( GetClassname(), g_pGameRules->VecWeaponRespawnSpot( this ), GetLocalAngles(), GetOwnerEntity() ); + + if ( pNewWeapon ) + { + pNewWeapon->AddEffects( EF_NODRAW );// invisible for now + pNewWeapon->SetTouch( NULL );// no touch + pNewWeapon->SetThink( &CBaseCombatWeapon::AttemptToMaterialize ); + + UTIL_DropToFloor( this, MASK_SOLID ); + + // not a typo! We want to know when the weapon the player just picked up should respawn! This new entity we created is the replacement, + // but when it should respawn is based on conditions belonging to the weapon that was taken. + pNewWeapon->SetNextThink( gpGlobals->curtime + g_pGameRules->FlWeaponRespawnTime( this ) ); + } + else + { + Warning("Respawn failed to create %s!\n", GetClassname() ); + } + + return pNewWeapon; +} + +//----------------------------------------------------------------------------- +// Purpose: Weapons ignore other weapons when LOS tracing +//----------------------------------------------------------------------------- +class CWeaponLOSFilter : public CTraceFilterSkipTwoEntities +{ + DECLARE_CLASS( CWeaponLOSFilter, CTraceFilterSkipTwoEntities ); +public: + CWeaponLOSFilter( IHandleEntity *pHandleEntity, IHandleEntity *pHandleEntity2, int collisionGroup ) : + CTraceFilterSkipTwoEntities( pHandleEntity, pHandleEntity2, collisionGroup ), m_pVehicle( NULL ) + { + // If the tracing entity is in a vehicle, then ignore it + if ( pHandleEntity != NULL ) + { + CBaseCombatCharacter *pBCC = ((CBaseEntity *)pHandleEntity)->MyCombatCharacterPointer(); + if ( pBCC != NULL ) + { + m_pVehicle = pBCC->GetVehicleEntity(); + } + } + } + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + CBaseEntity *pEntity = (CBaseEntity *)pServerEntity; + + if ( pEntity->GetCollisionGroup() == COLLISION_GROUP_WEAPON ) + return false; + + // Don't collide with the tracing entity's vehicle (if it exists) + if ( pServerEntity == m_pVehicle ) + return false; + + if ( pEntity->GetHealth() > 0 ) + { + CBreakable *pBreakable = dynamic_cast(pEntity); + if ( pBreakable && pBreakable->IsBreakable() && pBreakable->GetMaterialType() == matGlass) + { + return false; + } + } + + return BaseClass::ShouldHitEntity( pServerEntity, contentsMask ); + } + +private: + CBaseEntity *m_pVehicle; +}; + +//----------------------------------------------------------------------------- +// Purpose: Check the weapon LOS for an owner at an arbitrary position +// If bSetConditions is true, LOS related conditions will also be set +//----------------------------------------------------------------------------- +bool CBaseCombatWeapon::WeaponLOSCondition( const Vector &ownerPos, const Vector &targetPos, bool bSetConditions ) +{ + // -------------------- + // Check for occlusion + // -------------------- + CAI_BaseNPC* npcOwner = m_hOwner.Get()->MyNPCPointer(); + + // Find its relative shoot position + Vector vecRelativeShootPosition; + VectorSubtract( npcOwner->Weapon_ShootPosition(), npcOwner->GetAbsOrigin(), vecRelativeShootPosition ); + Vector barrelPos = ownerPos + vecRelativeShootPosition; + + // FIXME: If we're in a vehicle, we need some sort of way to handle shooting out of them + + // Use the custom LOS trace filter + CWeaponLOSFilter traceFilter( m_hOwner.Get(), npcOwner->GetEnemy(), COLLISION_GROUP_BREAKABLE_GLASS ); + trace_t tr; + UTIL_TraceLine( barrelPos, targetPos, MASK_SHOT, &traceFilter, &tr ); + + // See if we completed the trace without interruption + if ( tr.fraction == 1.0 ) + { + if ( ai_debug_shoot_positions.GetBool() ) + { + NDebugOverlay::Line( barrelPos, targetPos, 0, 255, 0, false, 1.0 ); + } + + return true; + } + + CBaseEntity *pHitEnt = tr.m_pEnt; + + CBasePlayer *pEnemyPlayer = ToBasePlayer( npcOwner->GetEnemy() ); + + // is player in a vehicle? if so, verify vehicle is target and return if so (so npc shoots at vehicle) + if ( pEnemyPlayer && pEnemyPlayer->IsInAVehicle() ) + { + // Ok, player in vehicle, check if vehicle is target we're looking at, fire if it is + // Also, check to see if the owner of the entity is the vehicle, in which case it's valid too. + // This catches vehicles that use bone followers. + CBaseEntity *pVehicle = pEnemyPlayer->GetVehicle()->GetVehicleEnt(); + if ( pHitEnt == pVehicle || pHitEnt->GetOwnerEntity() == pVehicle ) + return true; + } + + // Hitting our enemy is a success case + if ( pHitEnt == npcOwner->GetEnemy() ) + { + if ( ai_debug_shoot_positions.GetBool() ) + { + NDebugOverlay::Line( barrelPos, targetPos, 0, 255, 0, false, 1.0 ); + } + + return true; + } + + // If a vehicle is blocking the view, grab its driver and use that as the combat character + CBaseCombatCharacter *pBCC; + IServerVehicle *pVehicle = pHitEnt->GetServerVehicle(); + if ( pVehicle ) + { + pBCC = pVehicle->GetPassenger( ); + } + else + { + pBCC = ToBaseCombatCharacter( pHitEnt ); + } + + if ( pBCC ) + { +#ifdef MAPBASE + if ( npcOwner->IRelationType( pBCC ) <= D_FR ) +#else + if ( npcOwner->IRelationType( pBCC ) == D_HT ) +#endif + return true; + + if ( bSetConditions ) + { + npcOwner->SetCondition( COND_WEAPON_BLOCKED_BY_FRIEND ); + } + } + else if ( bSetConditions ) + { + npcOwner->SetCondition( COND_WEAPON_SIGHT_OCCLUDED ); + npcOwner->SetEnemyOccluder( pHitEnt ); + + if( ai_debug_shoot_positions.GetBool() ) + { + NDebugOverlay::Line( tr.startpos, tr.endpos, 255, 0, 0, false, 1.0 ); + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Base class always returns not bits +//----------------------------------------------------------------------------- +int CBaseCombatWeapon::WeaponRangeAttack1Condition( float flDot, float flDist ) +{ + if ( UsesPrimaryAmmo() && !HasPrimaryAmmo() ) + { + return COND_NO_PRIMARY_AMMO; + } + else if ( flDist < m_fMinRange1) + { + return COND_TOO_CLOSE_TO_ATTACK; + } + else if (flDist > m_fMaxRange1) + { + return COND_TOO_FAR_TO_ATTACK; + } + else if (flDot < 0.5) // UNDONE: Why check this here? Isn't the AI checking this already? + { + return COND_NOT_FACING_ATTACK; + } + + return COND_CAN_RANGE_ATTACK1; +} + +//----------------------------------------------------------------------------- +// Purpose: Base class always returns not bits +//----------------------------------------------------------------------------- +int CBaseCombatWeapon::WeaponRangeAttack2Condition( float flDot, float flDist ) +{ + // currently disabled + return COND_NONE; + + if ( m_bReloadsSingly ) + { + if (m_iClip2 <=0) + { + return COND_NO_SECONDARY_AMMO; + } + else if ( flDist < m_fMinRange2) + { + return COND_TOO_CLOSE_TO_ATTACK; + } + else if (flDist > m_fMaxRange2) + { + return COND_TOO_FAR_TO_ATTACK; + } + else if (flDot < 0.5) + { + return COND_NOT_FACING_ATTACK; + } + return COND_CAN_RANGE_ATTACK2; + } + + return COND_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: Base class always returns not bits +//----------------------------------------------------------------------------- +int CBaseCombatWeapon::WeaponMeleeAttack1Condition( float flDot, float flDist ) +{ + return COND_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: Base class always returns not bits +//----------------------------------------------------------------------------- +int CBaseCombatWeapon::WeaponMeleeAttack2Condition( float flDot, float flDist ) +{ + return COND_NONE; +} + +//==================================================================================== +// WEAPON DROPPING / DESTRUCTION +//==================================================================================== +void CBaseCombatWeapon::Delete( void ) +{ + SetTouch( NULL ); + // FIXME: why doesn't this just remove itself now? + SetThink(&CBaseCombatWeapon::SUB_Remove); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CBaseCombatWeapon::DestroyItem( void ) +{ + CBaseCombatCharacter *pOwner = m_hOwner.Get(); + + if ( pOwner ) + { + // if attached to a player, remove. + pOwner->RemovePlayerItem( this ); + } + + Kill( ); +} + +void CBaseCombatWeapon::Kill( void ) +{ + SetTouch( NULL ); + // FIXME: why doesn't this just remove itself now? + // FIXME: how is this different than Delete(), and why do they have the same code in them? + SetThink(&CBaseCombatWeapon::SUB_Remove); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//==================================================================================== +// FALL TO GROUND +//==================================================================================== +//----------------------------------------------------------------------------- +// Purpose: Setup for the fall +//----------------------------------------------------------------------------- +void CBaseCombatWeapon::FallInit( void ) +{ + SetModel( GetWorldModel() ); + VPhysicsDestroyObject(); + + if ( !VPhysicsInitNormal( SOLID_BBOX, GetSolidFlags() | FSOLID_TRIGGER, false ) ) + { + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_TRIGGER ); + } + else + { +#if !defined( CLIENT_DLL ) + // Constrained start? + if ( HasSpawnFlags( SF_WEAPON_START_CONSTRAINED ) ) + { + //Constrain the weapon in place + IPhysicsObject *pReferenceObject, *pAttachedObject; + + pReferenceObject = g_PhysWorldObject; + pAttachedObject = VPhysicsGetObject(); + + if ( pReferenceObject && pAttachedObject ) + { + constraint_fixedparams_t fixed; + fixed.Defaults(); + fixed.InitWithCurrentObjectState( pReferenceObject, pAttachedObject ); + + fixed.constraint.forceLimit = lbs2kg( 10000 ); + fixed.constraint.torqueLimit = lbs2kg( 10000 ); + + m_pConstraint = physenv->CreateFixedConstraint( pReferenceObject, pAttachedObject, NULL, fixed ); + + m_pConstraint->SetGameData( (void *) this ); + } + } +#endif //CLIENT_DLL + } + + SetPickupTouch(); + + SetThink( &CBaseCombatWeapon::FallThink ); + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Items that have just spawned run this think to catch them when +// they hit the ground. Once we're sure that the object is grounded, +// we change its solid type to trigger and set it in a large box that +// helps the player get it. +//----------------------------------------------------------------------------- +void CBaseCombatWeapon::FallThink ( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + bool shouldMaterialize = false; + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if ( pPhysics ) + { + shouldMaterialize = pPhysics->IsAsleep(); + } + else + { + shouldMaterialize = (GetFlags() & FL_ONGROUND) ? true : false; + } + + if ( shouldMaterialize ) + { + // clatter if we have an owner (i.e., dropped by someone) + // don't clatter if the gun is waiting to respawn (if it's waiting, it is invisible!) + if ( GetOwnerEntity() ) + { + EmitSound( "BaseCombatWeapon.WeaponDrop" ); + } + Materialize(); + } +} + +//==================================================================================== +// WEAPON SPAWNING +//==================================================================================== +//----------------------------------------------------------------------------- +// Purpose: Make a weapon visible and tangible +//-----------------------------------------------------------------------------// +void CBaseCombatWeapon::Materialize( void ) +{ + if ( IsEffectActive( EF_NODRAW ) ) + { + // changing from invisible state to visible. +#ifdef HL2MP + EmitSound( "AlyxEmp.Charge" ); +#else + EmitSound( "BaseCombatWeapon.WeaponMaterialize" ); +#endif + + RemoveEffects( EF_NODRAW ); + DoMuzzleFlash(); + } +#ifdef HL2MP + if ( HasSpawnFlags( SF_NORESPAWN ) == false ) + { + VPhysicsInitNormal( SOLID_BBOX, GetSolidFlags() | FSOLID_TRIGGER, false ); + SetMoveType( MOVETYPE_VPHYSICS ); + + HL2MPRules()->AddLevelDesignerPlacedObject( this ); + } +#else + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_TRIGGER ); +#endif + + SetPickupTouch(); + + SetThink (NULL); +} + +//----------------------------------------------------------------------------- +// Purpose: See if the game rules will let this weapon respawn +//----------------------------------------------------------------------------- +void CBaseCombatWeapon::AttemptToMaterialize( void ) +{ + float time = g_pGameRules->FlWeaponTryRespawn( this ); + + if ( time == 0 ) + { + Materialize(); + return; + } + + SetNextThink( gpGlobals->curtime + time ); +} + +//----------------------------------------------------------------------------- +// Purpose: Weapon has been picked up, should it respawn? +//----------------------------------------------------------------------------- +void CBaseCombatWeapon::CheckRespawn( void ) +{ + switch ( g_pGameRules->WeaponShouldRespawn( this ) ) + { + case GR_WEAPON_RESPAWN_YES: + Respawn(); + break; + case GR_WEAPON_RESPAWN_NO: + return; + break; + } +} + +class CWeaponList : public CAutoGameSystem +{ +public: + CWeaponList( char const *name ) : CAutoGameSystem( name ) + { + } + + + virtual void LevelShutdownPostEntity() + { + m_list.Purge(); + } + + void AddWeapon( CBaseCombatWeapon *pWeapon ) + { + m_list.AddToTail( pWeapon ); + } + + void RemoveWeapon( CBaseCombatWeapon *pWeapon ) + { + m_list.FindAndRemove( pWeapon ); + } + CUtlLinkedList< CBaseCombatWeapon * > m_list; +}; + +CWeaponList g_WeaponList( "CWeaponList" ); + +void OnBaseCombatWeaponCreated( CBaseCombatWeapon *pWeapon ) +{ + g_WeaponList.AddWeapon( pWeapon ); +} + +void OnBaseCombatWeaponDestroyed( CBaseCombatWeapon *pWeapon ) +{ + g_WeaponList.RemoveWeapon( pWeapon ); +} + +int CBaseCombatWeapon::GetAvailableWeaponsInBox( CBaseCombatWeapon **pList, int listMax, const Vector &mins, const Vector &maxs ) +{ + // linear search all weapons + int count = 0; + int index = g_WeaponList.m_list.Head(); + while ( index != g_WeaponList.m_list.InvalidIndex() ) + { + CBaseCombatWeapon *pWeapon = g_WeaponList.m_list[index]; + // skip any held weapon + if ( !pWeapon->GetOwner() ) + { + // restrict to mins/maxs + if ( IsPointInBox( pWeapon->GetAbsOrigin(), mins, maxs ) ) + { + if ( count < listMax ) + { + pList[count] = pWeapon; + count++; + } + } + } + index = g_WeaponList.m_list.Next( index ); + } + + return count; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseCombatWeapon::ObjectCaps( void ) +{ + int caps = BaseClass::ObjectCaps(); + if ( !IsFollowingEntity() && !HasSpawnFlags(SF_WEAPON_NO_PLAYER_PICKUP) ) + { + caps |= FCAP_IMPULSE_USE; + } + + return caps; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseCombatWeapon::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + + if ( pPlayer ) + { + m_OnPlayerUse.FireOutput( pActivator, pCaller ); + +#ifdef MAPBASE + // Mark that we're being +USE'd, not bumped + AddSpawnFlags(SF_WEAPON_USED); +#endif + + // + // Bump the weapon to try equipping it before picking it up physically. This is + // important in a few spots in the game where the player could potentially +use pickup + // and then THROW AWAY a vital weapon, rendering them unable to continue the game. + // + if ( pPlayer->BumpWeapon( this ) ) + { + OnPickedUp( pPlayer ); + } + else + { + pPlayer->PickupObject( this ); + } + +#ifdef MAPBASE + RemoveSpawnFlags(SF_WEAPON_USED); +#endif + } +} + diff --git a/sp/src/game/server/basecombatweapon.h b/sp/src/game/server/basecombatweapon.h new file mode 100644 index 00000000..0af362ca --- /dev/null +++ b/sp/src/game/server/basecombatweapon.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef COMBATWEAPON_H +#define COMBATWEAPON_H +#ifdef _WIN32 +#pragma once +#endif + +#include "entityoutput.h" +#include "basecombatweapon_shared.h" + +//----------------------------------------------------------------------------- +// Bullet types +//----------------------------------------------------------------------------- + +// ----------------------------------------- +// Sounds +// ----------------------------------------- + +struct animevent_t; + +extern void SpawnBlood(Vector vecSpot, const Vector &vecDir, int bloodColor, float flDamage); + +#endif // COMBATWEAPON_H diff --git a/sp/src/game/server/baseentity.cpp b/sp/src/game/server/baseentity.cpp new file mode 100644 index 00000000..1b71f9fd --- /dev/null +++ b/sp/src/game/server/baseentity.cpp @@ -0,0 +1,10486 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: The base class from which all game entities are derived. +// +//===========================================================================// + +#include "cbase.h" +#include "globalstate.h" +#include "isaverestore.h" +#include "client.h" +#include "decals.h" +#include "gamerules.h" +#include "entityapi.h" +#include "entitylist.h" +#include "eventqueue.h" +#include "hierarchy.h" +#include "basecombatweapon.h" +#include "const.h" +#include "player.h" // For debug draw sending +#include "ndebugoverlay.h" +#include "physics.h" +#include "model_types.h" +#include "team.h" +#include "sendproxy.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "baseentity.h" +#include "collisionutils.h" +#include "coordsize.h" +#include "animation.h" +#include "tier1/strtools.h" +#include "engine/IEngineSound.h" +#include "physics_saverestore.h" +#include "saverestore_utlvector.h" +#include "bone_setup.h" +#include "vcollide_parse.h" +#include "filters.h" +#include "te_effect_dispatch.h" +#include "AI_Criteria.h" +#include "AI_ResponseSystem.h" +#include "world.h" +#include "globals.h" +#include "saverestoretypes.h" +#include "SkyCamera.h" +#include "sceneentity.h" +#include "game.h" +#include "tier0/vprof.h" +#include "ai_basenpc.h" +#include "iservervehicle.h" +#include "eventlist.h" +#include "scriptevent.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "UtlCachedFileData.h" +#include "utlbuffer.h" +#include "positionwatcher.h" +#include "movetype_push.h" +#include "tier0/icommandline.h" +#include "vphysics/friction.h" +#include +#include "datacache/imdlcache.h" +#include "ModelSoundsCache.h" +#include "env_debughistory.h" +#include "tier1/utlstring.h" +#include "utlhashtable.h" +#ifdef MAPBASE +#include "mapbase/matchers.h" +#include "mapbase/datadesc_mod.h" +#endif + +#if defined( TF_DLL ) +#include "tf_gamerules.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern bool g_bTestMoveTypeStepSimulation; +extern ConVar sv_vehicle_autoaim_scale; + +// Init static class variables +bool CBaseEntity::m_bInDebugSelect = false; // Used for selection in debug overlays +int CBaseEntity::m_nDebugPlayer = -1; // Player doing the selection + +// This can be set before creating an entity to force it to use a particular edict. +edict_t *g_pForceAttachEdict = NULL; + +bool CBaseEntity::m_bDebugPause = false; // Whether entity i/o is paused. +int CBaseEntity::m_nDebugSteps = 1; // Number of entity outputs to fire before pausing again. +bool CBaseEntity::sm_bDisableTouchFuncs = false; // Disables PhysicsTouch and PhysicsStartTouch function calls +bool CBaseEntity::sm_bAccurateTriggerBboxChecks = true; // set to false for legacy behavior in ep1 + +int CBaseEntity::m_nPredictionRandomSeed = -1; +CBasePlayer *CBaseEntity::m_pPredictionPlayer = NULL; + +// Used to make sure nobody calls UpdateTransmitState directly. +int g_nInsideDispatchUpdateTransmitState = 0; + +// When this is false, throw an assert in debug when GetAbsAnything is called. Used when hierachy is incomplete/invalid. +bool CBaseEntity::s_bAbsQueriesValid = true; + + +ConVar sv_netvisdist( "sv_netvisdist", "10000", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Test networking visibility distance" ); + +ConVar sv_script_think_interval("sv_script_think_interval", "0.1"); + + +// This table encodes edict data. +void SendProxy_AnimTime( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ) +{ + CBaseEntity *pEntity = (CBaseEntity *)pStruct; + +#if defined( _DEBUG ) + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + Assert( pAnimating ); + + if ( pAnimating ) + { + Assert( !pAnimating->IsUsingClientSideAnimation() ); + } +#endif + + int ticknumber = TIME_TO_TICKS( pEntity->m_flAnimTime ); + // Tickbase is current tick rounded down to closes 100 ticks + int tickbase = gpGlobals->GetNetworkBase( gpGlobals->tickcount, pEntity->entindex() ); + int addt = 0; + // If it's within the last tick interval through the current one, then we can encode it + if ( ticknumber >= ( tickbase - 100 ) ) + { + addt = ( ticknumber - tickbase ) & 0xFF; + } + + pOut->m_Int = addt; +} + +// This table encodes edict data. +void SendProxy_SimulationTime( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ) +{ + CBaseEntity *pEntity = (CBaseEntity *)pStruct; + + int ticknumber = TIME_TO_TICKS( pEntity->m_flSimulationTime ); + // tickbase is current tick rounded down to closest 100 ticks + int tickbase = gpGlobals->GetNetworkBase( gpGlobals->tickcount, pEntity->entindex() ); + int addt = 0; + if ( ticknumber >= tickbase ) + { + addt = ( ticknumber - tickbase ) & 0xff; + } + + pOut->m_Int = addt; +} + +void* SendProxy_ClientSideAnimation( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ) +{ + CBaseEntity *pEntity = (CBaseEntity *)pStruct; + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + + if ( pAnimating && !pAnimating->IsUsingClientSideAnimation() ) + return (void*)pVarData; + else + return NULL; // Don't send animtime unless the client needs it. +} +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_ClientSideAnimation ); + + +BEGIN_SEND_TABLE_NOBASE( CBaseEntity, DT_AnimTimeMustBeFirst ) + // NOTE: Animtime must be sent before origin and angles ( from pev ) because it has a + // proxy on the client that stores off the old values before writing in the new values and + // if it is sent after the new values, then it will only have the new origin and studio model, etc. + // interpolation will be busted + SendPropInt (SENDINFO(m_flAnimTime), 8, SPROP_UNSIGNED|SPROP_CHANGES_OFTEN|SPROP_ENCODED_AGAINST_TICKCOUNT, SendProxy_AnimTime), +END_SEND_TABLE() + +#if !defined( NO_ENTITY_PREDICTION ) +BEGIN_SEND_TABLE_NOBASE( CBaseEntity, DT_PredictableId ) + SendPropPredictableId( SENDINFO( m_PredictableID ) ), + SendPropInt( SENDINFO( m_bIsPlayerSimulated ), 1, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +static void* SendProxy_SendPredictableId( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ) +{ + CBaseEntity *pEntity = (CBaseEntity *)pStruct; + if ( !pEntity || !pEntity->m_PredictableID->IsActive() ) + return NULL; + + int id_player_index = pEntity->m_PredictableID->GetPlayer(); + pRecipients->SetOnly( id_player_index ); + + return ( void * )pVarData; +} +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_SendPredictableId ); +#endif + +void SendProxy_Origin( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CBaseEntity *entity = (CBaseEntity*)pStruct; + Assert( entity ); + + const Vector *v; + + if ( !entity->UseStepSimulationNetworkOrigin( &v ) ) + { + v = &entity->GetLocalOrigin(); + } + + pOut->m_Vector[ 0 ] = v->x; + pOut->m_Vector[ 1 ] = v->y; + pOut->m_Vector[ 2 ] = v->z; +} + +//-------------------------------------------------------------------------------------------------------- +// Used when breaking up origin, note we still have to deal with StepSimulation +//-------------------------------------------------------------------------------------------------------- +void SendProxy_OriginXY( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CBaseEntity *entity = (CBaseEntity*)pStruct; + Assert( entity ); + + const Vector *v; + + if ( !entity->UseStepSimulationNetworkOrigin( &v ) ) + { + v = &entity->GetLocalOrigin(); + } + + pOut->m_Vector[ 0 ] = v->x; + pOut->m_Vector[ 1 ] = v->y; +} + +//-------------------------------------------------------------------------------------------------------- +// Used when breaking up origin, note we still have to deal with StepSimulation +//-------------------------------------------------------------------------------------------------------- +void SendProxy_OriginZ( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CBaseEntity *entity = (CBaseEntity*)pStruct; + Assert( entity ); + + const Vector *v; + + if ( !entity->UseStepSimulationNetworkOrigin( &v ) ) + { + v = &entity->GetLocalOrigin(); + } + + pOut->m_Float = v->z; +} + + +void SendProxy_Angles( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CBaseEntity *entity = (CBaseEntity*)pStruct; + Assert( entity ); + + const QAngle *a; + + if ( !entity->UseStepSimulationNetworkAngles( &a ) ) + { + a = &entity->GetLocalAngles(); + } + + pOut->m_Vector[ 0 ] = anglemod( a->x ); + pOut->m_Vector[ 1 ] = anglemod( a->y ); + pOut->m_Vector[ 2 ] = anglemod( a->z ); +} + +// This table encodes the CBaseEntity data. +IMPLEMENT_SERVERCLASS_ST_NOBASE( CBaseEntity, DT_BaseEntity ) + SendPropDataTable( "AnimTimeMustBeFirst", 0, &REFERENCE_SEND_TABLE(DT_AnimTimeMustBeFirst), SendProxy_ClientSideAnimation ), + SendPropInt (SENDINFO(m_flSimulationTime), SIMULATION_TIME_WINDOW_BITS, SPROP_UNSIGNED|SPROP_CHANGES_OFTEN|SPROP_ENCODED_AGAINST_TICKCOUNT, SendProxy_SimulationTime), + +#if PREDICTION_ERROR_CHECK_LEVEL > 1 + SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_NOSCALE|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ), +#else + SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_COORD|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ), +#endif + + SendPropInt (SENDINFO( m_ubInterpolationFrame ), NOINTERP_PARITY_MAX_BITS, SPROP_UNSIGNED ), + SendPropModelIndex(SENDINFO(m_nModelIndex)), + SendPropDataTable( SENDINFO_DT( m_Collision ), &REFERENCE_SEND_TABLE(DT_CollisionProperty) ), + SendPropInt (SENDINFO(m_nRenderFX), 8, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_nRenderMode), 8, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_fEffects), EF_MAX_BITS, SPROP_UNSIGNED), + SendPropInt (SENDINFO(m_clrRender), 32, SPROP_UNSIGNED), +#ifdef MAPBASE + // Keep consistent with VIEW_ID_COUNT in viewrender.h + SendPropInt (SENDINFO(m_iViewHideFlags), 9, SPROP_UNSIGNED ), + SendPropBool (SENDINFO(m_bDisableFlashlight) ), +#endif + SendPropInt (SENDINFO(m_iTeamNum), TEAMNUM_NUM_BITS, 0), + SendPropInt (SENDINFO(m_CollisionGroup), 5, SPROP_UNSIGNED), + SendPropFloat (SENDINFO(m_flElasticity), 0, SPROP_COORD), + SendPropFloat (SENDINFO(m_flShadowCastDistance), 12, SPROP_UNSIGNED ), + SendPropEHandle (SENDINFO(m_hOwnerEntity)), + SendPropEHandle (SENDINFO(m_hEffectEntity)), + SendPropEHandle (SENDINFO_NAME(m_hMoveParent, moveparent)), + SendPropInt (SENDINFO(m_iParentAttachment), NUM_PARENTATTACHMENT_BITS, SPROP_UNSIGNED), + + SendPropStringT( SENDINFO( m_iName ) ), + + SendPropInt (SENDINFO_NAME( m_MoveType, movetype ), MOVETYPE_MAX_BITS, SPROP_UNSIGNED ), + SendPropInt (SENDINFO_NAME( m_MoveCollide, movecollide ), MOVECOLLIDE_MAX_BITS, SPROP_UNSIGNED ), +#if PREDICTION_ERROR_CHECK_LEVEL > 1 + SendPropVector (SENDINFO(m_angRotation), -1, SPROP_NOSCALE|SPROP_CHANGES_OFTEN, 0, HIGH_DEFAULT, SendProxy_Angles ), +#else + SendPropQAngles (SENDINFO(m_angRotation), 13, SPROP_CHANGES_OFTEN, SendProxy_Angles ), +#endif + + SendPropInt ( SENDINFO( m_iTextureFrameIndex ), 8, SPROP_UNSIGNED ), + +#if !defined( NO_ENTITY_PREDICTION ) + SendPropDataTable( "predictable_id", 0, &REFERENCE_SEND_TABLE( DT_PredictableId ), SendProxy_SendPredictableId ), +#endif + + // FIXME: Collapse into another flag field? + SendPropInt (SENDINFO(m_bSimulatedEveryTick), 1, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_bAnimatedEveryTick), 1, SPROP_UNSIGNED ), + SendPropBool( SENDINFO( m_bAlternateSorting )), + +#ifdef TF_DLL + SendPropArray3( SENDINFO_ARRAY3(m_nModelIndexOverrides), SendPropInt( SENDINFO_ARRAY(m_nModelIndexOverrides), SP_MODEL_INDEX_BITS, 0 ) ), +#endif + +END_SEND_TABLE() + + +// dynamic models +class CBaseEntityModelLoadProxy +{ +protected: + class Handler : public IModelLoadCallback + { + public: + explicit Handler( CBaseEntity *pEntity ) : m_pEntity(pEntity) { } + virtual void OnModelLoadComplete( const model_t *pModel ); + CBaseEntity* m_pEntity; + }; + Handler* m_pHandler; + +public: + explicit CBaseEntityModelLoadProxy( CBaseEntity *pEntity ) : m_pHandler( new Handler( pEntity ) ) { } + ~CBaseEntityModelLoadProxy() { delete m_pHandler; } + void Register( int nModelIndex ) const { modelinfo->RegisterModelLoadCallback( nModelIndex, m_pHandler ); } + operator CBaseEntity * () const { return m_pHandler->m_pEntity; } + +private: + CBaseEntityModelLoadProxy( const CBaseEntityModelLoadProxy& ); + CBaseEntityModelLoadProxy& operator=( const CBaseEntityModelLoadProxy& ); +}; + +static CUtlHashtable< CBaseEntityModelLoadProxy, empty_t, PointerHashFunctor, PointerEqualFunctor, CBaseEntity * > sg_DynamicLoadHandlers; + +void CBaseEntityModelLoadProxy::Handler::OnModelLoadComplete( const model_t *pModel ) +{ + m_pEntity->OnModelLoadComplete( pModel ); + sg_DynamicLoadHandlers.Remove( m_pEntity ); // NOTE: destroys *this! +} + + +CBaseEntity::CBaseEntity( bool bServerOnly ) +{ + COMPILE_TIME_ASSERT( MOVETYPE_LAST < (1 << MOVETYPE_MAX_BITS) ); + COMPILE_TIME_ASSERT( MOVECOLLIDE_COUNT < (1 << MOVECOLLIDE_MAX_BITS) ); + +#ifdef _DEBUG + // necessary since in debug, we initialize vectors to NAN for debugging + m_vecAngVelocity.Init(); +// m_vecAbsAngVelocity.Init(); + m_vecViewOffset.Init(); + m_vecBaseVelocity.GetForModify().Init(); + m_vecVelocity.Init(); + m_vecAbsVelocity.Init(); +#endif + + m_bAlternateSorting = false; + m_CollisionGroup = COLLISION_GROUP_NONE; + m_iParentAttachment = 0; + CollisionProp()->Init( this ); + NetworkProp()->Init( this ); + + // NOTE: THIS MUST APPEAR BEFORE ANY SetMoveType() or SetNextThink() calls + AddEFlags( EFL_NO_THINK_FUNCTION | EFL_NO_GAME_PHYSICS_SIMULATION | EFL_USE_PARTITION_WHEN_NOT_SOLID ); + + // clear debug overlays + m_debugOverlays = 0; + m_pTimedOverlay = NULL; + m_pPhysicsObject = NULL; + m_flElasticity = 1.0f; + m_flShadowCastDistance = m_flDesiredShadowCastDistance = 0; + SetRenderColor( 255, 255, 255, 255 ); + m_iTeamNum = m_iInitialTeamNum = TEAM_UNASSIGNED; + m_nLastThinkTick = gpGlobals->tickcount; + m_nSimulationTick = -1; + SetIdentityMatrix( m_rgflCoordinateFrame ); + m_pBlocker = NULL; +#if _DEBUG + m_iCurrentThinkContext = NO_THINK_CONTEXT; +#endif + m_nWaterTouch = m_nSlimeTouch = 0; + + SetSolid( SOLID_NONE ); + ClearSolidFlags(); + + m_nModelIndex = 0; + m_bDynamicModelAllowed = false; + m_bDynamicModelPending = false; + m_bDynamicModelSetBounds = false; + + SetMoveType( MOVETYPE_NONE ); + SetOwnerEntity( NULL ); + SetCheckUntouch( false ); + SetModelIndex( 0 ); + SetModelName( NULL_STRING ); + m_nTransmitStateOwnedCounter = 0; + + SetCollisionBounds( vec3_origin, vec3_origin ); + ClearFlags(); + + SetFriction( 1.0f ); + + if ( bServerOnly ) + { + AddEFlags( EFL_SERVER_ONLY ); + } + NetworkProp()->MarkPVSInformationDirty(); + +#ifndef _XBOX + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Scale up our physics hull and test against the new one +// Input : *pNewCollide - New collision hull +//----------------------------------------------------------------------------- +void CBaseEntity::SetScaledPhysics( IPhysicsObject *pNewObject ) +{ + if ( pNewObject ) + { + AddSolidFlags( FSOLID_CUSTOMBOXTEST | FSOLID_CUSTOMRAYTEST ); + } + else + { + RemoveSolidFlags( FSOLID_CUSTOMBOXTEST | FSOLID_CUSTOMRAYTEST ); + } +} + +extern bool g_bDisableEhandleAccess; + +//----------------------------------------------------------------------------- +// Purpose: See note below +//----------------------------------------------------------------------------- +CBaseEntity::~CBaseEntity( ) +{ + // FIXME: This can't be called from UpdateOnRemove! There's at least one + // case where friction sounds are added between the call to UpdateOnRemove + ~CBaseEntity + PhysCleanupFrictionSounds( this ); + + Assert( !IsDynamicModelIndex( m_nModelIndex ) ); + Verify( !sg_DynamicLoadHandlers.Remove( this ) ); + + // In debug make sure that we don't call delete on an entity without setting + // the disable flag first! + // EHANDLE accessors will check, in debug, for access to entities during destruction of + // another entity. + // That kind of operation should only occur in UpdateOnRemove calls + // Deletion should only occur via UTIL_Remove(Immediate) calls, not via naked delete calls + Assert( g_bDisableEhandleAccess ); + + VPhysicsDestroyObject(); + + // Need to remove references to this entity before EHANDLES go null + { + g_bDisableEhandleAccess = false; + CBaseEntity::PhysicsRemoveTouchedList( this ); + CBaseEntity::PhysicsRemoveGroundList( this ); + SetGroundEntity( NULL ); // remove us from the ground entity if we are on it + DestroyAllDataObjects(); + g_bDisableEhandleAccess = true; + + // Remove this entity from the ent list (NOTE: This Makes EHANDLES go NULL) + gEntList.RemoveEntity( GetRefEHandle() ); + } +} + +void CBaseEntity::PostConstructor( const char *szClassname ) +{ + if ( szClassname ) + { + SetClassname(szClassname); + } + + Assert( m_iClassname != NULL_STRING && STRING(m_iClassname) != NULL ); + + // Possibly get an edict, and add self to global list of entites. + if ( IsEFlagSet( EFL_SERVER_ONLY ) ) + { + gEntList.AddNonNetworkableEntity( this ); + } + else + { + // Certain entities set up their edicts in the constructor + if ( !IsEFlagSet( EFL_NO_AUTO_EDICT_ATTACH ) ) + { + NetworkProp()->AttachEdict( g_pForceAttachEdict ); + g_pForceAttachEdict = NULL; + } + + // Some ents like the player override the AttachEdict function and do it at a different time. + // While precaching, they don't ever have an edict, so we don't need to add them to + // the entity list in that case. + if ( edict() ) + { + gEntList.AddNetworkableEntity( this, entindex() ); + + // Cache our IServerNetworkable pointer for the engine for fast access. + if ( edict() ) + edict()->m_pNetworkable = NetworkProp(); + } + } + + CheckHasThinkFunction( false ); + CheckHasGamePhysicsSimulation(); +} + +//----------------------------------------------------------------------------- +// Purpose: Called after player becomes active in the game +//----------------------------------------------------------------------------- +void CBaseEntity::PostClientActive( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Verifies that this entity's data description is valid in debug builds. +//----------------------------------------------------------------------------- +#ifdef _DEBUG +typedef CUtlVector< const char * > KeyValueNameList_t; + +static void AddDataMapFieldNamesToList( KeyValueNameList_t &list, datamap_t *pDataMap ) +{ + while (pDataMap != NULL) + { + for (int i = 0; i < pDataMap->dataNumFields; i++) + { + typedescription_t *pField = &pDataMap->dataDesc[i]; + + if (pField->fieldType == FIELD_EMBEDDED) + { + AddDataMapFieldNamesToList( list, pField->td ); + continue; + } + + if (pField->flags & FTYPEDESC_KEY) + { + list.AddToTail( pField->externalName ); + } + } + + pDataMap = pDataMap->baseMap; + } +} + +void CBaseEntity::ValidateDataDescription(void) +{ + // Multiple key fields that have the same name are not allowed - it creates an + // ambiguity when trying to parse keyvalues and outputs. + datamap_t *pDataMap = GetDataDescMap(); + if ((pDataMap == NULL) || pDataMap->bValidityChecked) + return; + + pDataMap->bValidityChecked = true; + + // Let's generate a list of all keyvalue strings in the entire hierarchy... + KeyValueNameList_t names(128); + AddDataMapFieldNamesToList( names, pDataMap ); + + for (int i = names.Count(); --i > 0; ) + { + for (int j = i - 1; --j >= 0; ) + { + if (!Q_stricmp(names[i], names[j])) + { + DevMsg( "%s has multiple data description entries for \"%s\"\n", STRING(m_iClassname), names[i]); + break; + } + } + } +} +#endif // _DEBUG + + +//----------------------------------------------------------------------------- +// Sets the collision bounds + the size +//----------------------------------------------------------------------------- +void CBaseEntity::SetCollisionBounds( const Vector& mins, const Vector &maxs ) +{ + m_Collision.SetCollisionBounds( mins, maxs ); +} + + +void CBaseEntity::StopFollowingEntity( ) +{ + if( !IsFollowingEntity() ) + { +// Assert( IsEffectActive( EF_BONEMERGE ) == 0 ); + return; + } + + SetParent( NULL ); + RemoveEffects( EF_BONEMERGE ); + RemoveSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); + CollisionRulesChanged(); +} + +bool CBaseEntity::IsFollowingEntity() +{ + return IsEffectActive( EF_BONEMERGE ) && (GetMoveType() == MOVETYPE_NONE) && GetMoveParent(); +} + +CBaseEntity *CBaseEntity::GetFollowedEntity() +{ + if (!IsFollowingEntity()) + return NULL; + return GetMoveParent(); +} + +void CBaseEntity::SetClassname( const char *className ) +{ + m_iClassname = AllocPooledString( className ); +} + +void CBaseEntity::SetModelIndex( int index ) +{ + if ( IsDynamicModelIndex( index ) && !(GetBaseAnimating() && m_bDynamicModelAllowed) ) + { + AssertMsg( false, "dynamic model support not enabled on server entity" ); + index = -1; + } + + if ( index != m_nModelIndex ) + { + if ( m_bDynamicModelPending ) + { + sg_DynamicLoadHandlers.Remove( this ); + } + + modelinfo->ReleaseDynamicModel( m_nModelIndex ); + modelinfo->AddRefDynamicModel( index ); + m_nModelIndex = index; + + m_bDynamicModelSetBounds = false; + + if ( IsDynamicModelIndex( index ) ) + { + m_bDynamicModelPending = true; + sg_DynamicLoadHandlers[ sg_DynamicLoadHandlers.Insert( this ) ].Register( index ); + } + else + { + m_bDynamicModelPending = false; + OnNewModel(); + } + } + DispatchUpdateTransmitState(); +} + +void CBaseEntity::ClearModelIndexOverrides( void ) +{ +#ifdef TF_DLL + for ( int index = 0 ; index < MAX_VISION_MODES ; index++ ) + { + m_nModelIndexOverrides.Set( index, 0 ); + } +#endif +} + +void CBaseEntity::SetModelIndexOverride( int index, int nValue ) +{ +#ifdef TF_DLL + if ( ( index >= VISION_MODE_NONE ) && ( index < MAX_VISION_MODES ) ) + { + if ( nValue != m_nModelIndexOverrides[index] ) + { + m_nModelIndexOverrides.Set( index, nValue ); + } + } +#endif +} + +// position to shoot at +Vector CBaseEntity::BodyTarget( const Vector &posSrc, bool bNoisy) +{ + return WorldSpaceCenter( ); +} + +// return the position of my head. someone's trying to attack it. +Vector CBaseEntity::HeadTarget( const Vector &posSrc ) +{ + return EyePosition(); +} + + +struct TimedOverlay_t +{ + char *msg; + int msgEndTime; + int msgStartTime; + TimedOverlay_t *pNextTimedOverlay; +}; + +//----------------------------------------------------------------------------- +// Purpose: Display an error message on the entity +// Input : +// Output : +//----------------------------------------------------------------------------- +void CBaseEntity::AddTimedOverlay( const char *msg, int endTime ) +{ + TimedOverlay_t *pNewTO = new TimedOverlay_t; + int len = strlen(msg); + pNewTO->msg = new char[len + 1]; + Q_strncpy(pNewTO->msg,msg, len+1); + pNewTO->msgEndTime = gpGlobals->curtime + endTime; + pNewTO->msgStartTime = gpGlobals->curtime; + pNewTO->pNextTimedOverlay = m_pTimedOverlay; + m_pTimedOverlay = pNewTO; +} + +//----------------------------------------------------------------------------- +// Purpose: Send debug overlay box to the client +// Input : +// Output : +//----------------------------------------------------------------------------- +void CBaseEntity::DrawBBoxOverlay( float flDuration ) +{ + if (edict()) + { + NDebugOverlay::EntityBounds(this, 255, 100, 0, 0, flDuration ); + + if ( CollisionProp()->IsSolidFlagSet( FSOLID_USE_TRIGGER_BOUNDS ) ) + { + Vector vecTriggerMins, vecTriggerMaxs; + CollisionProp()->WorldSpaceTriggerBounds( &vecTriggerMins, &vecTriggerMaxs ); + Vector center = 0.5f * (vecTriggerMins + vecTriggerMaxs); + Vector extents = vecTriggerMaxs - center; + NDebugOverlay::Box(center, -extents, extents, 0, 255, 255, 0, flDuration ); + } + } +} + + +void CBaseEntity::DrawAbsBoxOverlay() +{ + int red = 0; + int green = 200; + + if ( VPhysicsGetObject() && VPhysicsGetObject()->IsAsleep() ) + { + red = 90; + green = 120; + } + + if (edict()) + { + // Surrounding boxes are axially aligned, so ignore angles + Vector vecSurroundMins, vecSurroundMaxs; + CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); + Vector center = 0.5f * (vecSurroundMins + vecSurroundMaxs); + Vector extents = vecSurroundMaxs - center; + NDebugOverlay::Box(center, -extents, extents, red, green, 0, 0 ,0); + } +} + +void CBaseEntity::DrawRBoxOverlay() +{ + +} + +//----------------------------------------------------------------------------- +// Purpose: Draws an axis overlay at the origin and angles of the entity +//----------------------------------------------------------------------------- +void CBaseEntity::SendDebugPivotOverlay( void ) +{ + if ( edict() ) + { + NDebugOverlay::Axis( GetAbsOrigin(), GetAbsAngles(), 20, true, 0 ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : Add new entity positioned overlay text +// Input : How many lines to offset text from origin +// The text to print +// How long to display text +// The color of the text +// Output : +//------------------------------------------------------------------------------ +void CBaseEntity::EntityText( int text_offset, const char *text, float duration, int r, int g, int b, int a ) +{ + Vector origin; + Vector vecLocalCenter; + + VectorAdd( m_Collision.OBBMins(), m_Collision.OBBMaxs(), vecLocalCenter ); + vecLocalCenter *= 0.5f; + + if ( ( m_Collision.GetCollisionAngles() == vec3_angle ) || ( vecLocalCenter == vec3_origin ) ) + { + VectorAdd( vecLocalCenter, m_Collision.GetCollisionOrigin(), origin ); + } + else + { + VectorTransform( vecLocalCenter, m_Collision.CollisionToWorldTransform(), origin ); + } + + NDebugOverlay::EntityTextAtPosition( origin, text_offset, text, duration, r, g, b, a ); +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseEntity::DrawTimedOverlays(void) +{ + // Draw name first if I have an overlay or am in message mode + if ((m_debugOverlays & OVERLAY_MESSAGE_BIT)) + { + char tempstr[512]; + Q_snprintf( tempstr, sizeof( tempstr ), "[%s]", GetDebugName() ); + EntityText(0,tempstr, 0); + } + + // Now draw overlays + TimedOverlay_t* pTO = m_pTimedOverlay; + TimedOverlay_t* pNextTO = NULL; + TimedOverlay_t* pLastTO = NULL; + int nCount = 1; // Offset by one + while (pTO) + { + pNextTO = pTO->pNextTimedOverlay; + + // Remove old messages unless messages are paused + if ((!CBaseEntity::Debug_IsPaused() && gpGlobals->curtime > pTO->msgEndTime) || + (nCount > 10)) + { + if (pLastTO) + { + pLastTO->pNextTimedOverlay = pNextTO; + } + else + { + m_pTimedOverlay = pNextTO; + } + + delete pTO->msg; + delete pTO; + } + else + { + int nAlpha = 0; + + // If messages aren't paused fade out + if (!CBaseEntity::Debug_IsPaused()) + { + nAlpha = 255*((gpGlobals->curtime - pTO->msgStartTime)/(pTO->msgEndTime - pTO->msgStartTime)); + } + int r = 185; + int g = 145; + int b = 145; + + // Brighter when new message + if (nAlpha < 50) + { + r = 255; + g = 205; + b = 205; + } + if (nAlpha < 0) nAlpha = 0; + EntityText(nCount,pTO->msg, 0.0, r, g, b, 255-nAlpha); + nCount++; + + pLastTO = pTO; + } + pTO = pNextTO; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draw all overlays (should be implemented by subclass to add +// any additional non-text overlays) +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +void CBaseEntity::DrawDebugGeometryOverlays(void) +{ + DrawTimedOverlays(); + DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_NAME_BIT) + { + EntityText(0,GetDebugName(), 0); + } + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + DrawBBoxOverlay(); + } + if (m_debugOverlays & OVERLAY_ABSBOX_BIT ) + { + DrawAbsBoxOverlay(); + } + if (m_debugOverlays & OVERLAY_PIVOT_BIT) + { + SendDebugPivotOverlay(); + } + if( m_debugOverlays & OVERLAY_RBOX_BIT ) + { + DrawRBoxOverlay(); + } + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT) ) + { + // draw mass center + if ( VPhysicsGetObject() ) + { + Vector massCenter = VPhysicsGetObject()->GetMassCenterLocalSpace(); + Vector worldPos; + VPhysicsGetObject()->LocalToWorld( &worldPos, massCenter ); + NDebugOverlay::Cross3D( worldPos, 12, 255, 0, 0, false, 0 ); + DebugDrawContactPoints(VPhysicsGetObject()); + if ( GetMoveType() != MOVETYPE_VPHYSICS ) + { + Vector pos; + QAngle angles; + VPhysicsGetObject()->GetPosition( &pos, &angles ); + float dist = (pos - GetAbsOrigin()).Length(); + + Vector axis; + float deltaAngle; + RotationDeltaAxisAngle( angles, GetAbsAngles(), axis, deltaAngle ); + if ( dist > 2 || fabsf(deltaAngle) > 2 ) + { + Vector mins, maxs; + physcollision->CollideGetAABB( &mins, &maxs, VPhysicsGetObject()->GetCollide(), vec3_origin, vec3_angle ); + NDebugOverlay::BoxAngles( pos, mins, maxs, angles, 255, 255, 0, 16, 0 ); + } + } + } + } + if ( m_debugOverlays & OVERLAY_SHOW_BLOCKSLOS ) + { + if ( BlocksLOS() ) + { + NDebugOverlay::EntityBounds(this, 255, 255, 255, 0, 0 ); + } + } + if ( m_debugOverlays & OVERLAY_AUTOAIM_BIT && (GetFlags()&FL_AIMTARGET) && AI_GetSinglePlayer() != NULL ) + { + // Crude, but it gets the point across. + Vector vecCenter = GetAutoAimCenter(); + Vector vecRight, vecUp, vecDiag; + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + float radius = GetAutoAimRadius(); + + QAngle angles = pPlayer->EyeAngles(); + AngleVectors( angles, NULL, &vecRight, &vecUp ); + + int r,g,b; + + if( ((int)gpGlobals->curtime) % 2 == 1 ) + { + r = 255; + g = 255; + b = 255; + + if( pPlayer->GetActiveWeapon() != NULL ) + radius *= pPlayer->GetActiveWeapon()->WeaponAutoAimScale(); + + } + else + { + r = 255;g=0;b=0; + + if( !ShouldAttractAutoAim(pPlayer) ) + { + g = 255; + } + } + + if( pPlayer->IsInAVehicle() ) + { + radius *= sv_vehicle_autoaim_scale.GetFloat(); + } + + NDebugOverlay::Line( vecCenter, vecCenter + vecRight * radius, r, g, b, true, 0.1 ); + NDebugOverlay::Line( vecCenter, vecCenter - vecRight * radius, r, g, b, true, 0.1 ); + NDebugOverlay::Line( vecCenter, vecCenter + vecUp * radius, r, g, b, true, 0.1 ); + NDebugOverlay::Line( vecCenter, vecCenter - vecUp * radius, r, g, b, true, 0.1 ); + + vecDiag = vecRight + vecUp; + VectorNormalize( vecDiag ); + NDebugOverlay::Line( vecCenter - vecDiag * radius, vecCenter + vecDiag * radius, r, g, b, true, 0.1 ); + + vecDiag = vecRight - vecUp; + VectorNormalize( vecDiag ); + NDebugOverlay::Line( vecCenter - vecDiag * radius, vecCenter + vecDiag * radius, r, g, b, true, 0.1 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any text overlays (override in subclass to add additional text) +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CBaseEntity::DrawDebugTextOverlays(void) +{ + int offset = 1; + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf( tempstr, sizeof(tempstr), "(%d) Name: %s (%s)", entindex(), GetDebugName(), GetClassname() ); + EntityText(offset,tempstr, 0); + offset++; + + if( m_iGlobalname != NULL_STRING ) + { + Q_snprintf( tempstr, sizeof(tempstr), "GLOBALNAME: %s", STRING(m_iGlobalname) ); + EntityText(offset,tempstr, 0); + offset++; + } + + Vector vecOrigin = GetAbsOrigin(); + Q_snprintf( tempstr, sizeof(tempstr), "Position: %0.1f, %0.1f, %0.1f\n", vecOrigin.x, vecOrigin.y, vecOrigin.z ); + EntityText( offset, tempstr, 0 ); + offset++; + + if( GetModelName() != NULL_STRING || GetBaseAnimating() ) + { + Q_snprintf(tempstr, sizeof(tempstr), "Model:%s", STRING(GetModelName()) ); + EntityText(offset,tempstr,0); + offset++; + } + + if( m_hDamageFilter.Get() != NULL ) + { + Q_snprintf( tempstr, sizeof(tempstr), "DAMAGE FILTER:%s", m_hDamageFilter->GetDebugName() ); + EntityText( offset,tempstr,0 ); + offset++; + } + +#ifdef MAPBASE + if (m_ResponseContexts.Count() > 0) + { + const char *contexts = UTIL_VarArgs("%s:%s", STRING(m_ResponseContexts[0].m_iszName), STRING(m_ResponseContexts[0].m_iszValue)); + for (int i = 1; i < GetContextCount(); i++) + { + contexts = UTIL_VarArgs("%s,%s:%s", contexts, STRING(m_ResponseContexts[i].m_iszName), STRING(m_ResponseContexts[i].m_iszValue)); + } + Q_snprintf(tempstr, sizeof(tempstr), "Response Contexts:%s", contexts); + EntityText(offset, tempstr, 0); + offset++; + } +#endif + } + + if (m_debugOverlays & OVERLAY_VIEWOFFSET) + { + NDebugOverlay::Cross3D( EyePosition(), 16, 255, 0, 0, true, 0.05f ); + } + + return offset; +} + + +void CBaseEntity::SetParent( string_t newParent, CBaseEntity *pActivator, int iAttachment ) +{ + // find and notify the new parent +#ifdef MAPBASE + CBaseEntity *pParent = gEntList.FindEntityByName( NULL, newParent, this, pActivator ); +#else + CBaseEntity *pParent = gEntList.FindEntityByName( NULL, newParent, NULL, pActivator ); +#endif + + // debug check + if ( newParent != NULL_STRING && pParent == NULL ) + { + Msg( "Entity %s(%s) has bad parent %s\n", STRING(m_iClassname), GetDebugName(), STRING(newParent) ); + } + else + { + // make sure there isn't any ambiguity +#ifdef MAPBASE + if ( gEntList.FindEntityByName( pParent, newParent, this, pActivator ) ) +#else + if ( gEntList.FindEntityByName( pParent, newParent, NULL, pActivator ) ) +#endif + { + Msg( "Entity %s(%s) has ambigious parent %s\n", STRING(m_iClassname), GetDebugName(), STRING(newParent) ); + } + SetParent( pParent, iAttachment ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Move our points from parent to worldspace +// Input : *pParent - Parent to use as reference +//----------------------------------------------------------------------------- +void CBaseEntity::TransformStepData_ParentToWorld( CBaseEntity *pParent ) +{ + // Fix up our step simulation points to be in the proper local space + StepSimulationData *step = (StepSimulationData *) GetDataObject( STEPSIMULATION ); + if ( step != NULL ) + { + // Convert our positions + UTIL_ParentToWorldSpace( pParent, step->m_Previous2.vecOrigin, step->m_Previous2.qRotation ); + UTIL_ParentToWorldSpace( pParent, step->m_Previous.vecOrigin, step->m_Previous.qRotation ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Move step data between two parent-spaces +// Input : *pOldParent - parent we were attached to +// *pNewParent - parent we're now attached to +//----------------------------------------------------------------------------- +void CBaseEntity::TransformStepData_ParentToParent( CBaseEntity *pOldParent, CBaseEntity *pNewParent ) +{ + // Fix up our step simulation points to be in the proper local space + StepSimulationData *step = (StepSimulationData *) GetDataObject( STEPSIMULATION ); + if ( step != NULL ) + { + // Convert our positions + UTIL_ParentToWorldSpace( pOldParent, step->m_Previous2.vecOrigin, step->m_Previous2.qRotation ); + UTIL_WorldToParentSpace( pNewParent, step->m_Previous2.vecOrigin, step->m_Previous2.qRotation ); + + UTIL_ParentToWorldSpace( pOldParent, step->m_Previous.vecOrigin, step->m_Previous.qRotation ); + UTIL_WorldToParentSpace( pNewParent, step->m_Previous.vecOrigin, step->m_Previous.qRotation ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: After parenting to an object, we need to also correctly translate our +// step stimulation positions and angles into that parent space. Otherwise +// we end up splining between two different world spaces. +//----------------------------------------------------------------------------- +void CBaseEntity::TransformStepData_WorldToParent( CBaseEntity *pParent ) +{ + // Fix up our step simulation points to be in the proper local space + StepSimulationData *step = (StepSimulationData *) GetDataObject( STEPSIMULATION ); + if ( step != NULL ) + { + // Convert our positions + UTIL_WorldToParentSpace( pParent, step->m_Previous2.vecOrigin, step->m_Previous2.qRotation ); + UTIL_WorldToParentSpace( pParent, step->m_Previous.vecOrigin, step->m_Previous.qRotation ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the movement parent of this entity. This entity will be moved +// to a local coordinate calculated from its current absolute offset +// from the parent entity and will then follow the parent entity. +// Input : pParentEntity - This entity's new parent in the movement hierarchy. +//----------------------------------------------------------------------------- +void CBaseEntity::SetParent( CBaseEntity *pParentEntity, int iAttachment ) +{ + // If they didn't specify an attachment, use our current + if ( iAttachment == -1 ) + { + iAttachment = m_iParentAttachment; + } + + bool bWasNotParented = ( GetParent() == NULL ); + CBaseEntity *pOldParent = m_pParent; + + // notify the old parent of the loss + UnlinkFromParent( this ); + + // set the new name + m_pParent = pParentEntity; + + if ( m_pParent == this ) + { + // should never set parent to 'this' - makes no sense + Assert(0); + m_pParent = NULL; + } + + if ( m_pParent == NULL ) + { + m_iParent = NULL_STRING; + + // Transform step data from parent to worldspace + TransformStepData_ParentToWorld( pOldParent ); + return; + } + + m_iParent = m_pParent->m_iName; + + RemoveSolidFlags( FSOLID_ROOT_PARENT_ALIGNED ); + if ( pParentEntity ) + { + if ( const_cast(pParentEntity)->GetRootMoveParent()->GetSolid() == SOLID_BSP ) + { + AddSolidFlags( FSOLID_ROOT_PARENT_ALIGNED ); + } + else + { + if ( GetSolid() == SOLID_BSP ) + { + // Must be SOLID_VPHYSICS because parent might rotate + SetSolid( SOLID_VPHYSICS ); + } + } + } + // set the move parent if we have one + if ( edict() ) + { + // add ourselves to the list + LinkChild( m_pParent, this ); + + m_iParentAttachment = (char)iAttachment; + + EntityMatrix matrix, childMatrix; + matrix.InitFromEntity( const_cast(pParentEntity), m_iParentAttachment ); // parent->world + childMatrix.InitFromEntityLocal( this ); // child->world + Vector localOrigin = matrix.WorldToLocal( GetLocalOrigin() ); + + // I have the axes of local space in world space. (childMatrix) + // I want to compute those world space axes in the parent's local space + // and set that transform (as angles) on the child's object so the net + // result is that the child is now in parent space, but still oriented the same way + VMatrix tmp = matrix.Transpose(); // world->parent + tmp.MatrixMul( childMatrix, matrix ); // child->parent + QAngle angles; + MatrixToAngles( matrix, angles ); + SetLocalAngles( angles ); + UTIL_SetOrigin( this, localOrigin ); + + // Move our step data into the correct space + if ( bWasNotParented ) + { + // Transform step data from world to parent-space + TransformStepData_WorldToParent( this ); + } + else + { + // Transform step data between parent-spaces + TransformStepData_ParentToParent( pOldParent, this ); + } + } + if ( VPhysicsGetObject() ) + { + if ( VPhysicsGetObject()->IsStatic()) + { + if ( VPhysicsGetObject()->IsAttachedToConstraint(false) ) + { + Warning("SetParent on static object, all constraints attached to %s (%s)will now be broken!\n", GetDebugName(), GetClassname() ); + } + VPhysicsDestroyObject(); + VPhysicsInitShadow(false, false); + } + } + CollisionRulesChanged(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseEntity::ValidateEntityConnections() +{ + if ( m_target == NULL_STRING ) + return; + + if ( ClassMatches( "scripted_*" ) || + ClassMatches( "trigger_relay" ) || + ClassMatches( "trigger_auto" ) || + ClassMatches( "path_*" ) || + ClassMatches( "monster_*" ) || + ClassMatches( "trigger_teleport" ) || + ClassMatches( "func_train" ) || + ClassMatches( "func_tracktrain" ) || + ClassMatches( "func_plat*" ) || + ClassMatches( "npc_*" ) || + ClassMatches( "info_big*" ) || + ClassMatches( "env_texturetoggle" ) || + ClassMatches( "env_render" ) || + ClassMatches( "func_areaportalwindow") || + ClassMatches( "point_view*") || + ClassMatches( "func_traincontrols" ) || + ClassMatches( "multisource" ) || + ClassMatches( "xen_plant*" ) ) + return; + + datamap_t *dmap = GetDataDescMap(); + while ( dmap ) + { + int fields = dmap->dataNumFields; + for ( int i = 0; i < fields; i++ ) + { + typedescription_t *dataDesc = &dmap->dataDesc[i]; + if ( ( dataDesc->fieldType == FIELD_CUSTOM ) && ( dataDesc->flags & FTYPEDESC_OUTPUT ) ) + { + CBaseEntityOutput *pOutput = (CBaseEntityOutput *)((int)this + (int)dataDesc->fieldOffset[0]); + if ( pOutput->NumberOfElements() ) + return; + } + } + + dmap = dmap->baseMap; + } + + Vector vecLoc = WorldSpaceCenter(); + Warning("---------------------------------\n"); + Warning( "Entity %s - (%s) has a target and NO OUTPUTS\n", GetDebugName(), GetClassname() ); + Warning( "Location %f %f %f\n", vecLoc.x, vecLoc.y, vecLoc.z ); + Warning("---------------------------------\n"); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseEntity::FireNamedOutput( const char *pszOutput, variant_t variant, CBaseEntity *pActivator, CBaseEntity *pCaller, float flDelay ) +{ + if ( pszOutput == NULL ) + return; + + CBaseEntityOutput *pOutput = FindNamedOutput( pszOutput ); + if ( pOutput ) + { + pOutput->FireOutput( variant, pActivator, pCaller, flDelay ); + return; + } +} + +#ifdef MAPBASE_VSCRIPT +void CBaseEntity::ScriptFireOutput( const char *pszOutput, HSCRIPT hActivator, HSCRIPT hCaller, const char *szValue, float flDelay ) +{ + variant_t value; + value.SetString( MAKE_STRING(szValue) ); + + FireNamedOutput( pszOutput, value, ToEnt(hActivator), ToEnt(hCaller), flDelay ); +} + +float CBaseEntity::GetMaxOutputDelay( const char *pszOutput ) +{ + CBaseEntityOutput *pOutput = FindNamedOutput( pszOutput ); + if ( pOutput ) + { + return pOutput->GetMaxDelay(); + } + return 0; +} + +void CBaseEntity::CancelEventsByInput( const char *szInput ) +{ + g_EventQueue.CancelEventsByInput( this, szInput ); +} +#endif // MAPBASE_VSCRIPT + +CBaseEntityOutput *CBaseEntity::FindNamedOutput( const char *pszOutput ) +{ + if ( pszOutput == NULL ) + return NULL; + + datamap_t *dmap = GetDataDescMap(); + while ( dmap ) + { + int fields = dmap->dataNumFields; + for ( int i = 0; i < fields; i++ ) + { + typedescription_t *dataDesc = &dmap->dataDesc[i]; + if ( ( dataDesc->fieldType == FIELD_CUSTOM ) && ( dataDesc->flags & FTYPEDESC_OUTPUT ) ) + { + CBaseEntityOutput *pOutput = ( CBaseEntityOutput * )( ( int )this + ( int )dataDesc->fieldOffset[0] ); + if ( !Q_stricmp( dataDesc->externalName, pszOutput ) ) + { + return pOutput; + } + } + } + dmap = dmap->baseMap; + } + return NULL; +} + +void CBaseEntity::Activate( void ) +{ +#ifdef DEBUG + extern bool g_bCheckForChainedActivate; + extern bool g_bReceivedChainedActivate; + + if ( g_bCheckForChainedActivate && g_bReceivedChainedActivate ) + { + Assert( !"Multiple calls to base class Activate()\n" ); + } + g_bReceivedChainedActivate = true; +#endif + + // NOTE: This forces a team change so that stuff in the level + // that starts out on a team correctly changes team + if (m_iInitialTeamNum) + { + ChangeTeam( m_iInitialTeamNum ); + } + + // Get a handle to my damage filter entity if there is one. + if ( m_iszDamageFilterName != NULL_STRING ) + { + m_hDamageFilter = gEntList.FindEntityByName( NULL, m_iszDamageFilterName ); + } + + // Add any non-null context strings to our context vector + if ( m_iszResponseContext != NULL_STRING ) + { + AddContext( m_iszResponseContext.ToCStr() ); + } + +#ifdef HL1_DLL + ValidateEntityConnections(); +#endif //HL1_DLL +} + +//////////////////////////// old CBaseEntity stuff /////////////////////////////////// + + +// give health. +// Returns the amount of health actually taken. +int CBaseEntity::TakeHealth( float flHealth, int bitsDamageType ) +{ + if ( !edict() || m_takedamage < DAMAGE_YES ) + return 0; + + int iMax = GetMaxHealth(); + +// heal + if ( m_iHealth >= iMax ) + return 0; + + const int oldHealth = m_iHealth; + + m_iHealth += flHealth; + + if (m_iHealth > iMax) + m_iHealth = iMax; + + return m_iHealth - oldHealth; +} + +// inflict damage on this entity. bitsDamageType indicates type of damage inflicted, ie: DMG_CRUSH + +int CBaseEntity::OnTakeDamage( const CTakeDamageInfo &info ) +{ + Vector vecTemp; + + if ( !edict() || !m_takedamage ) + return 0; + + if ( info.GetInflictor() ) + { + vecTemp = info.GetInflictor()->WorldSpaceCenter() - ( WorldSpaceCenter() ); + } + else + { + vecTemp.Init( 1, 0, 0 ); + } + + // this global is still used for glass and other non-NPC killables, along with decals. + g_vecAttackDir = vecTemp; + VectorNormalize(g_vecAttackDir); + + // save damage based on the target's armor level + + // figure momentum add (don't let hurt brushes or other triggers move player) + + // physics objects have their own calcs for this: (don't let fire move things around!) + if ( !IsEFlagSet( EFL_NO_DAMAGE_FORCES ) ) + { + if ( ( GetMoveType() == MOVETYPE_VPHYSICS ) ) + { + VPhysicsTakeDamage( info ); + } + else + { + if ( info.GetInflictor() && (GetMoveType() == MOVETYPE_WALK || GetMoveType() == MOVETYPE_STEP) && + !info.GetAttacker()->IsSolidFlagSet(FSOLID_TRIGGER) ) + { + Vector vecDir, vecInflictorCentroid; + vecDir = WorldSpaceCenter( ); + vecInflictorCentroid = info.GetInflictor()->WorldSpaceCenter( ); + vecDir -= vecInflictorCentroid; + VectorNormalize( vecDir ); + + float flForce = info.GetDamage() * ((32 * 32 * 72.0) / (WorldAlignSize().x * WorldAlignSize().y * WorldAlignSize().z)) * 5; + + if (flForce > 1000.0) + flForce = 1000.0; + ApplyAbsVelocityImpulse( vecDir * flForce ); + } + } + } + + if ( m_takedamage != DAMAGE_EVENTS_ONLY ) + { + // do the damage + m_iHealth -= info.GetDamage(); + if (m_iHealth <= 0) + { + Event_Killed( info ); + return 0; + } + } + + return 1; +} + +//----------------------------------------------------------------------------- +// Purpose: Scale damage done and call OnTakeDamage +//----------------------------------------------------------------------------- +void CBaseEntity::TakeDamage( const CTakeDamageInfo &inputInfo ) +{ + if ( !g_pGameRules ) + return; + + bool bHasPhysicsForceDamage = !g_pGameRules->Damage_NoPhysicsForce( inputInfo.GetDamageType() ); + if ( bHasPhysicsForceDamage && inputInfo.GetDamageType() != DMG_GENERIC ) + { + // If you hit this assert, you've called TakeDamage with a damage type that requires a physics damage + // force & position without specifying one or both of them. Decide whether your damage that's causing + // this is something you believe should impart physics force on the receiver. If it is, you need to + // setup the damage force & position inside the CTakeDamageInfo (Utility functions for this are in + // takedamageinfo.cpp. If you think the damage shouldn't cause force (unlikely!) then you can set the + // damage type to DMG_GENERIC, or | DMG_CRUSH if you need to preserve the damage type for purposes of HUD display. + + if ( inputInfo.GetDamageForce() == vec3_origin || inputInfo.GetDamagePosition() == vec3_origin ) + { + static int warningCount = 0; + if ( ++warningCount < 10 ) + { + if ( inputInfo.GetDamageForce() == vec3_origin ) + { + DevWarning( "CBaseEntity::TakeDamage: with inputInfo.GetDamageForce() == vec3_origin\n" ); + } + if ( inputInfo.GetDamagePosition() == vec3_origin ) + { + DevWarning( "CBaseEntity::TakeDamage: with inputInfo.GetDamagePosition() == vec3_origin\n" ); + } + } + } + } + +#ifndef MAPBASE // Moved below the gamerules AllowDamage() check + // Make sure our damage filter allows the damage. + if ( !PassesDamageFilter( inputInfo )) + { + return; + } +#endif + + if( !g_pGameRules->AllowDamage(this, inputInfo) ) + { + return; + } + +#ifdef MAPBASE + // Make sure our damage filter allows the damage. + if ( !PassesFinalDamageFilter( inputInfo )) + { + return; + } +#endif + + if ( PhysIsInCallback() ) + { + PhysCallbackDamage( this, inputInfo ); + } + else + { + CTakeDamageInfo info = inputInfo; + + // Scale the damage by the attacker's modifier. + if ( info.GetAttacker() ) + { + info.ScaleDamage( info.GetAttacker()->GetAttackDamageScale( this ) ); + } + + // Scale the damage by my own modifiers + info.ScaleDamage( GetReceivedDamageScale( info.GetAttacker() ) ); + + //Msg("%s took %.2f Damage, at %.2f\n", GetClassname(), info.GetDamage(), gpGlobals->curtime ); + +#ifdef MAPBASE + // Modify damage if we have a filter that does that + DamageFilterDamageMod(info); +#endif + + OnTakeDamage( info ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a value that scales all damage done by this entity. +//----------------------------------------------------------------------------- +float CBaseEntity::GetAttackDamageScale( CBaseEntity *pVictim ) +{ + float flScale = 1; + FOR_EACH_LL( m_DamageModifiers, i ) + { + if ( !m_DamageModifiers[i]->IsDamageDoneToMe() ) + { + flScale *= m_DamageModifiers[i]->GetModifier(); + } + } + return flScale; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a value that scales all damage done to this entity +//----------------------------------------------------------------------------- +float CBaseEntity::GetReceivedDamageScale( CBaseEntity *pAttacker ) +{ + float flScale = 1; + FOR_EACH_LL( m_DamageModifiers, i ) + { + if ( m_DamageModifiers[i]->IsDamageDoneToMe() ) + { + flScale *= m_DamageModifiers[i]->GetModifier(); + } + } + return flScale; +} + + +//----------------------------------------------------------------------------- +// Purpose: Applies forces to our physics object in response to damage. +//----------------------------------------------------------------------------- +int CBaseEntity::VPhysicsTakeDamage( const CTakeDamageInfo &info ) +{ + // don't let physics impacts or fire cause objects to move (again) + bool bNoPhysicsForceDamage = g_pGameRules->Damage_NoPhysicsForce( info.GetDamageType() ); + if ( bNoPhysicsForceDamage || info.GetDamageType() == DMG_GENERIC ) + return 1; + + Assert(VPhysicsGetObject() != NULL); + if ( VPhysicsGetObject() ) + { + Vector force = info.GetDamageForce(); + Vector offset = info.GetDamagePosition(); + + // If you hit this assert, you've called TakeDamage with a damage type that requires a physics damage + // force & position without specifying one or both of them. Decide whether your damage that's causing + // this is something you believe should impart physics force on the receiver. If it is, you need to + // setup the damage force & position inside the CTakeDamageInfo (Utility functions for this are in + // takedamageinfo.cpp. If you think the damage shouldn't cause force (unlikely!) then you can set the + // damage type to DMG_GENERIC, or | DMG_CRUSH if you need to preserve the damage type for purposes of HUD display. +#if !defined( TF_DLL ) + Assert( force != vec3_origin && offset != vec3_origin ); +#else + // this was spamming the console for Payload maps in TF (trigger_hurt entity on the front of the cart) + if ( !TFGameRules() || TFGameRules()->GetGameType() != TF_GAMETYPE_ESCORT ) + { + Assert( force != vec3_origin && offset != vec3_origin ); + } +#endif + + unsigned short gameFlags = VPhysicsGetObject()->GetGameFlags(); + if ( gameFlags & FVPHYSICS_PLAYER_HELD ) + { + // if the player is holding the object, use it's real mass (player holding reduced the mass) + CBasePlayer *pPlayer = NULL; + + if ( AI_IsSinglePlayer() ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + else + { + // See which MP player is holding the physics object and then use that player to get the real mass of the object. + // This is ugly but better than having linkage between an object and its "holding" player. + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *tempPlayer = UTIL_PlayerByIndex( i ); + if ( tempPlayer && (tempPlayer->GetHeldObject() == this ) ) + { + pPlayer = tempPlayer; + break; + } + } + } + + if ( pPlayer ) + { + float mass = pPlayer->GetHeldObjectMass( VPhysicsGetObject() ); + if ( mass != 0.0f ) + { + float ratio = VPhysicsGetObject()->GetMass() / mass; + force *= ratio; + } + } + } + else if ( (gameFlags & FVPHYSICS_PART_OF_RAGDOLL) && (gameFlags & FVPHYSICS_CONSTRAINT_STATIC) ) + { + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + if ( !(pList[i]->GetGameFlags() & FVPHYSICS_CONSTRAINT_STATIC) ) + { + pList[i]->ApplyForceOffset( force, offset ); + return 1; + } + } + + } + VPhysicsGetObject()->ApplyForceOffset( force, offset ); + } + + return 1; +} + + // Character killed (only fired once) +void CBaseEntity::Event_Killed( const CTakeDamageInfo &info ) +{ +#ifdef MAPBASE_VSCRIPT + if (m_ScriptScope.IsInitialized() && g_Hook_OnDeath.CanRunInScope( m_ScriptScope )) + { + HSCRIPT hInfo = g_pScriptVM->RegisterInstance( const_cast(&info) ); + + // info + ScriptVariant_t functionReturn; + ScriptVariant_t args[] = { ScriptVariant_t( hInfo ) }; + if ( g_Hook_OnDeath.Call( m_ScriptScope, &functionReturn, args ) && (functionReturn.m_type == FIELD_BOOLEAN && functionReturn.m_bool == false) ) + { + // Make this entity cheat death + g_pScriptVM->RemoveInstance( hInfo ); + return; + } + + g_pScriptVM->RemoveInstance( hInfo ); + } +#endif + + if( info.GetAttacker() ) + { + info.GetAttacker()->Event_KilledOther(this, info); + } + + m_takedamage = DAMAGE_NO; + m_lifeState = LIFE_DEAD; + UTIL_Remove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: helper method to send a game event when this entity is killed. Note: +// gets called specifically for particular entities (mostly NPC), this +// does not get called for every entity +//----------------------------------------------------------------------------- +void CBaseEntity::SendOnKilledGameEvent( const CTakeDamageInfo &info ) +{ + IGameEvent *event = gameeventmanager->CreateEvent( "entity_killed" ); + if ( event ) + { + event->SetInt( "entindex_killed", entindex() ); + if ( info.GetAttacker()) + { + event->SetInt( "entindex_attacker", info.GetAttacker()->entindex() ); + } + if ( info.GetInflictor()) + { + event->SetInt( "entindex_inflictor", info.GetInflictor()->entindex() ); + } + event->SetInt( "damagebits", info.GetDamageType() ); + gameeventmanager->FireEvent( event ); + } +} + + +bool CBaseEntity::HasTarget( string_t targetname ) +{ + if( targetname != NULL_STRING && m_target != NULL_STRING ) + return FStrEq(STRING(targetname), STRING(m_target) ); + else + return false; +} + + +CBaseEntity *CBaseEntity::GetNextTarget( void ) +{ + if ( !m_target ) + return NULL; + return gEntList.FindEntityByName( NULL, m_target ); +} + +class CThinkContextsSaveDataOps : public CDefSaveRestoreOps +{ + virtual void Save( const SaveRestoreFieldInfo_t &fieldInfo, ISave *pSave ) + { + AssertMsg( fieldInfo.pTypeDesc->fieldSize == 1, "CThinkContextsSaveDataOps does not support arrays"); + + // Write out the vector + CUtlVector< thinkfunc_t > *pUtlVector = (CUtlVector< thinkfunc_t > *)fieldInfo.pField; + SaveUtlVector( pSave, pUtlVector, FIELD_EMBEDDED ); + + // Get our owner + CBaseEntity *pOwner = (CBaseEntity*)fieldInfo.pOwner; + + pSave->StartBlock(); + // Now write out all the functions + for ( int i = 0; i < pUtlVector->Size(); i++ ) + { +#ifdef WIN32 + void **ppV = (void**)&((*pUtlVector)[i].m_pfnThink); +#else + BASEPTR *ppV = &((*pUtlVector)[i].m_pfnThink); +#endif + bool bHasFunc = (*ppV != NULL); + pSave->WriteBool( &bHasFunc, 1 ); + if ( bHasFunc ) + { + pSave->WriteFunction( pOwner->GetDataDescMap(), "m_pfnThink", (inputfunc_t **)ppV, 1 ); + } + } + pSave->EndBlock(); + } + + virtual void Restore( const SaveRestoreFieldInfo_t &fieldInfo, IRestore *pRestore ) + { + AssertMsg( fieldInfo.pTypeDesc->fieldSize == 1, "CThinkContextsSaveDataOps does not support arrays"); + + // Read in the vector + CUtlVector< thinkfunc_t > *pUtlVector = (CUtlVector< thinkfunc_t > *)fieldInfo.pField; + RestoreUtlVector( pRestore, pUtlVector, FIELD_EMBEDDED ); + + // Get our owner + CBaseEntity *pOwner = (CBaseEntity*)fieldInfo.pOwner; + + pRestore->StartBlock(); + // Now read in all the functions + for ( int i = 0; i < pUtlVector->Size(); i++ ) + { + bool bHasFunc; + pRestore->ReadBool( &bHasFunc, 1 ); +#ifdef WIN32 + void **ppV = (void**)&((*pUtlVector)[i].m_pfnThink); +#else + BASEPTR *ppV = &((*pUtlVector)[i].m_pfnThink); + Q_memset( (void *)ppV, 0x0, sizeof(inputfunc_t) ); +#endif + if ( bHasFunc ) + { + SaveRestoreRecordHeader_t header; + pRestore->ReadHeader( &header ); + pRestore->ReadFunction( pOwner->GetDataDescMap(), (inputfunc_t **)ppV, 1, header.size ); + } + else + { + *ppV = NULL; + } + } + pRestore->EndBlock(); + } + + virtual bool IsEmpty( const SaveRestoreFieldInfo_t &fieldInfo ) + { + CUtlVector< thinkfunc_t > *pUtlVector = (CUtlVector< thinkfunc_t > *)fieldInfo.pField; + return ( pUtlVector->Count() == 0 ); + } + + virtual void MakeEmpty( const SaveRestoreFieldInfo_t &fieldInfo ) + { + BASEPTR pFunc = *((BASEPTR*)fieldInfo.pField); + pFunc = NULL; + } +}; +CThinkContextsSaveDataOps g_ThinkContextsSaveDataOps; +ISaveRestoreOps *thinkcontextFuncs = &g_ThinkContextsSaveDataOps; + +BEGIN_SIMPLE_DATADESC( thinkfunc_t ) + + DEFINE_FIELD( m_iszContext, FIELD_STRING ), + // DEFINE_FIELD( m_pfnThink, FIELD_FUNCTION ), // Manually written + DEFINE_FIELD( m_nNextThinkTick, FIELD_TICK ), + DEFINE_FIELD( m_nLastThinkTick, FIELD_TICK ), + +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( ResponseContext_t ) + + DEFINE_FIELD( m_iszName, FIELD_STRING ), + DEFINE_FIELD( m_iszValue, FIELD_STRING ), + DEFINE_FIELD( m_fExpirationTime, FIELD_TIME ), + +END_DATADESC() + +BEGIN_DATADESC_NO_BASE( CBaseEntity ) + + DEFINE_KEYFIELD( m_iClassname, FIELD_STRING, "classname" ), + DEFINE_GLOBAL_KEYFIELD( m_iGlobalname, FIELD_STRING, "globalname" ), + DEFINE_KEYFIELD( m_iParent, FIELD_STRING, "parentname" ), + + DEFINE_KEYFIELD( m_iHammerID, FIELD_INTEGER, "hammerid" ), // save ID numbers so that entities can be tracked between save/restore and vmf + + DEFINE_KEYFIELD( m_flSpeed, FIELD_FLOAT, "speed" ), + DEFINE_KEYFIELD( m_nRenderFX, FIELD_CHARACTER, "renderfx" ), + DEFINE_KEYFIELD( m_nRenderMode, FIELD_CHARACTER, "rendermode" ), + + // Consider moving to CBaseAnimating? + DEFINE_FIELD( m_flPrevAnimTime, FIELD_TIME ), + DEFINE_FIELD( m_flAnimTime, FIELD_TIME ), + DEFINE_FIELD( m_flSimulationTime, FIELD_TIME ), + DEFINE_FIELD( m_nLastThinkTick, FIELD_TICK ), + + DEFINE_FIELD(m_iszScriptId, FIELD_STRING), + // m_ScriptScope; + // m_hScriptInstance; + + DEFINE_KEYFIELD(m_iszVScripts, FIELD_STRING, "vscripts"), + DEFINE_KEYFIELD(m_iszScriptThinkFunction, FIELD_STRING, "thinkfunction"), + DEFINE_KEYFIELD( m_nNextThinkTick, FIELD_TICK, "nextthink" ), + DEFINE_KEYFIELD( m_fEffects, FIELD_INTEGER, "effects" ), + DEFINE_KEYFIELD( m_clrRender, FIELD_COLOR32, "rendercolor" ), + DEFINE_GLOBAL_KEYFIELD( m_nModelIndex, FIELD_SHORT, "modelindex" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iViewHideFlags, FIELD_INTEGER, "viewhideflags" ), + DEFINE_KEYFIELD( m_bDisableFlashlight, FIELD_BOOLEAN, "disableflashlight" ), +#endif +#if !defined( NO_ENTITY_PREDICTION ) + // DEFINE_FIELD( m_PredictableID, CPredictableId ), +#endif + DEFINE_FIELD( touchStamp, FIELD_INTEGER ), + DEFINE_CUSTOM_FIELD( m_aThinkFunctions, thinkcontextFuncs ), + // m_iCurrentThinkContext (not saved, debug field only, and think transient to boot) + + DEFINE_UTLVECTOR(m_ResponseContexts, FIELD_EMBEDDED), + DEFINE_KEYFIELD( m_iszResponseContext, FIELD_STRING, "ResponseContext" ), + + DEFINE_FIELD( m_pfnThink, FIELD_FUNCTION ), + DEFINE_FIELD( m_pfnTouch, FIELD_FUNCTION ), + DEFINE_FIELD( m_pfnUse, FIELD_FUNCTION ), + DEFINE_FIELD( m_pfnBlocked, FIELD_FUNCTION ), + DEFINE_FIELD( m_pfnMoveDone, FIELD_FUNCTION ), + + DEFINE_FIELD( m_lifeState, FIELD_CHARACTER ), + DEFINE_FIELD( m_takedamage, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_iMaxHealth, FIELD_INTEGER, "max_health" ), + DEFINE_KEYFIELD( m_iHealth, FIELD_INTEGER, "health" ), + // DEFINE_FIELD( m_pLink, FIELD_CLASSPTR ), + DEFINE_KEYFIELD( m_target, FIELD_STRING, "target" ), + + DEFINE_KEYFIELD( m_iszDamageFilterName, FIELD_STRING, "damagefilter" ), + DEFINE_FIELD( m_hDamageFilter, FIELD_EHANDLE ), + + DEFINE_FIELD( m_debugOverlays, FIELD_INTEGER ), + + DEFINE_GLOBAL_FIELD( m_pParent, FIELD_EHANDLE ), + DEFINE_FIELD( m_iParentAttachment, FIELD_CHARACTER ), + DEFINE_GLOBAL_FIELD( m_hMoveParent, FIELD_EHANDLE ), + DEFINE_GLOBAL_FIELD( m_hMoveChild, FIELD_EHANDLE ), + DEFINE_GLOBAL_FIELD( m_hMovePeer, FIELD_EHANDLE ), + + DEFINE_FIELD( m_iEFlags, FIELD_INTEGER ), + + DEFINE_FIELD( m_iName, FIELD_STRING ), + DEFINE_EMBEDDED( m_Collision ), + DEFINE_EMBEDDED( m_Network ), + + DEFINE_FIELD( m_MoveType, FIELD_CHARACTER ), + DEFINE_FIELD( m_MoveCollide, FIELD_CHARACTER ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_hOwnerEntity, FIELD_EHANDLE, "OwnerEntity" ), +#else + DEFINE_FIELD( m_hOwnerEntity, FIELD_EHANDLE ), +#endif + DEFINE_FIELD( m_CollisionGroup, FIELD_INTEGER ), + DEFINE_PHYSPTR( m_pPhysicsObject), + DEFINE_FIELD( m_flElasticity, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flShadowCastDistance, FIELD_FLOAT, "shadowcastdist" ), + DEFINE_FIELD( m_flDesiredShadowCastDistance, FIELD_FLOAT ), + + DEFINE_INPUT( m_iInitialTeamNum, FIELD_INTEGER, "TeamNum" ), + DEFINE_FIELD( m_iTeamNum, FIELD_INTEGER ), + +// DEFINE_FIELD( m_bSentLastFrame, FIELD_INTEGER ), + + DEFINE_FIELD( m_hGroundEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_flGroundChangeTime, FIELD_TIME ), + DEFINE_GLOBAL_KEYFIELD( m_ModelName, FIELD_MODELNAME, "model" ), + + DEFINE_KEYFIELD( m_vecBaseVelocity, FIELD_VECTOR, "basevelocity" ), + DEFINE_FIELD( m_vecAbsVelocity, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_vecAngVelocity, FIELD_VECTOR, "avelocity" ), +// DEFINE_FIELD( m_vecAbsAngVelocity, FIELD_VECTOR ), + DEFINE_ARRAY( m_rgflCoordinateFrame, FIELD_FLOAT, 12 ), // NOTE: MUST BE IN LOCAL SPACE, NOT POSITION_VECTOR!!! (see CBaseEntity::Restore) + + DEFINE_KEYFIELD( m_nWaterLevel, FIELD_CHARACTER, "waterlevel" ), + DEFINE_FIELD( m_nWaterType, FIELD_CHARACTER ), + DEFINE_FIELD( m_pBlocker, FIELD_EHANDLE ), + + DEFINE_KEYFIELD( m_flGravity, FIELD_FLOAT, "gravity" ), + DEFINE_KEYFIELD( m_flFriction, FIELD_FLOAT, "friction" ), + + // Local time is local to each object. It doesn't need to be re-based if the clock + // changes. Therefore it is saved as a FIELD_FLOAT, not a FIELD_TIME + DEFINE_KEYFIELD( m_flLocalTime, FIELD_FLOAT, "ltime" ), + DEFINE_FIELD( m_flVPhysicsUpdateLocalTime, FIELD_FLOAT ), + DEFINE_FIELD( m_flMoveDoneTime, FIELD_FLOAT ), + +// DEFINE_FIELD( m_nPushEnumCount, FIELD_INTEGER ), + + DEFINE_FIELD( m_vecAbsOrigin, FIELD_POSITION_VECTOR ), + DEFINE_KEYFIELD( m_vecVelocity, FIELD_VECTOR, "velocity" ), + DEFINE_KEYFIELD( m_iTextureFrameIndex, FIELD_CHARACTER, "texframeindex" ), + DEFINE_FIELD( m_bSimulatedEveryTick, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bAnimatedEveryTick, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bAlternateSorting, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_spawnflags, FIELD_INTEGER, "spawnflags" ), + DEFINE_FIELD( m_nTransmitStateOwnedCounter, FIELD_CHARACTER ), + DEFINE_FIELD( m_angAbsRotation, FIELD_VECTOR ), + DEFINE_FIELD( m_vecOrigin, FIELD_VECTOR ), // NOTE: MUST BE IN LOCAL SPACE, NOT POSITION_VECTOR!!! (see CBaseEntity::Restore) + DEFINE_FIELD( m_angRotation, FIELD_VECTOR ), + + DEFINE_KEYFIELD( m_vecViewOffset, FIELD_VECTOR, "view_ofs" ), + +#ifdef MAPBASE + // You know, m_fFlags access + DEFINE_KEYFIELD( m_fFlags, FIELD_INTEGER, "m_fFlags" ), +#else + DEFINE_FIELD( m_fFlags, FIELD_INTEGER ), +#endif +#if !defined( NO_ENTITY_PREDICTION ) +// DEFINE_FIELD( m_bIsPlayerSimulated, FIELD_INTEGER ), +// DEFINE_FIELD( m_hPlayerSimulationOwner, FIELD_EHANDLE ), +#endif + // DEFINE_FIELD( m_pTimedOverlay, TimedOverlay_t* ), + DEFINE_FIELD( m_nSimulationTick, FIELD_TICK ), + // DEFINE_FIELD( m_RefEHandle, CBaseHandle ), + +// DEFINE_FIELD( m_nWaterTouch, FIELD_INTEGER ), +// DEFINE_FIELD( m_nSlimeTouch, FIELD_INTEGER ), + DEFINE_FIELD( m_flNavIgnoreUntilTime, FIELD_TIME ), + +// DEFINE_FIELD( m_bToolRecording, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_ToolHandle, FIELD_INTEGER ), + + // NOTE: This is tricky. TeamNum must be saved, but we can't directly + // read it in, because we can only set it after the team entity has been read in, + // which may or may not actually occur before the entity is parsed. + // Therefore, we set the TeamNum from the InitialTeamNum in Activate + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetTeam", InputSetTeam ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Kill", InputKill ), + DEFINE_INPUTFUNC( FIELD_VOID, "KillHierarchy", InputKillHierarchy ), + DEFINE_INPUTFUNC( FIELD_VOID, "Use", InputUse ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "Alpha", InputAlpha ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "AlternativeSorting", InputAlternativeSorting ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "Color", InputColor ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetParent", InputSetParent ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetParentAttachment", InputSetParentAttachment ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetParentAttachmentMaintainOffset", InputSetParentAttachmentMaintainOffset ), + DEFINE_INPUTFUNC( FIELD_VOID, "ClearParent", InputClearParent ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetDamageFilter", InputSetDamageFilter ), + + DEFINE_INPUTFUNC( FIELD_VOID, "EnableDamageForces", InputEnableDamageForces ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableDamageForces", InputDisableDamageForces ), + + DEFINE_INPUTFUNC( FIELD_STRING, "DispatchEffect", InputDispatchEffect ), + DEFINE_INPUTFUNC( FIELD_STRING, "DispatchResponse", InputDispatchResponse ), + + // Entity I/O methods to alter context + DEFINE_INPUTFUNC( FIELD_STRING, "AddContext", InputAddContext ), + DEFINE_INPUTFUNC( FIELD_STRING, "RemoveContext", InputRemoveContext ), + DEFINE_INPUTFUNC( FIELD_STRING, "ClearContext", InputClearContext ), + + DEFINE_INPUTFUNC( FIELD_VOID, "DisableShadow", InputDisableShadow ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableShadow", InputEnableShadow ), + + DEFINE_INPUTFUNC( FIELD_STRING, "AddOutput", InputAddOutput ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "ChangeVariable", InputChangeVariable ), +#endif + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_INPUT, "PassUser1", InputPassUser1 ), + DEFINE_INPUTFUNC( FIELD_INPUT, "PassUser2", InputPassUser2 ), + DEFINE_INPUTFUNC( FIELD_INPUT, "PassUser3", InputPassUser3 ), + DEFINE_INPUTFUNC( FIELD_INPUT, "PassUser4", InputPassUser4 ), + + DEFINE_INPUTFUNC( FIELD_VOID, "FireUser1", InputFireUser1 ), + DEFINE_INPUTFUNC( FIELD_VOID, "FireUser2", InputFireUser2 ), + DEFINE_INPUTFUNC( FIELD_VOID, "FireUser3", InputFireUser3 ), + DEFINE_INPUTFUNC( FIELD_VOID, "FireUser4", InputFireUser4 ), + + DEFINE_INPUTFUNC( FIELD_VOID, "FireRandomUser", InputFireRandomUser ), + DEFINE_INPUTFUNC( FIELD_INPUT, "PassRandomUser", InputPassRandomUser ), +#else + DEFINE_INPUTFUNC( FIELD_STRING, "FireUser1", InputFireUser1 ), + DEFINE_INPUTFUNC( FIELD_STRING, "FireUser2", InputFireUser2 ), + DEFINE_INPUTFUNC( FIELD_STRING, "FireUser3", InputFireUser3 ), + DEFINE_INPUTFUNC( FIELD_STRING, "FireUser4", InputFireUser4 ), +#endif + + DEFINE_INPUTFUNC(FIELD_STRING, "RunScriptFile", InputRunScriptFile), + DEFINE_INPUTFUNC(FIELD_STRING, "RunScriptCode", InputRunScript), + DEFINE_INPUTFUNC(FIELD_STRING, "CallScriptFunction", InputCallScriptFunction), +#ifdef MAPBASE_VSCRIPT + DEFINE_INPUTFUNC(FIELD_STRING, "RunScriptCodeQuotable", InputRunScriptQuotable), + DEFINE_INPUTFUNC(FIELD_VOID, "ClearScriptScope", InputClearScriptScope), +#endif + +#ifdef MAPBASE + DEFINE_OUTPUT( m_OutUser1, "OutUser1" ), + DEFINE_OUTPUT( m_OutUser2, "OutUser2" ), + DEFINE_OUTPUT( m_OutUser3, "OutUser3" ), + DEFINE_OUTPUT( m_OutUser4, "OutUser4" ), +#endif + + DEFINE_OUTPUT( m_OnUser1, "OnUser1" ), + DEFINE_OUTPUT( m_OnUser2, "OnUser2" ), + DEFINE_OUTPUT( m_OnUser3, "OnUser3" ), + DEFINE_OUTPUT( m_OnUser4, "OnUser4" ), + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetEntityName", InputSetEntityName ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget", InputSetTarget ), + DEFINE_INPUTFUNC( FIELD_EHANDLE, "SetOwnerEntity", InputSetOwnerEntity ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddHealth", InputAddHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveHealth", InputRemoveHealth ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxHealth", InputSetMaxHealth ), + + DEFINE_INPUTFUNC( FIELD_STRING, "FireOutput", InputFireOutput ), + DEFINE_INPUTFUNC( FIELD_STRING, "RemoveOutput", InputRemoveOutput ), + //DEFINE_INPUTFUNC( FIELD_STRING, "CancelOutput", InputCancelOutput ), // Find a way to implement this + DEFINE_INPUTFUNC( FIELD_STRING, "ReplaceOutput", InputReplaceOutput ), + DEFINE_INPUTFUNC( FIELD_STRING, "AcceptInput", InputAcceptInput ), + DEFINE_INPUTFUNC( FIELD_VOID, "CancelPending", InputCancelPending ), + + DEFINE_INPUTFUNC( FIELD_VOID, "FreeChildren", InputFreeChildren ), + + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetLocalOrigin", InputSetLocalOrigin ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetLocalAngles", InputSetLocalAngles ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetAbsOrigin", InputSetAbsOrigin ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetAbsAngles", InputSetAbsAngles ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetLocalVelocity", InputSetLocalVelocity ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetLocalAngularVelocity", InputSetLocalAngularVelocity ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddSpawnFlags", InputAddSpawnFlags ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveSpawnFlags", InputRemoveSpawnFlags ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetRenderMode", InputSetRenderMode ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetRenderFX", InputSetRenderFX ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetViewHideFlags", InputSetViewHideFlags ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddEffects", InputAddEffects ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveEffects", InputRemoveEffects ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableDraw", InputDrawEntity ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableDraw", InputUndrawEntity ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableReceivingFlashlight", InputEnableReceivingFlashlight ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableReceivingFlashlight", InputDisableReceivingFlashlight ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddEFlags", InputAddEFlags ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveEFlags", InputRemoveEFlags ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddSolidFlags", InputAddSolidFlags ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveSolidFlags", InputRemoveSolidFlags ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMoveType", InputSetMoveType ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetCollisionGroup", InputSetCollisionGroup ), + + DEFINE_INPUTFUNC( FIELD_EHANDLE, "Touch", InputTouch ), + + DEFINE_INPUTFUNC( FIELD_INPUT, "KilledNPC", InputKilledNPC ), + + DEFINE_INPUTFUNC( FIELD_VOID, "KillIfNotVisible", InputKillIfNotVisible ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "KillWhenNotVisible", InputKillWhenNotVisible ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetThinkNull", InputSetThinkNull ), + + DEFINE_OUTPUT( m_OnKilled, "OnKilled" ), +#endif + + // Function Pointers + DEFINE_FUNCTION( SUB_Remove ), + DEFINE_FUNCTION( SUB_DoNothing ), + DEFINE_FUNCTION( SUB_StartFadeOut ), + DEFINE_FUNCTION( SUB_StartFadeOutInstant ), + DEFINE_FUNCTION( SUB_FadeOut ), + DEFINE_FUNCTION( SUB_Vanish ), + DEFINE_FUNCTION( SUB_CallUseToggle ), + DEFINE_THINKFUNC( ShadowCastDistThink ), + DEFINE_THINKFUNC( ScriptThink ), +#ifdef MAPBASE_VSCRIPT + DEFINE_THINKFUNC( ScriptContextThink ), +#endif + +#ifdef MAPBASE + DEFINE_FUNCTION( SUB_RemoveWhenNotVisible ), +#endif + + DEFINE_FIELD( m_hEffectEntity, FIELD_EHANDLE ), + + //DEFINE_FIELD( m_DamageModifiers, FIELD_?? ), // can't save? + // DEFINE_FIELD( m_fDataObjectTypes, FIELD_INTEGER ), + +#ifdef TF_DLL + DEFINE_ARRAY( m_nModelIndexOverrides, FIELD_INTEGER, MAX_VISION_MODES ), +#endif + +END_DATADESC() + + +#ifdef MAPBASE_VSCRIPT +ScriptHook_t CBaseEntity::g_Hook_UpdateOnRemove; +ScriptHook_t CBaseEntity::g_Hook_VPhysicsCollision; +ScriptHook_t CBaseEntity::g_Hook_FireBullets; +ScriptHook_t CBaseEntity::g_Hook_OnDeath; +ScriptHook_t CBaseEntity::g_Hook_HandleInteraction; +#endif + +BEGIN_ENT_SCRIPTDESC_ROOT( CBaseEntity, "Root class of all server-side entities" ) + DEFINE_SCRIPT_INSTANCE_HELPER( &g_BaseEntityScriptInstanceHelper ) + DEFINE_SCRIPTFUNC_NAMED( ConnectOutputToScript, "ConnectOutput", "Adds an I/O connection that will call the named function when the specified output fires" ) + DEFINE_SCRIPTFUNC_NAMED( DisconnectOutputFromScript, "DisconnectOutput", "Removes a connected script function from an I/O event." ) + + DEFINE_SCRIPTFUNC( GetHealth, "" ) + DEFINE_SCRIPTFUNC( SetHealth, "" ) + DEFINE_SCRIPTFUNC( GetMaxHealth, "" ) + DEFINE_SCRIPTFUNC( SetMaxHealth, "" ) + + DEFINE_SCRIPTFUNC( SetModel, "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetModelName, "GetModelName", "Returns the name of the model" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptStopSound, "StopSound", "Stops a sound from this entity." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptEmitSound, "EmitSound", "Plays a sound from this entity." ) + DEFINE_SCRIPTFUNC_NAMED( VScriptPrecacheScriptSound, "PrecacheSoundScript", "Precache a sound for later playing." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSoundDuration, "GetSoundDuration", "Returns float duration of the sound. Takes soundname and optional actormodelname.") + + + DEFINE_SCRIPTFUNC( GetClassname, "" ) + DEFINE_SCRIPTFUNC_NAMED( GetEntityNameAsCStr, "GetName", "" ) +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC( GetDebugName, "If name exists returns name, otherwise returns classname" ) + DEFINE_SCRIPTFUNC_NAMED( SetNameAsCStr, "SetName", "" ) +#endif + DEFINE_SCRIPTFUNC( GetPreTemplateName, "Get the entity name stripped of template unique decoration" ) + + DEFINE_SCRIPTFUNC_NAMED( GetAbsOrigin, "GetOrigin", "" ) + DEFINE_SCRIPTFUNC( SetAbsOrigin, "SetAbsOrigin" ) +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC( SetAbsAngles, "SetAbsAngles" ) + + DEFINE_SCRIPTFUNC( GetLocalOrigin, "GetLocalOrigin" ) + DEFINE_SCRIPTFUNC( SetLocalOrigin, "SetLocalOrigin" ) + DEFINE_SCRIPTFUNC( GetLocalAngles, "GetLocalAngles" ) + DEFINE_SCRIPTFUNC( SetLocalAngles, "SetLocalAngles" ) +#endif + + DEFINE_SCRIPTFUNC_NAMED( ScriptSetOrigin, "SetOrigin", "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetForward, "SetForwardVector", "Set the orientation of the entity to have this forward vector" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetForward, "GetForwardVector", "Get the forward vector of the entity" ) +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC_NAMED( ScriptGetRight, "GetRightVector", "Get the right vector of the entity" ) +#endif + DEFINE_SCRIPTFUNC_NAMED( ScriptGetLeft, "GetLeftVector", SCRIPT_HIDE ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetUp, "GetUpVector", "Get the up vector of the entity" ) + +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC_NAMED( ScriptSetOriginAngles, "SetOriginAngles", "Set both the origin and the angles" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetOriginAnglesVelocity, "SetOriginAnglesVelocity", "Set the origin, the angles, and the velocity" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptEntityToWorldTransform, "EntityToWorldTransform", "Get the entity's transform" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetPhysicsObject, "GetPhysicsObject", "Get the entity's physics object if it has one" ) + + DEFINE_SCRIPTFUNC( ApplyAbsVelocityImpulse, "" ) + DEFINE_SCRIPTFUNC( ApplyLocalAngularVelocityImpulse, "" ) + + DEFINE_SCRIPTFUNC( BodyTarget, "" ) + DEFINE_SCRIPTFUNC( HeadTarget, "" ) +#endif + + DEFINE_SCRIPTFUNC_NAMED( GetAbsVelocity, "GetVelocity", "" ) + DEFINE_SCRIPTFUNC_NAMED( SetAbsVelocity, "SetVelocity", "" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptSetLocalAngularVelocity, "SetAngularVelocity", "Set the local angular velocity - takes float pitch,yaw,roll velocities" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetLocalAngularVelocity, "GetAngularVelocity", "Get the local angular velocity - returns a vector of pitch,yaw,roll" ) + + DEFINE_SCRIPTFUNC_NAMED( WorldSpaceCenter, "GetCenter", "Get vector to center of object - absolute coords") + DEFINE_SCRIPTFUNC_NAMED( ScriptEyePosition, "EyePosition", "Get vector to eye position - absolute coords") +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC_NAMED( ScriptEyeAngles, "EyeAngles", "Get eye pitch, yaw, roll as a vector" ) +#endif + DEFINE_SCRIPTFUNC_NAMED( ScriptSetAngles, "SetAngles", "Set entity pitch, yaw, roll") + DEFINE_SCRIPTFUNC_NAMED( ScriptGetAngles, "GetAngles", "Get entity pitch, yaw, roll as a vector") + + DEFINE_SCRIPTFUNC( SetSize, "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetBoundingMins, "GetBoundingMins", "Get a vector containing min bounds, centered on object") + DEFINE_SCRIPTFUNC_NAMED( ScriptGetBoundingMaxs, "GetBoundingMaxs", "Get a vector containing max bounds, centered on object") + + DEFINE_SCRIPTFUNC_NAMED( Remove, "Destroy", "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetOwner, "SetOwner", "" ) + DEFINE_SCRIPTFUNC_NAMED( GetTeamNumber, "GetTeam", "" ) + DEFINE_SCRIPTFUNC_NAMED( ChangeTeam, "SetTeam", "" ) + +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC_NAMED( ScriptSetParent, "SetParent", "" ) +#endif + DEFINE_SCRIPTFUNC_NAMED( ScriptGetMoveParent, "GetMoveParent", "If in hierarchy, retrieves the entity's parent" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetRootMoveParent, "GetRootMoveParent", "If in hierarchy, walks up the hierarchy to find the root parent" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptFirstMoveChild, "FirstMoveChild", "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptNextMovePeer, "NextMovePeer", "" ) + + DEFINE_SCRIPTFUNC_NAMED( KeyValueFromString, "__KeyValueFromString", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( KeyValueFromFloat, "__KeyValueFromFloat", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( KeyValueFromInt, "__KeyValueFromInt", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( KeyValueFromVector, "__KeyValueFromVector", SCRIPT_HIDE ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetModelKeyValues, "GetModelKeyValues", "Get a KeyValue class instance on this entity's model") + +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC_NAMED( ScriptIsVisible, "IsVisible", "Check if the specified position can be visible to this entity." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptIsEntVisible, "IsEntVisible", "Check if the specified entity can be visible to this entity." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptIsVisibleWithMask, "IsVisibleWithMask", "Check if the specified position can be visible to this entity with a specific trace mask." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptTakeDamage, "TakeDamage", "Apply damage to this entity with a given info handle" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptFireBullets, "FireBullets", "Fire bullets from entity with a given info handle" ) + + DEFINE_SCRIPTFUNC( TakeHealth, "Give this entity health" ) + DEFINE_SCRIPTFUNC( IsAlive, "Return true if this entity is alive" ) + + DEFINE_SCRIPTFUNC( GetWaterLevel, "Get current level of water submergence" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetContext, "GetContext", "Get a response context value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptAddContext, "AddContext", "Add a response context value" ) + DEFINE_SCRIPTFUNC( GetContextExpireTime, "Get a response context's expiration time" ) + DEFINE_SCRIPTFUNC( GetContextCount, "Get the number of response contexts" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetContextIndex, "GetContextIndex", "Get a response context at a specific index in the form of a table" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptFollowEntity, "FollowEntity", "Begin following the specified entity. This makes this entity non-solid, parents it to the target entity, and teleports it to the specified entity's origin. The second parameter is whether or not to use bonemerging while following." ) + DEFINE_SCRIPTFUNC( StopFollowingEntity, "Stops following an entity if we're following one." ) + DEFINE_SCRIPTFUNC( IsFollowingEntity, "Returns true if this entity is following another entity." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetFollowedEntity, "GetFollowedEntity", "Get the entity we're following." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptClassify, "Classify", "Get Class_T class ID (corresponds to the CLASS_ set of constants)" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptAcceptInput, "AcceptInput", "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptFireOutput, "FireOutput", "Fire an entity output" ) + DEFINE_SCRIPTFUNC( GetMaxOutputDelay, "Get the longest delay for all events attached to an output" ) + DEFINE_SCRIPTFUNC( CancelEventsByInput, "Cancel all I/O events for this entity, match input" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptAddOutput, "AddOutput", "Add an output" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetKeyValue, "GetKeyValue", "Get a keyvalue" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorVector, "GetRenderColorVector", "Get the render color as a vector" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorR, "GetRenderColorR", "Get the render color's R value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorG, "GetRenderColorG", "Get the render color's G value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorB, "GetRenderColorB", "Get the render color's B value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetAlpha, "GetRenderAlpha", "Get the render color's alpha value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorVector, "SetRenderColorVector", "Set the render color as a vector" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColor, "SetRenderColor", "Set the render color" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorR, "SetRenderColorR", "Set the render color's R value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorG, "SetRenderColorG", "Set the render color's G value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorB, "SetRenderColorB", "Set the render color's B value" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetAlpha, "SetRenderAlpha", "Set the render color's alpha value" ) + + // LEGACY + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorVector, "GetColorVector", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorR, "GetColorR", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorG, "GetColorG", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetColorB, "GetColorB", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetAlpha, "GetAlpha", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorVector, "SetColorVector", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorR, "SetColorR", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorG, "SetColorG", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetColorB, "SetColorB", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetAlpha, "SetAlpha", SCRIPT_HIDE ) + // END LEGACY + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetRenderMode, "GetRenderMode", "Get render mode" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetRenderMode, "SetRenderMode", "Set render mode" ) + + DEFINE_SCRIPTFUNC( GetSpawnFlags, "Get spawnflags" ) + DEFINE_SCRIPTFUNC( AddSpawnFlags, "Add spawnflag(s)" ) + DEFINE_SCRIPTFUNC( RemoveSpawnFlags, "Remove spawnflag(s)" ) + DEFINE_SCRIPTFUNC( ClearSpawnFlags, "Clear spawnflag(s)" ) + DEFINE_SCRIPTFUNC( HasSpawnFlags, "Check if the entity has specific spawnflag(s) ticked" ) + + DEFINE_SCRIPTFUNC( GetEffects, "Get effects" ) + DEFINE_SCRIPTFUNC( AddEffects, "Add effect(s)" ) + DEFINE_SCRIPTFUNC( RemoveEffects, "Remove effect(s)" ) + DEFINE_SCRIPTFUNC( ClearEffects, "Clear effect(s)" ) + DEFINE_SCRIPTFUNC( SetEffects, "Set effect(s)" ) + DEFINE_SCRIPTFUNC( IsEffectActive, "Check if an effect is active" ) + + DEFINE_SCRIPTFUNC( GetFlags, "Get flags" ) + DEFINE_SCRIPTFUNC( AddFlag, "Add flag" ) + DEFINE_SCRIPTFUNC( RemoveFlag, "Remove flag" ) + + DEFINE_SCRIPTFUNC( GetEFlags, "Get Eflags" ) + DEFINE_SCRIPTFUNC( AddEFlags, "Add Eflags" ) + DEFINE_SCRIPTFUNC( RemoveEFlags, "Remove Eflags" ) + + DEFINE_SCRIPTFUNC( GetTransmitState, "" ) + DEFINE_SCRIPTFUNC( SetTransmitState, "" ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetMoveType, "GetMoveType", "Get the move type" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetMoveType, "SetMoveType", "Set the move type" ) + + DEFINE_SCRIPTFUNC( GetCollisionGroup, "Get the collision group" ) + DEFINE_SCRIPTFUNC( SetCollisionGroup, "Set the collision group" ) + + DEFINE_SCRIPTFUNC( GetGravity, "" ) + DEFINE_SCRIPTFUNC( SetGravity, "" ) + DEFINE_SCRIPTFUNC( GetFriction, "" ) + DEFINE_SCRIPTFUNC( SetFriction, "" ) + DEFINE_SCRIPTFUNC( GetMass, "" ) + DEFINE_SCRIPTFUNC( SetMass, "" ) + + DEFINE_SCRIPTFUNC( GetSolidFlags, "Get solid flags" ) + DEFINE_SCRIPTFUNC( AddSolidFlags, "Add solid flags" ) + DEFINE_SCRIPTFUNC( RemoveSolidFlags, "Remove solid flags" ) + + DEFINE_SCRIPTFUNC( IsPlayer, "Returns true if this entity is a player." ) + DEFINE_SCRIPTFUNC( IsNPC, "Returns true if this entity is a NPC." ) + DEFINE_SCRIPTFUNC( IsCombatCharacter, "Returns true if this entity is a combat character (player or NPC)." ) + DEFINE_SCRIPTFUNC_NAMED( IsBaseCombatWeapon, "IsWeapon", "Returns true if this entity is a weapon." ) + DEFINE_SCRIPTFUNC( IsWorld, "Returns true if this entity is the world." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptDispatchInteraction, "DispatchInteraction", "Dispatches an interaction on this entity. See the g_interaction set of constants for more information." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetTakeDamage, "GetTakeDamage", "Gets this entity's m_takedamage value. (DAMAGE_YES, DAMAGE_NO, etc.)" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetTakeDamage, "SetTakeDamage", "Sets this entity's m_takedamage value. (DAMAGE_YES, DAMAGE_NO, etc.)" ) + + // DEFINE_SCRIPTFUNC( IsMarkedForDeletion, "Returns true if the entity is valid and marked for deletion." ) +#endif + + DEFINE_SCRIPTFUNC( ValidateScriptScope, "Ensure that an entity's script scope has been created" ) + DEFINE_SCRIPTFUNC( GetScriptScope, "Retrieve the script-side data associated with an entity" ) +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC( GetOrCreatePrivateScriptScope, "Create and retrieve the script-side data associated with an entity" ) +#endif + DEFINE_SCRIPTFUNC( GetScriptId, "Retrieve the unique identifier used to refer to the entity within the scripting system" ) + DEFINE_SCRIPTFUNC_NAMED( GetScriptOwnerEntity, "GetOwner", "Gets this entity's owner" ) + DEFINE_SCRIPTFUNC_NAMED( SetScriptOwnerEntity, "SetOwner", "Sets this entity's owner" ) + DEFINE_SCRIPTFUNC( entindex, "" ) + +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC_NAMED( ScriptSetThinkFunction, "SetThinkFunction", "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptStopThinkFunction, "StopThinkFunction", "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetContextThink, "SetContextThink", "Set a think function on this entity." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetThink, "SetThink", "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptStopThink, "StopThink", "" ) + + // + // Hooks + // + DEFINE_SIMPLE_SCRIPTHOOK( CBaseEntity::g_Hook_UpdateOnRemove, "UpdateOnRemove", FIELD_VOID, "Called when the entity is being removed." ) + + BEGIN_SCRIPTHOOK( CBaseEntity::g_Hook_VPhysicsCollision, "VPhysicsCollision", FIELD_VOID, "Called for every single VPhysics-related collision experienced by this entity." ) + DEFINE_SCRIPTHOOK_PARAM( "entity", FIELD_HSCRIPT ) + DEFINE_SCRIPTHOOK_PARAM( "speed", FIELD_FLOAT ) + DEFINE_SCRIPTHOOK_PARAM( "point", FIELD_VECTOR ) + DEFINE_SCRIPTHOOK_PARAM( "normal", FIELD_VECTOR ) + END_SCRIPTHOOK() + + BEGIN_SCRIPTHOOK( CBaseEntity::g_Hook_FireBullets, "FireBullets", FIELD_VOID, "Called for every single VPhysics-related collision experienced by this entity." ) + DEFINE_SCRIPTHOOK_PARAM( "entity", FIELD_HSCRIPT ) + DEFINE_SCRIPTHOOK_PARAM( "speed", FIELD_FLOAT ) + DEFINE_SCRIPTHOOK_PARAM( "point", FIELD_VECTOR ) + DEFINE_SCRIPTHOOK_PARAM( "normal", FIELD_VECTOR ) + END_SCRIPTHOOK() + + BEGIN_SCRIPTHOOK( CBaseEntity::g_Hook_OnDeath, "OnDeath", FIELD_BOOLEAN, "Called when the entity dies (Event_Killed). Returning false makes the entity cancel death, although this could have unforeseen consequences. For hooking any damage instead of just death, see filter_script and PassesFinalDamageFilter." ) + DEFINE_SCRIPTHOOK_PARAM( "info", FIELD_HSCRIPT ) + END_SCRIPTHOOK() + + BEGIN_SCRIPTHOOK( CBaseEntity::g_Hook_HandleInteraction, "HandleInteraction", FIELD_BOOLEAN, "Called for internal game interactions. See the g_interaction set of constants for more information. Returning true or false will return that value without falling to any internal handling. Returning nothing will allow the interaction to fall to any internal handling." ) + DEFINE_SCRIPTHOOK_PARAM( "interaction", FIELD_INTEGER ) + //DEFINE_SCRIPTHOOK_PARAM( "data", FIELD_VARIANT ) + DEFINE_SCRIPTHOOK_PARAM( "sourceEnt", FIELD_HSCRIPT ) + END_SCRIPTHOOK() +#endif +END_SCRIPTDESC(); + + +// For code error checking +extern bool g_bReceivedChainedUpdateOnRemove; + +//----------------------------------------------------------------------------- +// Purpose: Called just prior to object destruction +// Entities that need to unlink themselves from other entities should do the unlinking +// here rather than in their destructor. The reason why is that when the global entity list +// is told to Clear(), it first takes a pass through all active entities and calls UTIL_Remove +// on each such entity. Then it calls the delete function on each deleted entity in the list. +// In the old code, the objects were simply destroyed in order and there was no guarantee that the +// destructor of one object would not try to access another object that might already have been +// destructed (especially since the entity list order is more or less random!). +// NOTE: You should never call delete directly on an entity (there's an assert now), see note +// at CBaseEntity::~CBaseEntity for more information. +// +// NOTE: You should chain to BaseClass::UpdateOnRemove after doing your own cleanup code, e.g.: +// +// void CDerived::UpdateOnRemove( void ) +// { +// ... cleanup code +// ... +// +// BaseClass::UpdateOnRemove(); +// } +// +// In general, this function updates global tables that need to know about entities being removed +//----------------------------------------------------------------------------- +void CBaseEntity::UpdateOnRemove( void ) +{ + g_bReceivedChainedUpdateOnRemove = true; + + // Virtual call to shut down any looping sounds. + StopLoopingSounds(); + + // Notifies entity listeners, etc + gEntList.NotifyRemoveEntity( GetRefEHandle() ); + + if ( edict() ) + { + AddFlag( FL_KILLME ); + if ( GetFlags() & FL_GRAPHED ) + { + /* <> + // this entity was a LinkEnt in the world node graph, so we must remove it from + // the graph since we are removing it from the world. + for ( int i = 0 ; i < WorldGraph.m_cLinks ; i++ ) + { + if ( WorldGraph.m_pLinkPool [ i ].m_pLinkEnt == pev ) + { + // if this link has a link ent which is the same ent that is removing itself, remove it! + WorldGraph.m_pLinkPool [ i ].m_pLinkEnt = NULL; + } + } + */ + } + } + + if ( m_iGlobalname != NULL_STRING ) + { + // NOTE: During level shutdown the global list will suppress this + // it assumes your changing levels or the game will end + // causing the whole list to be flushed + GlobalEntity_SetState( m_iGlobalname, GLOBAL_DEAD ); + } + + VPhysicsDestroyObject(); + + // This is only here to allow the MOVETYPE_NONE to be set without the + // assertion triggering. Why do we bother setting the MOVETYPE to none here? + RemoveEffects( EF_BONEMERGE ); + SetMoveType(MOVETYPE_NONE); + + // If we have a parent, unlink from it. + UnlinkFromParent( this ); + + // Any children still connected are orphans, mark all for delete + CUtlVector childrenList; + GetAllChildren( this, childrenList ); + if ( childrenList.Count() ) + { + DevMsg( 2, "Warning: Deleting orphaned children of %s\n", GetClassname() ); + for ( int i = childrenList.Count()-1; i >= 0; --i ) + { + UTIL_Remove( childrenList[i] ); + } + } + + SetGroundEntity( NULL ); + + if ( m_bDynamicModelPending ) + { + sg_DynamicLoadHandlers.Remove( this ); + } + + if ( IsDynamicModelIndex( m_nModelIndex ) ) + { + modelinfo->ReleaseDynamicModel( m_nModelIndex ); // no-op if not dynamic + m_nModelIndex = -1; + } + + if ( m_hScriptInstance ) + { +#ifdef MAPBASE_VSCRIPT + if (m_ScriptScope.IsInitialized()) + { + g_Hook_UpdateOnRemove.Call( m_ScriptScope, NULL, NULL ); + } +#endif // MAPBASE_VSCRIPT + + g_pScriptVM->RemoveInstance( m_hScriptInstance ); + m_hScriptInstance = NULL; + +#ifdef MAPBASE_VSCRIPT + FOR_EACH_VEC( m_ScriptThinkFuncs, i ) + { + HSCRIPT h = m_ScriptThinkFuncs[i]->m_hfnThink; + if ( h ) g_pScriptVM->ReleaseScript( h ); + } + m_ScriptThinkFuncs.PurgeAndDeleteElements(); +#endif // MAPBASE_VSCRIPT + } +} + +//----------------------------------------------------------------------------- +// capabilities +//----------------------------------------------------------------------------- +int CBaseEntity::ObjectCaps( void ) +{ +#if 1 + model_t *pModel = GetModel(); + bool bIsBrush = ( pModel && modelinfo->GetModelType( pModel ) == mod_brush ); + + // We inherit our parent's use capabilities so that we can forward use commands + // to our parent. + CBaseEntity *pParent = GetParent(); + if ( pParent ) + { + int caps = pParent->ObjectCaps(); + + if ( !bIsBrush ) + caps &= ( FCAP_ACROSS_TRANSITION | FCAP_IMPULSE_USE | FCAP_CONTINUOUS_USE | FCAP_ONOFF_USE | FCAP_DIRECTIONAL_USE ); + else + caps &= ( FCAP_IMPULSE_USE | FCAP_CONTINUOUS_USE | FCAP_ONOFF_USE | FCAP_DIRECTIONAL_USE ); + + if ( pParent->IsPlayer() ) + caps |= FCAP_ACROSS_TRANSITION; + + return caps; + } + else if ( !bIsBrush ) + { + return FCAP_ACROSS_TRANSITION; + } + + return 0; +#else + // We inherit our parent's use capabilities so that we can forward use commands + // to our parent. + int parentCaps = 0; + if (GetParent()) + { + parentCaps = GetParent()->ObjectCaps(); + parentCaps &= ( FCAP_IMPULSE_USE | FCAP_CONTINUOUS_USE | FCAP_ONOFF_USE | FCAP_DIRECTIONAL_USE ); + } + + model_t *pModel = GetModel(); + if ( pModel && modelinfo->GetModelType( pModel ) == mod_brush ) + return parentCaps; + + return FCAP_ACROSS_TRANSITION | parentCaps; +#endif +} + +void CBaseEntity::StartTouch( CBaseEntity *pOther ) +{ + // notify parent + if ( m_pParent != NULL ) + m_pParent->StartTouch( pOther ); +} + +void CBaseEntity::Touch( CBaseEntity *pOther ) +{ + if ( m_pfnTouch ) + (this->*m_pfnTouch)( pOther ); + + // notify parent of touch + if ( m_pParent != NULL ) + m_pParent->Touch( pOther ); +} + +void CBaseEntity::EndTouch( CBaseEntity *pOther ) +{ + // notify parent + if ( m_pParent != NULL ) + { + m_pParent->EndTouch( pOther ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Dispatches blocked events to this entity's blocked handler, set via SetBlocked. +// Input : pOther - The entity that is blocking us. +//----------------------------------------------------------------------------- +void CBaseEntity::Blocked( CBaseEntity *pOther ) +{ + if ( m_pfnBlocked ) + { + (this->*m_pfnBlocked)( pOther ); + } + + // + // Forward the blocked event to our parent, if any. + // + if ( m_pParent != NULL ) + { + m_pParent->Blocked( pOther ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Dispatches use events to this entity's use handler, set via SetUse. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CBaseEntity::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( m_pfnUse != NULL ) + { + (this->*m_pfnUse)( pActivator, pCaller, useType, value ); + } + else + { + // + // We don't handle use events. Forward to our parent, if any. + // + if ( m_pParent != NULL ) + { + m_pParent->Use( pActivator, pCaller, useType, value ); + } + } +} + +static CBaseEntity *FindPhysicsBlocker( IPhysicsObject *pPhysics, physicspushlist_t &list, const Vector &pushVel ) +{ + IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); + CBaseEntity *pBlocker = NULL; + float maxForce = 0; + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + CBaseEntity *pOtherEntity = static_cast(pOther->GetGameData()); + bool inList = false; + for ( int i = 0; i < list.pushedCount; i++ ) + { + if ( pOtherEntity == list.pushedEnts[i] ) + { + inList = true; + break; + } + } + + Vector normal; + pSnapshot->GetSurfaceNormal(normal); + float dot = DotProduct( pushVel, pSnapshot->GetNormalForce() * normal ); + if ( !pBlocker || (!inList && dot > maxForce) ) + { + pBlocker = pOtherEntity; + if ( !inList ) + { + maxForce = dot; + } + } + + pSnapshot->NextFrictionData(); + } + pPhysics->DestroyFrictionSnapshot( pSnapshot ); + + return pBlocker; +} + + +struct pushblock_t +{ + physicspushlist_t *pList; + CBaseEntity *pRootParent; + CBaseEntity *pBlockedEntity; + float moveBackFraction; + float movetime; +}; + +static void ComputePushStartMatrix( matrix3x4_t &start, CBaseEntity *pEntity, const pushblock_t ¶ms ) +{ + Vector localOrigin; + QAngle localAngles; + if ( params.pList ) + { + localOrigin = params.pList->localOrigin; + localAngles = params.pList->localAngles; + } + else + { + localOrigin = params.pRootParent->GetAbsOrigin() - params.pRootParent->GetAbsVelocity() * params.movetime; + localAngles = params.pRootParent->GetAbsAngles() - params.pRootParent->GetLocalAngularVelocity() * params.movetime; + } + matrix3x4_t xform, delta; + AngleMatrix( localAngles, localOrigin, xform ); + + matrix3x4_t srcInv; + // xform = src(-1) * dest + MatrixInvert( params.pRootParent->EntityToWorldTransform(), srcInv ); + ConcatTransforms( xform, srcInv, delta ); + ConcatTransforms( delta, pEntity->EntityToWorldTransform(), start ); +} + +#define DEBUG_PUSH_MESSAGES 0 +static void CheckPushedEntity( CBaseEntity *pEntity, pushblock_t ¶ms ) +{ + IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); + if ( !pPhysics ) + return; + // somehow we've got a static or motion disabled physics object in hierarchy! + // This is not allowed! Don't test blocking in that case. + Assert(pPhysics->IsMoveable()); + if ( !pPhysics->IsMoveable() || !pPhysics->GetShadowController() ) + { +#if DEBUG_PUSH_MESSAGES + Msg("Blocking %s, not moveable!\n", pEntity->GetClassname()); +#endif + return; + } + + bool checkrot = true; + bool checkmove = true; + Vector origin; + QAngle angles; + pPhysics->GetShadowPosition( &origin, &angles ); + float fraction = -1.0f; + + matrix3x4_t parentDelta; + if ( pEntity == params.pRootParent ) + { + if ( pEntity->GetLocalAngularVelocity() == vec3_angle ) + checkrot = false; + if ( pEntity->GetLocalVelocity() == vec3_origin) + checkmove = false; + } + else + { +#if DEBUG_PUSH_MESSAGES + if ( pPhysics->IsAttachedToConstraint(false)) + { + Msg("Warning, hierarchical entity is attached to a constraint %s\n", pEntity->GetClassname()); + } +#endif + } + + if ( checkmove ) + { + // project error onto the axis of movement + Vector dir = pEntity->GetAbsVelocity(); + float speed = VectorNormalize(dir); + Vector targetPos; + pPhysics->GetShadowController()->GetTargetPosition( &targetPos, NULL ); + float targetAmount = DotProduct(targetPos, dir); + float currentAmount = DotProduct(origin, dir); + float entityAmount = DotProduct(pEntity->GetAbsOrigin(), dir); + + // if target and entity origin are not in sync, then the position of the entity was updated + // by something outside of push physics + if ( (targetAmount - entityAmount) > 1 ) + { + pEntity->UpdatePhysicsShadowToCurrentPosition(0); +#if DEBUG_PUSH_MESSAGES + Warning("Someone slammed the position of a %s\n", pEntity->GetClassname() ); +#endif + } + else + { + float dist = targetAmount - currentAmount; + if ( dist > 1 ) + { + #if DEBUG_PUSH_MESSAGES + const char *pName = pEntity->GetClassname(); + Msg( "%s blocked by %.2f units\n", pName, dist ); + #endif + float movementAmount = targetAmount - (speed * params.movetime); + if ( pEntity == params.pRootParent ) + { + if ( params.pList ) + { + Vector localVel = pEntity->GetLocalVelocity(); + VectorNormalize(localVel); + float localTargetAmt = DotProduct(pEntity->GetLocalOrigin(), localVel); + movementAmount = targetAmount + DotProduct(params.pList->localOrigin, localVel) - localTargetAmt; + } + } + else + { + matrix3x4_t start; + ComputePushStartMatrix( start, pEntity, params ); + Vector startPos; + MatrixPosition( start, startPos ); + movementAmount = DotProduct(startPos, dir); + } + float expectedDist = targetAmount - movementAmount; + // compute the fraction to move back the AI to match the physics + if ( expectedDist <= 0 ) + { + fraction = 1; + } + else + { + fraction = dist / expectedDist; + fraction = clamp(fraction, 0.f, 1.f); + } + } + } + } + + if ( checkrot ) + { + Vector axis; + float deltaAngle; + RotationDeltaAxisAngle( angles, pEntity->GetAbsAngles(), axis, deltaAngle ); + if ( fabsf(deltaAngle) > 0.5f ) + { + Vector targetAxis; + QAngle targetRot; + float deltaTargetAngle; + pPhysics->GetShadowController()->GetTargetPosition( NULL, &targetRot ); + RotationDeltaAxisAngle( angles, targetRot, targetAxis, deltaTargetAngle ); + if ( fabsf(deltaTargetAngle) > 0.01f ) + { + float expectedDist = deltaAngle; +#if DEBUG_PUSH_MESSAGES + const char *pName = pEntity->GetClassname(); + Msg( "%s blocked by %.2f degrees\n", pName, deltaAngle ); + if ( pPhysics->IsAsleep() ) + { + Msg("Asleep while blocked?\n"); + } + if ( pPhysics->GetGameFlags() & FVPHYSICS_PENETRATING ) + { + Msg("Blocking for penetration!\n"); + } +#endif + if ( pEntity == params.pRootParent ) + { + expectedDist = pEntity->GetLocalAngularVelocity().Length() * params.movetime; + } + else + { + matrix3x4_t start; + ComputePushStartMatrix( start, pEntity, params ); + Vector startAxis; + float startAngle; + Vector startPos; + QAngle startAngles; + MatrixAngles( start, startAngles, startPos ); + RotationDeltaAxisAngle( startAngles, pEntity->GetAbsAngles(), startAxis, startAngle ); + expectedDist = startAngle * DotProduct( startAxis, axis ); + } + + float t = expectedDist != 0.0f ? fabsf(deltaAngle / expectedDist) : 1.0f; + t = clamp(t,0.f,1.f); + fraction = MAX(fraction, t); + } + else + { + pEntity->UpdatePhysicsShadowToCurrentPosition(0); +#if DEBUG_PUSH_MESSAGES + Warning("Someone slammed the position of a %s\n", pEntity->GetClassname() ); +#endif + } + } + } + if ( fraction >= params.moveBackFraction ) + { + params.moveBackFraction = fraction; + params.pBlockedEntity = pEntity; + } +} + +void CBaseEntity::VPhysicsUpdatePusher( IPhysicsObject *pPhysics ) +{ + float movetime = m_flLocalTime - m_flVPhysicsUpdateLocalTime; + if (movetime <= 0) + return; + + // only reconcile pushers on the final vphysics tick + if ( !PhysIsFinalTick() ) + return; + + Vector origin; + QAngle angles; + + // physics updated the shadow, so check to see if I got blocked + // NOTE: SOLID_BSP cannont compute consistent collisions wrt vphysics, so + // don't allow vphysics to block. Assume game physics has handled it. + if ( GetSolid() != SOLID_BSP && pPhysics->GetShadowPosition( &origin, &angles ) ) + { + CUtlVector list; + GetAllInHierarchy( this, list ); + //NDebugOverlay::BoxAngles( origin, CollisionProp()->OBBMins(), CollisionProp()->OBBMaxs(), angles, 255,0,0,0, gpGlobals->frametime); + + physicspushlist_t *pList = NULL; + if ( HasDataObjectType(PHYSICSPUSHLIST) ) + { + pList = (physicspushlist_t *)GetDataObject( PHYSICSPUSHLIST ); + Assert(pList); + } + bool checkrot = (GetLocalAngularVelocity() != vec3_angle) ? true : false; + bool checkmove = (GetLocalVelocity() != vec3_origin) ? true : false; + + pushblock_t params; + params.pRootParent = this; + params.pList = pList; + params.pBlockedEntity = NULL; + params.moveBackFraction = 0.0f; + params.movetime = movetime; + for ( int i = 0; i < list.Count(); i++ ) + { + if ( list[i]->IsSolid() ) + { + CheckPushedEntity( list[i], params ); + } + } + + float physLocalTime = m_flLocalTime; + if ( params.pBlockedEntity ) + { + float moveback = movetime * params.moveBackFraction; + if ( moveback > 0 ) + { + physLocalTime = m_flLocalTime - moveback; + // add 1% noise for bouncing in collision. + if ( physLocalTime <= (m_flVPhysicsUpdateLocalTime + movetime * 0.99f) ) + { + CBaseEntity *pBlocked = NULL; + IPhysicsObject *pOther; + if ( params.pBlockedEntity->VPhysicsGetObject()->GetContactPoint( NULL, &pOther ) ) + { + pBlocked = static_cast(pOther->GetGameData()); + } + // UNDONE: Need to traverse hierarchy here? Shouldn't. + if ( pList ) + { + SetLocalOrigin( pList->localOrigin ); + SetLocalAngles( pList->localAngles ); + physLocalTime = pList->localMoveTime; + for ( int i = 0; i < pList->pushedCount; i++ ) + { + CBaseEntity *pEntity = pList->pushedEnts[i]; + if ( !pEntity ) + continue; + + pEntity->SetAbsOrigin( pEntity->GetAbsOrigin() - pList->pushVec[i] ); + } + CBaseEntity *pPhysicsBlocker = FindPhysicsBlocker( VPhysicsGetObject(), *pList, pList->pushVec[0] ); + if ( pPhysicsBlocker ) + { + pBlocked = pPhysicsBlocker; + } + } + else + { + Vector origin = GetLocalOrigin(); + QAngle angles = GetLocalAngles(); + + if ( checkmove ) + { + origin -= GetLocalVelocity() * moveback; + } + if ( checkrot ) + { + // BUGBUG: This is pretty hack-tastic! + angles -= GetLocalAngularVelocity() * moveback; + } + + SetLocalOrigin( origin ); + SetLocalAngles( angles ); + } + + if ( pBlocked ) + { + Blocked( pBlocked ); + } + m_flLocalTime = physLocalTime; + } + } + } + } + + // this data is no longer useful, free the memory + if ( HasDataObjectType(PHYSICSPUSHLIST) ) + { + DestroyDataObject( PHYSICSPUSHLIST ); + } + + m_flVPhysicsUpdateLocalTime = m_flLocalTime; + if ( m_flMoveDoneTime <= m_flLocalTime && m_flMoveDoneTime > 0 ) + { + SetMoveDoneTime( -1 ); + MoveDone(); + } +} + + +void CBaseEntity::SetMoveDoneTime( float flDelay ) +{ + if (flDelay >= 0) + { + m_flMoveDoneTime = GetLocalTime() + flDelay; + } + else + { + m_flMoveDoneTime = -1; + } + CheckHasGamePhysicsSimulation(); +} + +//----------------------------------------------------------------------------- +// Purpose: Relinks all of a parents children into the collision tree +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsRelinkChildren( float dt ) +{ + CBaseEntity *child; + + // iterate through all children + for ( child = FirstMoveChild(); child != NULL; child = child->NextMovePeer() ) + { + if ( child->IsSolid() || child->IsSolidFlagSet(FSOLID_TRIGGER) ) + { + child->PhysicsTouchTriggers(); + } + + // + // Update their physics shadows. We should never have any children of + // movetype VPHYSICS. + // + if ( child->GetMoveType() != MOVETYPE_VPHYSICS ) + { + child->UpdatePhysicsShadowToCurrentPosition( dt ); + } + else if ( child->GetOwnerEntity() != this ) + { + // the only case where this is valid is if this entity is an attached ragdoll. + // So assert here to catch the non-ragdoll case. + Assert( 0 ); + } + + if ( child->FirstMoveChild() ) + { + child->PhysicsRelinkChildren(dt); + } + } +} + +void CBaseEntity::PhysicsTouchTriggers( const Vector *pPrevAbsOrigin ) +{ + edict_t *pEdict = edict(); + if ( pEdict && !IsWorld() ) + { + Assert(CollisionProp()); + bool isTriggerCheckSolids = IsSolidFlagSet( FSOLID_TRIGGER ); + bool isSolidCheckTriggers = IsSolid() && !isTriggerCheckSolids; // NOTE: Moving triggers (items, ammo etc) are not + // checked against other triggers to reduce the number of touchlinks created + if ( !(isSolidCheckTriggers || isTriggerCheckSolids) ) + return; + + if ( GetSolid() == SOLID_BSP ) + { + if ( !GetModel() && Q_strlen( STRING( GetModelName() ) ) == 0 ) + { + Warning( "Inserted %s with no model\n", GetClassname() ); + return; + } + } + + SetCheckUntouch( true ); + if ( isSolidCheckTriggers ) + { + engine->SolidMoved( pEdict, CollisionProp(), pPrevAbsOrigin, sm_bAccurateTriggerBboxChecks ); + } + if ( isTriggerCheckSolids ) + { + engine->TriggerMoved( pEdict, sm_bAccurateTriggerBboxChecks ); + } + } +} + +void CBaseEntity::VPhysicsShadowCollision( int index, gamevcollisionevent_t *pEvent ) +{ +} + + + +void CBaseEntity::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + // filter out ragdoll props hitting other parts of itself too often + // UNDONE: Store a sound time for this entity (not just this pair of objects) + // and filter repeats on that? + int otherIndex = !index; + CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex]; + +#ifdef MAPBASE_VSCRIPT + if (m_ScriptScope.IsInitialized() && g_Hook_VPhysicsCollision.CanRunInScope(m_ScriptScope)) + { + Vector vecContactPoint; + pEvent->pInternalData->GetContactPoint( vecContactPoint ); + + Vector vecSurfaceNormal; + pEvent->pInternalData->GetSurfaceNormal( vecSurfaceNormal ); + + // entity, speed, point, normal + ScriptVariant_t args[] = { ScriptVariant_t( pHitEntity->GetScriptInstance() ), pEvent->collisionSpeed, vecContactPoint, vecSurfaceNormal }; + g_Hook_VPhysicsCollision.Call( m_ScriptScope, NULL, args ); + } +#endif + + // Don't make sounds / effects if neither entity is MOVETYPE_VPHYSICS. The game + // physics should have done so. + if ( GetMoveType() != MOVETYPE_VPHYSICS && pHitEntity->GetMoveType() != MOVETYPE_VPHYSICS ) + return; + + if ( pEvent->deltaCollisionTime < 0.5 && (pHitEntity == this) ) + return; + + // don't make noise for hidden/invisible/sky materials + surfacedata_t *phit = physprops->GetSurfaceData( pEvent->surfaceProps[otherIndex] ); + const surfacedata_t *pprops = physprops->GetSurfaceData( pEvent->surfaceProps[index] ); + if ( phit->game.material == 'X' || pprops->game.material == 'X' ) + return; + + if ( pHitEntity == this ) + { + PhysCollisionSound( this, pEvent->pObjects[index], CHAN_BODY, pEvent->surfaceProps[index], pEvent->surfaceProps[otherIndex], pEvent->deltaCollisionTime, pEvent->collisionSpeed ); + } + else + { + PhysCollisionSound( this, pEvent->pObjects[index], CHAN_STATIC, pEvent->surfaceProps[index], pEvent->surfaceProps[otherIndex], pEvent->deltaCollisionTime, pEvent->collisionSpeed ); + } + PhysCollisionScreenShake( pEvent, index ); + +#if HL2_EPISODIC + // episodic does something different for when advisor shields are struck + if ( phit->game.material == 'Z' || pprops->game.material == 'Z') + { + PhysCollisionWarpEffect( pEvent, phit ); + } + else + { + PhysCollisionDust( pEvent, phit ); + } +#else + PhysCollisionDust( pEvent, phit ); +#endif +} + +void CBaseEntity::VPhysicsFriction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit ) +{ + PhysFrictionSound( this, pObject, energy, surfaceProps, surfacePropsHit ); +} + + +void CBaseEntity::VPhysicsSwapObject( IPhysicsObject *pSwap ) +{ + if ( !pSwap ) + { + PhysRemoveShadow(this); + } + + if ( !m_pPhysicsObject ) + { + Warning( "Bad vphysics swap for %s\n", STRING(m_iClassname) ); + } + m_pPhysicsObject = pSwap; +} + + +// Tells the physics shadow to update it's target to the current position +void CBaseEntity::UpdatePhysicsShadowToCurrentPosition( float deltaTime ) +{ + if ( GetMoveType() != MOVETYPE_VPHYSICS ) + { + IPhysicsObject *pPhys = VPhysicsGetObject(); + if ( pPhys ) + { + pPhys->UpdateShadow( GetAbsOrigin(), GetAbsAngles(), false, deltaTime ); + } + } +} + +int CBaseEntity::VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ) +{ + IPhysicsObject *pPhys = VPhysicsGetObject(); + if ( pPhys ) + { + // multi-object entities must implement this function + Assert( !(pPhys->GetGameFlags() & FVPHYSICS_MULTIOBJECT_ENTITY) ); + if ( listMax > 0 ) + { + pList[0] = pPhys; + return 1; + } + } + return 0; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CBaseEntity::VPhysicsIsFlesh( void ) +{ + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + int material = pList[i]->GetMaterialIndex(); + const surfacedata_t *pSurfaceData = physprops->GetSurfaceData( material ); + // Is flesh ?, don't allow pickup + if ( pSurfaceData->game.material == CHAR_TEX_ANTLION || pSurfaceData->game.material == CHAR_TEX_FLESH || pSurfaceData->game.material == CHAR_TEX_BLOODYFLESH || pSurfaceData->game.material == CHAR_TEX_ALIENFLESH ) + return true; + } + return false; +} + +bool CBaseEntity::Intersects( CBaseEntity *pOther ) +{ + if ( !edict() || !pOther->edict() ) + return false; + + CCollisionProperty *pMyProp = CollisionProp(); + CCollisionProperty *pOtherProp = pOther->CollisionProp(); + + return IsOBBIntersectingOBB( + pMyProp->GetCollisionOrigin(), pMyProp->GetCollisionAngles(), pMyProp->OBBMins(), pMyProp->OBBMaxs(), + pOtherProp->GetCollisionOrigin(), pOtherProp->GetCollisionAngles(), pOtherProp->OBBMins(), pOtherProp->OBBMaxs() ); +} + +extern ConVar ai_LOS_mode; + +//========================================================= +// FVisible - returns true if a line can be traced from +// the caller's eyes to the target +//========================================================= +bool CBaseEntity::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) +{ + VPROF( "CBaseEntity::FVisible" ); + + if ( pEntity->GetFlags() & FL_NOTARGET ) + return false; + +#if HL1_DLL + // FIXME: only block LOS through opaque water + // don't look through water + if ((m_nWaterLevel != 3 && pEntity->m_nWaterLevel == 3) + || (m_nWaterLevel == 3 && pEntity->m_nWaterLevel == 0)) + return false; +#endif + + Vector vecLookerOrigin = EyePosition();//look through the caller's 'eyes' + Vector vecTargetOrigin = pEntity->EyePosition(); + + trace_t tr; + if ( !IsXbox() && ai_LOS_mode.GetBool() ) + { + UTIL_TraceLine(vecLookerOrigin, vecTargetOrigin, traceMask, this, COLLISION_GROUP_NONE, &tr); + } + else + { + // If we're doing an LOS search, include NPCs. + if ( traceMask == MASK_BLOCKLOS ) + { + traceMask = MASK_BLOCKLOS_AND_NPCS; + } + + // Player sees through nodraw + if ( IsPlayer() ) + { + traceMask &= ~CONTENTS_BLOCKLOS; + } + + // Use the custom LOS trace filter + CTraceFilterLOS traceFilter( this, COLLISION_GROUP_NONE, pEntity ); + UTIL_TraceLine( vecLookerOrigin, vecTargetOrigin, traceMask, &traceFilter, &tr ); + } + + if (tr.fraction != 1.0 || tr.startsolid ) + { + // If we hit the entity we're looking for, it's visible + if ( tr.m_pEnt == pEntity ) + return true; + + // Got line of sight on the vehicle the player is driving! + if ( pEntity && pEntity->IsPlayer() ) + { + CBasePlayer *pPlayer = assert_cast( pEntity ); + if ( tr.m_pEnt == pPlayer->GetVehicleEntity() ) + return true; + } + + if (ppBlocker) + { + *ppBlocker = tr.m_pEnt; + } + + return false;// Line of sight is not established + } + + return true;// line of sight is valid. +} + +//========================================================= +// FVisible - returns true if a line can be traced from +// the caller's eyes to the wished position. +//========================================================= +bool CBaseEntity::FVisible( const Vector &vecTarget, int traceMask, CBaseEntity **ppBlocker ) +{ +#if HL1_DLL + + // don't look through water + // FIXME: only block LOS through opaque water + bool inWater = ( UTIL_PointContents( vecTarget ) & (CONTENTS_SLIME|CONTENTS_WATER) ) ? true : false; + + // Don't allow it if we're straddling two areas + if ( ( m_nWaterLevel == 3 && !inWater ) || ( m_nWaterLevel != 3 && inWater ) ) + return false; + +#endif + + trace_t tr; + Vector vecLookerOrigin = EyePosition();// look through the caller's 'eyes' + + if ( ai_LOS_mode.GetBool() ) + { + UTIL_TraceLine( vecLookerOrigin, vecTarget, traceMask, this, COLLISION_GROUP_NONE, &tr); + } + else + { + // If we're doing an LOS search, include NPCs. + if ( traceMask == MASK_BLOCKLOS ) + { + traceMask = MASK_BLOCKLOS_AND_NPCS; + } + + // Player sees through nodraw and blocklos + if ( IsPlayer() ) + { + traceMask |= CONTENTS_IGNORE_NODRAW_OPAQUE; + traceMask &= ~CONTENTS_BLOCKLOS; + } + + // Use the custom LOS trace filter + CTraceFilterLOS traceFilter( this, COLLISION_GROUP_NONE ); + UTIL_TraceLine( vecLookerOrigin, vecTarget, traceMask, &traceFilter, &tr ); + } + + if (tr.fraction != 1.0) + { + if (ppBlocker) + { + *ppBlocker = tr.m_pEnt; + } + return false;// Line of sight is not established + } + + return true;// line of sight is valid. +} + +extern ConVar ai_debug_los; +//----------------------------------------------------------------------------- +// Purpose: Turn on prop LOS debugging mode +//----------------------------------------------------------------------------- +void CC_AI_LOS_Debug( IConVar *var, const char *pOldString, float flOldValue ) +{ + int iLOSMode = ai_debug_los.GetInt(); + for ( CBaseEntity *pEntity = gEntList.FirstEnt(); pEntity != NULL; pEntity = gEntList.NextEnt(pEntity) ) + { + if ( iLOSMode == 1 && pEntity->IsSolid() ) + { + pEntity->m_debugOverlays |= OVERLAY_SHOW_BLOCKSLOS; + } + else if ( iLOSMode == 2 ) + { + pEntity->m_debugOverlays |= OVERLAY_SHOW_BLOCKSLOS; + } + else + { + pEntity->m_debugOverlays &= ~OVERLAY_SHOW_BLOCKSLOS; + } + } +} +ConVar ai_debug_los("ai_debug_los", "0", FCVAR_CHEAT, "NPC Line-Of-Sight debug mode. If 1, solid entities that block NPC LOC will be highlighted with white bounding boxes. If 2, it'll show non-solid entities that would do it if they were solid.", CC_AI_LOS_Debug ); + + +Class_T CBaseEntity::Classify ( void ) +{ + return CLASS_NONE; +} + +float CBaseEntity::GetAutoAimRadius() +{ + if( g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE ) + return 48.0f; + else + return 24.0f; +} + +//----------------------------------------------------------------------------- +// Changes the shadow cast distance over time +//----------------------------------------------------------------------------- +void CBaseEntity::ShadowCastDistThink( ) +{ + SetShadowCastDistance( m_flDesiredShadowCastDistance ); + SetContextThink( NULL, gpGlobals->curtime, "ShadowCastDistThink" ); +} + +void CBaseEntity::SetShadowCastDistance( float flDesiredDistance, float flDelay ) +{ + m_flDesiredShadowCastDistance = flDesiredDistance; + if ( m_flDesiredShadowCastDistance != m_flShadowCastDistance ) + { + SetContextThink( &CBaseEntity::ShadowCastDistThink, gpGlobals->curtime + flDelay, "ShadowCastDistThink" ); + } +} + + +/* +================ +TraceAttack +================ +*/ + +//----------------------------------------------------------------------------- +// Purpose: Returns whether a damage info can damage this entity. +//----------------------------------------------------------------------------- +bool CBaseEntity::PassesDamageFilter( const CTakeDamageInfo &info ) +{ + if (m_hDamageFilter) + { + CBaseFilter *pFilter = (CBaseFilter *)(m_hDamageFilter.Get()); +#ifdef MAPBASE + return pFilter->PassesDamageFilter(this, info); +#else + return pFilter->PassesDamageFilter(info); +#endif + } + + return true; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: A damage filter pass for when this is most certainly the part where we might actually take damage. +// Made for the "damage" family of filters, including filter_damage_transfer. +//----------------------------------------------------------------------------- +bool CBaseEntity::PassesFinalDamageFilter( const CTakeDamageInfo &info ) +{ + if (!PassesDamageFilter(info)) + return false; + + if (m_hDamageFilter) + { + CBaseFilter *pFilter = (CBaseFilter *)(m_hDamageFilter.Get()); + if (!pFilter->PassesFinalDamageFilter(this, info)) + { + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: A hack for damage transfers. +//----------------------------------------------------------------------------- +bool CBaseEntity::DamageFilterAllowsBlood( const CTakeDamageInfo &info ) +{ + if (m_hDamageFilter) + { + CBaseFilter *pFilter = (CBaseFilter *)(m_hDamageFilter.Get()); + if (!pFilter->BloodAllowed(this, info)) + { + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Modifies damage taken. Returns true if damage was successfully modded. +//----------------------------------------------------------------------------- +bool CBaseEntity::DamageFilterDamageMod( CTakeDamageInfo &info ) +{ + if (m_hDamageFilter) + { + CBaseFilter *pFilter = (CBaseFilter *)(m_hDamageFilter.Get()); + if (pFilter->DamageMod(this, info)) + { + return true; + } + } + + return false; +} +#endif + +FORCEINLINE bool NamesMatch( const char *pszQuery, string_t nameToMatch ) +{ +#ifdef MAPBASE + // NamesMatch has been turned into Matcher_NamesMatch in matchers.h + // for a wider range of accessibility and flexibility. + return Matcher_NamesMatch(pszQuery, STRING(nameToMatch)); +#else + if ( nameToMatch == NULL_STRING ) + return (!pszQuery || *pszQuery == 0 || *pszQuery == '*'); + + const char *pszNameToMatch = STRING(nameToMatch); + + // If the pointers are identical, we're identical + if ( pszNameToMatch == pszQuery ) + return true; + + while ( *pszNameToMatch && *pszQuery ) + { + unsigned char cName = *pszNameToMatch; + unsigned char cQuery = *pszQuery; + // simple ascii case conversion + if ( cName == cQuery ) + ; + else if ( cName - 'A' <= (unsigned char)'Z' - 'A' && cName - 'A' + 'a' == cQuery ) + ; + else if ( cName - 'a' <= (unsigned char)'z' - 'a' && cName - 'a' + 'A' == cQuery ) + ; + else + break; + ++pszNameToMatch; + ++pszQuery; + } + + if ( *pszQuery == 0 && *pszNameToMatch == 0 ) + return true; + + // @TODO (toml 03-18-03): Perhaps support real wildcards. Right now, only thing supported is trailing * + if ( *pszQuery == '*' ) + return true; + + return false; +#endif +} + +bool CBaseEntity::NameMatchesComplex( const char *pszNameOrWildcard ) +{ + if ( !Q_stricmp( "!player", pszNameOrWildcard) ) + return IsPlayer(); + + return NamesMatch( pszNameOrWildcard, m_iName ); +} + +bool CBaseEntity::ClassMatchesComplex( const char *pszClassOrWildcard ) +{ + return NamesMatch( pszClassOrWildcard, m_iClassname ); +} + +void CBaseEntity::MakeDormant( void ) +{ + AddEFlags( EFL_DORMANT ); + + // disable thinking for dormant entities + SetThink( NULL ); + + if ( !edict() ) + return; + + SETBITS( m_iEFlags, EFL_DORMANT ); + + // Don't touch + AddSolidFlags( FSOLID_NOT_SOLID ); + // Don't move + SetMoveType( MOVETYPE_NONE ); + // Don't draw + AddEffects( EF_NODRAW ); + // Don't think + SetNextThink( TICK_NEVER_THINK ); +} + +int CBaseEntity::IsDormant( void ) +{ + return IsEFlagSet( EFL_DORMANT ); +} + + +bool CBaseEntity::IsInWorld( void ) const +{ + if ( !edict() ) + return true; + + // position + if (GetAbsOrigin().x >= MAX_COORD_INTEGER) return false; + if (GetAbsOrigin().y >= MAX_COORD_INTEGER) return false; + if (GetAbsOrigin().z >= MAX_COORD_INTEGER) return false; + if (GetAbsOrigin().x <= MIN_COORD_INTEGER) return false; + if (GetAbsOrigin().y <= MIN_COORD_INTEGER) return false; + if (GetAbsOrigin().z <= MIN_COORD_INTEGER) return false; + // speed + if (GetAbsVelocity().x >= 2000) return false; + if (GetAbsVelocity().y >= 2000) return false; + if (GetAbsVelocity().z >= 2000) return false; + if (GetAbsVelocity().x <= -2000) return false; + if (GetAbsVelocity().y <= -2000) return false; + if (GetAbsVelocity().z <= -2000) return false; + + return true; +} + + +bool CBaseEntity::IsViewable( void ) +{ + if ( IsEffectActive( EF_NODRAW ) ) + { + return false; + } + + if (IsBSPModel()) + { + if (GetMoveType() != MOVETYPE_NONE) + { + return true; + } + } + else if (GetModelIndex() != 0) + { + // check for total transparency??? + return true; + } + return false; +} + + +int CBaseEntity::ShouldToggle( USE_TYPE useType, int currentState ) +{ + if ( useType != USE_TOGGLE && useType != USE_SET ) + { + if ( (currentState && useType == USE_ON) || (!currentState && useType == USE_OFF) ) + return 0; + } + return 1; +} + + +// NOTE: szName must be a pointer to constant memory, e.g. "NPC_class" because the entity +// will keep a pointer to it after this call. +CBaseEntity *CBaseEntity::Create( const char *szName, const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner ) +{ + CBaseEntity *pEntity = CreateNoSpawn( szName, vecOrigin, vecAngles, pOwner ); + + DispatchSpawn( pEntity ); + return pEntity; +} + + + +// NOTE: szName must be a pointer to constant memory, e.g. "NPC_class" because the entity +// will keep a pointer to it after this call. +CBaseEntity * CBaseEntity::CreateNoSpawn( const char *szName, const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner ) +{ + CBaseEntity *pEntity = CreateEntityByName( szName ); + if ( !pEntity ) + { + Assert( !"CreateNoSpawn: only works for CBaseEntities" ); + return NULL; + } + + pEntity->SetLocalOrigin( vecOrigin ); + pEntity->SetLocalAngles( vecAngles ); + pEntity->SetOwnerEntity( pOwner ); + + gEntList.NotifyCreateEntity( pEntity ); + + return pEntity; +} + +Vector CBaseEntity::GetSoundEmissionOrigin() const +{ + return WorldSpaceCenter(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Saves the current object out to disk, by iterating through the objects +// data description hierarchy +// Input : &save - save buffer which the class data is written to +// Output : int - 0 if the save failed, 1 on success +//----------------------------------------------------------------------------- +int CBaseEntity::Save( ISave &save ) +{ + // loop through the data description list, saving each data desc block + int status = SaveDataDescBlock( save, GetDataDescMap() ); + + return status; +} + +//----------------------------------------------------------------------------- +// Purpose: Recursively saves all the classes in an object, in reverse order (top down) +// Output : int 0 on failure, 1 on success +//----------------------------------------------------------------------------- +int CBaseEntity::SaveDataDescBlock( ISave &save, datamap_t *dmap ) +{ + return save.WriteAll( this, dmap ); +} + +//----------------------------------------------------------------------------- +// Purpose: Restores the current object from disk, by iterating through the objects +// data description hierarchy +// Input : &restore - restore buffer which the class data is read from +// Output : int - 0 if the restore failed, 1 on success +//----------------------------------------------------------------------------- +int CBaseEntity::Restore( IRestore &restore ) +{ + // This is essential to getting the spatial partition info correct + CollisionProp()->DestroyPartitionHandle(); + + // loops through the data description list, restoring each data desc block in order + int status = RestoreDataDescBlock( restore, GetDataDescMap() ); + + // --------------------------------------------------------------- + // HACKHACK: We don't know the space of these vectors until now + // if they are worldspace, fix them up. + // --------------------------------------------------------------- + { + CGameSaveRestoreInfo *pGameInfo = restore.GetGameSaveRestoreInfo(); + Vector parentSpaceOffset = pGameInfo->modelSpaceOffset; + if ( !GetParent() ) + { + // parent is the world, so parent space is worldspace + // so update with the worldspace leveltransition transform + parentSpaceOffset += pGameInfo->GetLandmark(); + } + + // NOTE: Do *not* use GetAbsOrigin() here because it will + // try to recompute m_rgflCoordinateFrame! + MatrixSetColumn( m_vecAbsOrigin, 3, m_rgflCoordinateFrame ); + + m_vecOrigin += parentSpaceOffset; + } + + // Gotta do this after the coordframe is set up as it depends on it. + + // By definition, the surrounding bounds are dirty + // Also, twiddling with the flags here ensures it gets added to the KD tree dirty list + // (We don't want to use the saved version of this flag) + RemoveEFlags( EFL_DIRTY_SPATIAL_PARTITION ); + CollisionProp()->MarkSurroundingBoundsDirty(); + + if ( edict() && GetModelIndex() != 0 && GetModelName() != NULL_STRING && restore.GetPrecacheMode() ) + { + PrecacheModel( STRING( GetModelName() ) ); + + //Adrian: We should only need to do this after we precache. No point in setting the model again. + SetModelIndex( modelinfo->GetModelIndex( STRING(GetModelName() ) ) ); + } + + // Restablish ground entity + if ( m_hGroundEntity != NULL ) + { + m_hGroundEntity->AddEntityToGroundList( this ); + } + + return status; +} + + +//----------------------------------------------------------------------------- +// handler to do stuff before you are saved +//----------------------------------------------------------------------------- +void CBaseEntity::OnSave( IEntitySaveUtils *pUtils ) +{ + // Here, we must force recomputation of all abs data so it gets saved correctly + // We can't leave the dirty bits set because the loader can't cope with it. + CalcAbsolutePosition(); + CalcAbsoluteVelocity(); +} + +//----------------------------------------------------------------------------- +// handler to do stuff after you are restored +//----------------------------------------------------------------------------- +void CBaseEntity::OnRestore() +{ +#ifndef MAPBASE // It's your fault if you're trying to load old, broken saves from a possibly closed 2013 beta in Mapbase. +#if defined( PORTAL ) || defined( HL2_EPISODIC ) || defined ( HL2_DLL ) || defined( HL2_LOSTCOAST ) + // We had a short period during the 2013 beta where the FL_* flags had a bogus value near the top, so detect + // these bad saves and just give up. Only saves from the short beta period should have been effected. + if ( GetFlags() & FL_FAKECLIENT ) + { + char szMsg[256]; + V_snprintf( szMsg, sizeof(szMsg), "\nInvalid save, unable to load. Please run \"map %s\" to restart this level manually\n\n", gpGlobals->mapname.ToCStr() ); + Msg( "%s", szMsg ); + + engine->ServerCommand("wait;wait;disconnect;showconsole\n"); + } +#endif +#endif + + SimThink_EntityChanged( this ); + + // touchlinks get recomputed + if ( IsEFlagSet( EFL_CHECK_UNTOUCH ) ) + { + RemoveEFlags( EFL_CHECK_UNTOUCH ); + SetCheckUntouch( true ); + } + + // disable touch functions while we recreate the touch links between entities + // NOTE: We don't do this on transitions, because we'd miss the OnStartTouch call! +#if !defined(HL2_DLL) || ( defined(HL2_DLL) && defined(HL2_EPISODIC) ) + CBaseEntity::sm_bDisableTouchFuncs = ( gpGlobals->eLoadType != MapLoad_Transition ); + PhysicsTouchTriggers(); + CBaseEntity::sm_bDisableTouchFuncs = false; +#endif // HL2_EPISODIC + + //Adrian: If I'm restoring with these fields it means I've become a client side ragdoll. + //Don't create another one, just wait until is my time of being removed. + if ( GetFlags() & FL_TRANSRAGDOLL ) + { + m_nRenderFX = kRenderFxNone; + AddEffects( EF_NODRAW ); + RemoveFlag( FL_DISSOLVING | FL_ONFIRE ); + } + + if ( m_pParent ) + { + CBaseEntity *pChild = m_pParent->FirstMoveChild(); + while ( pChild ) + { + if ( pChild == this ) + break; + pChild = pChild->NextMovePeer(); + } + if ( pChild != this ) + { +#if _DEBUG + // generally this means you've got something marked FCAP_DONT_SAVE + // in a hierarchy. That's probably ok given this fixup, but the hierarhcy + // linked list is just saved/loaded in-place + Warning("Fixing up parent on %s\n", GetClassname() ); +#endif + // We only need to be back in the parent's list because we're already in the right place and with the right data + LinkChild( m_pParent, this ); + } + } + + // We're not save/loading the PVS dirty state. Assume everything is dirty after a restore + NetworkProp()->MarkPVSInformationDirty(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Recursively restores all the classes in an object, in reverse order (top down) +// Output : int 0 on failure, 1 on success +//----------------------------------------------------------------------------- +int CBaseEntity::RestoreDataDescBlock( IRestore &restore, datamap_t *dmap ) +{ + return restore.ReadAll( this, dmap ); +} + +//----------------------------------------------------------------------------- + +bool CBaseEntity::ShouldSavePhysics() +{ + return true; +} + +//----------------------------------------------------------------------------- + +#include "tier0/memdbgoff.h" + +//----------------------------------------------------------------------------- +// CBaseEntity new/delete +// allocates and frees memory for itself from the engine-> +// All fields in the object are all initialized to 0. +//----------------------------------------------------------------------------- +void *CBaseEntity::operator new( size_t stAllocateBlock ) +{ + // call into engine to get memory + Assert( stAllocateBlock != 0 ); + return engine->PvAllocEntPrivateData(stAllocateBlock); +}; + +void *CBaseEntity::operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ) +{ + // call into engine to get memory + Assert( stAllocateBlock != 0 ); + return engine->PvAllocEntPrivateData(stAllocateBlock); +} + +void CBaseEntity::operator delete( void *pMem ) +{ + // get the engine to free the memory + engine->FreeEntPrivateData( pMem ); +} + +#include "tier0/memdbgon.h" + + +#ifdef _DEBUG +void CBaseEntity::FunctionCheck( void *pFunction, const char *name ) +{ +#ifdef USES_SAVERESTORE + // Note, if you crash here and your class is using multiple inheritance, it is + // probably the case that CBaseEntity (or a descendant) is not the first + // class in your list of ancestors, which it must be. + if (pFunction && !UTIL_FunctionToName( GetDataDescMap(), (inputfunc_t *)pFunction ) ) + { + Warning( "FUNCTION NOT IN TABLE!: %s:%s (%08lx)\n", STRING(m_iClassname), name, (unsigned long)pFunction ); + Assert(0); + } +#endif +} +#endif + + +bool CBaseEntity::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ + return false; +} + +//----------------------------------------------------------------------------- +// Perform hitbox test, returns true *if hitboxes were tested at all*!! +//----------------------------------------------------------------------------- +bool CBaseEntity::TestHitboxes( const Ray_t &ray, unsigned int fContentsMask, trace_t& tr ) +{ + return false; +} + + +void CBaseEntity::SetOwnerEntity( CBaseEntity* pOwner ) +{ + if ( m_hOwnerEntity.Get() != pOwner ) + { + m_hOwnerEntity = pOwner; + + CollisionRulesChanged(); + } +} + +void CBaseEntity::SetMoveType( MoveType_t val, MoveCollide_t moveCollide ) +{ +#ifdef _DEBUG + // Make sure the move type + move collide are compatible... + if ((val != MOVETYPE_FLY) && (val != MOVETYPE_FLYGRAVITY)) + { + Assert( moveCollide == MOVECOLLIDE_DEFAULT ); + } + + if ( m_MoveType == MOVETYPE_VPHYSICS && val != m_MoveType ) + { + if ( VPhysicsGetObject() && val != MOVETYPE_NONE ) + { + // What am I supposed to do with the physics object if + // you're changing away from MOVETYPE_VPHYSICS without making the object + // shadow? This isn't likely to work, assert. + // You probably meant to call VPhysicsInitShadow() instead of VPhysicsInitNormal()! + Assert( VPhysicsGetObject()->GetShadowController() ); + } + } +#endif + + if ( m_MoveType == val ) + { + m_MoveCollide = moveCollide; + return; + } + + // This is needed to the removal of MOVETYPE_FOLLOW: + // We can't transition from follow to a different movetype directly + // or the leaf code will break. + Assert( !IsEffectActive( EF_BONEMERGE ) ); + m_MoveType = val; + m_MoveCollide = moveCollide; + + CollisionRulesChanged(); + + switch( m_MoveType ) + { + case MOVETYPE_WALK: + { + SetSimulatedEveryTick( true ); + SetAnimatedEveryTick( true ); + } + break; + case MOVETYPE_STEP: + { + // This will probably go away once I remove the cvar that controls the test code + SetSimulatedEveryTick( g_bTestMoveTypeStepSimulation ? true : false ); + SetAnimatedEveryTick( false ); + } + break; + case MOVETYPE_FLY: + case MOVETYPE_FLYGRAVITY: + { + // Initialize our water state, because these movetypes care about transitions in/out of water + UpdateWaterState(); + } + break; + default: + { + SetSimulatedEveryTick( true ); + SetAnimatedEveryTick( false ); + } + } + + // This will probably go away or be handled in a better way once I remove the cvar that controls the test code + CheckStepSimulationChanged(); + CheckHasGamePhysicsSimulation(); +} + +void CBaseEntity::Spawn( void ) +{ +} + + +CBaseEntity* CBaseEntity::Instance( const CBaseHandle &hEnt ) +{ + return gEntList.GetBaseEntity( hEnt ); +} + +int CBaseEntity::GetTransmitState( void ) +{ + edict_t *ed = edict(); + + if ( !ed ) + return 0; + + return ed->m_fStateFlags; +} + +int CBaseEntity::SetTransmitState( int nFlag) +{ + edict_t *ed = edict(); + + if ( !ed ) + return 0; + + // clear current flags = check ShouldTransmit() + ed->ClearTransmitState(); + + int oldFlags = ed->m_fStateFlags; + ed->m_fStateFlags |= nFlag; + + // Tell the engine (used for a network backdoor optimization). + if ( (oldFlags & FL_EDICT_DONTSEND) != (ed->m_fStateFlags & FL_EDICT_DONTSEND) ) + engine->NotifyEdictFlagsChange( entindex() ); + + return ed->m_fStateFlags; +} + +int CBaseEntity::UpdateTransmitState() +{ + // If you get this assert, you should be calling DispatchUpdateTransmitState + // instead of UpdateTransmitState. + Assert( g_nInsideDispatchUpdateTransmitState > 0 ); + + // If an object is the moveparent of something else, don't skip it just because it's marked EF_NODRAW or else + // the client won't have a proper origin for the child since the hierarchy won't be correctly transmitted down + if ( IsEffectActive( EF_NODRAW ) && + !m_hMoveChild.Get() ) + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } + + if ( !IsEFlagSet( EFL_FORCE_CHECK_TRANSMIT ) ) + { + if ( !GetModelIndex() || !GetModelName() ) + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } + } + + // Always send the world + if ( GetModelIndex() == 1 ) + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + if ( IsEFlagSet( EFL_IN_SKYBOX ) ) + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + // by default cull against PVS + return SetTransmitState( FL_EDICT_PVSCHECK ); +} + +int CBaseEntity::DispatchUpdateTransmitState() +{ + edict_t *ed = edict(); + if ( m_nTransmitStateOwnedCounter != 0 ) + return ed ? ed->m_fStateFlags : 0; + + g_nInsideDispatchUpdateTransmitState++; + int ret = UpdateTransmitState(); + g_nInsideDispatchUpdateTransmitState--; + + return ret; +} + +//----------------------------------------------------------------------------- +// Purpose: Note, an entity can override the send table ( e.g., to send less data or to send minimal data for +// objects ( prob. players ) that are not in the pvs. +// Input : **ppSendTable - +// *recipient - +// *pvs - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +int CBaseEntity::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + int fFlags = DispatchUpdateTransmitState(); + + if ( fFlags & FL_EDICT_PVSCHECK ) + { + return FL_EDICT_PVSCHECK; + } + else if ( fFlags & FL_EDICT_ALWAYS ) + { + return FL_EDICT_ALWAYS; + } + else if ( fFlags & FL_EDICT_DONTSEND ) + { + return FL_EDICT_DONTSEND; + } + +// if ( IsToolRecording() ) +// { +// return FL_EDICT_ALWAYS; +// } + + CBaseEntity *pRecipientEntity = CBaseEntity::Instance( pInfo->m_pClientEnt ); + + Assert( pRecipientEntity->IsPlayer() ); + + CBasePlayer *pRecipientPlayer = static_cast( pRecipientEntity ); + + + // FIXME: Refactor once notion of "team" is moved into HL2 code + // Team rules may tell us that we should + if ( pRecipientPlayer->GetTeam() ) + { + if ( pRecipientPlayer->GetTeam()->ShouldTransmitToPlayer( pRecipientPlayer, this )) + return FL_EDICT_ALWAYS; + } + + +/*#ifdef INVASION_DLL + // Check test network vis distance stuff. Eventually network LOD will do this. + float flTestDistSqr = pRecipientEntity->GetAbsOrigin().DistToSqr( WorldSpaceCenter() ); + if ( flTestDistSqr > sv_netvisdist.GetFloat() * sv_netvisdist.GetFloat() ) + return TRANSMIT_NO; // TODO doesn't work with HLTV +#endif*/ + + // by default do a PVS check + + return FL_EDICT_PVSCHECK; +} + + +//----------------------------------------------------------------------------- +// Rules about which entities need to transmit along with me +//----------------------------------------------------------------------------- +void CBaseEntity::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + int index = entindex(); + + // Are we already marked for transmission? + if ( pInfo->m_pTransmitEdict->Get( index ) ) + return; + + CServerNetworkProperty *pNetworkParent = NetworkProp()->GetNetworkParent(); + + pInfo->m_pTransmitEdict->Set( index ); + + // HLTV/Replay need to know if this entity is culled by PVS limits + if ( pInfo->m_pTransmitAlways ) + { + // in HLTV/Replay mode always transmit entitys with move-parents + // HLTV/Replay can't resolve the mode-parents relationships + if ( bAlways || pNetworkParent ) + { + // tell HLTV/Replay that this entity is always transmitted + pInfo->m_pTransmitAlways->Set( index ); + } + else + { + // HLTV/Replay will PVS cull this entity, so update the + // node/cluster infos if necessary + m_Network.RecomputePVSInformation(); + } + } + + // Force our aiment and move parent to be sent. + if ( pNetworkParent ) + { + CBaseEntity *pMoveParent = pNetworkParent->GetBaseEntity(); + pMoveParent->SetTransmit( pInfo, bAlways ); + } +} + + +//----------------------------------------------------------------------------- +// Returns which skybox the entity is in +//----------------------------------------------------------------------------- +CSkyCamera *CBaseEntity::GetEntitySkybox() +{ + int area = engine->GetArea( WorldSpaceCenter() ); + + CSkyCamera *pCur = GetSkyCameraList(); + while ( pCur ) + { + if ( engine->CheckAreasConnected( area, pCur->m_skyboxData.area ) ) + return pCur; + + pCur = pCur->m_pNext; + } + + return NULL; +} + +bool CBaseEntity::DetectInSkybox() +{ + if ( GetEntitySkybox() != NULL ) + { + AddEFlags( EFL_IN_SKYBOX ); + return true; + } + + RemoveEFlags( EFL_IN_SKYBOX ); + return false; +} + + +//------------------------------------------------------------------------------ +// Computes a world-aligned bounding box that surrounds everything in the entity +//------------------------------------------------------------------------------ +void CBaseEntity::ComputeWorldSpaceSurroundingBox( Vector *pMins, Vector *pMaxs ) +{ + // Should never get here.. only use USE_GAME_CODE with bounding boxes + // if you have an implementation for this method + Assert( 0 ); +} + + +//------------------------------------------------------------------------------ +// Purpose : If name exists returns name, otherwise returns classname +// Input : +// Output : +//------------------------------------------------------------------------------ +const char *CBaseEntity::GetDebugName(void) +{ + if ( this == NULL ) + return "<>"; + + if ( m_iName.Get() != NULL_STRING ) + { + return STRING(m_iName.Get()); + } + else + { + return STRING(m_iClassname); + } +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseEntity::DrawInputOverlay(const char *szInputName, CBaseEntity *pCaller, variant_t Value) +{ + char bigstring[1024]; + if ( Value.FieldType() == FIELD_INTEGER ) + { + Q_snprintf( bigstring,sizeof(bigstring), "%3.1f (%s,%d) <-- (%s)\n", gpGlobals->curtime, szInputName, Value.Int(), pCaller ? pCaller->GetDebugName() : NULL); + } + else if ( Value.FieldType() == FIELD_STRING ) + { + Q_snprintf( bigstring,sizeof(bigstring), "%3.1f (%s,%s) <-- (%s)\n", gpGlobals->curtime, szInputName, Value.String(), pCaller ? pCaller->GetDebugName() : NULL); + } + else + { + Q_snprintf( bigstring,sizeof(bigstring), "%3.1f (%s) <-- (%s)\n", gpGlobals->curtime, szInputName, pCaller ? pCaller->GetDebugName() : NULL); + } + AddTimedOverlay(bigstring, 10.0); + + if ( Value.FieldType() == FIELD_INTEGER ) + { + DevMsg( 2, "input: (%s,%d) -> (%s,%s), from (%s)\n", szInputName, Value.Int(), STRING(m_iClassname), GetDebugName(), pCaller ? pCaller->GetDebugName() : NULL); + } + else if ( Value.FieldType() == FIELD_STRING ) + { + DevMsg( 2, "input: (%s,%s) -> (%s,%s), from (%s)\n", szInputName, Value.String(), STRING(m_iClassname), GetDebugName(), pCaller ? pCaller->GetDebugName() : NULL); + } + else + DevMsg( 2, "input: (%s) -> (%s,%s), from (%s)\n", szInputName, STRING(m_iClassname), GetDebugName(), pCaller ? pCaller->GetDebugName() : NULL); +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseEntity::DrawOutputOverlay(CEventAction *ev) +{ + // Print to entity + char bigstring[1024]; + if ( ev->m_flDelay ) + { + Q_snprintf( bigstring,sizeof(bigstring), "%3.1f (%s) --> (%s),%.1f) \n", gpGlobals->curtime, STRING(ev->m_iTargetInput), STRING(ev->m_iTarget), ev->m_flDelay); + } + else + { + Q_snprintf( bigstring,sizeof(bigstring), "%3.1f (%s) --> (%s)\n", gpGlobals->curtime, STRING(ev->m_iTargetInput), STRING(ev->m_iTarget)); + } + AddTimedOverlay(bigstring, 10.0); + + // Now print to the console + if ( ev->m_flDelay ) + { + DevMsg( 2, "output: (%s,%s) -> (%s,%s,%.1f)\n", STRING(m_iClassname), GetDebugName(), STRING(ev->m_iTarget), STRING(ev->m_iTargetInput), ev->m_flDelay ); + } + else + { + DevMsg( 2, "output: (%s,%s) -> (%s,%s)\n", STRING(m_iClassname), GetDebugName(), STRING(ev->m_iTarget), STRING(ev->m_iTargetInput) ); + } +} + + +//----------------------------------------------------------------------------- +// Entity events... these are events targetted to a particular entity +// Each event defines its own well-defined event data structure +//----------------------------------------------------------------------------- +void CBaseEntity::OnEntityEvent( EntityEvent_t event, void *pEventData ) +{ + switch( event ) + { + case ENTITY_EVENT_WATER_TOUCH: + { + int nContents = (int)pEventData; + if ( !nContents || (nContents & CONTENTS_WATER) ) + { + ++m_nWaterTouch; + } + if ( nContents & CONTENTS_SLIME ) + { + ++m_nSlimeTouch; + } + } + break; + + case ENTITY_EVENT_WATER_UNTOUCH: + { + int nContents = (int)pEventData; + if ( !nContents || (nContents & CONTENTS_WATER) ) + { + --m_nWaterTouch; + } + if ( nContents & CONTENTS_SLIME ) + { + --m_nSlimeTouch; + } + } + break; + + default: + return; + } + + // Only do this for vphysics objects + if ( GetMoveType() != MOVETYPE_VPHYSICS ) + return; + + int nNewContents = 0; + if ( m_nWaterTouch > 0 ) + { + nNewContents |= CONTENTS_WATER; + } + + if ( m_nSlimeTouch > 0 ) + { + nNewContents |= CONTENTS_SLIME; + } + + if (( nNewContents & MASK_WATER ) == 0) + { + SetWaterLevel( 0 ); + SetWaterType( CONTENTS_EMPTY ); + return; + } + + SetWaterLevel( 1 ); + SetWaterType( nNewContents ); +} + + +ConVar ent_messages_draw( "ent_messages_draw", "0", FCVAR_CHEAT, "Visualizes all entity input/output activity." ); + + +//----------------------------------------------------------------------------- +// Purpose: calls the appropriate message mapped function in the entity according +// to the fired action. +// Input : char *szInputName - input destination +// *pActivator - entity which initiated this sequence of actions +// *pCaller - entity from which this event is sent +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseEntity::AcceptInput( const char *szInputName, CBaseEntity *pActivator, CBaseEntity *pCaller, variant_t Value, int outputID ) +{ + if ( ent_messages_draw.GetBool() ) + { + if ( pCaller != NULL ) + { + NDebugOverlay::Line( pCaller->GetAbsOrigin(), GetAbsOrigin(), 255, 255, 255, false, 3 ); + NDebugOverlay::Box( pCaller->GetAbsOrigin(), Vector(-4, -4, -4), Vector(4, 4, 4), 255, 0, 0, 0, 3 ); + } + + NDebugOverlay::Text( GetAbsOrigin(), szInputName, false, 3 ); + NDebugOverlay::Box( GetAbsOrigin(), Vector(-4, -4, -4), Vector(4, 4, 4), 0, 255, 0, 0, 3 ); + } + + // loop through the data description list, restoring each data desc block + for ( datamap_t *dmap = GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the actions in the data description, looking for a match + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & FTYPEDESC_INPUT ) + { + if ( !Q_stricmp(dmap->dataDesc[i].externalName, szInputName) ) + { + // found a match + + // mapper debug message +#ifdef MAPBASE + CGMsg( 2, CON_GROUP_IO_SYSTEM, "(%0.2f) input %s: %s.%s(%s)\n", gpGlobals->curtime, pCaller ? STRING(pCaller->m_iName.Get()) : "", GetDebugName(), szInputName, Value.String() ); +#else + DevMsg( 2, "(%0.2f) input %s: %s.%s(%s)\n", gpGlobals->curtime, pCaller ? STRING(pCaller->m_iName.Get()) : "", GetDebugName(), szInputName, Value.String() ); +#endif + ADD_DEBUG_HISTORY( HISTORY_ENTITY_IO, szBuffer ); + + if (m_debugOverlays & OVERLAY_MESSAGE_BIT) + { + DrawInputOverlay(szInputName,pCaller,Value); + } + + // convert the value if necessary + if ( Value.FieldType() != dmap->dataDesc[i].fieldType ) + { + if ( !(Value.FieldType() == FIELD_VOID && dmap->dataDesc[i].fieldType == FIELD_STRING) ) // allow empty strings + { +#ifdef MAPBASE + // Activator, etc. support for EHANDLE convert + if ( !Value.Convert( (fieldtype_t)dmap->dataDesc[i].fieldType, this, pActivator, pCaller ) ) + { + bool bBadConversion = true; + + // Attempt to convert to string and back. + // Almost all field types support being converted to a string, and many support being parsed from a string too. + fieldtype_t originalfield = Value.FieldType(); + if (Value.Convert(FIELD_STRING)) + { + bBadConversion = !(Value.Convert((fieldtype_t)dmap->dataDesc[i].fieldType, this, pActivator, pCaller)); + if (!bBadConversion) + { + // Actual support should be added for each field, but if it works, it works. + // Warning against it only matters if you're a programmer and want to add support for each field. + // Only send a warning in dev mode. + DevWarning("!! Had to convert to string and back\n" + "!! Source Field Type: %i, Target Field Type: %i\n", + originalfield, dmap->dataDesc[i].fieldType); + } + } + + if (bBadConversion) + { + Warning( "!! ERROR: bad input/output link:\n!! Unable to convert value \"%s\" from %s (%s) to field type %i\n!! Target Entity: %s (%s), Input: %s\n", + Value.GetDebug(), + ( pCaller != NULL ) ? STRING(pCaller->m_iClassname) : "", + ( pCaller != NULL ) ? STRING(pCaller->m_iName.Get()) : "", + dmap->dataDesc[i].fieldType, + STRING(m_iClassname), GetDebugName(), szInputName ); + return false; + } + } +#else + if ( !Value.Convert( (fieldtype_t)dmap->dataDesc[i].fieldType ) ) + { + // bad conversion + Warning( "!! ERROR: bad input/output link:\n!! %s(%s,%s) doesn't match type from %s(%s)\n", + STRING(m_iClassname), GetDebugName(), szInputName, + ( pCaller != NULL ) ? STRING(pCaller->m_iClassname) : "", + ( pCaller != NULL ) ? STRING(pCaller->m_iName.Get()) : "" ); + return false; + } +#endif + } + } + + // call the input handler, or if there is none just set the value + inputfunc_t pfnInput = dmap->dataDesc[i].inputFunc; + + if ( pfnInput ) + { + // Package the data into a struct for passing to the input handler. + inputdata_t data; + data.pActivator = pActivator; + data.pCaller = pCaller; + data.value = Value; + data.nOutputID = outputID; + + + // Now, see if there's a function named Input in this entity's script file. + // If so, execute it and let it decide whether to allow the default behavior to also execute. + bool bCallInputFunc = true; // Always assume default behavior (do call the input function) + + if ( m_ScriptScope.IsInitialized() ) + { + ScriptVariant_t functionReturn; + if ( ScriptInputHook( szInputName, pActivator, pCaller, Value, functionReturn ) ) + { + bCallInputFunc = functionReturn.m_bool; + } + } + + if( bCallInputFunc ) + { + (this->*pfnInput)( data ); + } + } + else if ( dmap->dataDesc[i].flags & FTYPEDESC_KEY ) + { + // set the value directly + Value.SetOther( ((char*)this) + dmap->dataDesc[i].fieldOffset[ TD_OFFSET_NORMAL ]); + + // TODO: if this becomes evil and causes too many full entity updates, then we should make + // a macro like this: + // + // define MAKE_INPUTVAR(x) void Note##x##Modified() { x.GetForModify(); } + // + // Then the datadesc points at that function and we call it here. The only pain is to add + // that function for all the DEFINE_INPUT calls. + NetworkStateChanged(); + } + + return true; + } + } + } + } + +#ifdef MAPBASE_VSCRIPT + // Allow VScript to handle unhandled inputs. + if (m_ScriptScope.IsInitialized()) + { + ScriptVariant_t functionReturn; + + if ( ScriptInputHook( szInputName, pActivator, pCaller, Value, functionReturn ) ) + { + if (functionReturn.m_bool) + return true; + } + } +#endif + +#ifdef MAPBASE + CGMsg( 2, CON_GROUP_IO_SYSTEM, "unhandled input: (%s) -> (%s,%s)\n", szInputName, STRING(m_iClassname), GetDebugName() ); +#else + DevMsg( 2, "unhandled input: (%s) -> (%s,%s)\n", szInputName, STRING(m_iClassname), GetDebugName()/*,", from (%s,%s)" STRING(pCaller->m_iClassname), STRING(pCaller->m_iName.Get())*/ ); +#endif + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseEntity::ScriptInputHook( const char *szInputName, CBaseEntity *pActivator, CBaseEntity *pCaller, variant_t Value, ScriptVariant_t &functionReturn ) +{ + char szScriptFunctionName[255]; + Q_strcpy( szScriptFunctionName, "Input" ); + Q_strcat( szScriptFunctionName, szInputName, 255 ); + + g_pScriptVM->SetValue( "activator", ( pActivator ) ? ScriptVariant_t( pActivator->GetScriptInstance() ) : SCRIPT_VARIANT_NULL ); + g_pScriptVM->SetValue( "caller", ( pCaller ) ? ScriptVariant_t( pCaller->GetScriptInstance() ) : SCRIPT_VARIANT_NULL ); +#ifdef MAPBASE_VSCRIPT + Value.SetScriptVariant( functionReturn ); + g_pScriptVM->SetValue( "parameter", functionReturn ); +#endif + + bool bHandled = false; + if( CallScriptFunction( szScriptFunctionName, &functionReturn ) ) + { + bHandled = true; + } + + g_pScriptVM->ClearValue( "activator" ); + g_pScriptVM->ClearValue( "caller" ); +#ifdef MAPBASE_VSCRIPT + g_pScriptVM->ClearValue( "parameter" ); +#endif + + return bHandled; +} + +#ifdef MAPBASE_VSCRIPT +bool CBaseEntity::ScriptAcceptInput( const char *szInputName, const char *szValue, HSCRIPT hActivator, HSCRIPT hCaller ) +{ + variant_t value; + value.SetString( MAKE_STRING(szValue) ); + + return AcceptInput( szInputName, ToEnt(hActivator), ToEnt(hCaller), value, 0 ); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for the entity alpha. +// Input : nAlpha - Alpha value (0 - 255). +//----------------------------------------------------------------------------- +void CBaseEntity::InputAlpha( inputdata_t &inputdata ) +{ + SetRenderColorA( clamp( inputdata.value.Int(), 0, 255 ) ); +} + + +//----------------------------------------------------------------------------- +// Activate alternative sorting +//----------------------------------------------------------------------------- +void CBaseEntity::InputAlternativeSorting( inputdata_t &inputdata ) +{ + m_bAlternateSorting = inputdata.value.Bool(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for the entity color. Ignores alpha since that is handled +// by a separate input handler. +// Input : Color32 new value for color (alpha is ignored). +//----------------------------------------------------------------------------- +void CBaseEntity::InputColor( inputdata_t &inputdata ) +{ + color32 clr = inputdata.value.Color32(); + + SetRenderColor( clr.r, clr.g, clr.b ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called whenever the entity is 'Used'. This can be when a player hits +// use, or when an entity targets it without an output name (legacy entities) +//----------------------------------------------------------------------------- +void CBaseEntity::InputUse( inputdata_t &inputdata ) +{ + Use( inputdata.pActivator, inputdata.pCaller, (USE_TYPE)inputdata.nOutputID, 0 ); + +#ifdef MAPBASE + IGameEvent *event = gameeventmanager->CreateEvent( "player_use" ); + if ( event ) + { + event->SetInt( "userid", inputdata.pActivator && inputdata.pActivator->IsPlayer() ? + ((CBasePlayer*)inputdata.pActivator)->GetUserID() : 0 ); + event->SetInt( "entity", entindex() ); + gameeventmanager->FireEvent( event ); + } +#endif // MAPBASE +} + + +//----------------------------------------------------------------------------- +// Purpose: Reads an output variable, by string name, from an entity +// Input : char *varName - the string name of the variable +// variant_t *var - the value is stored here +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseEntity::ReadKeyField( const char *varName, variant_t *var ) +{ + if ( !varName ) + return false; + + // loop through the data description list, restoring each data desc block + for ( datamap_t *dmap = GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the readable fields in the data description, looking for a match + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & (FTYPEDESC_OUTPUT | FTYPEDESC_KEY) ) + { + if ( !Q_stricmp(dmap->dataDesc[i].externalName, varName) ) + { + var->Set( dmap->dataDesc[i].fieldType, ((char*)this) + dmap->dataDesc[i].fieldOffset[ TD_OFFSET_NORMAL ] ); + return true; + } + } + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the damage filter on the object +//----------------------------------------------------------------------------- +void CBaseEntity::InputEnableDamageForces( inputdata_t &inputdata ) +{ + RemoveEFlags( EFL_NO_DAMAGE_FORCES ); +} + +void CBaseEntity::InputDisableDamageForces( inputdata_t &inputdata ) +{ + AddEFlags( EFL_NO_DAMAGE_FORCES ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the damage filter on the object +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetDamageFilter( inputdata_t &inputdata ) +{ + // Get a handle to my damage filter entity if there is one. + m_iszDamageFilterName = inputdata.value.StringID(); + if ( m_iszDamageFilterName != NULL_STRING ) + { + m_hDamageFilter = gEntList.FindEntityByName( NULL, m_iszDamageFilterName ); + } + else + { + m_hDamageFilter = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Dispatch effects on this entity +//----------------------------------------------------------------------------- +void CBaseEntity::InputDispatchEffect( inputdata_t &inputdata ) +{ + const char *sEffect = inputdata.value.String(); + if ( sEffect && sEffect[0] ) + { + CEffectData data; + GetInputDispatchEffectPosition( sEffect, data.m_vOrigin, data.m_vAngles ); + AngleVectors( data.m_vAngles, &data.m_vNormal ); + data.m_vStart = data.m_vOrigin; + data.m_nEntIndex = entindex(); + + // Clip off leading attachment point numbers + while ( sEffect[0] >= '0' && sEffect[0] <= '9' ) + { + sEffect++; + } + DispatchEffect( sEffect, data ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the origin at which to play an inputted dispatcheffect +//----------------------------------------------------------------------------- +void CBaseEntity::GetInputDispatchEffectPosition( const char *sInputString, Vector &pOrigin, QAngle &pAngles ) +{ + pOrigin = GetAbsOrigin(); + pAngles = GetAbsAngles(); +} + +//----------------------------------------------------------------------------- +// Purpose: Marks the entity for deletion +//----------------------------------------------------------------------------- +void CBaseEntity::InputKill( inputdata_t &inputdata ) +{ + // tell owner ( if any ) that we're dead.This is mostly for NPCMaker functionality. + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + pOwner->DeathNotice( this ); + SetOwnerEntity( NULL ); + } + +#ifdef MAPBASE + m_OnKilled.FireOutput( inputdata.pActivator, this ); +#endif + +#ifdef MAPBASE + // Kick players + if ( IsPlayer() ) + { + engine->ServerCommand( UTIL_VarArgs( "kickid %d CBaseEntity::InputKill()\n", engine->GetPlayerUserId(edict()) ) ); + } + else + { + UTIL_Remove( this ); + } +#else + UTIL_Remove( this ); +#endif +} + +void CBaseEntity::InputKillHierarchy( inputdata_t &inputdata ) +{ + CBaseEntity *pChild, *pNext; + for ( pChild = FirstMoveChild(); pChild; pChild = pNext ) + { + pNext = pChild->NextMovePeer(); + pChild->InputKillHierarchy( inputdata ); + } + + // tell owner ( if any ) that we're dead. This is mostly for NPCMaker functionality. + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + pOwner->DeathNotice( this ); + SetOwnerEntity( NULL ); + } + +#ifdef MAPBASE + m_OnKilled.FireOutput( inputdata.pActivator, this ); + + // Kicking players in InputKillHierarchy does not exist in future Valve games + // if ( IsPlayer() ) +#endif + + UTIL_Remove( this ); +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler for changing this entity's movement parent. +//------------------------------------------------------------------------------ +void CBaseEntity::InputSetParent( inputdata_t &inputdata ) +{ + // If we had a parent attachment, clear it, because it's no longer valid. + if ( m_iParentAttachment ) + { + m_iParentAttachment = 0; + } + + SetParent( inputdata.value.StringID(), inputdata.pActivator ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CBaseEntity::SetParentAttachment( const char *szInputName, const char *szAttachment, bool bMaintainOffset ) +{ + // Must have a parent + if ( !m_pParent ) + { + Warning("ERROR: Tried to %s for entity %s (%s), but it has no parent.\n", szInputName, GetClassname(), GetDebugName() ); + return; + } + + // Valid only on CBaseAnimating + CBaseAnimating *pAnimating = m_pParent->GetBaseAnimating(); + if ( !pAnimating ) + { + Warning("ERROR: Tried to %s for entity %s (%s), but its parent has no model.\n", szInputName, GetClassname(), GetDebugName() ); + return; + } + + // Lookup the attachment + int iAttachment = pAnimating->LookupAttachment( szAttachment ); + if ( iAttachment <= 0 ) + { + Warning("ERROR: Tried to %s for entity %s (%s), but it has no attachment named %s.\n", szInputName, GetClassname(), GetDebugName(), szAttachment ); + return; + } + + m_iParentAttachment = iAttachment; + SetParent( m_pParent, m_iParentAttachment ); + + // Now move myself directly onto the attachment point + SetMoveType( MOVETYPE_NONE ); + + if ( !bMaintainOffset ) + { + SetLocalOrigin( vec3_origin ); + SetLocalAngles( vec3_angle ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for changing this entity's movement parent's attachment point +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetParentAttachment( inputdata_t &inputdata ) +{ + SetParentAttachment( "SetParentAttachment", inputdata.value.String(), false ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for changing this entity's movement parent's attachment point +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetParentAttachmentMaintainOffset( inputdata_t &inputdata ) +{ + SetParentAttachment( "SetParentAttachmentMaintainOffset", inputdata.value.String(), true ); +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler for clearing this entity's movement parent. +//------------------------------------------------------------------------------ +void CBaseEntity::InputClearParent( inputdata_t &inputdata ) +{ + SetParent( NULL ); +} + + +//------------------------------------------------------------------------------ +// Purpose : Returns velcocity of base entity. If physically simulated gets +// velocity from physics object +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBaseEntity::GetVelocity(Vector *vVelocity, AngularImpulse *vAngVelocity) +{ + if (GetMoveType()==MOVETYPE_VPHYSICS && m_pPhysicsObject) + { + m_pPhysicsObject->GetVelocity(vVelocity,vAngVelocity); + } + else + { + if (vVelocity != NULL) + { + *vVelocity = GetAbsVelocity(); + } + if (vAngVelocity != NULL) + { + QAngle tmp = GetLocalAngularVelocity(); + QAngleToAngularImpulse( tmp, *vAngVelocity ); + } + } +} + +bool CBaseEntity::IsMoving() +{ + Vector velocity; + GetVelocity( &velocity, NULL ); + return velocity != vec3_origin; +} + +//----------------------------------------------------------------------------- +// Purpose: Retrieves the coordinate frame for this entity. +// Input : forward - Receives the entity's forward vector. +// right - Receives the entity's right vector. +// up - Receives the entity's up vector. +//----------------------------------------------------------------------------- +void CBaseEntity::GetVectors(Vector* pForward, Vector* pRight, Vector* pUp) const +{ + // This call is necessary to cause m_rgflCoordinateFrame to be recomputed + const matrix3x4_t &entityToWorld = EntityToWorldTransform(); + + if (pForward != NULL) + { + MatrixGetColumn( entityToWorld, 0, *pForward ); + } + + if (pRight != NULL) + { + MatrixGetColumn( entityToWorld, 1, *pRight ); + *pRight *= -1.0f; + } + + if (pUp != NULL) + { + MatrixGetColumn( entityToWorld, 2, *pUp ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the model, validates that it's of the appropriate type +// Input : *szModelName - +//----------------------------------------------------------------------------- +void CBaseEntity::SetModel( const char *szModelName ) +{ + int modelIndex = modelinfo->GetModelIndex( szModelName ); + const model_t *model = modelinfo->GetModel( modelIndex ); + if ( model && modelinfo->GetModelType( model ) != mod_brush ) + { + Msg( "Setting CBaseEntity to non-brush model %s\n", szModelName ); + } + UTIL_SetModel( this, szModelName ); +} + +//------------------------------------------------------------------------------ + +CStudioHdr *CBaseEntity::OnNewModel() +{ + // Do nothing. + return NULL; +} + + +//================================================================================ +// TEAM HANDLING +//================================================================================ +void CBaseEntity::InputSetTeam( inputdata_t &inputdata ) +{ + ChangeTeam( inputdata.value.Int() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Put the entity in the specified team +//----------------------------------------------------------------------------- +void CBaseEntity::ChangeTeam( int iTeamNum ) +{ + m_iTeamNum = iTeamNum; +} + +//----------------------------------------------------------------------------- +// Get the Team this entity is on +//----------------------------------------------------------------------------- +CTeam *CBaseEntity::GetTeam( void ) const +{ + return GetGlobalTeam( m_iTeamNum ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if these players are both in at least one team together +//----------------------------------------------------------------------------- +bool CBaseEntity::InSameTeam( CBaseEntity *pEntity ) const +{ + if ( !pEntity ) + return false; + + return ( pEntity->GetTeam() == GetTeam() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the string name of the players team +//----------------------------------------------------------------------------- +const char *CBaseEntity::TeamID( void ) const +{ + if ( GetTeam() == NULL ) + return ""; + + return GetTeam()->GetName(); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the player is on the same team +//----------------------------------------------------------------------------- +bool CBaseEntity::IsInTeam( CTeam *pTeam ) const +{ + return ( GetTeam() == pTeam ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseEntity::GetTeamNumber( void ) const +{ + return m_iTeamNum; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseEntity::IsInAnyTeam( void ) const +{ + return ( GetTeam() != NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the type of damage that this entity inflicts. +//----------------------------------------------------------------------------- +int CBaseEntity::GetDamageType() const +{ + return DMG_GENERIC; +} + + +//----------------------------------------------------------------------------- +// process notification +//----------------------------------------------------------------------------- + +void CBaseEntity::NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ) +{ +} + + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseEntity::DispatchInteraction( int interactionType, void *data, CBaseCombatCharacter* sourceEnt ) +{ + if (interactionType <= 0) + return false; + + if (m_ScriptScope.IsInitialized() && g_Hook_HandleInteraction.CanRunInScope( m_ScriptScope )) + { + //HSCRIPT hData = g_pScriptVM->RegisterInstance( data ); + + // interaction, data, sourceEnt + ScriptVariant_t functionReturn; + ScriptVariant_t args[] = { interactionType/*, ScriptVariant_t( hData )*/, ScriptVariant_t( ToHScript( sourceEnt ) ) }; + if ( g_Hook_HandleInteraction.Call( m_ScriptScope, &functionReturn, args ) && (functionReturn.m_type == FIELD_BOOLEAN) ) + { + // Return the interaction here + //g_pScriptVM->RemoveInstance( hData ); + return functionReturn.m_bool; + } + + //g_pScriptVM->RemoveInstance( hData ); + } + + return HandleInteraction( interactionType, data, sourceEnt ); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Holds an entity's previous abs origin and angles at the time of +// teleportation. Used for child & constrained entity fixup to prevent +// lazy updates of abs origins and angles from messing things up. +//----------------------------------------------------------------------------- +struct TeleportListEntry_t +{ + CBaseEntity *pEntity; + Vector prevAbsOrigin; + QAngle prevAbsAngles; +}; + + +static void TeleportEntity( CBaseEntity *pSourceEntity, TeleportListEntry_t &entry, const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ) +{ + CBaseEntity *pTeleport = entry.pEntity; + Vector prevOrigin = entry.prevAbsOrigin; + QAngle prevAngles = entry.prevAbsAngles; + + int nSolidFlags = pTeleport->GetSolidFlags(); + pTeleport->AddSolidFlags( FSOLID_NOT_SOLID ); + + // I'm teleporting myself + if ( pSourceEntity == pTeleport ) + { + if ( newAngles ) + { + pTeleport->SetLocalAngles( *newAngles ); + if ( pTeleport->IsPlayer() ) + { + CBasePlayer *pPlayer = (CBasePlayer *)pTeleport; + pPlayer->SnapEyeAngles( *newAngles ); + } + } + + if ( newVelocity ) + { + pTeleport->SetAbsVelocity( *newVelocity ); + pTeleport->SetBaseVelocity( vec3_origin ); + } + + if ( newPosition ) + { + pTeleport->IncrementInterpolationFrame(); + UTIL_SetOrigin( pTeleport, *newPosition ); + } + } + else + { + // My parent is teleporting, just update my position & physics + pTeleport->CalcAbsolutePosition(); + } + IPhysicsObject *pPhys = pTeleport->VPhysicsGetObject(); + bool rotatePhysics = false; + + // handle physics objects / shadows + if ( pPhys ) + { + if ( newVelocity ) + { + pPhys->SetVelocity( newVelocity, NULL ); + } + const QAngle *rotAngles = &pTeleport->GetAbsAngles(); + // don't rotate physics on players or bbox entities + if (pTeleport->IsPlayer() || pTeleport->GetSolid() == SOLID_BBOX ) + { + rotAngles = &vec3_angle; + } + else + { + rotatePhysics = true; + } + + pPhys->SetPosition( pTeleport->GetAbsOrigin(), *rotAngles, true ); + } + + g_pNotify->ReportTeleportEvent( pTeleport, prevOrigin, prevAngles, rotatePhysics ); + + pTeleport->SetSolidFlags( nSolidFlags ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Recurses an entity hierarchy and fills out a list of all entities +// in the hierarchy with their current origins and angles. +// +// This list is necessary to keep lazy updates of abs origins and angles +// from messing up our child/constrained entity fixup. +//----------------------------------------------------------------------------- +static void BuildTeleportList_r( CBaseEntity *pTeleport, CUtlVector &teleportList ) +{ + TeleportListEntry_t entry; + + entry.pEntity = pTeleport; + entry.prevAbsOrigin = pTeleport->GetAbsOrigin(); + entry.prevAbsAngles = pTeleport->GetAbsAngles(); + + teleportList.AddToTail( entry ); + + CBaseEntity *pList = pTeleport->FirstMoveChild(); + while ( pList ) + { + BuildTeleportList_r( pList, teleportList ); + pList = pList->NextMovePeer(); + } +} + + +static CUtlVector g_TeleportStack; +void CBaseEntity::Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ) +{ + if ( g_TeleportStack.Find( this ) >= 0 ) + return; + int index = g_TeleportStack.AddToTail( this ); + + CUtlVector teleportList; + BuildTeleportList_r( this, teleportList ); + + int i; + for ( i = 0; i < teleportList.Count(); i++) + { + TeleportEntity( this, teleportList[i], newPosition, newAngles, newVelocity ); + } + + for (i = 0; i < teleportList.Count(); i++) + { + teleportList[i].pEntity->CollisionRulesChanged(); + } + + if ( IsPlayer() ) + { + // Tell the client being teleported + IGameEvent *event = gameeventmanager->CreateEvent( "base_player_teleported" ); + if ( event ) + { + event->SetInt( "entindex", entindex() ); + gameeventmanager->FireEventClientSide( event ); + } + } + + Assert( g_TeleportStack[index] == this ); + g_TeleportStack.FastRemove( index ); + + // FIXME: add an initializer function to StepSimulationData + StepSimulationData *step = ( StepSimulationData * )GetDataObject( STEPSIMULATION ); + if (step) + { + Q_memset( step, 0, sizeof( *step ) ); + } +} + +// Stuff implemented for weapon prediction code +void CBaseEntity::SetSize( const Vector &vecMin, const Vector &vecMax ) +{ + UTIL_SetSize( this, vecMin, vecMax ); +} + +CStudioHdr *ModelSoundsCache_LoadModel( const char *filename ) +{ + // Load the file + int idx = engine->PrecacheModel( filename, true ); + if ( idx != -1 ) + { + model_t *mdl = (model_t *)modelinfo->GetModel( idx ); + if ( mdl ) + { + CStudioHdr *studioHdr = new CStudioHdr( modelinfo->GetStudiomodel( mdl ), mdlcache ); + if ( studioHdr->IsValid() ) + { + return studioHdr; + } + } + } + return NULL; +} + +void ModelSoundsCache_FinishModel( CStudioHdr *hdr ) +{ + Assert( hdr ); + delete hdr; +} + +void ModelSoundsCache_PrecacheScriptSound( const char *soundname ) +{ + CBaseEntity::PrecacheScriptSound( soundname ); +} + +static CUtlCachedFileData< CModelSoundsCache > g_ModelSoundsCache( "modelsounds.cache", MODELSOUNDSCACHE_VERSION, 0, UTL_CACHED_FILE_USE_FILESIZE, false ); + +void ClearModelSoundsCache() +{ + if ( IsX360() ) + { + return; + } + + g_ModelSoundsCache.Reload(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool ModelSoundsCacheInit() +{ + if ( IsX360() ) + { + return true; + } + + return g_ModelSoundsCache.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void ModelSoundsCacheShutdown() +{ + if ( IsX360() ) + { + return; + } + + g_ModelSoundsCache.Shutdown(); +} + +static CUtlSymbolTable g_ModelSoundsSymbolHelper( 0, 32, true ); +class CModelSoundsCacheSaver: public CAutoGameSystem +{ +public: + CModelSoundsCacheSaver( const char *name ) : CAutoGameSystem( name ) + { + } + virtual void LevelInitPostEntity() + { + if ( IsX360() ) + { + return; + } + + if ( g_ModelSoundsCache.IsDirty() ) + { + g_ModelSoundsCache.Save(); + } + } + virtual void LevelShutdownPostEntity() + { + if ( IsX360() ) + { + // Unforunate that this table must persist through duration of level. + // It is the common case that PrecacheModel() still gets called (and needs this table), + // after LevelInitPostEntity, as PrecacheModel() redundantly precaches. + g_ModelSoundsSymbolHelper.RemoveAll(); + return; + } + + if ( g_ModelSoundsCache.IsDirty() ) + { + g_ModelSoundsCache.Save(); + } + } +}; + +static CModelSoundsCacheSaver g_ModelSoundsCacheSaver( "CModelSoundsCacheSaver" ); + +//#define WATCHACCESS +#if defined( WATCHACCESS ) + +static bool g_bWatching = true; + +void ModelLogFunc( const char *fileName, const char *accessType ) +{ + if ( g_bWatching && !CBaseEntity::IsPrecacheAllowed() ) + { + if ( Q_stristr( fileName, ".vcd" ) ) + { + Msg( "%s\n", fileName ); + } + } +} + +class CWatchForModelAccess: public CAutoGameSystem +{ +public: + virtual bool Init() + { + filesystem->AddLoggingFunc(&ModelLogFunc); + return true; + } + + virtual void Shutdown() + { + filesystem->RemoveLoggingFunc(&ModelLogFunc); + } + +}; + +static CWatchForModelAccess g_WatchForModels; + +#endif + +// HACK: This must match the #define in cl_animevent.h in the client .dll code!!! +#define CL_EVENT_SOUND 5004 +#define CL_EVENT_FOOTSTEP_LEFT 6004 +#define CL_EVENT_FOOTSTEP_RIGHT 6005 +#define CL_EVENT_MFOOTSTEP_LEFT 6006 +#define CL_EVENT_MFOOTSTEP_RIGHT 6007 + +//----------------------------------------------------------------------------- +// Precache model sound. Requires a local symbol table to prevent +// a very expensive call to PrecacheScriptSound(). +//----------------------------------------------------------------------------- +void CBaseEntity::PrecacheSoundHelper( const char *pName ) +{ + if ( !IsX360() ) + { + // 360 only + Assert( 0 ); + return; + } + + if ( !pName || !pName[0] ) + { + return; + } + + if ( UTL_INVAL_SYMBOL == g_ModelSoundsSymbolHelper.Find( pName ) ) + { + g_ModelSoundsSymbolHelper.AddString( pName ); + + // very expensive, only call when required + PrecacheScriptSound( pName ); + } +} + +//----------------------------------------------------------------------------- +// Precache model components +//----------------------------------------------------------------------------- +void CBaseEntity::PrecacheModelComponents( int nModelIndex ) +{ + + model_t *pModel = (model_t *)modelinfo->GetModel( nModelIndex ); + if ( !pModel || modelinfo->GetModelType( pModel ) != mod_studio ) + { + return; + } + + // sounds + if ( IsPC() ) + { + const char *name = modelinfo->GetModelName( pModel ); + if ( !g_ModelSoundsCache.EntryExists( name ) ) + { + char extension[ 8 ]; + Q_ExtractFileExtension( name, extension, sizeof( extension ) ); + + if ( Q_stristr( extension, "mdl" ) ) + { + DevMsg( 2, "Late precache of %s, need to rebuild modelsounds.cache\n", name ); + } + else + { + if ( !extension[ 0 ] ) + { + Warning( "Precache of %s ambigious (no extension specified)\n", name ); + } + else + { + Warning( "Late precache of %s (file missing?)\n", name ); + } + return; + } + } + + CModelSoundsCache *entry = g_ModelSoundsCache.Get( name ); + Assert( entry ); + if ( entry ) + { + entry->PrecacheSoundList(); + } + } + + // particles + { + // Check keyvalues for auto-emitting particles + KeyValues *pModelKeyValues = new KeyValues(""); + KeyValues::AutoDelete autodelete_pModelKeyValues( pModelKeyValues ); + if ( pModelKeyValues->LoadFromBuffer( modelinfo->GetModelName( pModel ), modelinfo->GetModelKeyValueText( pModel ) ) ) + { + KeyValues *pParticleEffects = pModelKeyValues->FindKey("Particles"); + if ( pParticleEffects ) + { + // Start grabbing the sounds and slotting them in + for ( KeyValues *pSingleEffect = pParticleEffects->GetFirstSubKey(); pSingleEffect; pSingleEffect = pSingleEffect->GetNextKey() ) + { + const char *pParticleEffectName = pSingleEffect->GetString( "name", "" ); + PrecacheParticleSystem( pParticleEffectName ); + } + } + } + } + + // model anim event owned components + { + // Check animevents for particle events + CStudioHdr studioHdr( modelinfo->GetStudiomodel( pModel ), mdlcache ); + if ( studioHdr.IsValid() ) + { + // force animation event resolution!!! + VerifySequenceIndex( &studioHdr ); + + int nSeqCount = studioHdr.GetNumSeq(); + for ( int i = 0; i < nSeqCount; ++i ) + { + mstudioseqdesc_t &seq = studioHdr.pSeqdesc( i ); + int nEventCount = seq.numevents; + for ( int j = 0; j < nEventCount; ++j ) + { + mstudioevent_t *pEvent = seq.pEvent( j ); + + if ( !( pEvent->type & AE_TYPE_NEWEVENTSYSTEM ) || ( pEvent->type & AE_TYPE_CLIENT ) ) + { + if ( pEvent->event == AE_CL_CREATE_PARTICLE_EFFECT ) + { + char token[256]; + const char *pOptions = pEvent->pszOptions(); + nexttoken( token, pOptions, ' ', sizeof( token ) ); + if ( token ) + { + PrecacheParticleSystem( token ); + } + continue; + } + } + + // 360 precaches the model sounds now at init time, the cost is now ~250 msecs worst case. + // The disk based solution was not needed. Now at runtime partly due to already crawling the sequences + // for the particles and the expensive part was redundant PrecacheScriptSound(), which is now prevented + // by a local symbol table. + if ( IsX360() ) + { + switch ( pEvent->event ) + { + default: + { + if ( ( pEvent->type & AE_TYPE_NEWEVENTSYSTEM ) && ( pEvent->event == AE_SV_PLAYSOUND ) ) + { + PrecacheSoundHelper( pEvent->pszOptions() ); + } + } + break; + case CL_EVENT_FOOTSTEP_LEFT: + case CL_EVENT_FOOTSTEP_RIGHT: + { + char soundname[256]; + char const *options = pEvent->pszOptions(); + if ( !options || !options[0] ) + { + options = "NPC_CombineS"; + } + + Q_snprintf( soundname, sizeof( soundname ), "%s.RunFootstepLeft", options ); + PrecacheSoundHelper( soundname ); + Q_snprintf( soundname, sizeof( soundname ), "%s.RunFootstepRight", options ); + PrecacheSoundHelper( soundname ); + Q_snprintf( soundname, sizeof( soundname ), "%s.FootstepLeft", options ); + PrecacheSoundHelper( soundname ); + Q_snprintf( soundname, sizeof( soundname ), "%s.FootstepRight", options ); + PrecacheSoundHelper( soundname ); + } + break; + case AE_CL_PLAYSOUND: + { + if ( !( pEvent->type & AE_TYPE_CLIENT ) ) + break; + + if ( pEvent->pszOptions()[0] ) + { + PrecacheSoundHelper( pEvent->pszOptions() ); + } + else + { + Warning( "-- Error --: empty soundname, .qc error on AE_CL_PLAYSOUND in model %s, sequence %s, animevent # %i\n", + studioHdr.GetRenderHdr()->pszName(), seq.pszLabel(), j+1 ); + } + } + break; + case CL_EVENT_SOUND: + case SCRIPT_EVENT_SOUND: + case SCRIPT_EVENT_SOUND_VOICE: + { + PrecacheSoundHelper( pEvent->pszOptions() ); + } + break; + } + } + } + } + } + } +} + + + +//----------------------------------------------------------------------------- +// Purpose: Add model to level precache list +// Input : *name - model name +// Output : int -- model index for model +//----------------------------------------------------------------------------- +int CBaseEntity::PrecacheModel( const char *name, bool bPreload ) +{ + if ( !name || !*name ) + { + Msg( "Attempting to precache model, but model name is NULL\n"); + return -1; + } + + // Warn on out of order precache + if ( !CBaseEntity::IsPrecacheAllowed() ) + { + if ( !engine->IsModelPrecached( name ) ) + { + Assert( !"CBaseEntity::PrecacheModel: too late" ); + Warning( "Late precache of %s\n", name ); + } + } +#if defined( WATCHACCESS ) + else + { + g_bWatching = false; + } +#endif + + int idx = engine->PrecacheModel( name, bPreload ); + if ( idx != -1 ) + { + PrecacheModelComponents( idx ); + } + +#if defined( WATCHACCESS ) + g_bWatching = true; +#endif + + return idx; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseEntity::Remove( ) +{ + UTIL_Remove( this ); +} + +// Entity degugging console commands +extern CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ); +extern void SetDebugBits( CBasePlayer* pPlayer, const char *name, int bit ); +extern CBaseEntity *GetNextCommandEntity( CBasePlayer *pPlayer, const char *name, CBaseEntity *ent ); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void ConsoleFireTargets( CBasePlayer *pPlayer, const char *name) +{ + // If no name was given use the picker + if (FStrEq(name,"")) + { + CBaseEntity *pEntity = FindPickerEntity( pPlayer ); + if ( pEntity && !pEntity->IsMarkedForDeletion()) + { + Msg( "[%03d] Found: %s, firing\n", gpGlobals->tickcount%1000, pEntity->GetDebugName()); + pEntity->Use( pPlayer, pPlayer, USE_TOGGLE, 0 ); + return; + } + } + // Otherwise use name or classname + FireTargets( name, pPlayer, pPlayer, USE_TOGGLE, 0 ); +} + +#ifdef MAPBASE +inline bool UtlStringLessFunc( const CUtlString &lhs, const CUtlString &rhs ) +{ + return Q_stricmp( lhs.String(), rhs.String() ) < 0; +} + +//------------------------------------------------------------------------------ +// Purpose : More concommands needed access to entities, so this has been moved to its own function. +// Input : cmdname - The name of the command. +// &commands - Where the complete autocompletes should be sent to. +// substring - The current search query. (only pool entities that start with this) +// checklen - The number of characters to check. +// Output : A pointer to a cUtlRBTRee containing all of the entities. +//------------------------------------------------------------------------------ +static int AutoCompleteEntities(const char *cmdname, CUtlVector< CUtlString > &commands, CUtlRBTree< CUtlString > &symbols, char *substring, int checklen = 0) +{ + CBaseEntity *pos = NULL; + while ((pos = gEntList.NextEnt(pos)) != NULL) + { + const char *name = pos->GetClassname(); + if (pos->GetEntityName() == NULL_STRING || Q_strnicmp(STRING(pos->GetEntityName()), substring, checklen)) + { + if (Q_strnicmp(pos->GetClassname(), substring, checklen)) + continue; + } + else + name = STRING(pos->GetEntityName()); + + CUtlString sym = name; + int idx = symbols.Find(sym); + if (idx == symbols.InvalidIndex()) + { + symbols.Insert(sym); + } + + // Too many + if (symbols.Count() >= COMMAND_COMPLETION_MAXITEMS) + break; + } + + // Now fill in the results + for (int i = symbols.FirstInorder(); i != symbols.InvalidIndex(); i = symbols.NextInorder(i)) + { + const char *name = symbols[i].String(); + + char buf[512]; + Q_strncpy(buf, name, sizeof(buf)); + Q_strlower(buf); + + CUtlString command; + command = CFmtStr("%s %s", cmdname, buf); + commands.AddToTail(command); + } + + return symbols.Count(); +} +#endif + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Name( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_NAME_BIT); +} +static ConCommand ent_name("ent_name", CC_Ent_Name, 0, FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void DumpScriptScope(CBasePlayer* pPlayer, const char* name) +{ + CBaseEntity* pEntity = NULL; + while ((pEntity = GetNextCommandEntity(pPlayer, name, pEntity)) != NULL) + { + if (pEntity->m_ScriptScope.IsInitialized()) + { + Msg("----Script Dump for entity %s\n", pEntity->GetDebugName()); + HSCRIPT hDumpScopeFunc = g_pScriptVM->LookupFunction("__DumpScope"); + g_pScriptVM->Call(hDumpScopeFunc, NULL, true, NULL, 1, (HSCRIPT)pEntity->m_ScriptScope); + Msg("----End Script Dump\n"); + } + else + { + DevWarning("ent_script_dump: Entity %s has no script scope!\n", pEntity->GetDebugName()); + } + } +} + +//------------------------------------------------------------------------------ +void CC_Ent_Script_Dump( const CCommand& args ) +{ + DumpScriptScope(UTIL_GetCommandClient(),args[1]); +} +static ConCommand ent_script_dump("ent_script_dump", CC_Ent_Script_Dump, "Dumps the names and values of this entity's script scope to the console\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +#ifdef MAPBASE +class CEntTextAutoCompletionFunctor : public ICommandCallback, public ICommandCompletionCallback +{ +public: + virtual void CommandCallback( const CCommand &command ) + { + SetDebugBits(UTIL_GetCommandClient(), command.Arg(1), OVERLAY_TEXT_BIT); + } + + virtual int CommandCompletionCallback( const char *partial, CUtlVector< CUtlString > &commands ) + { + if ( !g_pGameRules ) + { + return 0; + } + + const char *cmdname = "ent_text"; + + char *substring = (char *)partial; + if ( Q_strstr( partial, cmdname ) ) + { + substring = (char *)partial + strlen( cmdname ) + 1; + } + + int checklen = Q_strlen( substring ); + + CUtlRBTree< CUtlString > symbols( 0, 0, UtlStringLessFunc ); + return AutoCompleteEntities(cmdname, commands, symbols, substring, checklen); + } +}; + +static CEntTextAutoCompletionFunctor g_EntTextAutoComplete; +static ConCommand ent_text("ent_text", &g_EntTextAutoComplete, "Displays text debugging information about the given entity(ies) on top of the entity (See Overlay Text)\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT, &g_EntTextAutoComplete); +#else +void CC_Ent_Text( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_TEXT_BIT); +} +static ConCommand ent_text("ent_text", CC_Ent_Text, "Displays text debugging information about the given entity(ies) on top of the entity (See Overlay Text)\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); +#endif + +//------------------------------------------------------------------------------ +void CC_Ent_BBox( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_BBOX_BIT); +} +static ConCommand ent_bbox("ent_bbox", CC_Ent_BBox, "Displays the movement bounding box for the given entity(ies) in orange. Some entites will also display entity specific overlays.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +void CC_Ent_AbsBox( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_ABSBOX_BIT); +} +static ConCommand ent_absbox("ent_absbox", CC_Ent_AbsBox, "Displays the total bounding box for the given entity(s) in green. Some entites will also display entity specific overlays.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +void CC_Ent_RBox( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_RBOX_BIT); +} +static ConCommand ent_rbox("ent_rbox", CC_Ent_RBox, "Displays the total bounding box for the given entity(s) in green. Some entites will also display entity specific overlays.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +void CC_Ent_AttachmentPoints( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_ATTACHMENTS_BIT); +} +static ConCommand ent_attachments("ent_attachments", CC_Ent_AttachmentPoints, "Displays the attachment points on an entity.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +void CC_Ent_ViewOffset( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_VIEWOFFSET); +} +static ConCommand ent_viewoffset("ent_viewoffset", CC_Ent_ViewOffset, "Displays the eye position for the given entity(ies) in red.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +void CC_Ent_Remove( const CCommand& args ) +{ + CBaseEntity *pEntity = NULL; + + // If no name was given set bits based on the picked + if ( FStrEq( args[1],"") ) + { + pEntity = FindPickerEntity( UTIL_GetCommandClient() ); + } + else + { + int index = atoi( args[1] ); + if ( index ) + { + pEntity = CBaseEntity::Instance( index ); + } + else + { + // Otherwise set bits based on name or classname + CBaseEntity *ent = NULL; + while ( (ent = gEntList.NextEnt(ent)) != NULL ) + { + if ( (ent->GetEntityName() != NULL_STRING && FStrEq(args[1], STRING(ent->GetEntityName()))) || + (ent->m_iClassname != NULL_STRING && FStrEq(args[1], STRING(ent->m_iClassname))) || + (ent->GetClassname()!=NULL && FStrEq(args[1], ent->GetClassname()))) + { + pEntity = ent; + break; + } + } + } + } + + // Found one? + if ( pEntity ) + { + Msg( "Removed %s(%s)\n", STRING(pEntity->m_iClassname), pEntity->GetDebugName() ); + UTIL_Remove( pEntity ); + } +} +static ConCommand ent_remove("ent_remove", CC_Ent_Remove, "Removes the given entity(s)\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +void CC_Ent_RemoveAll( const CCommand& args ) +{ + // If no name was given remove based on the picked + if ( args.ArgC() < 2 ) + { + Msg( "Removes all entities of the specified type\n\tArguments: {entity_name} / {class_name}\n" ); + } + else + { + // Otherwise remove based on name or classname + int iCount = 0; + CBaseEntity *ent = NULL; + while ( (ent = gEntList.NextEnt(ent)) != NULL ) + { + if ( (ent->GetEntityName() != NULL_STRING && FStrEq(args[1], STRING(ent->GetEntityName()))) || + (ent->m_iClassname != NULL_STRING && FStrEq(args[1], STRING(ent->m_iClassname))) || + (ent->GetClassname()!=NULL && FStrEq(args[1], ent->GetClassname()))) + { + UTIL_Remove( ent ); + iCount++; + } + } + + if ( iCount ) + { + Msg( "Removed %d %s's\n", iCount, args[1] ); + } + else + { + Msg( "No %s found.\n", args[1] ); + } + } +} +static ConCommand ent_remove_all("ent_remove_all", CC_Ent_RemoveAll, "Removes all entities of the specified type\n\tArguments: {entity_name} / {class_name} ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +void CC_Ent_SetName( const CCommand& args ) +{ + CBaseEntity *pEntity = NULL; + + if ( args.ArgC() < 1 ) + { + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if (!pPlayer) + return; + + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage:\n ent_setname \n" ); + } + else + { + // If no name was given set bits based on the picked + if ( FStrEq( args[2],"") ) + { + pEntity = FindPickerEntity( UTIL_GetCommandClient() ); + } + else + { + // Otherwise set bits based on name or classname + CBaseEntity *ent = NULL; + while ( (ent = gEntList.NextEnt(ent)) != NULL ) + { + if ( (ent->GetEntityName() != NULL_STRING && FStrEq(args[1], STRING(ent->GetEntityName()))) || + (ent->m_iClassname != NULL_STRING && FStrEq(args[1], STRING(ent->m_iClassname))) || + (ent->GetClassname()!=NULL && FStrEq(args[1], ent->GetClassname()))) + { + pEntity = ent; + break; + } + } + } + + // Found one? + if ( pEntity ) + { + Msg( "Set the name of %s to %s\n", STRING(pEntity->m_iClassname), args[1] ); + pEntity->SetName( AllocPooledString( args[1] ) ); + } + } +} +static ConCommand ent_setname("ent_setname", CC_Ent_SetName, "Sets the targetname of the given entity(s)\n\tArguments: {new entity name} {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +void CC_Find_Ent( const CCommand& args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Total entities: %d (%d edicts)\n", gEntList.NumberOfEntities(), gEntList.NumberOfEdicts() ); + Msg( "Format: find_ent \n" ); + return; + } + + int iCount = 0; + const char *pszSubString = args[1]; + Msg("Searching for entities with class/target name containing substring: '%s'\n", pszSubString ); + + CBaseEntity *ent = NULL; + while ( (ent = gEntList.NextEnt(ent)) != NULL ) + { + const char *pszClassname = ent->GetClassname(); + const char *pszTargetname = STRING(ent->GetEntityName()); + + bool bMatches = false; + if ( pszClassname && pszClassname[0] ) + { + if ( Q_stristr( pszClassname, pszSubString ) ) + { + bMatches = true; + } + } + + if ( !bMatches && pszTargetname && pszTargetname[0] ) + { + if ( Q_stristr( pszTargetname, pszSubString ) ) + { + bMatches = true; + } + } + + if ( bMatches ) + { + iCount++; + Msg(" '%s' : '%s' (entindex %d) \n", ent->GetClassname(), ent->GetEntityName().ToCStr(), ent->entindex() ); + } + } + + Msg("Found %d matches.\n", iCount); +} +static ConCommand find_ent("find_ent", CC_Find_Ent, "Find and list all entities with classnames or targetnames that contain the specified substring.\nFormat: find_ent \n", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +void CC_Find_Ent_Index( const CCommand& args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Format: find_ent_index \n" ); + return; + } + + int iIndex = atoi(args[1]); + CBaseEntity *pEnt = UTIL_EntityByIndex( iIndex ); + if ( pEnt ) + { + Msg(" '%s' : '%s' (entindex %d) \n", pEnt->GetClassname(), pEnt->GetEntityName().ToCStr(), iIndex ); + } + else + { + Msg("Found no entity at %d.\n", iIndex); + } +} +static ConCommand find_ent_index("find_ent_index", CC_Find_Ent_Index, "Display data for entity matching specified index.\nFormat: find_ent_index \n", FCVAR_CHEAT); + +// Purpose : +//------------------------------------------------------------------------------ +void CC_Ent_Dump( const CCommand& args ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if (!pPlayer) + { + return; + } + + if ( args.ArgC() < 2 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage:\n ent_dump \n" ); + } + else + { + // iterate through all the ents of this name, printing out their details + CBaseEntity *ent = NULL; + bool bFound = false; + while ( ( ent = gEntList.FindEntityByName(ent, args[1] ) ) != NULL ) + { + bFound = true; + for ( datamap_t *dmap = ent->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the actions in the data description, printing out details + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + variant_t var; + if ( ent->ReadKeyField( dmap->dataDesc[i].externalName, &var) ) + { + char buf[256]; + buf[0] = 0; + switch( var.FieldType() ) + { + case FIELD_STRING: + Q_strncpy( buf, var.String() ,sizeof(buf)); + break; + case FIELD_INTEGER: + if ( var.Int() ) + Q_snprintf( buf,sizeof(buf), "%d", var.Int() ); + break; + case FIELD_FLOAT: + if ( var.Float() ) + Q_snprintf( buf,sizeof(buf), "%.2f", var.Float() ); + break; + case FIELD_EHANDLE: + { + // get the entities name + if ( var.Entity() ) + { + Q_snprintf( buf,sizeof(buf), "%s", STRING(var.Entity()->GetEntityName()) ); + } + } + break; + } + + // don't print out the duplicate keys + if ( !Q_stricmp("parentname",dmap->dataDesc[i].externalName) || !Q_stricmp("targetname",dmap->dataDesc[i].externalName) ) + continue; + + // don't print out empty keys + if ( buf[0] ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs(" %s: %s\n", dmap->dataDesc[i].externalName, buf) ); + } + } + } + } + } + + if ( !bFound ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "ent_dump: no such entity" ); + } + } +} +static ConCommand ent_dump("ent_dump", CC_Ent_Dump, "Usage:\n ent_dump \n", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_FireTarget( const CCommand& args ) +{ + ConsoleFireTargets(UTIL_GetCommandClient(),args[1]); +} +static ConCommand firetarget("firetarget", CC_Ent_FireTarget, 0, FCVAR_CHEAT); + +#ifndef MAPBASE +static bool UtlStringLessFunc( const CUtlString &lhs, const CUtlString &rhs ) +{ + return Q_stricmp( lhs.String(), rhs.String() ) < 0; +} +#endif + +class CEntFireAutoCompletionFunctor : public ICommandCallback, public ICommandCompletionCallback +{ +public: + virtual void CommandCallback( const CCommand &command ) + { + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if (!pPlayer) + { + return; + } + + // fires a command from the console + if ( command.ArgC() < 2 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage:\n ent_fire [action] [value] [delay]\n" ); + } + else + { + const char *target = "", *action = "Use"; + variant_t value; +#ifdef MAPBASE + float delay = 0; +#else + int delay = 0; +#endif + + target = STRING( AllocPooledString(command.Arg( 1 ) ) ); + + // Don't allow them to run anything on a point_servercommand unless they're the host player. Otherwise they can ent_fire + // and run any command on the server. Admittedly, they can only do the ent_fire if sv_cheats is on, but + // people complained about users resetting the rcon password if the server briefly turned on cheats like this: + // give point_servercommand + // ent_fire point_servercommand command "rcon_password mynewpassword" + // + // Robin: Unfortunately, they get around point_servercommand checks with this: + // ent_create point_servercommand; ent_setname mine; ent_fire mine command "rcon_password mynewpassword" + // So, I'm removing the ability for anyone to execute ent_fires on dedicated servers (we can't check to see if + // this command is going to connect with a point_servercommand entity here, because they could delay the event and create it later). + if ( engine->IsDedicatedServer() ) + { + // We allow people with disabled autokick to do it, because they already have rcon. + if ( pPlayer->IsAutoKickDisabled() == false ) + return; + } + else if ( gpGlobals->maxClients > 1 ) + { + // On listen servers with more than 1 player, only allow the host to issue ent_fires. + CBasePlayer *pHostPlayer = UTIL_GetListenServerHost(); + if ( pPlayer != pHostPlayer ) + return; + } + + if ( command.ArgC() >= 3 ) + { + action = STRING( AllocPooledString(command.Arg( 2 )) ); + } + if ( command.ArgC() >= 4 ) + { + value.SetString( AllocPooledString(command.Arg( 3 )) ); + } + if ( command.ArgC() >= 5 ) + { +#ifdef MAPBASE + delay = atof( command.Arg( 4 ) ); +#else + delay = atoi( command.Arg( 4 ) ); +#endif + } + + g_EventQueue.AddEvent( target, action, value, delay, pPlayer, pPlayer ); + } + } + + virtual int CommandCompletionCallback( const char *partial, CUtlVector< CUtlString > &commands ) + { + if ( !g_pGameRules ) + { + return 0; + } + + const char *cmdname = "ent_fire"; + + char *substring = (char *)partial; + if ( Q_strstr( partial, cmdname ) ) + { + substring = (char *)partial + strlen( cmdname ) + 1; + } + + int checklen = 0; + char *space = Q_strstr( substring, " " ); + if ( space ) + { + return EntFire_AutoCompleteInput( partial, commands );; + } + else + { + checklen = Q_strlen( substring ); + } + +#ifdef MAPBASE + CUtlRBTree< CUtlString > symbols( 0, 0, UtlStringLessFunc ); + return AutoCompleteEntities(cmdname, commands, symbols, substring, checklen); +#else + CUtlRBTree< CUtlString > symbols( 0, 0, UtlStringLessFunc ); + + CBaseEntity *pos = NULL; + while ( ( pos = gEntList.NextEnt( pos ) ) != NULL ) + { + // Check target name against partial string + if ( pos->GetEntityName() == NULL_STRING ) + continue; + + if ( Q_strnicmp( STRING( pos->GetEntityName() ), substring, checklen ) ) + continue; + + CUtlString sym = STRING( pos->GetEntityName() ); + int idx = symbols.Find( sym ); + if ( idx == symbols.InvalidIndex() ) + { + symbols.Insert( sym ); + } + + // Too many + if ( symbols.Count() >= COMMAND_COMPLETION_MAXITEMS ) + break; + } + + // Now fill in the results + for ( int i = symbols.FirstInorder(); i != symbols.InvalidIndex(); i = symbols.NextInorder( i ) ) + { + const char *name = symbols[ i ].String(); + + char buf[ 512 ]; + Q_strncpy( buf, name, sizeof( buf ) ); + Q_strlower( buf ); + + CUtlString command; + command = CFmtStr( "%s %s", cmdname, buf ); + commands.AddToTail( command ); + } + + return symbols.Count(); +#endif + } +private: + int EntFire_AutoCompleteInput( const char *partial, CUtlVector< CUtlString > &commands ) + { + const char *cmdname = "ent_fire"; + + char *substring = (char *)partial; + if ( Q_strstr( partial, cmdname ) ) + { + substring = (char *)partial + strlen( cmdname ) + 1; + } + + int checklen = 0; + char *space = Q_strstr( substring, " " ); + if ( !space ) + { + Assert( !"CC_EntFireAutoCompleteInputFunc is broken\n" ); + return 0; + } + + checklen = Q_strlen( substring ); + + char targetEntity[ 256 ]; + targetEntity[0] = 0; + int nEntityNameLength = (space-substring); + Q_strncat( targetEntity, substring, sizeof( targetEntity ), nEntityNameLength ); + + // Find the target entity by name +#ifdef MAPBASE + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + CBaseEntity *target = gEntList.FindEntityGeneric( NULL, targetEntity, pPlayer, pPlayer, pPlayer ); +#else + CBaseEntity *target = gEntList.FindEntityByName( NULL, targetEntity ); +#endif + if ( target == NULL ) + return 0; + + CUtlRBTree< CUtlString > symbols( 0, 0, UtlStringLessFunc ); + + // Find the next portion of the text chain, if any (removing space) + int nInputNameLength = (checklen-nEntityNameLength-1); + + // Starting past the last space, this is the remainder of the string + char *inputPartial = ( checklen > nEntityNameLength ) ? (space+1) : NULL; + + for ( datamap_t *dmap = target->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // Make sure we don't keep adding things in if the satisfied the limit + if ( symbols.Count() >= COMMAND_COMPLETION_MAXITEMS ) + break; + + int c = dmap->dataNumFields; + for ( int i = 0; i < c; i++ ) + { + typedescription_t *field = &dmap->dataDesc[ i ]; + + // Only want inputs + if ( !( field->flags & FTYPEDESC_INPUT ) ) + continue; + +#ifndef MAPBASE // What did input variables ever do to you? + + // Only want input functions + if ( field->flags & FTYPEDESC_SAVE ) + continue; + +#endif + + // See if we've got a partial string for the input name already + if ( inputPartial != NULL ) + { + if ( Q_strnicmp( inputPartial, field->externalName, nInputNameLength ) ) + continue; + } + + CUtlString sym = field->externalName; + + int idx = symbols.Find( sym ); + if ( idx == symbols.InvalidIndex() ) + { + symbols.Insert( sym ); + } + + // Too many items have been added + if ( symbols.Count() >= COMMAND_COMPLETION_MAXITEMS ) + break; + } + } + + // Now fill in the results + for ( int i = symbols.FirstInorder(); i != symbols.InvalidIndex(); i = symbols.NextInorder( i ) ) + { + const char *name = symbols[ i ].String(); + + char buf[ 512 ]; + Q_strncpy( buf, name, sizeof( buf ) ); + Q_strlower( buf ); + + CUtlString command; + command = CFmtStr( "%s %s %s", cmdname, targetEntity, buf ); + commands.AddToTail( command ); + } + + return symbols.Count(); + } +}; + +static CEntFireAutoCompletionFunctor g_EntFireAutoComplete; +static ConCommand ent_fire("ent_fire", &g_EntFireAutoComplete, "Usage:\n ent_fire [action] [value] [delay]\n", FCVAR_CHEAT, &g_EntFireAutoComplete ); + +void CC_Ent_CancelPendingEntFires( const CCommand& args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if (!pPlayer) + return; + + g_EventQueue.CancelEvents( pPlayer ); +} +static ConCommand ent_cancelpendingentfires("ent_cancelpendingentfires", CC_Ent_CancelPendingEntFires, "Cancels all ent_fire created outputs that are currently waiting for their delay to expire." ); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Info( const CCommand& args ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if (!pPlayer) + { + return; + } + + if ( args.ArgC() < 2 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage:\n ent_info \n" ); + } + else + { + // iterate through all the ents printing out their details + CBaseEntity *ent = CreateEntityByName( args[1] ); + + if ( ent ) + { + datamap_t *dmap; + for ( dmap = ent->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the actions in the data description, printing out details + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & FTYPEDESC_OUTPUT ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs(" output: %s\n", dmap->dataDesc[i].externalName) ); + } + } + } + + for ( dmap = ent->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the actions in the data description, printing out details + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & FTYPEDESC_INPUT ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs(" input: %s\n", dmap->dataDesc[i].externalName) ); + } + } + } + + delete ent; + } + else + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs("no such entity %s\n", args[1]) ); + } + } +} +static ConCommand ent_info("ent_info", CC_Ent_Info, "Usage:\n ent_info \n", FCVAR_CHEAT); + +#ifdef MAPBASE +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Info_Datatable( const CCommand& args ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if (!pPlayer) + { + return; + } + + if ( args.ArgC() < 2 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage:\n ent_info_datatable \n" ); + } + else + { + // Each element corresponds to a specific field type. + // Hey, if you've got a better idea, be my guest. + static const char *g_FieldStrings[FIELD_TYPECOUNT] = + { + "VOID", + "FLOAT", + "STRING", + "VECTOR", + "QUATERNION", + "INTEGER", + "BOOLEAN", + "SHORT", + "CHARACTER", + "COLOR32", + "EMBEDDED", + "CUSTOM", + + "CLASSPTR", + "EHANDLE", + "EDICT", + + "POSITION_VECTOR", + "TIME", + "TICK", + "MODELNAME", + "SOUNDNAME", + + "INPUT", + "FUNCTION", + "VMATRIX", + "VMATRIX_WORLDSPACE", + "MATRIX3X4_WORLDSPACE", + "INTERVAL", + "MODELINDEX", + "MATERIALINDEX", + + "VECTOR2D", + }; + + // iterate through all the ents printing out their details + CBaseEntity *ent = CreateEntityByName( args[1] ); + + if ( ent ) + { +#define ENT_INFO_BY_HIERARCHY 1 +#ifdef ENT_INFO_BY_HIERARCHY + CUtlVector dmap_namelist; + + CUtlVector< CUtlVector > dmap_fieldlist; + CUtlVector< CUtlVector > dmap_fieldtypelist; + + datamap_t *dmap; + int dmapnum = 0; + for ( dmap = ent->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + dmap_fieldlist.AddToTail(); + dmap_fieldtypelist.AddToTail(); + + // search through all the actions in the data description, printing out details + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + dmap_fieldlist[dmapnum].AddToTail(dmap->dataDesc[i].fieldName); + dmap_fieldtypelist[dmapnum].AddToTail(dmap->dataDesc[i].fieldType); + } + + dmapnum++; + dmap_namelist.AddToTail(dmap->dataClassName); + } + + char offset[64] = { 0 }; // Needed so garbage isn't spewed at the beginning + for ( int i = 0; i < dmapnum; i++ ) + { + Q_strncat(offset, " ", sizeof(offset)); + + // Header for each class + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs("%s=========| %s |=========\n", offset, dmap_namelist[i]) ); + + Q_strncat(offset, " ", sizeof(offset)); + + int iFieldCount = dmap_fieldlist[i].Count(); + for ( int index = 0; index < iFieldCount; index++ ) + { + int iType = dmap_fieldtypelist[i][index]; + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs("%s%s (%i): %s\n", offset, g_FieldStrings[iType], iType, dmap_fieldlist[i][index]) ); + } + + // Clean up after ourselves + dmap_fieldlist[i].RemoveAll(); + dmap_fieldtypelist[i].RemoveAll(); + } +#else // This sorts by field type instead + CUtlVector fieldlist[FIELD_TYPECOUNT]; + + datamap_t *dmap; + for ( dmap = ent->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the actions in the data description, printing out details + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + fieldlist[dmap->dataDesc[i].fieldType].AddToTail(dmap->dataDesc[i].fieldName); + } + } + + for ( int i = 0; i < FIELD_TYPECOUNT; i++ ) + { + const char *typestring = g_FieldStrings[i]; + for ( int index = 0; index < fieldlist[i].Count(); index++ ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs(" %s (%i): %s\n", typestring, i, fieldlist[i][index]) ); + } + + // Clean up after ourselves + fieldlist[i].RemoveAll(); + } +#endif + + delete ent; + } + else + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs("no such entity %s\n", args[1]) ); + } + } +} +static ConCommand ent_info_datatable("ent_info_datatable", CC_Ent_Info_Datatable, "Usage:\n ent_info_datatable \n", FCVAR_CHEAT); +#endif + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Messages( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_MESSAGE_BIT); +} +static ConCommand ent_messages("ent_messages", CC_Ent_Messages ,"Toggles input/output message display for the selected entity(ies). The name of the entity will be displayed as well as any messages that it sends or receives.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Pause( void ) +{ + if (CBaseEntity::Debug_IsPaused()) + { + Msg( "Resuming entity I/O events\n" ); + CBaseEntity::Debug_Pause(false); + } + else + { + Msg( "Pausing entity I/O events\n" ); + CBaseEntity::Debug_Pause(true); + } +} +static ConCommand ent_pause("ent_pause", CC_Ent_Pause, "Toggles pausing of input/output message processing for entities. When turned on processing of all message will stop. Any messages displayed with 'ent_messages' will stop fading and be displayed indefinitely. To step through the messages one by one use 'ent_step'.", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +// Purpose : Enables the entity picker, revelaing debug information about the +// entity under the crosshair. +// Input : an optional command line argument "full" enables all debug info. +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Picker( void ) +{ + CBaseEntity::m_bInDebugSelect = CBaseEntity::m_bInDebugSelect ? false : true; + + // Remember the player that's making this request + CBaseEntity::m_nDebugPlayer = UTIL_GetCommandClientIndex(); +} +static ConCommand picker("picker", CC_Ent_Picker, "Toggles 'picker' mode. When picker is on, the bounding box, pivot and debugging text is displayed for whatever entity the player is looking at.\n\tArguments: full - enables all debug information", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Pivot( const CCommand& args ) +{ + SetDebugBits(UTIL_GetCommandClient(),args[1],OVERLAY_PIVOT_BIT); +} +static ConCommand ent_pivot("ent_pivot", CC_Ent_Pivot, "Displays the pivot for the given entity(ies).\n\t(y=up=green, z=forward=blue, x=left=red). \n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Ent_Step( const CCommand& args ) +{ + int nSteps = atoi(args[1]); + if (nSteps <= 0) + { + nSteps = 1; + } + CBaseEntity::Debug_SetSteps(nSteps); +} +static ConCommand ent_step("ent_step", CC_Ent_Step, "When 'ent_pause' is set this will step through one waiting input / output message at a time.", FCVAR_CHEAT); + +void CBaseEntity::SetCheckUntouch( bool check ) +{ + // Invalidate touchstamp + if ( check ) + { + touchStamp++; + if ( !IsEFlagSet( EFL_CHECK_UNTOUCH ) ) + { + AddEFlags( EFL_CHECK_UNTOUCH ); + EntityTouch_Add( this ); + } + } + else + { + RemoveEFlags( EFL_CHECK_UNTOUCH ); + } +} + +model_t *CBaseEntity::GetModel( void ) +{ + return (model_t *)modelinfo->GetModel( GetModelIndex() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculates the absolute position of an edict in the world +// assumes the parent's absolute origin has already been calculated +//----------------------------------------------------------------------------- +void CBaseEntity::CalcAbsolutePosition( void ) +{ + if (!IsEFlagSet( EFL_DIRTY_ABSTRANSFORM )) + return; + + RemoveEFlags( EFL_DIRTY_ABSTRANSFORM ); + + // Plop the entity->parent matrix into m_rgflCoordinateFrame + AngleMatrix( m_angRotation, m_vecOrigin, m_rgflCoordinateFrame ); + + CBaseEntity *pMoveParent = GetMoveParent(); + if ( !pMoveParent ) + { + // no move parent, so just copy existing values + m_vecAbsOrigin = m_vecOrigin; + m_angAbsRotation = m_angRotation; + if ( HasDataObjectType( POSITIONWATCHER ) ) + { + ReportPositionChanged( this ); + } + return; + } + + // concatenate with our parent's transform + matrix3x4_t tmpMatrix, scratchSpace; + ConcatTransforms( GetParentToWorldTransform( scratchSpace ), m_rgflCoordinateFrame, tmpMatrix ); + MatrixCopy( tmpMatrix, m_rgflCoordinateFrame ); + + // pull our absolute position out of the matrix + MatrixGetColumn( m_rgflCoordinateFrame, 3, m_vecAbsOrigin ); + + // if we have any angles, we have to extract our absolute angles from our matrix + if (( m_angRotation == vec3_angle ) && ( m_iParentAttachment == 0 )) + { + // just copy our parent's absolute angles + VectorCopy( pMoveParent->GetAbsAngles(), m_angAbsRotation ); + } + else + { + MatrixAngles( m_rgflCoordinateFrame, m_angAbsRotation ); + } + if ( HasDataObjectType( POSITIONWATCHER ) ) + { + ReportPositionChanged( this ); + } +} + +void CBaseEntity::CalcAbsoluteVelocity() +{ + if (!IsEFlagSet( EFL_DIRTY_ABSVELOCITY )) + return; + + RemoveEFlags( EFL_DIRTY_ABSVELOCITY ); + + CBaseEntity *pMoveParent = GetMoveParent(); + if ( !pMoveParent ) + { + m_vecAbsVelocity = m_vecVelocity; + return; + } + + // This transforms the local velocity into world space + VectorRotate( m_vecVelocity, pMoveParent->EntityToWorldTransform(), m_vecAbsVelocity ); + + // Now add in the parent abs velocity + m_vecAbsVelocity += pMoveParent->GetAbsVelocity(); +} + +// FIXME: While we're using (dPitch, dYaw, dRoll) as our local angular velocity +// representation, we can't actually solve this problem +/* +void CBaseEntity::CalcAbsoluteAngularVelocity() +{ + if (!IsEFlagSet( EFL_DIRTY_ABSANGVELOCITY )) + return; + + RemoveEFlags( EFL_DIRTY_ABSANGVELOCITY ); + + CBaseEntity *pMoveParent = GetMoveParent(); + if ( !pMoveParent ) + { + m_vecAbsAngVelocity = m_vecAngVelocity; + return; + } + + // This transforms the local ang velocity into world space + matrix3x4_t angVelToParent, angVelToWorld; + AngleMatrix( m_vecAngVelocity, angVelToParent ); + ConcatTransforms( pMoveParent->EntityToWorldTransform(), angVelToParent, angVelToWorld ); + MatrixAngles( angVelToWorld, m_vecAbsAngVelocity ); +} +*/ + +//----------------------------------------------------------------------------- +// Computes the abs position of a point specified in local space +//----------------------------------------------------------------------------- +void CBaseEntity::ComputeAbsPosition( const Vector &vecLocalPosition, Vector *pAbsPosition ) +{ + CBaseEntity *pMoveParent = GetMoveParent(); + if ( !pMoveParent ) + { + *pAbsPosition = vecLocalPosition; + } + else + { + VectorTransform( vecLocalPosition, pMoveParent->EntityToWorldTransform(), *pAbsPosition ); + } +} + + +//----------------------------------------------------------------------------- +// Computes the abs position of a point specified in local space +//----------------------------------------------------------------------------- +void CBaseEntity::ComputeAbsDirection( const Vector &vecLocalDirection, Vector *pAbsDirection ) +{ + CBaseEntity *pMoveParent = GetMoveParent(); + if ( !pMoveParent ) + { + *pAbsDirection = vecLocalDirection; + } + else + { + VectorRotate( vecLocalDirection, pMoveParent->EntityToWorldTransform(), *pAbsDirection ); + } +} + + +matrix3x4_t& CBaseEntity::GetParentToWorldTransform( matrix3x4_t &tempMatrix ) +{ + CBaseEntity *pMoveParent = GetMoveParent(); + if ( !pMoveParent ) + { + Assert( false ); + SetIdentityMatrix( tempMatrix ); + return tempMatrix; + } + + if ( m_iParentAttachment != 0 ) + { + MDLCACHE_CRITICAL_SECTION(); + + CBaseAnimating *pAnimating = pMoveParent->GetBaseAnimating(); + if ( pAnimating && pAnimating->GetAttachment( m_iParentAttachment, tempMatrix ) ) + { + return tempMatrix; + } + } + + // If we fall through to here, then just use the move parent's abs origin and angles. + return pMoveParent->EntityToWorldTransform(); +} + + +//----------------------------------------------------------------------------- +// These methods recompute local versions as well as set abs versions +//----------------------------------------------------------------------------- +void CBaseEntity::SetAbsOrigin( const Vector& absOrigin ) +{ + AssertMsg( absOrigin.IsValid(), "Invalid origin set" ); + + // This is necessary to get the other fields of m_rgflCoordinateFrame ok + CalcAbsolutePosition(); + + if ( m_vecAbsOrigin == absOrigin ) + return; + + // All children are invalid, but we are not + InvalidatePhysicsRecursive( POSITION_CHANGED ); + RemoveEFlags( EFL_DIRTY_ABSTRANSFORM ); + + m_vecAbsOrigin = absOrigin; + + MatrixSetColumn( absOrigin, 3, m_rgflCoordinateFrame ); + + Vector vecNewOrigin; + CBaseEntity *pMoveParent = GetMoveParent(); + if (!pMoveParent) + { + vecNewOrigin = absOrigin; + } + else + { + matrix3x4_t tempMat; + matrix3x4_t &parentTransform = GetParentToWorldTransform( tempMat ); + + // Moveparent case: transform the abs position into local space + VectorITransform( absOrigin, parentTransform, vecNewOrigin ); + } + + if (m_vecOrigin != vecNewOrigin) + { + m_vecOrigin = vecNewOrigin; + SetSimulationTime( gpGlobals->curtime ); + } +} + +void CBaseEntity::SetAbsAngles( const QAngle& absAngles ) +{ + // This is necessary to get the other fields of m_rgflCoordinateFrame ok + CalcAbsolutePosition(); + + // FIXME: The normalize caused problems in server code like momentary_rot_button that isn't + // handling things like +/-180 degrees properly. This should be revisited. + //QAngle angleNormalize( AngleNormalize( absAngles.x ), AngleNormalize( absAngles.y ), AngleNormalize( absAngles.z ) ); + + if ( m_angAbsRotation == absAngles ) + return; + + // All children are invalid, but we are not + InvalidatePhysicsRecursive( ANGLES_CHANGED ); + RemoveEFlags( EFL_DIRTY_ABSTRANSFORM ); + + m_angAbsRotation = absAngles; + AngleMatrix( absAngles, m_rgflCoordinateFrame ); + MatrixSetColumn( m_vecAbsOrigin, 3, m_rgflCoordinateFrame ); + + QAngle angNewRotation; + CBaseEntity *pMoveParent = GetMoveParent(); + if (!pMoveParent) + { + angNewRotation = absAngles; + } + else + { + if ( m_angAbsRotation == pMoveParent->GetAbsAngles() ) + { + angNewRotation.Init( ); + } + else + { + // Moveparent case: transform the abs transform into local space + matrix3x4_t worldToParent, localMatrix; + MatrixInvert( pMoveParent->EntityToWorldTransform(), worldToParent ); + ConcatTransforms( worldToParent, m_rgflCoordinateFrame, localMatrix ); + MatrixAngles( localMatrix, angNewRotation ); + } + } + + if (m_angRotation != angNewRotation) + { + m_angRotation = angNewRotation; + SetSimulationTime( gpGlobals->curtime ); + } +} + +void CBaseEntity::SetAbsVelocity( const Vector &vecAbsVelocity ) +{ + if ( m_vecAbsVelocity == vecAbsVelocity ) + return; + + // The abs velocity won't be dirty since we're setting it here + // All children are invalid, but we are not + InvalidatePhysicsRecursive( VELOCITY_CHANGED ); + RemoveEFlags( EFL_DIRTY_ABSVELOCITY ); + + m_vecAbsVelocity = vecAbsVelocity; + + // NOTE: Do *not* do a network state change in this case. + // m_vecVelocity is only networked for the player, which is not manual mode + CBaseEntity *pMoveParent = GetMoveParent(); + if (!pMoveParent) + { + m_vecVelocity = vecAbsVelocity; + return; + } + + // First subtract out the parent's abs velocity to get a relative + // velocity measured in world space + Vector relVelocity; + VectorSubtract( vecAbsVelocity, pMoveParent->GetAbsVelocity(), relVelocity ); + + // Transform relative velocity into parent space + Vector vNew; + VectorIRotate( relVelocity, pMoveParent->EntityToWorldTransform(), vNew ); + m_vecVelocity = vNew; +} + +// FIXME: While we're using (dPitch, dYaw, dRoll) as our local angular velocity +// representation, we can't actually solve this problem +/* +void CBaseEntity::SetAbsAngularVelocity( const QAngle &vecAbsAngVelocity ) +{ + // The abs velocity won't be dirty since we're setting it here + // All children are invalid, but we are not + InvalidatePhysicsRecursive( EFL_DIRTY_ABSANGVELOCITY ); + RemoveEFlags( EFL_DIRTY_ABSANGVELOCITY ); + + m_vecAbsAngVelocity = vecAbsAngVelocity; + + CBaseEntity *pMoveParent = GetMoveParent(); + if (!pMoveParent) + { + m_vecAngVelocity = vecAbsAngVelocity; + return; + } + + // NOTE: We *can't* subtract out parent ang velocity, it's nonsensical + matrix3x4_t entityToWorld; + AngleMatrix( vecAbsAngVelocity, entityToWorld ); + + // Moveparent case: transform the abs relative angular vel into local space + matrix3x4_t worldToParent, localMatrix; + MatrixInvert( pMoveParent->EntityToWorldTransform(), worldToParent ); + ConcatTransforms( worldToParent, entityToWorld, localMatrix ); + MatrixAngles( localMatrix, m_vecAngVelocity ); +} +*/ + +//----------------------------------------------------------------------------- +// Methods that modify local physics state, and let us know to compute abs state later +//----------------------------------------------------------------------------- +void CBaseEntity::SetLocalOrigin( const Vector& origin ) +{ + // Safety check against NaN's or really huge numbers + if ( !IsEntityPositionReasonable( origin ) ) + { + if ( CheckEmitReasonablePhysicsSpew() ) + { + Warning( "Bad SetLocalOrigin(%f,%f,%f) on %s\n", origin.x, origin.y, origin.z, GetDebugName() ); + } + Assert( false ); + return; + } + +// if ( !origin.IsValid() ) +// { +// AssertMsg( 0, "Bad origin set" ); +// return; +// } + + if (m_vecOrigin != origin) + { + // Sanity check to make sure the origin is valid. +#ifdef _DEBUG + float largeVal = 1024 * 128; + Assert( origin.x >= -largeVal && origin.x <= largeVal ); + Assert( origin.y >= -largeVal && origin.y <= largeVal ); + Assert( origin.z >= -largeVal && origin.z <= largeVal ); +#endif + + InvalidatePhysicsRecursive( POSITION_CHANGED ); + m_vecOrigin = origin; + SetSimulationTime( gpGlobals->curtime ); + } +} + +void CBaseEntity::SetLocalAngles( const QAngle& angles ) +{ + // NOTE: The angle normalize is a little expensive, but we can save + // a bunch of time in interpolation if we don't have to invalidate everything + // and sometimes it's off by a normalization amount + + // FIXME: The normalize caused problems in server code like momentary_rot_button that isn't + // handling things like +/-180 degrees properly. This should be revisited. + //QAngle angleNormalize( AngleNormalize( angles.x ), AngleNormalize( angles.y ), AngleNormalize( angles.z ) ); + + // Safety check against NaN's or really huge numbers + if ( !IsEntityQAngleReasonable( angles ) ) + { + if ( CheckEmitReasonablePhysicsSpew() ) + { + Warning( "Bad SetLocalAngles(%f,%f,%f) on %s\n", angles.x, angles.y, angles.z, GetDebugName() ); + } + Assert( false ); + return; + } + + if (m_angRotation != angles) + { + InvalidatePhysicsRecursive( ANGLES_CHANGED ); + m_angRotation = angles; + SetSimulationTime( gpGlobals->curtime ); + } +} + +void CBaseEntity::SetLocalVelocity( const Vector &inVecVelocity ) +{ + Vector vecVelocity = inVecVelocity; + + // Safety check against receive a huge impulse, which can explode physics + switch ( CheckEntityVelocity( vecVelocity ) ) + { + case -1: + Warning( "Discarding SetLocalVelocity(%f,%f,%f) on %s\n", vecVelocity.x, vecVelocity.y, vecVelocity.z, GetDebugName() ); + Assert( false ); + return; + case 0: + if ( CheckEmitReasonablePhysicsSpew() ) + { + Warning( "Clamping SetLocalVelocity(%f,%f,%f) on %s\n", inVecVelocity.x, inVecVelocity.y, inVecVelocity.z, GetDebugName() ); + } + break; + } + + if (m_vecVelocity != vecVelocity) + { + InvalidatePhysicsRecursive( VELOCITY_CHANGED ); + m_vecVelocity = vecVelocity; + } +} + +void CBaseEntity::SetLocalAngularVelocity( const QAngle &vecAngVelocity ) +{ + // Safety check against NaN's or really huge numbers + if ( !IsEntityQAngleVelReasonable( vecAngVelocity ) ) + { + if ( CheckEmitReasonablePhysicsSpew() ) + { + Warning( "Bad SetLocalAngularVelocity(%f,%f,%f) on %s\n", vecAngVelocity.x, vecAngVelocity.y, vecAngVelocity.z, GetDebugName() ); + } + Assert( false ); + return; + } + + if (m_vecAngVelocity != vecAngVelocity) + { +// InvalidatePhysicsRecursive( EFL_DIRTY_ABSANGVELOCITY ); + m_vecAngVelocity = vecAngVelocity; + } +} + + +//----------------------------------------------------------------------------- +// Sets the local position from a transform +//----------------------------------------------------------------------------- +void CBaseEntity::SetLocalTransform( const matrix3x4_t &localTransform ) +{ + // FIXME: Should angles go away? Should we just use transforms? + Vector vecLocalOrigin; + QAngle vecLocalAngles; + MatrixGetColumn( localTransform, 3, vecLocalOrigin ); + MatrixAngles( localTransform, vecLocalAngles ); + SetLocalOrigin( vecLocalOrigin ); + SetLocalAngles( vecLocalAngles ); +} + + +//----------------------------------------------------------------------------- +// Is the entity floating? +//----------------------------------------------------------------------------- +bool CBaseEntity::IsFloating() +{ + if ( !IsEFlagSet(EFL_TOUCHING_FLUID) ) + return false; + + IPhysicsObject *pObject = VPhysicsGetObject(); + if ( !pObject ) + return false; + + int nMaterialIndex = pObject->GetMaterialIndex(); + + float flDensity; + float flThickness; + float flFriction; + float flElasticity; + physprops->GetPhysicsProperties( nMaterialIndex, &flDensity, + &flThickness, &flFriction, &flElasticity ); + + // FIXME: This really only works for water at the moment.. + // Owing the check for density == 1000 + return (flDensity < 1000.0f); +} + + +//----------------------------------------------------------------------------- +// Purpose: Created predictable and sets up Id. Note that persist is ignored on the server. +// Input : *classname - +// *module - +// line - +// persist - +// Output : CBaseEntity +//----------------------------------------------------------------------------- +CBaseEntity *CBaseEntity::CreatePredictedEntityByName( const char *classname, const char *module, int line, bool persist /* = false */ ) +{ +#if !defined( NO_ENTITY_PREDICTION ) + CBasePlayer *player = CBaseEntity::GetPredictionPlayer(); + Assert( player ); + + CBaseEntity *ent = NULL; + + int command_number = player->CurrentCommandNumber(); + int player_index = player->entindex() - 1; + + CPredictableId testId; + testId.Init( player_index, command_number, classname, module, line ); + + ent = CreateEntityByName( classname ); + // No factory??? + if ( !ent ) + return NULL; + + ent->SetPredictionEligible( true ); + + // Set up "shared" id number + ent->m_PredictableID.GetForModify().SetRaw( testId.GetRaw() ); + + return ent; +#else + return NULL; +#endif + +} + +void CBaseEntity::SetPredictionEligible( bool canpredict ) +{ +// Nothing in game code m_bPredictionEligible = canpredict; +} + +//----------------------------------------------------------------------------- +// These could be virtual, but only the player is overriding them +// NOTE: If you make any of these virtual, remove this implementation!!! +//----------------------------------------------------------------------------- +void CBaseEntity::AddPoints( int score, bool bAllowNegativeScore ) +{ + CBasePlayer *pPlayer = ToBasePlayer(this); + if ( pPlayer ) + { + pPlayer->CBasePlayer::AddPoints( score, bAllowNegativeScore ); + } +} + +void CBaseEntity::AddPointsToTeam( int score, bool bAllowNegativeScore ) +{ + CBasePlayer *pPlayer = ToBasePlayer(this); + if ( pPlayer ) + { + pPlayer->CBasePlayer::AddPointsToTeam( score, bAllowNegativeScore ); + } +} + +void CBaseEntity::ViewPunch( const QAngle &angleOffset ) +{ + CBasePlayer *pPlayer = ToBasePlayer(this); + if ( pPlayer ) + { + pPlayer->CBasePlayer::ViewPunch( angleOffset ); + } +} + +void CBaseEntity::VelocityPunch( const Vector &vecForce ) +{ + CBasePlayer *pPlayer = ToBasePlayer(this); + if ( pPlayer ) + { + pPlayer->CBasePlayer::VelocityPunch( vecForce ); + } +} +//----------------------------------------------------------------------------- + + +//----------------------------------------------------------------------------- +// Purpose: Tell clients to remove all decals from this entity +//----------------------------------------------------------------------------- +void CBaseEntity::RemoveAllDecals( void ) +{ + EntityMessageBegin( this ); + WRITE_BYTE( BASEENTITY_MSG_REMOVE_DECALS ); + MessageEnd(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : set - +//----------------------------------------------------------------------------- +void CBaseEntity::ModifyOrAppendCriteria( AI_CriteriaSet& set ) +{ + // TODO + // Append chapter/day? + + set.AppendCriteria( "randomnum", UTIL_VarArgs("%d", RandomInt(0,100)) ); + // Append map name + set.AppendCriteria( "map", gpGlobals->mapname.ToCStr() ); + // Append our classname and game name + set.AppendCriteria( "classname", GetClassname() ); + set.AppendCriteria( "name", GetEntityName().ToCStr() ); + + // Append our health + set.AppendCriteria( "health", UTIL_VarArgs( "%i", GetHealth() ) ); + + float healthfrac = 0.0f; + if ( GetMaxHealth() > 0 ) + { + healthfrac = (float)GetHealth() / (float)GetMaxHealth(); + } + + set.AppendCriteria( "healthfrac", UTIL_VarArgs( "%.3f", healthfrac ) ); + + // Go through all the global states and append them + + for ( int i = 0; i < GlobalEntity_GetNumGlobals(); i++ ) + { + const char *szGlobalName = GlobalEntity_GetName(i); + int iGlobalState = (int)GlobalEntity_GetStateByIndex(i); + set.AppendCriteria( szGlobalName, UTIL_VarArgs( "%i", iGlobalState ) ); + } + +#ifndef MAPBASE // We do this later now so contexts can override criteria. I originally didn't want to remove it here in case there would be problems, but I think I have all of the bases covered. + // Append anything from I/O or keyvalues pairs + AppendContextToCriteria( set ); +#endif + + if( hl2_episodic.GetBool() ) + { + set.AppendCriteria( "episodic", "1" ); + } + + // Append anything from world I/O/keyvalues with "world" as prefix +#ifdef MAPBASE + CWorld *world = GetWorldEntity(); +#else + CWorld *world = dynamic_cast< CWorld * >( CBaseEntity::Instance( engine->PEntityOfEntIndex( 0 ) ) ); +#endif + if ( world ) + { + world->AppendContextToCriteria( set, "world" ); + } + +#ifdef MAPBASE + // Append base stuff + set.AppendCriteria("spawnflags", UTIL_VarArgs("%i", GetSpawnFlags())); + set.AppendCriteria("flags", UTIL_VarArgs("%i", GetFlags())); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : set - +// "" - +//----------------------------------------------------------------------------- +void CBaseEntity::AppendContextToCriteria( AI_CriteriaSet& set, const char *prefix /*= ""*/ ) +{ + RemoveExpiredConcepts(); + + int c = GetContextCount(); + int i; + + char sz[ 128 ]; + for ( i = 0; i < c; i++ ) + { + const char *name = GetContextName( i ); + const char *value = GetContextValue( i ); + + Q_snprintf( sz, sizeof( sz ), "%s%s", prefix, name ); + + set.AppendCriteria( sz, value ); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Input : set - +// "" - +//----------------------------------------------------------------------------- +void CBaseEntity::ReAppendContextCriteria( AI_CriteriaSet& set ) +{ + // Append contexts again. This allows it to override standard criteria, including that of derived classes. + CWorld *world = GetWorldEntity(); + if ( world ) + { + // I didn't know this until recently, but world contexts are actually prefixed by "world". + // I'm changing this here to reduce confusion and allow greater potential. + // (e.g. disabling combine soldier episodic on/off in Episodic binaries with world context "episodic:0", even though that's not happening anymore) + // The old prefixed ones still exist. They're just not appended again. + world->AppendContextToCriteria( set ); + } + + AppendContextToCriteria( set ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Removes expired concepts from list +// Output : +//----------------------------------------------------------------------------- +void CBaseEntity::RemoveExpiredConcepts( void ) +{ + int c = GetContextCount(); + int i; + + for ( i = 0; i < c; i++ ) + { + if ( ContextExpired( i ) ) + { + m_ResponseContexts.Remove( i ); + c--; + i--; + continue; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Get current context count +// Output : int +//----------------------------------------------------------------------------- +int CBaseEntity::GetContextCount() const +{ + return m_ResponseContexts.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// Output : const char +//----------------------------------------------------------------------------- +const char *CBaseEntity::GetContextName( int index ) const +{ + if ( index < 0 || index >= m_ResponseContexts.Count() ) + { + Assert( 0 ); + return ""; + } + + return m_ResponseContexts[ index ].m_iszName.ToCStr(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// Output : const char +//----------------------------------------------------------------------------- +const char *CBaseEntity::GetContextValue( int index ) const +{ + if ( index < 0 || index >= m_ResponseContexts.Count() ) + { + Assert( 0 ); + return ""; + } + + return m_ResponseContexts[ index ].m_iszValue.ToCStr(); + +} + +//----------------------------------------------------------------------------- +// Purpose: Check if context has expired +// Input : index - +// Output : bool +//----------------------------------------------------------------------------- +bool CBaseEntity::ContextExpired( int index ) const +{ + if ( index < 0 || index >= m_ResponseContexts.Count() ) + { + Assert( 0 ); + return true; + } + + if ( !m_ResponseContexts[ index ].m_fExpirationTime ) + { + return false; + } + + return ( m_ResponseContexts[ index ].m_fExpirationTime <= gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: Search for index of named context string +// Input : *name - +// Output : int +//----------------------------------------------------------------------------- +int CBaseEntity::FindContextByName( const char *name ) const +{ + int c = m_ResponseContexts.Count(); + for ( int i = 0; i < c; i++ ) + { + if ( FStrEq( name, GetContextName( i ) ) ) + return i; + } + + return -1; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Searches entity for named context string and/or value. +// Intended to be called by entities rather than the conventional response system. +// Input : *name - Context name. +// *value - Context value. (optional) +// Output : bool +//----------------------------------------------------------------------------- +bool CBaseEntity::HasContext( const char *name, const char *value ) const +{ + int c = m_ResponseContexts.Count(); + for ( int i = 0; i < c; i++ ) + { + if ( Matcher_NamesMatch( name, STRING(m_ResponseContexts[i].m_iszName) ) ) + { + if (value == NULL) + return true; + else + return Matcher_Match(STRING(m_ResponseContexts[i].m_iszValue), value); + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Searches entity for named context string and/or value. +// Intended to be called by entities rather than the conventional response system. +// Input : *name - Context name. +// *value - Context value. (optional) +// Output : bool +//----------------------------------------------------------------------------- +bool CBaseEntity::HasContext( string_t name, string_t value ) const +{ + int c = m_ResponseContexts.Count(); + for ( int i = 0; i < c; i++ ) + { + if ( name == m_ResponseContexts[i].m_iszName ) + { + if (value == NULL_STRING) + return true; + else + return value == m_ResponseContexts[i].m_iszValue; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Searches entity for named context string and/or value. +// Intended to be called by entities rather than the conventional response system. +// Input : *nameandvalue - Context name and value. +// Output : bool +//----------------------------------------------------------------------------- +bool CBaseEntity::HasContext( const char *nameandvalue ) const +{ + char key[ 128 ]; + char value[ 128 ]; + + const char *p = nameandvalue; + while ( p ) + { + p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), NULL ); + + return HasContext( key, value ); + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// Output : const char +//----------------------------------------------------------------------------- +const char *CBaseEntity::GetContextValue( const char *contextName ) const +{ + int idx = FindContextByName( contextName ); + if ( idx == -1 ) + return ""; + + return m_ResponseContexts[ idx ].m_iszValue.ToCStr(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +float CBaseEntity::GetContextExpireTime( const char *name ) +{ + int idx = FindContextByName( name ); + if ( idx == -1 ) + return 0.0f; + + return m_ResponseContexts[ idx ].m_fExpirationTime; +} + +//----------------------------------------------------------------------------- +// Purpose: Internal method or removing contexts and can remove multiple contexts in one call +// Input : *contextName - +//----------------------------------------------------------------------------- +void CBaseEntity::RemoveContext( const char *contextName ) +{ + char key[ 128 ]; + char value[ 128 ]; + float duration; + + const char *p = contextName; + while ( p ) + { + duration = 0.0f; + p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), &duration ); + if ( duration ) + { + duration += gpGlobals->curtime; + } + + int iIndex = FindContextByName( key ); + if ( iIndex != -1 ) + { + m_ResponseContexts.Remove( iIndex ); + } + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +// Input : inputdata - +//----------------------------------------------------------------------------- +void CBaseEntity::InputAddContext( inputdata_t& inputdata ) +{ + const char *contextName = inputdata.value.String(); + AddContext( contextName ); +} + + +//----------------------------------------------------------------------------- +// Purpose: User inputs. These fire the corresponding user outputs, and are +// a means of forwarding messages through !activator to a target known +// known by !activator but not by the targetting entity. +// +// For example, say you have three identical trains, following the same +// path. Each train has a sprite in hierarchy with it that needs to +// toggle on/off as it passes each path_track. You would hook each train's +// OnUser1 output to it's sprite's Toggle input, then connect each path_track's +// OnPass output to !activator's FireUser1 input. +//----------------------------------------------------------------------------- +void CBaseEntity::InputFireUser1( inputdata_t& inputdata ) +{ + m_OnUser1.FireOutput( inputdata.pActivator, this ); +} + + +void CBaseEntity::InputFireUser2( inputdata_t& inputdata ) +{ + m_OnUser2.FireOutput( inputdata.pActivator, this ); +} + + +void CBaseEntity::InputFireUser3( inputdata_t& inputdata ) +{ + m_OnUser3.FireOutput( inputdata.pActivator, this ); +} + + +void CBaseEntity::InputFireUser4( inputdata_t& inputdata ) +{ + m_OnUser4.FireOutput( inputdata.pActivator, this ); +} + + +#ifdef MAPBASE +void CBaseEntity::InputPassUser1( inputdata_t& inputdata ) +{ + m_OutUser1.Set( inputdata.value, inputdata.pActivator, this ); +} + +void CBaseEntity::InputPassUser2( inputdata_t& inputdata ) +{ + m_OutUser2.Set( inputdata.value, inputdata.pActivator, this ); +} + +void CBaseEntity::InputPassUser3( inputdata_t& inputdata ) +{ + m_OutUser3.Set( inputdata.value, inputdata.pActivator, this ); +} + +void CBaseEntity::InputPassUser4( inputdata_t& inputdata ) +{ + m_OutUser4.Set( inputdata.value, inputdata.pActivator, this ); +} + + +void CBaseEntity::InputFireRandomUser( inputdata_t& inputdata ) +{ + switch (RandomInt(1, 4)) + { + case 1: m_OnUser1.FireOutput( inputdata.pActivator, this ); break; + case 2: m_OnUser2.FireOutput( inputdata.pActivator, this ); break; + case 3: m_OnUser3.FireOutput( inputdata.pActivator, this ); break; + case 4: m_OnUser4.FireOutput( inputdata.pActivator, this ); break; + } +} + +void CBaseEntity::InputPassRandomUser( inputdata_t& inputdata ) +{ + switch (RandomInt(1, 4)) + { + case 1: m_OutUser1.Set( inputdata.value, inputdata.pActivator, this ); break; + case 2: m_OutUser2.Set( inputdata.value, inputdata.pActivator, this ); break; + case 3: m_OutUser3.Set( inputdata.value, inputdata.pActivator, this ); break; + case 4: m_OutUser4.Set( inputdata.value, inputdata.pActivator, this ); break; + } +} +#endif + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Sets the entity's targetname. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetEntityName( inputdata_t& inputdata ) +{ + SetName( inputdata.value.StringID() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the generic target field. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetTarget( inputdata_t& inputdata ) +{ + m_target = inputdata.value.StringID(); + Activate(); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our owner entity. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetOwnerEntity( inputdata_t& inputdata ) +{ + SetOwnerEntity(inputdata.value.Entity()); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for adding to the entity's health. +// Input : Integer health points to add. +//----------------------------------------------------------------------------- +void CBaseEntity::InputAddHealth( inputdata_t &inputdata ) +{ + TakeHealth( abs(inputdata.value.Int()), DMG_GENERIC ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for removing health from the entity. +// Input : Integer health points to remove. +//----------------------------------------------------------------------------- +void CBaseEntity::InputRemoveHealth( inputdata_t &inputdata ) +{ + TakeDamage( CTakeDamageInfo( this, this, abs(inputdata.value.Int()), DMG_GENERIC ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetHealth( inputdata_t &inputdata ) +{ + int iNewHealth = inputdata.value.Int(); + int iDelta = abs(GetHealth() - iNewHealth); + if ( iNewHealth > GetHealth() ) + { + TakeHealth( iDelta, DMG_GENERIC ); + } + else if ( iNewHealth < GetHealth() ) + { + TakeDamage( CTakeDamageInfo( this, this, iDelta, DMG_GENERIC ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetMaxHealth( inputdata_t &inputdata ) +{ + int iNewMaxHealth = inputdata.value.Int(); + SetMaxHealth(iNewMaxHealth); + + if (GetHealth() > iNewMaxHealth) + { + SetHealth(iNewMaxHealth); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Forces the named output to fire. +// In addition to the output itself, parameter may include !activator, !caller, and what to pass. +//----------------------------------------------------------------------------- +void CBaseEntity::InputFireOutput( inputdata_t& inputdata ) +{ + char sParameter[MAX_PATH]; + Q_strncpy( sParameter, inputdata.value.String(), sizeof(sParameter) ); + if ( sParameter ) + { + int iter = 0; + char *data[5] = {sParameter}; + char *sToken = strtok( sParameter, ":" ); + while ( sToken && iter < 5 ) + { + data[iter] = sToken; + iter++; + sToken = strtok( NULL, ":" ); + } + + //DevMsg("data[0]: %s\ndata[1]: %s\ndata[2]: %s\ndata[3]: %s\ndata[4]: %s\n", data[0], data[1], data[2], data[3], data[4]); + + // Format: :::: + // + // data[0] = Output Name + // data[1] = Activator + // data[2] = Caller + // data[3] = Parameter + // data[4] = Delay + // + CBaseEntity *pActivator = inputdata.pActivator; + if (data[1]) + pActivator = gEntList.FindEntityByName(NULL, data[1], this, inputdata.pActivator, inputdata.pCaller); + + CBaseEntity *pCaller = this; + if (data[2]) + pCaller = gEntList.FindEntityByName(NULL, data[2], this, inputdata.pActivator, inputdata.pCaller); + + variant_t parameter; + if (data[3]) + { + parameter.SetString(MAKE_STRING(data[3])); + } + + float flDelay = 0.0f; + if (data[4]) + flDelay = atof(data[4]); + + FireNamedOutput(data[0], parameter, pActivator, pCaller, flDelay); + //Msg("Output Name: %s, Activator: %s, Caller: %s, Data: %s, Delay: %f\n", data[0], pActivator->GetDebugName(), pCaller->GetDebugName(), parameter.String(), flDelay); + } + else + { + Warning("FireOutput input fired with bad parameter. Format: ::::\n"); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Removes all outputs of the specified name. +//----------------------------------------------------------------------------- +void CBaseEntity::InputRemoveOutput( inputdata_t& inputdata ) +{ + const char *szOutput = inputdata.value.String(); + datamap_t *dmap = GetDataDescMap(); + while ( dmap ) + { + int fields = dmap->dataNumFields; + for ( int i = 0; i < fields; i++ ) + { + typedescription_t *dataDesc = &dmap->dataDesc[i]; + if ( ( dataDesc->fieldType == FIELD_CUSTOM ) && ( dataDesc->flags & FTYPEDESC_OUTPUT ) ) + { + // If our names match, remove + if (Matcher_NamesMatch(szOutput, dataDesc->externalName)) + { + CBaseEntityOutput *pOutput = (CBaseEntityOutput *)((int)this + (int)dataDesc->fieldOffset[0]); + pOutput->DeleteAllElements(); + } + } + } + + dmap = dmap->baseMap; + } +} + +// Find a way to implement this +/* +//------------------------------------------------------------------------------ +// Purpose: Cancels all I/O events of a specific output. +//------------------------------------------------------------------------------ +void CBaseEntity::InputCancelOutput( inputdata_t &inputdata ) +{ + +} +*/ + +//----------------------------------------------------------------------------- +// Purpose: Replaces all outputs of the specified name. +//----------------------------------------------------------------------------- +void CBaseEntity::InputReplaceOutput( inputdata_t& inputdata ) +{ + char sParameter[128]; + Q_strncpy( sParameter, inputdata.value.String(), sizeof(sParameter) ); + if (!sParameter) + return; + + int iter = 0; + char *data[2]; + char *sToken = strtok( sParameter, ": " ); + while ( sToken && iter < 2 ) + { + data[iter] = sToken; + iter++; + sToken = strtok( NULL, ": " ); + } + + const char *szOutput = data[0]; + const char *szNewOutput = data[1]; + if (!szOutput || !szNewOutput) + { + Warning("ReplaceOutput input fired with bad parameter. Format: :\n"); + return; + } + + int iOutputsReplaced = 0; + datamap_t *dmap = GetDataDescMap(); + while ( dmap ) + { + int fields = dmap->dataNumFields; + for ( int i = 0; i < fields; i++ ) + { + typedescription_t *dataDesc = &dmap->dataDesc[i]; + if ( ( dataDesc->fieldType == FIELD_CUSTOM ) && ( dataDesc->flags & FTYPEDESC_OUTPUT ) ) + { + // If our names match, replace + if (Matcher_NamesMatch(szOutput, dataDesc->externalName)) + { + CBaseEntityOutput *pOutput = (CBaseEntityOutput *)((int)this + (int)dataDesc->fieldOffset[0]); + const char *szTarget; + const char *szInputName; + const char *szParam; + float flDelay; + int iNumTimes; + char szData[256]; + for ( CEventAction *ev = pOutput->GetActionList(); ev != NULL; ev = ev->m_pNext ) + { + // This is the only way I think we could do this. Accomplishes the job more or less anyway + szTarget = STRING(ev->m_iTarget); + szInputName = STRING(ev->m_iTargetInput); + szParam = ev->m_iParameter == NULL_STRING ? "" : STRING(ev->m_iParameter); + flDelay = ev->m_flDelay; + iNumTimes = ev->m_nTimesToFire; + Q_snprintf(szData, sizeof(szData), "%s,%s,%s,%f,%i", szTarget, szInputName, szParam, flDelay, iNumTimes); + + KeyValue(szNewOutput, szData); + + DevMsg("ReplaceOutput: %s %s\n", szNewOutput, szData); + + iOutputsReplaced++; + } + pOutput->DeleteAllElements(); + } + } + } + + dmap = dmap->baseMap; + } + + if (iOutputsReplaced == 0) + { + Warning("ReplaceOutput unable to find %s on %s\n", szOutput, GetDebugName()); + } + else + { + DevMsg("Replaced %i instances of %s with %s on %s\n", iOutputsReplaced, szOutput, szNewOutput, GetDebugName()); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Forces the named input to fire...what? +// Inputception...or is it just "Inception"? Whatever. +// True inception would be using this input to fire AcceptInput. +// (it would probably crash, I haven't tested it) +// +// In addition to the input itself, parameter may include !activator, !caller, and what to pass. +//----------------------------------------------------------------------------- +void CBaseEntity::InputAcceptInput( inputdata_t& inputdata ) +{ + char sParameter[MAX_PATH]; + Q_strncpy( sParameter, inputdata.value.String(), sizeof(sParameter) ); + if ( sParameter ) + { + int iter = 0; + char *data[5] = {sParameter}; + char *sToken = strtok( sParameter, ":" ); + while ( sToken && iter < 5 ) + { + data[iter] = sToken; + iter++; + sToken = strtok( NULL, ":" ); + } + + //DevMsg("data[0]: %s\ndata[1]: %s\ndata[2]: %s\ndata[3]: %s\ndata[4]: %s\n", data[0], data[1], data[2], data[3], data[4]); + + // Format: :::: + // + // data[0] = Input Name + // data[1] = Parameter + // data[2] = Activator + // data[3] = Caller + // data[4] = Output ID + // + variant_t parameter; + if (data[1]) + { + parameter.SetString(MAKE_STRING(data[1])); + } + + CBaseEntity *pActivator = inputdata.pActivator; + if (data[2]) + pActivator = gEntList.FindEntityByName(NULL, data[2], this, inputdata.pActivator, inputdata.pCaller); + + CBaseEntity *pCaller = this; + if (data[3]) + pCaller = gEntList.FindEntityByName(NULL, data[3], this, inputdata.pActivator, inputdata.pCaller); + + int iOutputID = -1; + if (data[4]) + iOutputID = atoi(data[4]); + + AcceptInput(data[0], pActivator, pCaller, parameter, iOutputID); + Msg("Input Name: %s, Activator: %s, Caller: %s, Data: %s, Output ID: %i\n", data[0], pActivator ? pActivator->GetDebugName() : "None", pCaller ? pCaller->GetDebugName() : "None", parameter.String(), iOutputID); + } + else + { + Warning("AcceptInput input fired with bad parameter. Format: ::::\n"); + } +} + +//------------------------------------------------------------------------------ +// Purpose: Cancels any I/O events in the queue that were fired by this entity. +//------------------------------------------------------------------------------ +void CBaseEntity::InputCancelPending( inputdata_t &inputdata ) +{ + g_EventQueue.CancelEvents( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Frees all of our children, entities parented to this entity. +//----------------------------------------------------------------------------- +void CBaseEntity::InputFreeChildren( inputdata_t& inputdata ) +{ + UnlinkAllChildren( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our origin. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetLocalOrigin( inputdata_t& inputdata ) +{ + Vector vec; + inputdata.value.Vector3D(vec); + SetLocalOrigin(vec); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our angles. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetLocalAngles( inputdata_t& inputdata ) +{ + QAngle ang; + inputdata.value.Angle3D(ang); + SetLocalAngles(ang); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our origin. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetAbsOrigin( inputdata_t& inputdata ) +{ + Vector vec; + inputdata.value.Vector3D(vec); + SetAbsOrigin(vec); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our angles. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetAbsAngles( inputdata_t& inputdata ) +{ + QAngle ang; + inputdata.value.Angle3D(ang); + SetAbsAngles(ang); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our velocity. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetLocalVelocity( inputdata_t& inputdata ) +{ + Vector vec; + inputdata.value.Vector3D(vec); + SetLocalVelocity(vec); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our angular velocity. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetLocalAngularVelocity( inputdata_t& inputdata ) +{ + Vector vec; + inputdata.value.Vector3D(vec); + SetLocalAngularVelocity(QAngle(vec.x, vec.y, vec.z)); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds spawn flags. +//----------------------------------------------------------------------------- +void CBaseEntity::InputAddSpawnFlags( inputdata_t& inputdata ) +{ + AddSpawnFlags(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes spawn flags. +//----------------------------------------------------------------------------- +void CBaseEntity::InputRemoveSpawnFlags( inputdata_t& inputdata ) +{ + RemoveSpawnFlags(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our render mode. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetRenderMode( inputdata_t& inputdata ) +{ + SetRenderMode((RenderMode_t)inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our render FX. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetRenderFX( inputdata_t& inputdata ) +{ + m_nRenderFX = inputdata.value.Int(); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our view hide flags. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetViewHideFlags( inputdata_t& inputdata ) +{ + m_iViewHideFlags = inputdata.value.Int(); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds effects. +//----------------------------------------------------------------------------- +void CBaseEntity::InputAddEffects( inputdata_t& inputdata ) +{ + AddEffects(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes effects. +//----------------------------------------------------------------------------- +void CBaseEntity::InputRemoveEffects( inputdata_t& inputdata ) +{ + RemoveEffects(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Shortcut for removing nodraw. +//----------------------------------------------------------------------------- +void CBaseEntity::InputDrawEntity( inputdata_t& inputdata ) +{ + RemoveEffects(EF_NODRAW); +} + +//----------------------------------------------------------------------------- +// Purpose: Shortcut to adding nodraw. +//----------------------------------------------------------------------------- +void CBaseEntity::InputUndrawEntity( inputdata_t& inputdata ) +{ + AddEffects(EF_NODRAW); +} + +//----------------------------------------------------------------------------- +// Purpose: Inspired by the Portal 2 input of the same name. +//----------------------------------------------------------------------------- +void CBaseEntity::InputEnableReceivingFlashlight( inputdata_t& inputdata ) +{ + m_bDisableFlashlight = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Inspired by the Portal 2 input of the same name. +//----------------------------------------------------------------------------- +void CBaseEntity::InputDisableReceivingFlashlight( inputdata_t& inputdata ) +{ + m_bDisableFlashlight = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds eflags. +//----------------------------------------------------------------------------- +void CBaseEntity::InputAddEFlags( inputdata_t& inputdata ) +{ + AddEFlags(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes eflags. +//----------------------------------------------------------------------------- +void CBaseEntity::InputRemoveEFlags( inputdata_t& inputdata ) +{ + RemoveEFlags(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds solid flags. +//----------------------------------------------------------------------------- +void CBaseEntity::InputAddSolidFlags( inputdata_t& inputdata ) +{ + AddSolidFlags(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes solid flags. +//----------------------------------------------------------------------------- +void CBaseEntity::InputRemoveSolidFlags( inputdata_t& inputdata ) +{ + RemoveSolidFlags(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the movetype. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetMoveType( inputdata_t& inputdata ) +{ + SetMoveType((MoveType_t)inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the collision group. +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetCollisionGroup( inputdata_t& inputdata ) +{ + SetCollisionGroup(inputdata.value.Int()); +} + +//----------------------------------------------------------------------------- +// Purpose: Touch touch :) +//----------------------------------------------------------------------------- +void CBaseEntity::InputTouch( inputdata_t& inputdata ) +{ + if (inputdata.value.Entity()) + Touch( inputdata.value.Entity() ); + else + Warning( "%s InputTouch: Can't touch null entity", GetDebugName() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Passes KilledNPC to our possibly more capable parents. +//----------------------------------------------------------------------------- +void CBaseEntity::InputKilledNPC( inputdata_t &inputdata ) +{ + // Don't get stuck in an endless loop + if (inputdata.value.Int() > 16) + return; + else + inputdata.value.SetInt(inputdata.value.Int() + 1); + + if (GetOwnerEntity()) + { + GetOwnerEntity()->AcceptInput("KilledNPC", inputdata.pActivator, inputdata.pCaller, inputdata.value, inputdata.nOutputID); + } + else if (HasPhysicsAttacker(4.0f)) + { + HasPhysicsAttacker(4.0f)->AcceptInput("KilledNPC", inputdata.pActivator, inputdata.pCaller, inputdata.value, inputdata.nOutputID); + } + else if (GetMoveParent()) + { + GetMoveParent()->AcceptInput("KilledNPC", inputdata.pActivator, inputdata.pCaller, inputdata.value, inputdata.nOutputID); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Remove if not visible by any players +//----------------------------------------------------------------------------- +void CBaseEntity::InputKillIfNotVisible( inputdata_t& inputdata ) +{ +#ifdef MAPBASE_MP + // Go through each client and check if we're in their viewcone. + // If we're in someone's viewcone, return immediately. + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer && pPlayer->FInViewCone( this ) ) + return; + } +#else + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( !pPlayer || !pPlayer->FInViewCone( this ) ) +#endif + { + m_OnKilled.FireOutput(inputdata.pActivator, this); + UTIL_Remove(this); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Remove when not visible by any players +//----------------------------------------------------------------------------- +void CBaseEntity::InputKillWhenNotVisible( inputdata_t& inputdata ) +{ + SetContextThink( &CBaseEntity::SUB_RemoveWhenNotVisible, gpGlobals->curtime + inputdata.value.Float(), "SUB_RemoveWhenNotVisible" ); + //SetRenderColorA( 255 ); + //m_nRenderMode = kRenderNormal; +} + +//----------------------------------------------------------------------------- +// Purpose: Stop thinking +//----------------------------------------------------------------------------- +void CBaseEntity::InputSetThinkNull( inputdata_t& inputdata ) +{ + const char *szContext = inputdata.value.String(); + if (szContext && szContext[0] != '\0') + { + SetContextThink( NULL, TICK_NEVER_THINK, szContext ); + } + else + { + SetThink( NULL ); + SetNextThink( TICK_NEVER_THINK ); + } +} +#endif + + +//--------------------------------------------------------- +// Use the string as the filename of a script file +// that should be loaded from disk, compiled, and run. +//--------------------------------------------------------- +void CBaseEntity::InputRunScriptFile(inputdata_t& inputdata) +{ + RunScriptFile(inputdata.value.String()); +} + +//--------------------------------------------------------- +// Send the string to the VM as source code and execute it +//--------------------------------------------------------- +void CBaseEntity::InputRunScript(inputdata_t& inputdata) +{ + RunScript(inputdata.value.String(), "InputRunScript"); +} + +//--------------------------------------------------------- +// Make an explicit function call. +//--------------------------------------------------------- +void CBaseEntity::InputCallScriptFunction(inputdata_t& inputdata) +{ + CallScriptFunction(inputdata.value.String(), NULL); +} + +#ifdef MAPBASE_VSCRIPT +//--------------------------------------------------------- +// Send the string to the VM as source code and execute it +//--------------------------------------------------------- +void CBaseEntity::InputRunScriptQuotable(inputdata_t& inputdata) +{ + char szQuotableCode[1024]; + if (V_StrSubst( inputdata.value.String(), "''", "\"", szQuotableCode, sizeof( szQuotableCode ), false )) + { + RunScript( szQuotableCode, "InputRunScriptQuotable" ); + } + else + { + RunScript( inputdata.value.String(), "InputRunScriptQuotable" ); + } +} + +//--------------------------------------------------------- +// Clear this entity's script scope +//--------------------------------------------------------- +void CBaseEntity::InputClearScriptScope(inputdata_t& inputdata) +{ + m_ScriptScope.Term(); +} +#endif + +// #define VMPROFILE // define to profile vscript calls + +#ifdef VMPROFILE +float g_debugCumulativeTime = 0.0; +float g_debugCounter = 0; + +#define START_VMPROFILE float debugStartTime = Plat_FloatTime(); +#define UPDATE_VMPROFILE \ + g_debugCumulativeTime += Plat_FloatTime() - debugStartTime; \ + g_debugCounter++; \ + if ( g_debugCounter >= 500 ) \ + { \ + DevMsg("***VSCRIPT PROFILE***: %s %s: %6.4f milliseconds\n", "500 vscript function calls", "", g_debugCumulativeTime*1000.0 ); \ + g_debugCounter = 0; \ + g_debugCumulativeTime = 0.0; \ + } \ + +#else + +#define START_VMPROFILE +#define UPDATE_VMPROFILE + +#endif // VMPROFILE + +//----------------------------------------------------------------------------- +// Returns true if the function was located and called. false otherwise. +// NOTE: Assumes the function takes no parameters at the moment. +//----------------------------------------------------------------------------- +bool CBaseEntity::CallScriptFunction(const char* pFunctionName, ScriptVariant_t* pFunctionReturn) +{ + START_VMPROFILE + + if (!ValidateScriptScope()) + { + DevMsg("\n***\nFAILED to create private ScriptScope. ABORTING script\n***\n"); + return false; + } + + + HSCRIPT hFunc = m_ScriptScope.LookupFunction(pFunctionName); + + if (hFunc) + { + m_ScriptScope.Call(hFunc, pFunctionReturn); + m_ScriptScope.ReleaseFunction(hFunc); + + UPDATE_VMPROFILE + + return true; + } + + return false; +} + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +// Gets a function handle +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::LookupScriptFunction(const char* pFunctionName) +{ + START_VMPROFILE + + if (!m_ScriptScope.IsInitialized()) + { + return NULL; + } + + return m_ScriptScope.LookupFunction(pFunctionName); +} + +//----------------------------------------------------------------------------- +// Calls and releases a function handle (ASSUMES SCRIPT SCOPE AND FUNCTION ARE VALID!) +//----------------------------------------------------------------------------- +bool CBaseEntity::CallScriptFunctionHandle(HSCRIPT hFunc, ScriptVariant_t* pFunctionReturn) +{ + m_ScriptScope.Call(hFunc, pFunctionReturn); + m_ScriptScope.ReleaseFunction(hFunc); + + UPDATE_VMPROFILE + + return true; +} +#endif + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::ConnectOutputToScript(const char* pszOutput, const char* pszScriptFunc) +{ + CBaseEntityOutput* pOutput = FindNamedOutput(pszOutput); + if (!pOutput) + { + DevMsg(2, "Script failed to find output \"%s\"\n", pszOutput); + return; + } + + string_t iszSelf = AllocPooledString("!self"); // @TODO: cache this [4/25/2008 tom] + CEventAction* pAction = pOutput->GetActionList(); + while (pAction) + { + if (pAction->m_iTarget == iszSelf && + pAction->m_flDelay == 0 && + pAction->m_nTimesToFire == EVENT_FIRE_ALWAYS && + V_strcmp(STRING(pAction->m_iTargetInput), "CallScriptFunction") == 0 && + V_strcmp(STRING(pAction->m_iParameter), pszScriptFunc) == 0) + { + return; + } + pAction = pAction->m_pNext; + } + + pAction = new CEventAction(NULL); + pAction->m_iTarget = iszSelf; + pAction->m_iTargetInput = AllocPooledString("CallScriptFunction"); + pAction->m_iParameter = AllocPooledString(pszScriptFunc); + pOutput->AddEventAction(pAction); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::DisconnectOutputFromScript(const char* pszOutput, const char* pszScriptFunc) +{ + CBaseEntityOutput* pOutput = FindNamedOutput(pszOutput); + if (!pOutput) + { + DevMsg(2, "Script failed to find output \"%s\"\n", pszOutput); + return; + } + + string_t iszSelf = AllocPooledString("!self"); // @TODO: cache this [4/25/2008 tom] + CEventAction* pAction = pOutput->GetActionList(); + while (pAction) + { + if (pAction->m_iTarget == iszSelf && + pAction->m_flDelay == 0 && + pAction->m_nTimesToFire == EVENT_FIRE_ALWAYS && + V_strcmp(STRING(pAction->m_iTargetInput), "CallScriptFunction") == 0 && + V_strcmp(STRING(pAction->m_iParameter), pszScriptFunc) == 0) + { + pOutput->RemoveEventAction(pAction); + delete pAction; + return; + } + pAction = pAction->m_pNext; + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::ScriptThink(void) +{ + ScriptVariant_t varThinkRetVal; + if (CallScriptFunction(m_iszScriptThinkFunction.ToCStr(), &varThinkRetVal)) + { + float flThinkFrequency = 0.0f; + if (!varThinkRetVal.AssignTo(&flThinkFrequency)) + { + // use default think interval if script think function doesn't provide one + flThinkFrequency = sv_script_think_interval.GetFloat(); + } + + SetNextThink( gpGlobals->curtime + flThinkFrequency, "ScriptThink" ); + } + else + { + DevWarning("%s FAILED to call script think function %s!\n", GetDebugName(), STRING(m_iszScriptThinkFunction)); + } +} + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::ScriptSetThinkFunction( const char *szFunc, float flTime ) +{ + // Empty string stops thinking + if (!szFunc || szFunc[0] == '\0') + { + ScriptStopThinkFunction(); + } + else + { + m_iszScriptThinkFunction = AllocPooledString(szFunc); + flTime = max( 0, flTime ); + SetContextThink( &CBaseEntity::ScriptThink, gpGlobals->curtime + flTime, "ScriptThink" ); + } +} + +void CBaseEntity::ScriptStopThinkFunction() +{ + m_iszScriptThinkFunction = NULL_STRING; + SetContextThink( NULL, TICK_NEVER_THINK, "ScriptThink" ); +} +#endif // MAPBASE_VSCRIPT + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +const char* CBaseEntity::GetScriptId() +{ +#ifdef MAPBASE_VSCRIPT + return STRING(m_iszScriptId); +#else + return STRING(m_iszScriptThinkFunction); +#endif +} + +//----------------------------------------------------------------------------- +// Recreate the old behaviour of GetScriptId under a new function +//----------------------------------------------------------------------------- +// const char* CBaseEntity::GetScriptThinkFunction() +// { +// return STRING(m_iszScriptThinkFunction); +// } + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::GetScriptScope() +{ + return m_ScriptScope; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::ScriptGetMoveParent(void) +{ + return ToHScript(GetMoveParent()); +} +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::ScriptGetRootMoveParent() +{ + return ToHScript(GetRootMoveParent()); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::ScriptFirstMoveChild(void) +{ + return ToHScript(FirstMoveChild()); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::ScriptNextMovePeer(void) +{ + return ToHScript(NextMovePeer()); +} + +//----------------------------------------------------------------------------- +// Purpose: Load, compile, and run a script file from disk. +// Input : *pScriptFile - The filename of the script file. +// bUseRootScope - If true, runs this script in the root scope, not +// in this entity's private scope. +//----------------------------------------------------------------------------- +bool CBaseEntity::RunScriptFile(const char* pScriptFile, bool bUseRootScope) +{ + if (!ValidateScriptScope()) + { + DevMsg("\n***\nFAILED to create private ScriptScope. ABORTING script\n***\n"); + return false; + } + + if (bUseRootScope) + { + return VScriptRunScript(pScriptFile); + } + else + { + return VScriptRunScript(pScriptFile, m_ScriptScope, true); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Compile and execute a discrete string of script source code +// Input : *pScriptText - A string containing script code to compile and run +//----------------------------------------------------------------------------- +bool CBaseEntity::RunScript(const char* pScriptText, const char* pDebugFilename) +{ + if (!ValidateScriptScope()) + { + DevMsg("\n***\nFAILED to create private ScriptScope. ABORTING script\n***\n"); + return false; + } + + if (m_ScriptScope.Run(pScriptText, pDebugFilename) == SCRIPT_ERROR) + { + DevWarning(" Entity %s encountered an error in RunScript()\n", GetDebugName()); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *contextName - +//----------------------------------------------------------------------------- +void CBaseEntity::AddContext( const char *contextName ) +{ + char key[ 128 ]; + char value[ 128 ]; + float duration; + + const char *p = contextName; + while ( p ) + { + duration = 0.0f; + p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), &duration ); + if ( duration ) + { + duration += gpGlobals->curtime; + } + + int iIndex = FindContextByName( key ); + if ( iIndex != -1 ) + { + // Set the existing context to the new value + m_ResponseContexts[iIndex].m_iszValue = AllocPooledString( value ); + m_ResponseContexts[iIndex].m_fExpirationTime = duration; + continue; + } + + ResponseContext_t newContext; + newContext.m_iszName = AllocPooledString( key ); + newContext.m_iszValue = AllocPooledString( value ); + newContext.m_fExpirationTime = duration; + + m_ResponseContexts.AddToTail( newContext ); + } +} + +#ifdef MAPBASE +void CBaseEntity::AddContext( const char *name, const char *value, float duration ) +{ + int iIndex = FindContextByName( name ); + if ( iIndex != -1 ) + { + // Set the existing context to the new value + m_ResponseContexts[iIndex].m_iszValue = AllocPooledString( value ); + m_ResponseContexts[iIndex].m_fExpirationTime = duration; + return; + } + + ResponseContext_t newContext; + newContext.m_iszName = AllocPooledString( name ); + newContext.m_iszValue = AllocPooledString( value ); + newContext.m_fExpirationTime = duration; + + m_ResponseContexts.AddToTail( newContext ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +// Input : inputdata - +//----------------------------------------------------------------------------- +void CBaseEntity::InputRemoveContext( inputdata_t& inputdata ) +{ + const char *contextName = inputdata.value.String(); + int idx = FindContextByName( contextName ); + if ( idx == -1 ) + return; + + m_ResponseContexts.Remove( idx ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : inputdata - +//----------------------------------------------------------------------------- +void CBaseEntity::InputClearContext( inputdata_t& inputdata ) +{ + m_ResponseContexts.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : IResponseSystem +//----------------------------------------------------------------------------- +IResponseSystem *CBaseEntity::GetResponseSystem() +{ + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : inputdata - +//----------------------------------------------------------------------------- +void CBaseEntity::InputDispatchResponse( inputdata_t& inputdata ) +{ + DispatchResponse( inputdata.value.String() ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::InputDisableShadow( inputdata_t &inputdata ) +{ + AddEffects( EF_NOSHADOW ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::InputEnableShadow( inputdata_t &inputdata ) +{ + RemoveEffects( EF_NOSHADOW ); +} + +//----------------------------------------------------------------------------- +// Purpose: An input to add a new connection from this entity +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBaseEntity::InputAddOutput( inputdata_t &inputdata ) +{ + char sOutputName[MAX_PATH]; + Q_strncpy( sOutputName, inputdata.value.String(), sizeof(sOutputName) ); + char *sChar = strchr( sOutputName, ' ' ); + if ( sChar ) + { + *sChar = '\0'; + // Now replace all the :'s in the string with ,'s. + // Has to be done this way because Hammer doesn't allow ,'s inside parameters. + char *sColon = strchr( sChar+1, ':' ); + while ( sColon ) + { + *sColon = ','; + sColon = strchr( sChar+1, ':' ); + } + KeyValue( sOutputName, sChar+1 ); + } + else + { + Warning("AddOutput input fired with bad string. Format: ,,,,\n"); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBaseEntity::InputChangeVariable( inputdata_t &inputdata ) +{ + const char *szKeyName = NULL; + const char *szValue = NULL; + + char sOutputName[MAX_PATH]; + Q_strncpy( sOutputName, inputdata.value.String(), sizeof(sOutputName) ); + char *sChar = strchr( sOutputName, ' ' ); + if ( sChar ) + { + *sChar = '\0'; + // Now replace all the :'s in the string with ,'s. + // Has to be done this way because Hammer doesn't allow ,'s inside parameters. + char *sColon = strchr( sChar+1, ':' ); + while ( sColon ) + { + *sColon = ','; + sColon = strchr( sChar+1, ':' ); + } + + szKeyName = sOutputName; + szValue = sChar + 1; + } + else + { + Warning("ChangeVariable input fired with bad string. Format: ,,,,\n"); + return; + } + + if (szKeyName == NULL) + return; + + for ( datamap_t *dmap = GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the readable fields in the data description, looking for a match + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & (FTYPEDESC_SAVE | FTYPEDESC_KEY) ) + { + if ( Matcher_NamesMatch(szKeyName, dmap->dataDesc[i].fieldName) ) + { + // Copied from ::ParseKeyvalue...or technically logic_datadesc_accessor... + typedescription_t *pField = &dmap->dataDesc[i]; + char *data = Datadesc_SetFieldString( szValue, this, pField, NULL ); + + if (!data) + { + Warning( "%s cannot set field of type %i.\n", GetDebugName(), dmap->dataDesc[i].fieldType ); + } + } + } + } + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *conceptName - +//----------------------------------------------------------------------------- +void CBaseEntity::DispatchResponse( const char *conceptName ) +{ + IResponseSystem *rs = GetResponseSystem(); + if ( !rs ) + return; + + AI_CriteriaSet set; + // Always include the concept name + set.AppendCriteria( "concept", conceptName, CONCEPT_WEIGHT ); + // Let NPC fill in most match criteria + ModifyOrAppendCriteria( set ); + + // Append local player criteria to set,too + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if( pPlayer ) + pPlayer->ModifyOrAppendPlayerCriteria( set ); + +#ifdef MAPBASE + ReAppendContextCriteria( set ); +#endif + + // Now that we have a criteria set, ask for a suitable response + AI_Response result; + bool found = rs->FindBestResponse( set, result ); + if ( !found ) + { + return; + } + + // Handle the response here... + char response[ 256 ]; + result.GetResponse( response, sizeof( response ) ); +#ifdef MAPBASE + if (response[0] == '$') + { + const char *context = response + 1; + const char *replace = GetContextValue(context); + + if (replace) + { + DevMsg("Replacing %s with %s...\n", response, replace); + Q_strncpy(response, replace, sizeof(response)); + + // Precache it now because it may not have been precached before + switch ( result.GetType() ) + { + case RESPONSE_SPEAK: + { + PrecacheScriptSound( response ); + } + break; + + case RESPONSE_SCENE: + { + // TODO: Gender handling? + PrecacheInstancedScene( response ); + } + break; + } + } + } +#endif + switch ( result.GetType() ) + { + case RESPONSE_SPEAK: + { + EmitSound( response ); + } + break; + case RESPONSE_SENTENCE: + { +#ifdef MAPBASE + if (response[0] != '!') + { + SENTENCEG_PlayRndSz( edict(), response, 1, result.GetSoundLevel(), 0, PITCH_NORM ); + break; + } +#endif + int sentenceIndex = SENTENCEG_Lookup( response ); + if( sentenceIndex == -1 ) + { + // sentence not found + break; + } + + // FIXME: Get pitch from npc? + CPASAttenuationFilter filter( this ); + CBaseEntity::EmitSentenceByIndex( filter, entindex(), CHAN_VOICE, sentenceIndex, 1, result.GetSoundLevel(), 0, PITCH_NORM ); + } + break; + case RESPONSE_SCENE: + { +#ifdef MAPBASE + // Most flexing actors that use scenes override DispatchResponse via CAI_Expresser in ai_speech. + // So, in order for non-actors to use scenes by themselves, they actually don't really use them at all. + // Most scenes that would be used as responses only have one sound, so we take the first sound in a scene and emit it manually. + // + // Of course, env_speaker uses scenes without using itself as an actor, but that overrides DispatchResponse in Mapbase + // with the original code intact. Hopefully no other entity uses this like that... + + //if (!ClassMatches("env_speaker")) + { + // Expand gender string + GenderExpandString( response, response, sizeof( response ) ); + + // Trust that it's been precached + const char *pszSound = GetFirstSoundInScene(response); + EmitSound(pszSound); + } + //else + // InstancedScriptedScene(NULL, response); +#else + // Try to fire scene w/o an actor + InstancedScriptedScene( NULL, response ); +#endif + } + break; + case RESPONSE_PRINT: + { + + } + break; + default: + // Don't know how to handle .vcds!!! + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseEntity::DumpResponseCriteria( void ) +{ + Msg("----------------------------------------------\n"); + Msg("RESPONSE CRITERIA FOR: %s (%s)\n", GetClassname(), GetDebugName() ); + + AI_CriteriaSet set; + // Let NPC fill in most match criteria + ModifyOrAppendCriteria( set ); + + // Append local player criteria to set,too + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer ) + { + pPlayer->ModifyOrAppendPlayerCriteria( set ); + } + +#ifdef MAPBASE + ReAppendContextCriteria( set ); +#endif + + // Now dump it all to console + set.Describe(); +} + +//------------------------------------------------------------------------------ +void CC_Ent_Show_Response_Criteria( const CCommand& args ) +{ + CBaseEntity *pEntity = NULL; + while ( (pEntity = GetNextCommandEntity( UTIL_GetCommandClient(), args[1], pEntity )) != NULL ) + { + pEntity->DumpResponseCriteria(); + } +} +static ConCommand ent_show_response_criteria("ent_show_response_criteria", CC_Ent_Show_Response_Criteria, "Print, to the console, an entity's current criteria set used to select responses.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at ", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose: Show an entity's autoaim radius +//------------------------------------------------------------------------------ +void CC_Ent_Autoaim( const CCommand& args ) +{ + SetDebugBits( UTIL_GetCommandClient(),args[1], OVERLAY_AUTOAIM_BIT ); +} +static ConCommand ent_autoaim("ent_autoaim", CC_Ent_Autoaim, "Displays the entity's autoaim radius.\n\tArguments: {entity_name} / {class_name} / no argument picks what player is looking at", FCVAR_CHEAT ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CAI_BaseNPC *CBaseEntity::MyNPCPointer( void ) +{ + if ( IsNPC() ) + return assert_cast(this); + + return NULL; +} + +ConVar step_spline( "step_spline", "0" ); + +//----------------------------------------------------------------------------- +// Purpose: Run one tick's worth of faked simulation +// Input : *step - +//----------------------------------------------------------------------------- +void CBaseEntity::ComputeStepSimulationNetwork( StepSimulationData *step ) +{ + if ( !step ) + { + Assert( !"ComputeStepSimulationNetworkOriginAndAngles with NULL step\n" ); + return; + } + + // Don't run again if we've already calculated this tick + if ( step->m_nLastProcessTickCount == gpGlobals->tickcount ) + { + return; + } + + step->m_nLastProcessTickCount = gpGlobals->tickcount; + + // Origin + // It's inactive + if ( step->m_bOriginActive ) + { + // First see if any external code moved the entity + if ( GetStepOrigin() != step->m_Next.vecOrigin ) + { + step->m_bOriginActive = false; + } + else + { + // Compute interpolated info based on tick interval + float frac = 1.0f; + int tickdelta = step->m_Next.nTickCount - step->m_Previous.nTickCount; + if ( tickdelta > 0 ) + { + frac = (float)( gpGlobals->tickcount - step->m_Previous.nTickCount ) / (float) tickdelta; + frac = clamp( frac, 0.0f, 1.0f ); + } + + if (step->m_Previous2.nTickCount == 0 || step->m_Previous2.nTickCount >= step->m_Previous.nTickCount) + { + Vector delta = step->m_Next.vecOrigin - step->m_Previous.vecOrigin; + VectorMA( step->m_Previous.vecOrigin, frac, delta, step->m_vecNetworkOrigin ); + } + else if (!step_spline.GetBool()) + { + StepSimulationStep *pOlder = &step->m_Previous; + StepSimulationStep *pNewer = &step->m_Next; + + if (step->m_Discontinuity.nTickCount > step->m_Previous.nTickCount) + { + if (gpGlobals->tickcount > step->m_Discontinuity.nTickCount) + { + pOlder = &step->m_Discontinuity; + } + else + { + pNewer = &step->m_Discontinuity; + } + + tickdelta = pNewer->nTickCount - pOlder->nTickCount; + if ( tickdelta > 0 ) + { + frac = (float)( gpGlobals->tickcount - pOlder->nTickCount ) / (float) tickdelta; + frac = clamp( frac, 0.0f, 1.0f ); + } + } + + Vector delta = pNewer->vecOrigin - pOlder->vecOrigin; + VectorMA( pOlder->vecOrigin, frac, delta, step->m_vecNetworkOrigin ); + } + else + { + Hermite_Spline( step->m_Previous2.vecOrigin, step->m_Previous.vecOrigin, step->m_Next.vecOrigin, frac, step->m_vecNetworkOrigin ); + } + } + } + + // Angles + if ( step->m_bAnglesActive ) + { + // See if external code changed the orientation of the entity + if ( GetStepAngles() != step->m_angNextRotation ) + { + step->m_bAnglesActive = false; + } + else + { + // Compute interpolated info based on tick interval + float frac = 1.0f; + int tickdelta = step->m_Next.nTickCount - step->m_Previous.nTickCount; + if ( tickdelta > 0 ) + { + frac = (float)( gpGlobals->tickcount - step->m_Previous.nTickCount ) / (float) tickdelta; + frac = clamp( frac, 0.0f, 1.0f ); + } + + if (step->m_Previous2.nTickCount == 0 || step->m_Previous2.nTickCount >= step->m_Previous.nTickCount) + { + // Pure blend between start/end orientations + Quaternion outangles; + QuaternionBlend( step->m_Previous.qRotation, step->m_Next.qRotation, frac, outangles ); + QuaternionAngles( outangles, step->m_angNetworkAngles ); + } + else if (!step_spline.GetBool()) + { + StepSimulationStep *pOlder = &step->m_Previous; + StepSimulationStep *pNewer = &step->m_Next; + + if (step->m_Discontinuity.nTickCount > step->m_Previous.nTickCount) + { + if (gpGlobals->tickcount > step->m_Discontinuity.nTickCount) + { + pOlder = &step->m_Discontinuity; + } + else + { + pNewer = &step->m_Discontinuity; + } + + tickdelta = pNewer->nTickCount - pOlder->nTickCount; + if ( tickdelta > 0 ) + { + frac = (float)( gpGlobals->tickcount - pOlder->nTickCount ) / (float) tickdelta; + frac = clamp( frac, 0.0f, 1.0f ); + } + } + + // Pure blend between start/end orientations + Quaternion outangles; + QuaternionBlend( pOlder->qRotation, pNewer->qRotation, frac, outangles ); + QuaternionAngles( outangles, step->m_angNetworkAngles ); + } + else + { + // FIXME: enable spline interpolation when turning is debounced. + Quaternion outangles; + Hermite_Spline( step->m_Previous2.qRotation, step->m_Previous.qRotation, step->m_Next.qRotation, frac, outangles ); + QuaternionAngles( outangles, step->m_angNetworkAngles ); + } + } + } + +} + + +//----------------------------------------------------------------------------- +bool CBaseEntity::UseStepSimulationNetworkOrigin( const Vector **out_v ) +{ + Assert( out_v ); + + + if ( g_bTestMoveTypeStepSimulation && + GetMoveType() == MOVETYPE_STEP && + HasDataObjectType( STEPSIMULATION ) ) + { + StepSimulationData *step = ( StepSimulationData * )GetDataObject( STEPSIMULATION ); + ComputeStepSimulationNetwork( step ); + *out_v = &step->m_vecNetworkOrigin; + + return step->m_bOriginActive; + } + + return false; +} + +//----------------------------------------------------------------------------- +bool CBaseEntity::UseStepSimulationNetworkAngles( const QAngle **out_a ) +{ + Assert( out_a ); + + if ( g_bTestMoveTypeStepSimulation && + GetMoveType() == MOVETYPE_STEP && + HasDataObjectType( STEPSIMULATION ) ) + { + StepSimulationData *step = ( StepSimulationData * )GetDataObject( STEPSIMULATION ); + ComputeStepSimulationNetwork( step ); + *out_a = &step->m_angNetworkAngles; + return step->m_bAnglesActive; + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- + +bool CBaseEntity::AddStepDiscontinuity( float flTime, const Vector &vecOrigin, const QAngle &vecAngles ) +{ + if ((GetMoveType() != MOVETYPE_STEP ) || !HasDataObjectType( STEPSIMULATION ) ) + { + return false; + } + + StepSimulationData *step = ( StepSimulationData * )GetDataObject( STEPSIMULATION ); + + if (!step) + { + Assert( 0 ); + return false; + } + + step->m_Discontinuity.nTickCount = TIME_TO_TICKS( flTime ); + step->m_Discontinuity.vecOrigin = vecOrigin; + AngleQuaternion( vecAngles, step->m_Discontinuity.qRotation ); + + return true; +} + + +Vector CBaseEntity::GetStepOrigin( void ) const +{ + return GetLocalOrigin(); +} + +QAngle CBaseEntity::GetStepAngles( void ) const +{ + return GetLocalAngles(); +} + +//----------------------------------------------------------------------------- +// Purpose: For each client who appears to be a valid recipient, checks the client has disabled CC and if so, removes them from +// the recipient list. +// Input : filter - +//----------------------------------------------------------------------------- +void CBaseEntity::RemoveRecipientsIfNotCloseCaptioning( CRecipientFilter& filter ) +{ + int c = filter.GetRecipientCount(); + for ( int i = c - 1; i >= 0; --i ) + { + int playerIndex = filter.GetRecipientIndex( i ); + + CBasePlayer *player = static_cast< CBasePlayer * >( CBaseEntity::Instance( playerIndex ) ); + if ( !player ) + continue; +#if !defined( _XBOX ) + const char *cvarvalue = engine->GetClientConVarValue( playerIndex, "closecaption" ); + Assert( cvarvalue ); + if ( !cvarvalue[ 0 ] ) + continue; + + int value = atoi( cvarvalue ); +#else + static ConVar *s_pCloseCaption = NULL; + if ( !s_pCloseCaption ) + { + s_pCloseCaption = cvar->FindVar( "closecaption" ); + if ( !s_pCloseCaption ) + { + Error( "XBOX couldn't find closecaption convar!!!" ); + } + } + + int value = s_pCloseCaption->GetInt(); +#endif + // No close captions? + if ( value == 0 ) + { + filter.RemoveRecipient( player ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Wrapper to emit a sentence and also a close caption token for the sentence as appropriate. +// Input : filter - +// iEntIndex - +// iChannel - +// iSentenceIndex - +// flVolume - +// iSoundlevel - +// iFlags - +// iPitch - +// bUpdatePositions - +// soundtime - +//----------------------------------------------------------------------------- +void CBaseEntity::EmitSentenceByIndex( IRecipientFilter& filter, int iEntIndex, int iChannel, int iSentenceIndex, + float flVolume, soundlevel_t iSoundlevel, int iFlags /*= 0*/, int iPitch /*=PITCH_NORM*/, + const Vector *pOrigin /*=NULL*/, const Vector *pDirection /*=NULL*/, + bool bUpdatePositions /*=true*/, float soundtime /*=0.0f*/ ) +{ + CUtlVector< Vector > dummy; + enginesound->EmitSentenceByIndex( filter, iEntIndex, iChannel, iSentenceIndex, + flVolume, iSoundlevel, iFlags, iPitch, 0, pOrigin, pDirection, &dummy, bUpdatePositions, soundtime ); +} + + +void CBaseEntity::SetRefEHandle( const CBaseHandle &handle ) +{ + m_RefEHandle = handle; + if ( edict() ) + { + COMPILE_TIME_ASSERT( NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS <= 8*sizeof( edict()->m_NetworkSerialNumber ) ); + edict()->m_NetworkSerialNumber = (m_RefEHandle.GetSerialNumber() & (1 << NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS) - 1); + } +} + + +bool CPointEntity::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "mins" ) || FStrEq( szKeyName, "maxs" ) ) + { + Warning("Warning! Can't specify mins/maxs for point entities! (%s)\n", GetClassname() ); + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +bool CServerOnlyPointEntity::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "mins" ) || FStrEq( szKeyName, "maxs" ) ) + { + Warning("Warning! Can't specify mins/maxs for point entities! (%s)\n", GetClassname() ); + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +bool CLogicalEntity::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "mins" ) || FStrEq( szKeyName, "maxs" ) ) + { + Warning("Warning! Can't specify mins/maxs for point entities! (%s)\n", GetClassname() ); + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the entity invisible, and makes it remove itself on the next frame +//----------------------------------------------------------------------------- +void CBaseEntity::RemoveDeferred( void ) +{ + // Set our next think to remove us + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + // Hide us completely + AddEffects( EF_NODRAW ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); +} + +#define MIN_CORPSE_FADE_TIME 10.0 +#define MIN_CORPSE_FADE_DIST 256.0 +#define MAX_CORPSE_FADE_DIST 1500.0 + +// +// fade out - slowly fades a entity out, then removes it. +// +// DON'T USE ME FOR GIBS AND STUFF IN MULTIPLAYER! +// SET A FUTURE THINK AND A RENDERMODE!! +void CBaseEntity::SUB_StartFadeOut( float delay, bool notSolid ) +{ + SetThink( &CBaseEntity::SUB_FadeOut ); + SetNextThink( gpGlobals->curtime + delay ); + SetRenderColorA( 255 ); + m_nRenderMode = kRenderNormal; + + if ( notSolid ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + SetLocalAngularVelocity( vec3_angle ); + } +} + +void CBaseEntity::SUB_StartFadeOutInstant() +{ + SUB_StartFadeOut( 0, true ); +} + +//----------------------------------------------------------------------------- +// Purpose: Vanish when players aren't looking +//----------------------------------------------------------------------------- +void CBaseEntity::SUB_Vanish( void ) +{ + //Always think again next frame + SetNextThink( gpGlobals->curtime + 0.1f ); + + CBasePlayer *pPlayer; + + //Get all players + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + //Get the next client + if ( ( pPlayer = UTIL_PlayerByIndex( i ) ) != NULL ) + { + Vector corpseDir = (GetAbsOrigin() - pPlayer->WorldSpaceCenter() ); + + float flDistSqr = corpseDir.LengthSqr(); + //If the player is close enough, don't fade out + if ( flDistSqr < (MIN_CORPSE_FADE_DIST*MIN_CORPSE_FADE_DIST) ) + return; + + // If the player's far enough away, we don't care about looking at it + if ( flDistSqr < (MAX_CORPSE_FADE_DIST*MAX_CORPSE_FADE_DIST) ) + { + VectorNormalize( corpseDir ); + + Vector plForward; + pPlayer->EyeVectors( &plForward ); + + float dot = plForward.Dot( corpseDir ); + + if ( dot > 0.0f ) + return; + } + } + } + + //If we're here, then we can vanish safely + m_iHealth = 0; + SetThink( &CBaseEntity::SUB_Remove ); +} + +void CBaseEntity::SUB_PerformFadeOut( void ) +{ + float dt = gpGlobals->frametime; + if ( dt > 0.1f ) + { + dt = 0.1f; + } + m_nRenderMode = kRenderTransTexture; + int speed = MAX(1,256*dt); // fade out over 1 second + SetRenderColorA( UTIL_Approach( 0, m_clrRender->a, speed ) ); +} + +bool CBaseEntity::SUB_AllowedToFade( void ) +{ + if( VPhysicsGetObject() ) + { + if( VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD || GetEFlags() & EFL_IS_BEING_LIFTED_BY_BARNACLE ) + return false; + } + + // on Xbox, allow these to fade out +#ifndef _XBOX + CBasePlayer *pPlayer = ( AI_IsSinglePlayer() ) ? UTIL_GetLocalPlayer() : NULL; + + if ( pPlayer && pPlayer->FInViewCone( this ) ) + return false; +#endif + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Fade out slowly +//----------------------------------------------------------------------------- +void CBaseEntity::SUB_FadeOut( void ) +{ + if ( SUB_AllowedToFade() == false ) + { + SetNextThink( gpGlobals->curtime + 1 ); + SetRenderColorA( 255 ); + return; + } + + SUB_PerformFadeOut(); + + if ( m_clrRender->a == 0 ) + { +#ifdef MAPBASE + // This was meant for KillWhenNotVisible before it used its own function, + // but there's not really any harm for keeping this here. + m_OnKilled.FireOutput(this, this); +#endif + UTIL_Remove(this); + } + else + { + SetNextThink( gpGlobals->curtime ); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: For KillWhenNotVisible, based off of SUB_FadeOut +//----------------------------------------------------------------------------- +void CBaseEntity::SUB_RemoveWhenNotVisible( void ) +{ + if ( SUB_AllowedToFade() == false ) + { + SetNextThink( gpGlobals->curtime + 1, "SUB_RemoveWhenNotVisible" ); + SetRenderColorA( 255 ); + return; + } + + SetRenderColorA( m_clrRender->a - 1 ); + + if ( m_clrRender->a == 0 ) + { + m_OnKilled.FireOutput(this, this); + UTIL_Remove(this); + } + else + { + SetNextThink( gpGlobals->curtime, "SUB_RemoveWhenNotVisible" ); + } +} +#endif + + +inline bool AnyPlayersInHierarchy_R( CBaseEntity *pEnt ) +{ + if ( pEnt->IsPlayer() ) + return true; + + for ( CBaseEntity *pCur = pEnt->FirstMoveChild(); pCur; pCur=pCur->NextMovePeer() ) + { + if ( AnyPlayersInHierarchy_R( pCur ) ) + return true; + } + + return false; +} + + +void CBaseEntity::RecalcHasPlayerChildBit() +{ + if ( AnyPlayersInHierarchy_R( this ) ) + AddEFlags( EFL_HAS_PLAYER_CHILD ); + else + RemoveEFlags( EFL_HAS_PLAYER_CHILD ); +} + + +bool CBaseEntity::DoesHavePlayerChild() +{ + return IsEFlagSet( EFL_HAS_PLAYER_CHILD ); +} + + +//------------------------------------------------------------------------------ +void CBaseEntity::IncrementInterpolationFrame() +{ + m_ubInterpolationFrame = (m_ubInterpolationFrame + 1) % NOINTERP_PARITY_MAX; +} + +//------------------------------------------------------------------------------ + +void CBaseEntity::OnModelLoadComplete( const model_t* model ) +{ + Assert( m_bDynamicModelPending && IsDynamicModelIndex( m_nModelIndex ) ); + Assert( model == modelinfo->GetModel( m_nModelIndex ) ); + + m_bDynamicModelPending = false; + + if ( m_bDynamicModelSetBounds ) + { + m_bDynamicModelSetBounds = false; + SetCollisionBoundsFromModel(); + } + + OnNewModel(); +} + +//------------------------------------------------------------------------------ + +void CBaseEntity::SetCollisionBoundsFromModel() +{ + if ( IsDynamicModelLoading() ) + { + m_bDynamicModelSetBounds = true; + return; + } + + if ( const model_t *pModel = GetModel() ) + { + Vector mns, mxs; + modelinfo->GetModelBounds( pModel, mns, mxs ); + UTIL_SetSize( this, mns, mxs ); + } +} + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::GetScriptInstance() +{ + if (!m_hScriptInstance) + { + if (m_iszScriptId == NULL_STRING) + { + char* szName = (char*)stackalloc(1024); + g_pScriptVM->GenerateUniqueKey((m_iName.Get() != NULL_STRING) ? STRING(GetEntityName()) : GetClassname(), szName, 1024); + m_iszScriptId = AllocPooledString(szName); + } + + m_hScriptInstance = g_pScriptVM->RegisterInstance(GetScriptDesc(), this); + g_pScriptVM->SetInstanceUniqeId(m_hScriptInstance, STRING(m_iszScriptId)); + } + return m_hScriptInstance; +} + +//----------------------------------------------------------------------------- +// Using my edict, cook up a unique VScript scope that's private to me, and +// persistent. +//----------------------------------------------------------------------------- +bool CBaseEntity::ValidateScriptScope() +{ + if (!m_ScriptScope.IsInitialized()) + { + if (scriptmanager == NULL) + { + ExecuteOnce(DevMsg("Cannot execute script because scripting is disabled (-scripting)\n")); + return false; + } + + if (g_pScriptVM == NULL) + { + ExecuteOnce(DevMsg(" Cannot execute script because there is no available VM\n")); + return false; + } + + // Force instance creation + GetScriptInstance(); + + EHANDLE hThis; + hThis.Set(this); + + bool bResult = m_ScriptScope.Init(STRING(m_iszScriptId)); + + if (!bResult) + { + DevMsg("%s couldn't create ScriptScope!\n", GetDebugName()); + return false; + } + g_pScriptVM->SetValue(m_ScriptScope, "self", GetScriptInstance()); + } + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Run all of the vscript files that are set in this entity's VSCRIPTS +// field in Hammer. The list is space-delimited. +//----------------------------------------------------------------------------- +void CBaseEntity::RunVScripts() +{ + if (m_iszVScripts == NULL_STRING) + { + return; + } + +#ifdef MAPBASE_VSCRIPT + if (g_pScriptVM == NULL) + { + return; + } +#endif + + ValidateScriptScope(); + + // All functions we want to have call chained instead of overwritten + // by other scripts in this entities list. + static const char* sCallChainFunctions[] = + { + "OnPostSpawn", + "Precache" + }; + + ScriptLanguage_t language = g_pScriptVM->GetLanguage(); + + // Make a call chainer for each in this entities scope + for (int j = 0; j < ARRAYSIZE(sCallChainFunctions); ++j) + { + + if (language == SL_PYTHON) + { + // UNDONE - handle call chaining in python + ; + } + else if (language == SL_SQUIRREL) + { + //TODO: For perf, this should be precompiled and the %s should be passed as a parameter + HSCRIPT hCreateChainScript = g_pScriptVM->CompileScript(CFmtStr("%sCallChain <- CSimpleCallChainer(\"%s\", self.GetScriptScope(), true)", sCallChainFunctions[j], sCallChainFunctions[j])); + g_pScriptVM->Run(hCreateChainScript, (HSCRIPT)m_ScriptScope); + } + } + + char szScriptsList[255]; + Q_strcpy(szScriptsList, STRING(m_iszVScripts)); + CUtlStringList szScripts; + + V_SplitString(szScriptsList, " ", szScripts); + + for (int i = 0; i < szScripts.Count(); i++) + { +#ifdef MAPBASE + CGMsg( 0, CON_GROUP_VSCRIPT, "%s executing script: %s\n", GetDebugName(), szScripts[i] ); +#else + Log( "%s executing script: %s\n", GetDebugName(), szScripts[i]); +#endif + + RunScriptFile(szScripts[i], IsWorld()); + + for (int j = 0; j < ARRAYSIZE(sCallChainFunctions); ++j) + { + if (language == SL_PYTHON) + { + // UNDONE - handle call chaining in python + ; + } + else if (language == SL_SQUIRREL) + { + //TODO: For perf, this should be precompiled and the %s should be passed as a parameter. + HSCRIPT hRunPostScriptExecute = g_pScriptVM->CompileScript(CFmtStr("%sCallChain.PostScriptExecute()", sCallChainFunctions[j])); + g_pScriptVM->Run(hRunPostScriptExecute, (HSCRIPT)m_ScriptScope); + } + } + } + + if (m_iszScriptThinkFunction != NULL_STRING) + { + SetContextThink(&CBaseEntity::ScriptThink, gpGlobals->curtime + sv_script_think_interval.GetFloat(), "ScriptThink"); + } +} + + +//-------------------------------------------------------------------------------------------------- +// This is called during entity spawning and after restore to allow scripts to precache any +// resources they need. +//-------------------------------------------------------------------------------------------------- +void CBaseEntity::RunPrecacheScripts(void) +{ + if (m_iszVScripts == NULL_STRING) + { + return; + } + +#ifdef MAPBASE_VSCRIPT + if (g_pScriptVM == NULL) + { + return; + } +#endif + + HSCRIPT hScriptPrecache = m_ScriptScope.LookupFunction("DispatchPrecache"); + if (hScriptPrecache) + { + g_pScriptVM->Call(hScriptPrecache, m_ScriptScope); + m_ScriptScope.ReleaseFunction(hScriptPrecache); + } +} + +void CBaseEntity::RunOnPostSpawnScripts(void) +{ + if (m_iszVScripts == NULL_STRING) + { + return; + } + +#ifdef MAPBASE_VSCRIPT + if (g_pScriptVM == NULL) + { + return; + } +#endif + + HSCRIPT hFuncConnect = g_pScriptVM->LookupFunction("ConnectOutputs"); + if (hFuncConnect) + { + g_pScriptVM->Call(hFuncConnect, NULL, true, NULL, (HSCRIPT)m_ScriptScope); + g_pScriptVM->ReleaseFunction(hFuncConnect); + } + + HSCRIPT hFuncDisp = m_ScriptScope.LookupFunction("DispatchOnPostSpawn"); + if (hFuncDisp) + { + variant_t variant; + variant.SetString(MAKE_STRING("DispatchOnPostSpawn")); + g_EventQueue.AddEvent(this, "CallScriptFunction", variant, 0, this, this); + m_ScriptScope.ReleaseFunction(hFuncDisp); + + } +} + +#ifndef MAPBASE_VSCRIPT // This is shared now +HSCRIPT CBaseEntity::GetScriptOwnerEntity() +{ + return ToHScript(GetOwnerEntity()); +} + +void CBaseEntity::SetScriptOwnerEntity(HSCRIPT pOwner) +{ + SetOwnerEntity(ToEnt(pOwner)); +} +#endif + +//----------------------------------------------------------------------------- +// VScript access to model's key values +// for iteration and value access, use: +// ScriptFindKey, ScriptGetFirstSubKey, ScriptGetString, +// ScriptGetInt, ScriptGetFloat, ScriptGetNextKey +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::ScriptGetModelKeyValues( void ) +{ + KeyValues *pModelKeyValues = new KeyValues(""); + HSCRIPT hScript = NULL; + const char *pszModelName = modelinfo->GetModelName( GetModel() ); + const char *pBuffer = modelinfo->GetModelKeyValueText( GetModel() ) ; + + if ( pModelKeyValues->LoadFromBuffer( pszModelName, pBuffer ) ) + { + // UNDONE: how does destructor get called on this +#ifdef MAPBASE_VSCRIPT + m_pScriptModelKeyValues = hScript = scriptmanager->CreateScriptKeyValues( g_pScriptVM, pModelKeyValues, true ); // Allow VScript to delete this when the instance is removed. +#else + m_pScriptModelKeyValues = new CScriptKeyValues( pModelKeyValues ); +#endif + + // UNDONE: who calls ReleaseInstance on this??? Does name need to be unique??? + +#ifndef MAPBASE_VSCRIPT + hScript = g_pScriptVM->RegisterInstance( m_pScriptModelKeyValues ); +#endif + + /* + KeyValues *pParticleEffects = pModelKeyValues->FindKey("Particles"); + if ( pParticleEffects ) + { + // Start grabbing the sounds and slotting them in + for ( KeyValues *pSingleEffect = pParticleEffects->GetFirstSubKey(); pSingleEffect; pSingleEffect = pSingleEffect->GetNextKey() ) + { + const char *pParticleEffectName = pSingleEffect->GetString( "name", "" ); + PrecacheParticleSystem( pParticleEffectName ); + } + } + */ + } + + return hScript; +} + +void CBaseEntity::ScriptSetLocalAngularVelocity(float pitchVel, float yawVel, float rollVel) +{ + QAngle qa; + qa.Init(pitchVel, yawVel, rollVel); + SetLocalAngularVelocity(qa); +} + +const Vector& CBaseEntity::ScriptGetLocalAngularVelocity(void) +{ + QAngle qa = GetLocalAngularVelocity(); + static Vector v; + v.x = qa.x; + v.y = qa.y; + v.z = qa.z; + return v; +} + +//----------------------------------------------------------------------------- +// Vscript: Gets the min collision bounds, centered on object +//----------------------------------------------------------------------------- +const Vector& CBaseEntity::ScriptGetBoundingMins(void) +{ + return m_Collision.OBBMins(); +} + +//----------------------------------------------------------------------------- +// Vscript: Gets the max collision bounds, centered on object +//----------------------------------------------------------------------------- +const Vector& CBaseEntity::ScriptGetBoundingMaxs(void) +{ + return m_Collision.OBBMaxs(); +} + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CBaseEntity::ScriptTakeDamage( HSCRIPT pInfo ) +{ + if (pInfo) + { + CTakeDamageInfo *info = HScriptToClass( pInfo ); //ToDamageInfo( pInfo ); + if (info) + { + return OnTakeDamage( *info ); + } + } + + return 0; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::ScriptFireBullets( HSCRIPT pInfo ) +{ + if (pInfo) + { + extern FireBulletsInfo_t *GetFireBulletsInfoFromInfo( HSCRIPT hBulletsInfo ); + FireBulletsInfo_t *info = GetFireBulletsInfoFromInfo( pInfo ); + if (info) + { + FireBullets( *info ); + } + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBaseEntity::ScriptAddContext( const char *name, const char *value, float duration ) +{ + AddContext( name, value, duration ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +const char *CBaseEntity::ScriptGetContext( const char *name ) +{ + return GetContextValue( name ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HSCRIPT CBaseEntity::ScriptGetContextIndex( int index ) +{ + if (index >= m_ResponseContexts.Count()) + return NULL; + + ScriptVariant_t varTable; + g_pScriptVM->CreateTable( varTable ); + + g_pScriptVM->SetValue( varTable, "name", STRING( m_ResponseContexts[index].m_iszName ) ); + g_pScriptVM->SetValue( varTable, "value", STRING( m_ResponseContexts[index].m_iszValue ) ); + g_pScriptVM->SetValue( varTable, "expiration_time", m_ResponseContexts[index].m_fExpirationTime ); + + return varTable.m_hScript; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CBaseEntity::ScriptClassify( void ) +{ + return (int)Classify(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CBaseEntity::ScriptAddOutput( const char *pszOutputName, const char *pszTarget, const char *pszAction, const char *pszParameter, float flDelay, int iMaxTimes ) +{ + const char *pszValue = UTIL_VarArgs("%s,%s,%s,%f,%i", pszTarget, pszAction, pszParameter, flDelay, iMaxTimes); + return KeyValue( pszOutputName, pszValue ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +const char *CBaseEntity::ScriptGetKeyValue( const char *pszKeyName ) +{ + static char szValue[128]; + GetKeyValue( pszKeyName, szValue, sizeof(szValue) ); + return szValue; +} + +//----------------------------------------------------------------------------- +// Vscript: Dispatch an interaction to the entity +//----------------------------------------------------------------------------- +bool CBaseEntity::ScriptDispatchInteraction( int interactionType, HSCRIPT data, HSCRIPT sourceEnt ) +{ + return DispatchInteraction( interactionType, data, ToEnt( sourceEnt ) ? ToEnt( sourceEnt )->MyCombatCharacterPointer() : NULL ); +} +#endif + + +#ifdef MAPBASE +extern int EntityFactory_AutoComplete( const char *cmdname, CUtlVector< CUtlString > &commands, CUtlRBTree< CUtlString > &symbols, char *substring, int checklen = 0 ); + +//------------------------------------------------------------------------------ +// Purpose: Create an entity of the given type +//------------------------------------------------------------------------------ +class CEntCreateAutoCompletionFunctor : public ICommandCallback, public ICommandCompletionCallback +{ +public: + virtual bool CreateAimed() { return false; } + + virtual void CommandCallback( const CCommand &args ) + { + MDLCACHE_CRITICAL_SECTION(); + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if (!pPlayer) + { + return; + } + + // Don't allow regular users to create point_servercommand entities for the same reason as blocking ent_fire + if ( !Q_stricmp( args[1], "point_servercommand" ) ) + { + if ( engine->IsDedicatedServer() ) + { + // We allow people with disabled autokick to do it, because they already have rcon. + if ( pPlayer->IsAutoKickDisabled() == false ) + return; + } + else if ( gpGlobals->maxClients > 1 ) + { + // On listen servers with more than 1 player, only allow the host to create point_servercommand. + CBasePlayer *pHostPlayer = UTIL_GetListenServerHost(); + if ( pPlayer != pHostPlayer ) + return; + } + } + + bool allowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + // Try to create entity + CBaseEntity *entity = dynamic_cast< CBaseEntity * >( CreateEntityByName(args[1]) ); + if (entity) + { + // Pass in any additional parameters. + for ( int i = 2; i + 1 < args.ArgC(); i += 2 ) + { + const char *pKeyName = args[i]; + const char *pValue = args[i+1]; + entity->KeyValue( pKeyName, pValue ); + } + + DispatchSpawn(entity); + + // Now attempt to drop into the world + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + + // Pass through the player's vehicle + CTraceFilterSkipTwoEntities filter( pPlayer, pPlayer->GetVehicleEntity(), COLLISION_GROUP_NONE ); + UTIL_TraceLine(pPlayer->EyePosition(), + pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH,MASK_SOLID, + &filter, &tr ); + + if ( tr.fraction != 1.0 ) + { + // Raise the end position a little up off the floor, place the npc and drop him down + tr.endpos.z += 12; + + if (CreateAimed()) + { + QAngle angles; + VectorAngles( forward, angles ); + angles.x = 0; + angles.z = 0; + entity->Teleport( &tr.endpos, &angles, NULL ); + } + else + { + entity->Teleport( &tr.endpos, NULL, NULL ); + } + + UTIL_DropToFloor( entity, MASK_SOLID ); + } + + entity->Activate(); + } + CBaseEntity::SetAllowPrecache( allowPrecache ); + } + + virtual int CommandCompletionCallback( const char *partial, CUtlVector< CUtlString > &commands ) + { + if ( !g_pGameRules ) + { + return 0; + } + + const char *cmdname = CreateAimed() ? "ent_create_aimed" : "ent_create"; + + char *substring = (char *)partial; + if ( Q_strstr( partial, cmdname ) ) + { + substring = (char *)partial + strlen( cmdname ) + 1; + } + + int checklen = Q_strlen( substring ); + + CUtlRBTree< CUtlString > symbols( 0, 0, UtlStringLessFunc ); + return EntityFactory_AutoComplete( cmdname, commands, symbols, substring, checklen ); + } +}; + +static CEntCreateAutoCompletionFunctor g_EntCreateAutoComplete; +static ConCommand ent_create("ent_create", &g_EntCreateAutoComplete, "Creates an entity of the given type where the player is looking. Additional parameters can be passed in in the form: ent_create ... ", FCVAR_GAMEDLL | FCVAR_CHEAT, &g_EntCreateAutoComplete); + +class CEntCreateAimedAutoCompletionFunctor : public CEntCreateAutoCompletionFunctor +{ +public: + virtual bool CreateAimed() { return true; } +}; + +static CEntCreateAimedAutoCompletionFunctor g_EntCreateAimedAutoComplete; + +static ConCommand ent_create_aimed("ent_create_aimed", &g_EntCreateAimedAutoComplete, "Creates an entity of the given type where the player is looking. Additional parameters can be passed in in the form: ent_create_aimed ... ", FCVAR_CHEAT, &g_EntCreateAimedAutoComplete); +#else +//------------------------------------------------------------------------------ +// Purpose: Create an NPC of the given type +//------------------------------------------------------------------------------ +void CC_Ent_Create( const CCommand& args ) +{ + MDLCACHE_CRITICAL_SECTION(); + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if (!pPlayer) + { + return; + } + + // Don't allow regular users to create point_servercommand entities for the same reason as blocking ent_fire + if ( !Q_stricmp( args[1], "point_servercommand" ) ) + { + if ( engine->IsDedicatedServer() ) + { + // We allow people with disabled autokick to do it, because they already have rcon. + if ( pPlayer->IsAutoKickDisabled() == false ) + return; + } + else if ( gpGlobals->maxClients > 1 ) + { + // On listen servers with more than 1 player, only allow the host to create point_servercommand. + CBasePlayer *pHostPlayer = UTIL_GetListenServerHost(); + if ( pPlayer != pHostPlayer ) + return; + } + } + + bool allowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + // Try to create entity + CBaseEntity *entity = dynamic_cast< CBaseEntity * >( CreateEntityByName(args[1]) ); + if (entity) + { + entity->Precache(); + + // Pass in any additional parameters. + for ( int i = 2; i + 1 < args.ArgC(); i += 2 ) + { + const char *pKeyName = args[i]; + const char *pValue = args[i+1]; + entity->KeyValue( pKeyName, pValue ); + } + + DispatchSpawn(entity); + + // Now attempt to drop into the world + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + UTIL_TraceLine(pPlayer->EyePosition(), + pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH,MASK_SOLID, + pPlayer, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 ) + { + // Raise the end position a little up off the floor, place the npc and drop him down + tr.endpos.z += 12; + entity->Teleport( &tr.endpos, NULL, NULL ); + UTIL_DropToFloor( entity, MASK_SOLID ); + } + + entity->Activate(); + } + CBaseEntity::SetAllowPrecache( allowPrecache ); +} +static ConCommand ent_create("ent_create", CC_Ent_Create, "Creates an entity of the given type where the player is looking. Additional parameters can be passed in in the form: ent_create ... ", FCVAR_GAMEDLL | FCVAR_CHEAT); +#endif + +//------------------------------------------------------------------------------ +// Purpose: Teleport a specified entity to where the player is looking +//------------------------------------------------------------------------------ +bool CC_GetCommandEnt( const CCommand& args, CBaseEntity **ent, Vector *vecTargetPoint, QAngle *vecPlayerAngle ) +{ + // Find the entity + *ent = NULL; + // First try using it as an entindex + int iEntIndex = atoi( args[1] ); + if ( iEntIndex ) + { + *ent = CBaseEntity::Instance( iEntIndex ); + } + else + { + // Try finding it by name + *ent = gEntList.FindEntityByName( NULL, args[1] ); + + if ( !*ent ) + { + // Finally, try finding it by classname + *ent = gEntList.FindEntityByClassname( NULL, args[1] ); + } + } + + if ( !*ent ) + { + Msg( "Couldn't find any entity named '%s'\n", args[1] ); + return false; + } + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( vecTargetPoint ) + { + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + UTIL_TraceLine(pPlayer->EyePosition(), + pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH,MASK_NPCSOLID, + pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction != 1.0 ) + { + *vecTargetPoint = tr.endpos; + } + } + + if ( vecPlayerAngle ) + { + *vecPlayerAngle = pPlayer->EyeAngles(); + } + + return true; +} + +#ifdef MAPBASE +class CEntTeleportAutoCompletionFunctor : public ICommandCallback, public ICommandCompletionCallback +{ +public: + virtual void CommandCallback( const CCommand &command ) + { + if ( command.ArgC() < 2 ) + { + Msg( "Format: ent_teleport \n" ); + return; + } + + CBaseEntity *pEnt; + Vector vecTargetPoint; + if ( CC_GetCommandEnt( command, &pEnt, &vecTargetPoint, NULL ) ) + { + pEnt->Teleport( &vecTargetPoint, NULL, NULL ); + } + } + + virtual int CommandCompletionCallback( const char *partial, CUtlVector< CUtlString > &commands ) + { + if ( !g_pGameRules ) + { + return 0; + } + + const char *cmdname = "ent_teleport"; + + char *substring = (char *)partial; + if ( Q_strstr( partial, cmdname ) ) + { + substring = (char *)partial + strlen( cmdname ) + 1; + } + + int checklen = Q_strlen( substring ); + + CUtlRBTree< CUtlString > symbols( 0, 0, UtlStringLessFunc ); + return AutoCompleteEntities(cmdname, commands, symbols, substring, checklen); + } +}; + +static CEntTeleportAutoCompletionFunctor g_EntTeleportAutoComplete; +static ConCommand ent_teleport("ent_teleport", &g_EntTeleportAutoComplete, "Teleport the specified entity to where the player is looking.\n\tFormat: ent_teleport ", FCVAR_CHEAT, &g_EntTeleportAutoComplete); +#else +//------------------------------------------------------------------------------ +// Purpose: Teleport a specified entity to where the player is looking +//------------------------------------------------------------------------------ +void CC_Ent_Teleport( const CCommand& args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Format: ent_teleport \n" ); + return; + } + + CBaseEntity *pEnt; + Vector vecTargetPoint; + if ( CC_GetCommandEnt( args, &pEnt, &vecTargetPoint, NULL ) ) + { + pEnt->Teleport( &vecTargetPoint, NULL, NULL ); + } +} + +static ConCommand ent_teleport("ent_teleport", CC_Ent_Teleport, "Teleport the specified entity to where the player is looking.\n\tFormat: ent_teleport ", FCVAR_CHEAT); +#endif + +//------------------------------------------------------------------------------ +// Purpose: Orient a specified entity to match the player's angles +//------------------------------------------------------------------------------ +void CC_Ent_Orient( const CCommand& args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Format: ent_orient \n" ); + return; + } + + CBaseEntity *pEnt; + QAngle vecPlayerAngles; + if ( CC_GetCommandEnt( args, &pEnt, NULL, &vecPlayerAngles ) ) + { + QAngle vecEntAngles = pEnt->GetAbsAngles(); + if ( args.ArgC() == 3 && !Q_strncmp( args[2], "allangles", 9 ) ) + { + vecEntAngles = vecPlayerAngles; + } + else + { + vecEntAngles[YAW] = vecPlayerAngles[YAW]; + } + + pEnt->SetAbsAngles( vecEntAngles ); + } +} + +static ConCommand ent_orient("ent_orient", CC_Ent_Orient, "Orient the specified entity to match the player's angles. By default, only orients target entity's YAW. Use the 'allangles' option to orient on all axis.\n\tFormat: ent_orient ", FCVAR_CHEAT); diff --git a/sp/src/game/server/baseentity.h b/sp/src/game/server/baseentity.h new file mode 100644 index 00000000..a96d443b --- /dev/null +++ b/sp/src/game/server/baseentity.h @@ -0,0 +1,3077 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef BASEENTITY_H +#define BASEENTITY_H +#ifdef _WIN32 +#pragma once +#endif + +#define TEAMNUM_NUM_BITS 6 + +#include "entitylist.h" +#include "entityoutput.h" +#include "networkvar.h" +#include "collisionproperty.h" +#include "ServerNetworkProperty.h" +#include "shareddefs.h" +#include "engine/ivmodelinfo.h" + +#include "vscript/ivscript.h" +#include "vscript_server.h" + +class CDamageModifier; +class CDmgAccumulator; + +struct CSoundParameters; + +class AI_CriteriaSet; +class IResponseSystem; +class IEntitySaveUtils; +class CRecipientFilter; +class CStudioHdr; + +// Matching the high level concept is significantly better than other criteria +// FIXME: Could do this in the script file by making it required and bumping up weighting there instead... +#define CONCEPT_WEIGHT 5.0f + +typedef CHandle EHANDLE; + +#define MANUALMODE_GETSET_PROP(type, accessorName, varName) \ + private:\ + type varName;\ + public:\ + inline const type& Get##accessorName##() const { return varName; } \ + inline type& Get##accessorName##() { return varName; } \ + inline void Set##accessorName##( const type &val ) { varName = val; m_NetStateMgr.StateChanged(); } + +#define MANUALMODE_GETSET_EHANDLE(type, accessorName, varName) \ + private:\ + CHandle varName;\ + public:\ + inline type* Get##accessorName##() { return varName.Get(); } \ + inline void Set##accessorName##( type *pType ) { varName = pType; m_NetStateMgr.StateChanged(); } + + +// saverestore.h declarations +class CSaveRestoreData; +struct typedescription_t; +class ISave; +class IRestore; +class CBaseEntity; +class CEntityMapData; +class CBaseCombatWeapon; +class IPhysicsObject; +class IPhysicsShadowController; +class CBaseCombatCharacter; +class CTeam; +class Vector; +struct gamevcollisionevent_t; +class CBaseAnimating; +class CBasePlayer; +class IServerVehicle; +struct solid_t; +struct notify_system_event_params_t; +class CAI_BaseNPC; +class CAI_Senses; +class CSquadNPC; +class variant_t; +class CEventAction; +typedef struct KeyValueData_s KeyValueData; +class CUserCmd; +class CSkyCamera; +class CEntityMapData; +class CWorld; +class INextBot; + + +typedef CUtlVector< CBaseEntity* > EntityList_t; + +#if defined( HL2_DLL ) + +// For CLASSIFY +enum Class_T +{ + CLASS_NONE=0, + CLASS_PLAYER, + CLASS_PLAYER_ALLY, + CLASS_PLAYER_ALLY_VITAL, + CLASS_ANTLION, + CLASS_BARNACLE, + CLASS_BULLSEYE, + //CLASS_BULLSQUID, + CLASS_CITIZEN_PASSIVE, + CLASS_CITIZEN_REBEL, + CLASS_COMBINE, + CLASS_COMBINE_GUNSHIP, + CLASS_CONSCRIPT, + CLASS_HEADCRAB, + //CLASS_HOUNDEYE, + CLASS_MANHACK, + CLASS_METROPOLICE, + CLASS_MILITARY, + CLASS_SCANNER, + CLASS_STALKER, + CLASS_VORTIGAUNT, + CLASS_ZOMBIE, + CLASS_PROTOSNIPER, + CLASS_MISSILE, + CLASS_FLARE, + CLASS_EARTH_FAUNA, + CLASS_HACKED_ROLLERMINE, + CLASS_COMBINE_HUNTER, + + NUM_AI_CLASSES +}; + +#elif defined( HL1_DLL ) + +enum Class_T +{ + CLASS_NONE = 0, + CLASS_MACHINE, + CLASS_PLAYER, + CLASS_HUMAN_PASSIVE, + CLASS_HUMAN_MILITARY, + CLASS_ALIEN_MILITARY, + CLASS_ALIEN_MONSTER, + CLASS_ALIEN_PREY, + CLASS_ALIEN_PREDATOR, + CLASS_INSECT, + CLASS_PLAYER_ALLY, + CLASS_PLAYER_BIOWEAPON, + CLASS_ALIEN_BIOWEAPON, + + NUM_AI_CLASSES +}; + +#elif defined( INVASION_DLL ) + +enum Class_T +{ + CLASS_NONE = 0, + CLASS_PLAYER, + CLASS_PLAYER_ALLY, + CLASS_PLAYER_ALLY_VITAL, + CLASS_ANTLION, + CLASS_BARNACLE, + CLASS_BULLSEYE, + //CLASS_BULLSQUID, + CLASS_CITIZEN_PASSIVE, + CLASS_CITIZEN_REBEL, + CLASS_COMBINE, + CLASS_COMBINE_GUNSHIP, + CLASS_CONSCRIPT, + CLASS_HEADCRAB, + //CLASS_HOUNDEYE, + CLASS_MANHACK, + CLASS_METROPOLICE, + CLASS_MILITARY, + CLASS_SCANNER, + CLASS_STALKER, + CLASS_VORTIGAUNT, + CLASS_ZOMBIE, + CLASS_PROTOSNIPER, + CLASS_MISSILE, + CLASS_FLARE, + CLASS_EARTH_FAUNA, + NUM_AI_CLASSES +}; + +#elif defined( CSTRIKE_DLL ) + +enum Class_T +{ + CLASS_NONE = 0, + CLASS_PLAYER, + CLASS_PLAYER_ALLY, + NUM_AI_CLASSES +}; + +#else + +enum Class_T +{ + CLASS_NONE = 0, + CLASS_PLAYER, + CLASS_PLAYER_ALLY, + NUM_AI_CLASSES +}; + +#endif + +// +// Structure passed to input handlers. +// +struct inputdata_t +{ + CBaseEntity *pActivator; // The entity that initially caused this chain of output events. + CBaseEntity *pCaller; // The entity that fired this particular output. + variant_t value; // The data parameter for this output. + int nOutputID; // The unique ID of the output that was fired. +}; + +// Serializable list of context as set by entity i/o and used for deducing proper +// speech state, et al. +struct ResponseContext_t +{ + DECLARE_SIMPLE_DATADESC(); + + string_t m_iszName; + string_t m_iszValue; + float m_fExpirationTime; // when to expire context (0 == never) +}; + + +//----------------------------------------------------------------------------- +// Entity events... targetted to a particular entity +// Each event has a well defined structure to use for parameters +//----------------------------------------------------------------------------- +enum EntityEvent_t +{ + ENTITY_EVENT_WATER_TOUCH = 0, // No data needed + ENTITY_EVENT_WATER_UNTOUCH, // No data needed + ENTITY_EVENT_PARENT_CHANGED, // No data needed +}; + + +//----------------------------------------------------------------------------- + +typedef void (CBaseEntity::*BASEPTR)(void); +typedef void (CBaseEntity::*ENTITYFUNCPTR)(CBaseEntity *pOther ); +typedef void (CBaseEntity::*USEPTR)( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + +#define DEFINE_THINKFUNC( function ) DEFINE_FUNCTION_RAW( function, BASEPTR ) +#define DEFINE_ENTITYFUNC( function ) DEFINE_FUNCTION_RAW( function, ENTITYFUNCPTR ) +#define DEFINE_USEFUNC( function ) DEFINE_FUNCTION_RAW( function, USEPTR ) + +// Things that toggle (buttons/triggers/doors) need this +enum TOGGLE_STATE +{ + TS_AT_TOP, + TS_AT_BOTTOM, + TS_GOING_UP, + TS_GOING_DOWN +}; + + +// Debug overlay bits +enum DebugOverlayBits_t +{ + OVERLAY_TEXT_BIT = 0x00000001, // show text debug overlay for this entity + OVERLAY_NAME_BIT = 0x00000002, // show name debug overlay for this entity + OVERLAY_BBOX_BIT = 0x00000004, // show bounding box overlay for this entity + OVERLAY_PIVOT_BIT = 0x00000008, // show pivot for this entity + OVERLAY_MESSAGE_BIT = 0x00000010, // show messages for this entity + OVERLAY_ABSBOX_BIT = 0x00000020, // show abs bounding box overlay + OVERLAY_RBOX_BIT = 0x00000040, // show the rbox overlay + OVERLAY_SHOW_BLOCKSLOS = 0x00000080, // show entities that block NPC LOS + OVERLAY_ATTACHMENTS_BIT = 0x00000100, // show attachment points + OVERLAY_AUTOAIM_BIT = 0x00000200, // Display autoaim radius + + OVERLAY_NPC_SELECTED_BIT = 0x00001000, // the npc is current selected + OVERLAY_NPC_NEAREST_BIT = 0x00002000, // show the nearest node of this npc + OVERLAY_NPC_ROUTE_BIT = 0x00004000, // draw the route for this npc + OVERLAY_NPC_TRIANGULATE_BIT = 0x00008000, // draw the triangulation for this npc + OVERLAY_NPC_ZAP_BIT = 0x00010000, // destroy the NPC + OVERLAY_NPC_ENEMIES_BIT = 0x00020000, // show npc's enemies + OVERLAY_NPC_CONDITIONS_BIT = 0x00040000, // show NPC's current conditions + OVERLAY_NPC_SQUAD_BIT = 0x00080000, // show npc squads + OVERLAY_NPC_TASK_BIT = 0x00100000, // show npc task details + OVERLAY_NPC_FOCUS_BIT = 0x00200000, // show line to npc's enemy and target + OVERLAY_NPC_VIEWCONE_BIT = 0x00400000, // show npc's viewcone + OVERLAY_NPC_KILL_BIT = 0x00800000, // kill the NPC, running all appropriate AI. + + OVERLAY_WC_CHANGE_ENTITY = 0x01000000, // object changed during WC edit + OVERLAY_BUDDHA_MODE = 0x02000000, // take damage but don't die + + OVERLAY_NPC_STEERING_REGULATIONS = 0x04000000, // Show the steering regulations associated with the NPC + + OVERLAY_TASK_TEXT_BIT = 0x08000000, // show task and schedule names when they start + + OVERLAY_PROP_DEBUG = 0x10000000, + + OVERLAY_NPC_RELATION_BIT = 0x20000000, // show relationships between target and all children + + OVERLAY_VIEWOFFSET = 0x40000000, // show view offset +}; + +struct TimedOverlay_t; + +/* ========= CBaseEntity ======== + + All objects in the game are derived from this. + +a list of all CBaseEntitys is kept in gEntList +================================ */ + +// creates an entity by string name, but does not spawn it +// If iForceEdictIndex is not -1, then it will use the edict by that index. If the index is +// invalid or there is already an edict using that index, it will error out. +CBaseEntity *CreateEntityByName( const char *className, int iForceEdictIndex = -1 ); +CBaseNetworkable *CreateNetworkableByName( const char *className ); + +CBaseEntity* ToEnt(HSCRIPT hScript); + +// creates an entity and calls all the necessary spawn functions +extern void SpawnEntityByName( const char *className, CEntityMapData *mapData = NULL ); + +// calls the spawn functions for an entity +extern int DispatchSpawn( CBaseEntity *pEntity, bool bRunVScripts = true); + +inline CBaseEntity *GetContainingEntity( edict_t *pent ); + +//----------------------------------------------------------------------------- +// Purpose: think contexts +//----------------------------------------------------------------------------- +struct thinkfunc_t +{ + BASEPTR m_pfnThink; + string_t m_iszContext; + int m_nNextThinkTick; + int m_nLastThinkTick; + + DECLARE_SIMPLE_DATADESC(); +}; + +#ifdef MAPBASE_VSCRIPT +struct scriptthinkfunc_t +{ + int m_nNextThinkTick; + HSCRIPT m_hfnThink; + unsigned short m_iContextHash; + bool m_bNoParam; +}; +#endif + +struct EmitSound_t; +struct rotatingpushmove_t; + +#define CREATE_PREDICTED_ENTITY( className ) \ + CBaseEntity::CreatePredictedEntityByName( className, __FILE__, __LINE__ ); + +// +// Base Entity. All entity types derive from this +// +class CBaseEntity : public IServerEntity +{ +public: + DECLARE_CLASS_NOBASE( CBaseEntity ); + + //---------------------------------------- + // Class vars and functions + //---------------------------------------- + static inline void Debug_Pause(bool bPause); + static inline bool Debug_IsPaused(void); + static inline void Debug_SetSteps(int nSteps); + static inline bool Debug_ShouldStep(void); + static inline bool Debug_Step(void); + + static bool m_bInDebugSelect; + static int m_nDebugPlayer; + +protected: + + static bool m_bDebugPause; // Whether entity i/o is paused for debugging. + static int m_nDebugSteps; // Number of entity outputs to fire before pausing again. + + static bool sm_bDisableTouchFuncs; // Disables PhysicsTouch and PhysicsStartTouch function calls +public: + static bool sm_bAccurateTriggerBboxChecks; // SOLID_BBOX entities do a fully accurate trigger vs bbox check when this is set + +public: + // If bServerOnly is true, then the ent never goes to the client. This is used + // by logical entities. + CBaseEntity( bool bServerOnly=false ); + virtual ~CBaseEntity(); + + // prediction system + DECLARE_PREDICTABLE(); + // network data + DECLARE_SERVERCLASS(); + // data description + DECLARE_DATADESC(); + // script description + DECLARE_ENT_SCRIPTDESC(); + + // memory handling + void *operator new( size_t stAllocateBlock ); + void *operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ); + void operator delete( void *pMem ); + void operator delete( void *pMem, int nBlockUse, const char *pFileName, int nLine ) { operator delete(pMem); } + + // Class factory + static CBaseEntity *CreatePredictedEntityByName( const char *classname, const char *module, int line, bool persist = false ); + +// IHandleEntity overrides. +public: + virtual void SetRefEHandle( const CBaseHandle &handle ); + virtual const CBaseHandle& GetRefEHandle() const; + +// IServerUnknown overrides + virtual ICollideable *GetCollideable(); + virtual IServerNetworkable *GetNetworkable(); + virtual CBaseEntity *GetBaseEntity(); + +// IServerEntity overrides. +public: + virtual void SetModelIndex( int index ); + virtual int GetModelIndex( void ) const; + virtual string_t GetModelName( void ) const; + + void ClearModelIndexOverrides( void ); + virtual void SetModelIndexOverride( int index, int nValue ); + +public: + // virtual methods for derived classes to override + virtual bool TestCollision( const Ray_t& ray, unsigned int mask, trace_t& trace ); + virtual bool TestHitboxes( const Ray_t &ray, unsigned int fContentsMask, trace_t& tr ); + virtual void ComputeWorldSpaceSurroundingBox( Vector *pWorldMins, Vector *pWorldMaxs ); + + // non-virtual methods. Don't override these! +public: + // An inline version the game code can use + CCollisionProperty *CollisionProp(); + const CCollisionProperty*CollisionProp() const; + CServerNetworkProperty *NetworkProp(); + const CServerNetworkProperty *NetworkProp() const; + + bool IsCurrentlyTouching( void ) const; + const Vector& GetAbsOrigin( void ) const; + const QAngle& GetAbsAngles( void ) const; + + SolidType_t GetSolid() const; + int GetSolidFlags( void ) const; + + int GetEFlags() const; + void SetEFlags( int iEFlags ); + void AddEFlags( int nEFlagMask ); + void RemoveEFlags( int nEFlagMask ); + bool IsEFlagSet( int nEFlagMask ) const; + + // Quick way to ask if we have a player entity as a child anywhere in our hierarchy. + void RecalcHasPlayerChildBit(); + bool DoesHavePlayerChild(); + + bool IsTransparent() const; + + void SetNavIgnore( float duration = FLT_MAX ); + void ClearNavIgnore(); + bool IsNavIgnored() const; + + // Is the entity floating? + bool IsFloating(); + + // Called by physics to see if we should avoid a collision test.... + virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const; + + // Move type / move collide + MoveType_t GetMoveType() const; + MoveCollide_t GetMoveCollide() const; + void SetMoveType( MoveType_t val, MoveCollide_t moveCollide = MOVECOLLIDE_DEFAULT ); + void SetMoveCollide( MoveCollide_t val ); + + // Returns the entity-to-world transform + matrix3x4_t &EntityToWorldTransform(); + const matrix3x4_t &EntityToWorldTransform() const; + + // Some helper methods that transform a point from entity space to world space + back + void EntityToWorldSpace( const Vector &in, Vector *pOut ) const; + void WorldToEntitySpace( const Vector &in, Vector *pOut ) const; + + // This function gets your parent's transform. If you're parented to an attachment, + // this calculates the attachment's transform and gives you that. + // + // You must pass in tempMatrix for scratch space - it may need to fill that in and return it instead of + // pointing you right at a variable in your parent. + matrix3x4_t& GetParentToWorldTransform( matrix3x4_t &tempMatrix ); + + // Externalized data objects ( see sharreddefs.h for DataObjectType_t ) + bool HasDataObjectType( int type ) const; + void AddDataObjectType( int type ); + void RemoveDataObjectType( int type ); + + void *GetDataObject( int type ); + void *CreateDataObject( int type ); + void DestroyDataObject( int type ); + void DestroyAllDataObjects( void ); + +public: + void SetScaledPhysics( IPhysicsObject *pNewObject ); + + // virtual methods; you can override these +public: + // Owner entity. + // FIXME: These are virtual only because of CNodeEnt + CBaseEntity *GetOwnerEntity() const; + virtual void SetOwnerEntity( CBaseEntity* pOwner ); + void SetEffectEntity( CBaseEntity *pEffectEnt ); + CBaseEntity *GetEffectEntity() const; + HSCRIPT GetScriptOwnerEntity(); + virtual void SetScriptOwnerEntity(HSCRIPT pOwner); + + // Only CBaseEntity implements these. CheckTransmit calls the virtual ShouldTransmit to see if the + // entity wants to be sent. If so, it calls SetTransmit, which will mark any dependents for transmission too. + virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo ); + + // update the global transmit state if a transmission rule changed + int SetTransmitState( int nFlag); + int GetTransmitState( void ); + int DispatchUpdateTransmitState(); + + // Do NOT call this directly. Use DispatchUpdateTransmitState. + virtual int UpdateTransmitState(); + + // Entities (like ropes) use this to own the transmit state of another entity + // by forcing it to not call UpdateTransmitState. + void IncrementTransmitStateOwnedCounter(); + void DecrementTransmitStateOwnedCounter(); + + // This marks the entity for transmission and passes the SetTransmit call to any dependents. + virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + + // This function finds out if the entity is in the 3D skybox. If so, it sets the EFL_IN_SKYBOX + // flag so the entity gets transmitted to all the clients. + // Entities usually call this during their Activate(). + // Returns true if the entity is in the skybox (and EFL_IN_SKYBOX was set). + bool DetectInSkybox(); + + // Returns which skybox the entity is in + CSkyCamera *GetEntitySkybox(); + + bool IsSimulatedEveryTick() const; + bool IsAnimatedEveryTick() const; + void SetSimulatedEveryTick( bool sim ); + void SetAnimatedEveryTick( bool anim ); + +public: + + virtual const char *GetTracerType( void ); + + // returns a pointer to the entities edict, if it has one. should be removed! + inline edict_t *edict( void ) { return NetworkProp()->edict(); } + inline const edict_t *edict( void ) const { return NetworkProp()->edict(); } + inline int entindex( ) const { return m_Network.entindex(); }; + inline int GetSoundSourceIndex() const { return entindex(); } + + // These methods encapsulate MOVETYPE_FOLLOW, which became obsolete + void FollowEntity( CBaseEntity *pBaseEntity, bool bBoneMerge = true ); + void StopFollowingEntity( ); // will also change to MOVETYPE_NONE + bool IsFollowingEntity(); + CBaseEntity *GetFollowedEntity(); + +#ifdef MAPBASE_VSCRIPT + void ScriptFollowEntity( HSCRIPT hBaseEntity, bool bBoneMerge ); + HSCRIPT ScriptGetFollowedEntity(); +#endif + + // initialization + virtual void Spawn( void ); + virtual void Precache( void ) {} + + virtual void SetModel( const char *szModelName ); + +protected: + // Notification on model load. May be called multiple times for dynamic models. + // Implementations must call BaseClass::OnNewModel and pass return value through. + virtual CStudioHdr *OnNewModel(); + +public: + virtual void PostConstructor( const char *szClassname ); + virtual void PostClientActive( void ); + virtual void ParseMapData( CEntityMapData *mapData ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual bool KeyValue( const char *szKeyName, float flValue ); + virtual bool KeyValue( const char *szKeyName, const Vector &vecValue ); + virtual bool GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ); + bool KeyValueFromString( const char *szKeyName, const char *szValue ) { return KeyValue( szKeyName, szValue ); } + bool KeyValueFromFloat( const char *szKeyName, float flValue ) { return KeyValue( szKeyName, flValue ); } + bool KeyValueFromInt( const char *szKeyName, int nValue ) { return KeyValue( szKeyName, nValue ); } + bool KeyValueFromVector( const char *szKeyName, const Vector &vecValue ) { return KeyValue( szKeyName, vecValue ); } + + void ValidateEntityConnections(); + void FireNamedOutput( const char *pszOutput, variant_t variant, CBaseEntity *pActivator, CBaseEntity *pCaller, float flDelay = 0.0f ); + CBaseEntityOutput *FindNamedOutput( const char *pszOutput ); +#ifdef MAPBASE_VSCRIPT + void ScriptFireOutput( const char *pszOutput, HSCRIPT hActivator, HSCRIPT hCaller, const char *szValue, float flDelay ); + float GetMaxOutputDelay( const char *pszOutput ); + void CancelEventsByInput( const char *szInput ); +#endif + + + // Activate - called for each entity after each load game and level load + virtual void Activate( void ); + + // Hierarchy traversal + CBaseEntity *GetMoveParent( void ); + CBaseEntity *GetRootMoveParent(); + CBaseEntity *FirstMoveChild( void ); + CBaseEntity *NextMovePeer( void ); + + void SetName( string_t newTarget ); +#ifdef MAPBASE_VSCRIPT + void SetNameAsCStr( const char *newTarget ); +#endif + void SetParent( string_t newParent, CBaseEntity *pActivator, int iAttachment = -1 ); + + // Set the movement parent. Your local origin and angles will become relative to this parent. + // If iAttachment is a valid attachment on the parent, then your local origin and angles + // are relative to the attachment on this entity. If iAttachment == -1, it'll preserve the + // current m_iParentAttachment. + virtual void SetParent( CBaseEntity* pNewParent, int iAttachment = -1 ); + CBaseEntity* GetParent(); + int GetParentAttachment(); + + string_t GetEntityName(); + const char* GetEntityNameAsCStr(); // This method is temporary for VSCRIPT functionality until we figure out what to do with string_t (sjb) + const char* GetPreTemplateName(); // Not threadsafe. Get the name stripped of template unique decoration + + bool NameMatches( const char *pszNameOrWildcard ); + bool ClassMatches( const char *pszClassOrWildcard ); + bool NameMatches( string_t nameStr ); + bool ClassMatches( string_t nameStr ); + +private: + bool NameMatchesComplex( const char *pszNameOrWildcard ); + bool ClassMatchesComplex( const char *pszClassOrWildcard ); + void TransformStepData_WorldToParent( CBaseEntity *pParent ); + void TransformStepData_ParentToParent( CBaseEntity *pOldParent, CBaseEntity *pNewParent ); + void TransformStepData_ParentToWorld( CBaseEntity *pParent ); + + +public: + int GetSpawnFlags( void ) const; + void AddSpawnFlags( int nFlags ); + void RemoveSpawnFlags( int nFlags ); + void ClearSpawnFlags( void ); + bool HasSpawnFlags( int nFlags ) const; + + int GetEffects( void ) const; + void AddEffects( int nEffects ); + void RemoveEffects( int nEffects ); + void ClearEffects( void ); + void SetEffects( int nEffects ); + bool IsEffectActive( int nEffects ) const; + + // makes the entity inactive + void MakeDormant( void ); + int IsDormant( void ); + + void RemoveDeferred( void ); // Sets the entity invisible, and makes it remove itself on the next frame + + // checks to see if the entity is marked for deletion + bool IsMarkedForDeletion( void ); + + // capabilities + virtual int ObjectCaps( void ); + + // Verifies that the data description is valid in debug builds. + #ifdef _DEBUG + void ValidateDataDescription(void); + #endif // _DEBUG + + // handles an input (usually caused by outputs) + // returns true if the the value in the pass in should be set, false if the input is to be ignored + virtual bool AcceptInput( const char *szInputName, CBaseEntity *pActivator, CBaseEntity *pCaller, variant_t Value, int outputID ); +#ifdef MAPBASE_VSCRIPT + bool ScriptAcceptInput(const char *szInputName, const char *szValue, HSCRIPT hActivator, HSCRIPT hCaller); +#endif + + bool ScriptInputHook( const char *szInputName, CBaseEntity *pActivator, CBaseEntity *pCaller, variant_t Value, ScriptVariant_t &functionReturn ); + + // + // Input handlers. + // + void InputAlternativeSorting( inputdata_t &inputdata ); + void InputAlpha( inputdata_t &inputdata ); + void InputColor( inputdata_t &inputdata ); + void InputSetParent( inputdata_t &inputdata ); + void SetParentAttachment( const char *szInputName, const char *szAttachment, bool bMaintainOffset ); + void InputSetParentAttachment( inputdata_t &inputdata ); + void InputSetParentAttachmentMaintainOffset( inputdata_t &inputdata ); + void InputClearParent( inputdata_t &inputdata ); + void InputSetTeam( inputdata_t &inputdata ); + void InputUse( inputdata_t &inputdata ); + void InputKill( inputdata_t &inputdata ); + void InputKillHierarchy( inputdata_t &inputdata ); + void InputSetDamageFilter( inputdata_t &inputdata ); + void InputDispatchEffect( inputdata_t &inputdata ); + void InputEnableDamageForces( inputdata_t &inputdata ); + void InputDisableDamageForces( inputdata_t &inputdata ); + void InputAddContext( inputdata_t &inputdata ); + void InputRemoveContext( inputdata_t &inputdata ); + void InputClearContext( inputdata_t &inputdata ); + void InputDispatchResponse( inputdata_t& inputdata ); + void InputDisableShadow( inputdata_t &inputdata ); + void InputEnableShadow( inputdata_t &inputdata ); + void InputAddOutput( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputChangeVariable( inputdata_t &inputdata ); +#endif + void InputFireUser1( inputdata_t &inputdata ); + void InputFireUser2( inputdata_t &inputdata ); + void InputFireUser3( inputdata_t &inputdata ); + void InputFireUser4( inputdata_t &inputdata ); + +#ifdef MAPBASE + void InputPassUser1( inputdata_t &inputdata ); + void InputPassUser2( inputdata_t &inputdata ); + void InputPassUser3( inputdata_t &inputdata ); + void InputPassUser4( inputdata_t &inputdata ); + + void InputFireRandomUser( inputdata_t &inputdata ); + void InputPassRandomUser( inputdata_t &inputdata ); + + void InputSetEntityName( inputdata_t &inputdata ); + + virtual void InputSetTarget( inputdata_t &inputdata ); + virtual void InputSetOwnerEntity( inputdata_t &inputdata ); + + virtual void InputAddHealth( inputdata_t &inputdata ); + virtual void InputRemoveHealth( inputdata_t &inputdata ); + virtual void InputSetHealth( inputdata_t &inputdata ); + + virtual void InputSetMaxHealth( inputdata_t &inputdata ); + + void InputFireOutput( inputdata_t &inputdata ); + void InputRemoveOutput( inputdata_t &inputdata ); + //virtual void InputCancelOutput( inputdata_t &inputdata ); // Find a way to implement this + void InputReplaceOutput( inputdata_t &inputdata ); + void InputAcceptInput( inputdata_t &inputdata ); + virtual void InputCancelPending( inputdata_t &inputdata ); + + void InputFreeChildren( inputdata_t &inputdata ); + + void InputSetLocalOrigin( inputdata_t &inputdata ); + void InputSetLocalAngles( inputdata_t &inputdata ); + void InputSetAbsOrigin( inputdata_t &inputdata ); + void InputSetAbsAngles( inputdata_t &inputdata ); + void InputSetLocalVelocity( inputdata_t &inputdata ); + void InputSetLocalAngularVelocity( inputdata_t &inputdata ); + + void InputAddSpawnFlags( inputdata_t &inputdata ); + void InputRemoveSpawnFlags( inputdata_t &inputdata ); + void InputSetRenderMode( inputdata_t &inputdata ); + void InputSetRenderFX( inputdata_t &inputdata ); + void InputSetViewHideFlags( inputdata_t &inputdata ); + void InputAddEffects( inputdata_t &inputdata ); + void InputRemoveEffects( inputdata_t &inputdata ); + void InputDrawEntity( inputdata_t &inputdata ); + void InputUndrawEntity( inputdata_t &inputdata ); + void InputEnableReceivingFlashlight( inputdata_t &inputdata ); + void InputDisableReceivingFlashlight( inputdata_t &inputdata ); + void InputAddEFlags( inputdata_t &inputdata ); + void InputRemoveEFlags( inputdata_t &inputdata ); + void InputAddSolidFlags( inputdata_t &inputdata ); + void InputRemoveSolidFlags( inputdata_t &inputdata ); + void InputSetMoveType( inputdata_t &inputdata ); + void InputSetCollisionGroup( inputdata_t &inputdata ); + + void InputTouch( inputdata_t &inputdata ); + + virtual void InputKilledNPC( inputdata_t &inputdata ); + + void InputKillIfNotVisible( inputdata_t &inputdata ); + void InputKillWhenNotVisible( inputdata_t &inputdata ); + + void InputSetThinkNull( inputdata_t &inputdata ); + + COutputEvent m_OnKilled; +#endif + + void InputRunScript(inputdata_t& inputdata); + void InputRunScriptFile(inputdata_t& inputdata); + void InputCallScriptFunction(inputdata_t& inputdata); +#ifdef MAPBASE_VSCRIPT + void InputRunScriptQuotable(inputdata_t& inputdata); + void InputClearScriptScope(inputdata_t& inputdata); +#endif + + bool RunScriptFile(const char* pScriptFile, bool bUseRootScope = false); + bool RunScript(const char* pScriptText, const char* pDebugFilename = "CBaseEntity::RunScript"); + + + // Returns the origin at which to play an inputted dispatcheffect + virtual void GetInputDispatchEffectPosition( const char *sInputString, Vector &pOrigin, QAngle &pAngles ); + + // tries to read a field from the entities data description - result is placed in variant_t + bool ReadKeyField( const char *varName, variant_t *var ); + + // classname access + void SetClassname( const char *className ); + const char* GetClassname(); + + // Debug Overlays + void EntityText( int text_offset, const char *text, float flDuration, int r = 255, int g = 255, int b = 255, int a = 255 ); + const char *GetDebugName(void); // do not make this virtual -- designed to handle NULL this + virtual void DrawDebugGeometryOverlays(void); + virtual int DrawDebugTextOverlays(void); + void DrawTimedOverlays( void ); + void DrawBBoxOverlay( float flDuration = 0.0f ); + void DrawAbsBoxOverlay(); + void DrawRBoxOverlay(); + + void DrawInputOverlay(const char *szInputName, CBaseEntity *pCaller, variant_t Value); + void DrawOutputOverlay(CEventAction *ev); + void SendDebugPivotOverlay( void ); + void AddTimedOverlay( const char *msg, int endTime ); + + void SetSolid( SolidType_t val ); + + // save/restore + // only overload these if you have special data to serialize + virtual int Save( ISave &save ); + virtual int Restore( IRestore &restore ); + virtual bool ShouldSavePhysics(); + + // handler to reset stuff before you are restored + // NOTE: Always chain to base class when implementing this! + virtual void OnSave( IEntitySaveUtils *pSaveUtils ); + + // handler to reset stuff after you are restored + // called after all entities have been loaded from all affected levels + // called before activate + // NOTE: Always chain to base class when implementing this! + virtual void OnRestore(); + + int GetTextureFrameIndex( void ); + void SetTextureFrameIndex( int iIndex ); + + // Entities block Line-Of-Sight for NPCs by default. + // Set this to false if you want to change this behavior. + void SetBlocksLOS( bool bBlocksLOS ); + bool BlocksLOS( void ); + + + void SetAIWalkable( bool bBlocksLOS ); + bool IsAIWalkable( void ); +private: + int SaveDataDescBlock( ISave &save, datamap_t *dmap ); + int RestoreDataDescBlock( IRestore &restore, datamap_t *dmap ); + +public: + // Networking related methods + void NetworkStateChanged(); + void NetworkStateChanged( void *pVar ); + +public: + void CalcAbsolutePosition(); + + // returns the edict index the entity requires when used in save/restore (eg players, world) + // -1 means it doesn't require any special index + virtual int RequiredEdictIndex( void ) { return -1; } + + // interface function pts + void (CBaseEntity::*m_pfnMoveDone)(void); + virtual void MoveDone( void ) { if (m_pfnMoveDone) (this->*m_pfnMoveDone)();}; + + // Why do we have two separate static Instance functions? + static CBaseEntity *Instance( const CBaseHandle &hEnt ); + static CBaseEntity *Instance( const edict_t *pent ); + static CBaseEntity *Instance( edict_t *pent ); + static CBaseEntity* Instance( int iEnt ); + + // Think function handling + void (CBaseEntity::*m_pfnThink)(void); + virtual void Think( void ) { if (m_pfnThink) (this->*m_pfnThink)();}; + + // Think functions with contexts + int RegisterThinkContext( const char *szContext ); + BASEPTR ThinkSet( BASEPTR func, float flNextThinkTime = 0, const char *szContext = NULL ); + void SetNextThink( float nextThinkTime, const char *szContext = NULL ); + float GetNextThink( const char *szContext = NULL ); + float GetLastThink( const char *szContext = NULL ); + int GetNextThinkTick( const char *szContext = NULL ); + int GetLastThinkTick( const char *szContext = NULL ); + + float GetAnimTime() const; + void SetAnimTime( float at ); + + float GetSimulationTime() const; + void SetSimulationTime( float st ); + + void SetRenderMode( RenderMode_t nRenderMode ); + RenderMode_t GetRenderMode() const; + +private: + // NOTE: Keep this near vtable so it's in cache with vtable. + CServerNetworkProperty m_Network; + +public: + // members + string_t m_iClassname; // identifier for entity creation and save/restore + string_t m_iGlobalname; // identifier for carrying entity across level transitions + string_t m_iParent; // the name of the entities parent; linked into m_pParent during Activate() + + int m_iHammerID; // Hammer unique edit id number + +public: + // was pev->speed + float m_flSpeed; + // was pev->renderfx + CNetworkVar( unsigned char, m_nRenderFX ); + // was pev->rendermode + CNetworkVar( unsigned char, m_nRenderMode ); + CNetworkVar( short, m_nModelIndex ); + +#ifdef TF_DLL + CNetworkArray( int, m_nModelIndexOverrides, MAX_VISION_MODES ); // used to override the base model index on the client if necessary +#endif + +#ifdef MAPBASE + // Prevents this entity from drawing under certain view IDs. Each flag is (1 << the view ID to hide from). + // For example, hiding an entity from VIEW_MONITOR prevents it from showing up on RT camera monitors + // and hiding an entity from VIEW_MAIN just prevents it from showing up through the player's own "eyes". + // Doing this via flags allows for the entity to be hidden from multiple view IDs at the same time. + // + // This was partly inspired by Underhell's keyvalue that allows entities to only render in mirrors and cameras. + CNetworkVar( int, m_iViewHideFlags ); + + // Disables receiving projected textures. Based on a keyvalue from later Source games. + CNetworkVar( bool, m_bDisableFlashlight ); +#endif + + // was pev->rendercolor + CNetworkColor32( m_clrRender ); + const color32 GetRenderColor() const; + void SetRenderColor( byte r, byte g, byte b ); + void SetRenderColor( byte r, byte g, byte b, byte a ); + void SetRenderColorR( byte r ); + void SetRenderColorG( byte g ); + void SetRenderColorB( byte b ); + void SetRenderColorA( byte a ); + + // was pev->animtime: consider moving to CBaseAnimating + float m_flPrevAnimTime; + CNetworkVar( float, m_flAnimTime ); // this is the point in time that the client will interpolate to position,angle,frame,etc. + CNetworkVar( float, m_flSimulationTime ); + + void IncrementInterpolationFrame(); // Call this to cause a discontinuity (teleport) + + CNetworkVar( int, m_ubInterpolationFrame ); + + int m_nLastThinkTick; + +#if !defined( NO_ENTITY_PREDICTION ) + // Certain entities (projectiles) can be created on the client and thus need a matching id number + CNetworkVar( CPredictableId, m_PredictableID ); +#endif + + // used so we know when things are no longer touching + int touchStamp; + +protected: + + // think function handling + enum thinkmethods_t + { + THINK_FIRE_ALL_FUNCTIONS, + THINK_FIRE_BASE_ONLY, + THINK_FIRE_ALL_BUT_BASE, + }; + int GetIndexForThinkContext( const char *pszContext ); + CUtlVector< thinkfunc_t > m_aThinkFunctions; + +#ifdef _DEBUG + int m_iCurrentThinkContext; +#endif + + void RemoveExpiredConcepts( void ); +#ifdef MAPBASE + // Some new code needs to access these functions from outside of the class. +public: +#endif + int GetContextCount() const; // Call RemoveExpiredConcepts to clean out expired concepts + const char *GetContextName( int index ) const; // note: context may be expired + const char *GetContextValue( int index ) const; // note: context may be expired + bool ContextExpired( int index ) const; + int FindContextByName( const char *name ) const; +#ifndef MAPBASE +public: +#endif + +#ifdef MAPBASE + bool HasContext( const char *name, const char *value ) const; + bool HasContext( string_t name, string_t value ) const; // NOTE: string_t version only compares pointers! + bool HasContext( const char *nameandvalue ) const; + const char *GetContextValue( const char *contextName ) const; + float GetContextExpireTime( const char *name ); + void RemoveContext( const char *nameandvalue ); + void AddContext( const char *name, const char *value, float duration = 0.0f ); +#endif + + void AddContext( const char *nameandvalue ); + +protected: + CUtlVector< ResponseContext_t > m_ResponseContexts; + + // Map defined context sets + string_t m_iszResponseContext; + +private: + CBaseEntity( CBaseEntity& ); + + // list handling + friend class CGlobalEntityList; + friend class CThinkSyncTester; + + // was pev->nextthink + CNetworkVarForDerived( int, m_nNextThinkTick ); + // was pev->effects + CNetworkVar( int, m_fEffects ); + +//////////////////////////////////////////////////////////////////////////// + + +public: + + // Returns a CBaseAnimating if the entity is derived from CBaseAnimating. + virtual CBaseAnimating* GetBaseAnimating() { return 0; } + + virtual IResponseSystem *GetResponseSystem(); + virtual void DispatchResponse( const char *conceptName ); + +// Classify - returns the type of group (i.e, "houndeye", or "human military" so that NPCs with different classnames +// still realize that they are teammates. (overridden for NPCs that form groups) + virtual Class_T Classify ( void ); + virtual void DeathNotice ( CBaseEntity *pVictim ) {}// NPC maker children use this to tell the NPC maker that they have died. + virtual bool ShouldAttractAutoAim( CBaseEntity *pAimingEnt ) { return ((GetFlags() & FL_AIMTARGET) != 0); } + virtual float GetAutoAimRadius(); + virtual Vector GetAutoAimCenter() { return WorldSpaceCenter(); } + + virtual ITraceFilter* GetBeamTraceFilter( void ); + + // Call this to do a TraceAttack on an entity, performs filtering. Don't call TraceAttack() directly except when chaining up to base class + void DispatchTraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator = NULL ); + virtual bool PassesDamageFilter( const CTakeDamageInfo &info ); +#ifdef MAPBASE + // Special filter functions made for the "damage" family of filters, including filter_damage_transfer. + bool PassesFinalDamageFilter( const CTakeDamageInfo &info ); + bool DamageFilterAllowsBlood( const CTakeDamageInfo &info ); + bool DamageFilterDamageMod( CTakeDamageInfo &info ); +#endif + + +protected: + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator = NULL ); + +public: + + virtual bool CanBeHitByMeleeAttack( CBaseEntity *pAttacker ) { return true; } + + // returns the amount of damage inflicted + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + // This is what you should call to apply damage to an entity. + void TakeDamage( const CTakeDamageInfo &info ); + virtual void AdjustDamageDirection( const CTakeDamageInfo &info, Vector &dir, CBaseEntity *pEnt ) {} + + virtual int TakeHealth( float flHealth, int bitsDamageType ); + + virtual bool IsAlive( void ); + // Entity killed (only fired once) + virtual void Event_Killed( const CTakeDamageInfo &info ); + + void SendOnKilledGameEvent( const CTakeDamageInfo &info ); + + // Notifier that I've killed some other entity. (called from Victim's Event_Killed). + virtual void Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) { return; } + + // UNDONE: Make this data? + virtual int BloodColor( void ); + + void TraceBleed( float flDamage, const Vector &vecDir, trace_t *ptr, int bitsDamageType ); + virtual bool IsTriggered( CBaseEntity *pActivator ) {return true;} + virtual bool IsNPC( void ) const { return false; } + CAI_BaseNPC *MyNPCPointer( void ); + virtual CBaseCombatCharacter *MyCombatCharacterPointer( void ) { return NULL; } + virtual INextBot *MyNextBotPointer( void ) { return NULL; } + virtual float GetDelay( void ) { return 0; } + virtual bool IsMoving( void ); + bool IsWorld() const { extern CWorld *g_WorldEntity; return (void *)this == (void *)g_WorldEntity; } // Ported from the Alien Swarm SDK to fix false IsWorld() positives on server-only entities + virtual char const *DamageDecal( int bitsDamageType, int gameMaterial ); + virtual void DecalTrace( trace_t *pTrace, char const *decalName ); + virtual void ImpactTrace( trace_t *pTrace, int iDamageType, const char *pCustomImpactName = NULL ); + + void AddPoints( int score, bool bAllowNegativeScore ); + void AddPointsToTeam( int score, bool bAllowNegativeScore ); + void RemoveAllDecals( void ); + + virtual bool OnControls( CBaseEntity *pControls ) { return false; } + virtual bool HasTarget( string_t targetname ); + virtual bool IsPlayer( void ) const { return false; } + virtual bool IsNetClient( void ) const { return false; } + virtual bool IsTemplate( void ) { return false; } + virtual bool IsBaseObject( void ) const { return false; } + virtual bool IsBaseTrain( void ) const { return false; } + bool IsBSPModel() const; + bool IsCombatCharacter() { return MyCombatCharacterPointer() == NULL ? false : true; } + bool IsInWorld( void ) const; + virtual bool IsCombatItem( void ) const { return false; } + + virtual bool IsBaseCombatWeapon( void ) const { return false; } + virtual bool IsWearable( void ) const { return false; } + virtual CBaseCombatWeapon *MyCombatWeaponPointer( void ) { return NULL; } + + // If this is a vehicle, returns the vehicle interface + virtual IServerVehicle* GetServerVehicle() { return NULL; } + + // UNDONE: Make this data instead of procedural? + virtual bool IsViewable( void ); // is this something that would be looked at (model, sprite, etc.)? + + // Team Handling + CTeam *GetTeam( void ) const; // Get the Team this entity is on + int GetTeamNumber( void ) const; // Get the Team number of the team this entity is on + virtual void ChangeTeam( int iTeamNum ); // Assign this entity to a team. + bool IsInTeam( CTeam *pTeam ) const; // Returns true if this entity's in the specified team + bool InSameTeam( CBaseEntity *pEntity ) const; // Returns true if the specified entity is on the same team as this one + bool IsInAnyTeam( void ) const; // Returns true if this entity is in any team + const char *TeamID( void ) const; // Returns the name of the team this entity is on. + + // Entity events... these are events targetted to a particular entity + // Each event defines its own well-defined event data structure + virtual void OnEntityEvent( EntityEvent_t event, void *pEventData ); + + // can stand on this entity? + bool IsStandable() const; + + // UNDONE: Do these three functions actually need to be virtual??? + virtual bool CanStandOn( CBaseEntity *pSurface ) const { return (pSurface && !pSurface->IsStandable()) ? false : true; } + virtual bool CanStandOn( edict_t *ent ) const { return CanStandOn( GetContainingEntity( ent ) ); } + virtual CBaseEntity *GetEnemy( void ) { return NULL; } + virtual CBaseEntity *GetEnemy( void ) const { return NULL; } + + + void ViewPunch( const QAngle &angleOffset ); + void VelocityPunch( const Vector &vecForce ); + + CBaseEntity *GetNextTarget( void ); + + // fundamental callbacks + void (CBaseEntity ::*m_pfnTouch)( CBaseEntity *pOther ); + void (CBaseEntity ::*m_pfnUse)( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void (CBaseEntity ::*m_pfnBlocked)( CBaseEntity *pOther ); + + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual void StartTouch( CBaseEntity *pOther ); + virtual void Touch( CBaseEntity *pOther ); + virtual void EndTouch( CBaseEntity *pOther ); + virtual void StartBlocked( CBaseEntity *pOther ) {} + virtual void Blocked( CBaseEntity *pOther ); + virtual void EndBlocked( void ) {} + + // Physics simulation + virtual void PhysicsSimulate( void ); + +public: + // HACKHACK:Get the trace_t from the last physics touch call (replaces the even-hackier global trace vars) + static const trace_t & GetTouchTrace( void ); + + // FIXME: Should be private, but I can't make em private just yet + void PhysicsImpact( CBaseEntity *other, trace_t &trace ); + void PhysicsMarkEntitiesAsTouching( CBaseEntity *other, trace_t &trace ); + void PhysicsMarkEntitiesAsTouchingEventDriven( CBaseEntity *other, trace_t &trace ); + void PhysicsTouchTriggers( const Vector *pPrevAbsOrigin = NULL ); + + // Physics helper + static void PhysicsRemoveTouchedList( CBaseEntity *ent ); + static void PhysicsNotifyOtherOfUntouch( CBaseEntity *ent, CBaseEntity *other ); + static void PhysicsRemoveToucher( CBaseEntity *other, touchlink_t *link ); + + groundlink_t *AddEntityToGroundList( CBaseEntity *other ); + void PhysicsStartGroundContact( CBaseEntity *pentOther ); + + static void PhysicsNotifyOtherOfGroundRemoval( CBaseEntity *ent, CBaseEntity *other ); + static void PhysicsRemoveGround( CBaseEntity *other, groundlink_t *link ); + static void PhysicsRemoveGroundList( CBaseEntity *ent ); + + void StartGroundContact( CBaseEntity *ground ); + void EndGroundContact( CBaseEntity *ground ); + + void SetGroundChangeTime( float flTime ); + float GetGroundChangeTime( void ); + + // Remove this as ground entity for all object resting on this object + void WakeRestingObjects(); + bool HasNPCsOnIt(); + + virtual void UpdateOnRemove( void ); + virtual void StopLoopingSounds( void ) {} + + // common member functions + void SUB_Remove( void ); + void SUB_DoNothing( void ); + void SUB_StartFadeOut( float delay = 10.0f, bool bNotSolid = true ); + void SUB_StartFadeOutInstant(); + void SUB_FadeOut ( void ); + void SUB_Vanish( void ); + void SUB_CallUseToggle( void ) { this->Use( this, this, USE_TOGGLE, 0 ); } + void SUB_PerformFadeOut( void ); + virtual bool SUB_AllowedToFade( void ); +#ifdef MAPBASE + // For KillWhenNotVisible + void SUB_RemoveWhenNotVisible( void ); +#endif + + // change position, velocity, orientation instantly + // passing NULL means no change + virtual void Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ); + // notify that another entity (that you were watching) was teleported + virtual void NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ); + + int ShouldToggle( USE_TYPE useType, int currentState ); + + // UNDONE: Move these virtuals to CBaseCombatCharacter? + virtual void MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ); + virtual int GetTracerAttachment( void ); + virtual void FireBullets( const FireBulletsInfo_t &info ); + virtual void DoImpactEffect( trace_t &tr, int nDamageType ); // give shooter a chance to do a custom impact. + + // OLD VERSION! Use the struct version + void FireBullets( int cShots, const Vector &vecSrc, const Vector &vecDirShooting, + const Vector &vecSpread, float flDistance, int iAmmoType, int iTracerFreq = 4, + int firingEntID = -1, int attachmentID = -1, int iDamage = 0, + CBaseEntity *pAttacker = NULL, bool bFirstShotAccurate = false, bool bPrimaryAttack = true ); + virtual void ModifyFireBulletsDamage( CTakeDamageInfo* dmgInfo ) {} + + virtual CBaseEntity *Respawn( void ) { return NULL; } + + // Method used to deal with attacks passing through triggers + void TraceAttackToTriggers( const CTakeDamageInfo &info, const Vector& start, const Vector& end, const Vector& dir ); + + // Do the bounding boxes of these two intersect? + bool Intersects( CBaseEntity *pOther ); + virtual bool IsLockedByMaster( void ) { return false; } + + // Health accessors. + virtual int GetMaxHealth() const { return m_iMaxHealth; } + void SetMaxHealth( int amt ) { m_iMaxHealth = amt; } + + int GetHealth() const { return m_iHealth; } + void SetHealth( int amt ) { m_iHealth = amt; } + + // Ugly code to lookup all functions to make sure they are in the table when set. +#ifdef _DEBUG + void FunctionCheck( void *pFunction, const char *name ); + + ENTITYFUNCPTR TouchSet( ENTITYFUNCPTR func, char *name ) + { +#ifdef GNUC + COMPILE_TIME_ASSERT( sizeof(func) == 8 ); +#else + COMPILE_TIME_ASSERT( sizeof(func) == 4 ); +#endif + m_pfnTouch = func; + FunctionCheck( *(reinterpret_cast(&m_pfnTouch)), name ); + return func; + } + USEPTR UseSet( USEPTR func, char *name ) + { +#ifdef GNUC + COMPILE_TIME_ASSERT( sizeof(func) == 8 ); +#else + COMPILE_TIME_ASSERT( sizeof(func) == 4 ); +#endif + m_pfnUse = func; + FunctionCheck( *(reinterpret_cast(&m_pfnUse)), name ); + return func; + } + ENTITYFUNCPTR BlockedSet( ENTITYFUNCPTR func, char *name ) + { +#ifdef GNUC + COMPILE_TIME_ASSERT( sizeof(func) == 8 ); +#else + COMPILE_TIME_ASSERT( sizeof(func) == 4 ); +#endif + m_pfnBlocked = func; + FunctionCheck( *(reinterpret_cast(&m_pfnBlocked)), name ); + return func; + } + +#endif + virtual void ModifyOrAppendCriteria( AI_CriteriaSet& set ); + void AppendContextToCriteria( AI_CriteriaSet& set, const char *prefix = "" ); +#ifdef MAPBASE + void ReAppendContextCriteria( AI_CriteriaSet& set ); +#endif + void DumpResponseCriteria( void ); + +private: + friend class CAI_Senses; + CBaseEntity *m_pLink;// used for temporary link-list operations. + +public: + // variables promoted from edict_t + string_t m_target; + CNetworkVarForDerived( int, m_iMaxHealth ); // CBaseEntity doesn't care about changes to this variable, but there are derived classes that do. + CNetworkVarForDerived( int, m_iHealth ); + + CNetworkVarForDerived( char, m_lifeState ); + CNetworkVarForDerived( char , m_takedamage ); + + // Damage filtering + string_t m_iszDamageFilterName; // The name of the entity to use as our damage filter. + EHANDLE m_hDamageFilter; // The entity that controls who can damage us. + + // Debugging / devolopment fields + int m_debugOverlays; // For debug only (bitfields) + TimedOverlay_t* m_pTimedOverlay; // For debug only + + // virtual functions used by a few classes + + // creates an entity of a specified class, by name + static CBaseEntity *Create( const char *szName, const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner = NULL ); + static CBaseEntity *CreateNoSpawn( const char *szName, const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner = NULL ); + + // Collision group accessors + int GetCollisionGroup() const; + void SetCollisionGroup( int collisionGroup ); + void CollisionRulesChanged(); + + // Damage accessors + virtual int GetDamageType() const; + virtual float GetDamage() { return 0; } + virtual void SetDamage(float flDamage) {} + +#ifdef MAPBASE + // Some entities want to use interactions regardless of whether they're a CBaseCombatCharacter. + // Valve ran into this issue with frag grenades when they started deriving from CBaseAnimating instead of CBaseCombatCharacter, + // preventing them from using the barnacle interactions for rigged grenade timing so it's guaranteed to blow up in the barnacle's face. + // We're used to unaltered behavior now, so we're not restoring that as default, but making this a "base entity" thing is supposed to help in situtions like those. + // + // Also, keep in mind pretty much all existing DispatchInteraction() calls are only performed on CBaseCombatCharacters. + // You'll need to change their code manually if you want other, non-character entities to use the interaction. + bool DispatchInteraction( int interactionType, void *data, CBaseCombatCharacter* sourceEnt ); + + // Do not call HandleInteraction directly, use DispatchInteraction + virtual bool HandleInteraction( int interactionType, void *data, CBaseCombatCharacter* sourceEnt ) { return false; } +#endif + + virtual Vector EyePosition( void ); // position of eyes + virtual const QAngle &EyeAngles( void ); // Direction of eyes in world space + virtual const QAngle &LocalEyeAngles( void ); // Direction of eyes + virtual Vector EarPosition( void ); // position of ears + + Vector EyePosition( void ) const; // position of eyes + const QAngle &EyeAngles( void ) const; // Direction of eyes in world space + const QAngle &LocalEyeAngles( void ) const; // Direction of eyes + Vector EarPosition( void ) const; // position of ears + + virtual Vector BodyTarget( const Vector &posSrc, bool bNoisy = true); // position to shoot at + virtual Vector HeadTarget( const Vector &posSrc ); + virtual void GetVectors(Vector* forward, Vector* right, Vector* up) const; + + virtual const Vector &GetViewOffset() const; + virtual void SetViewOffset( const Vector &v ); + + // NOTE: Setting the abs velocity in either space will cause a recomputation + // in the other space, so setting the abs velocity will also set the local vel + void SetLocalVelocity( const Vector &vecVelocity ); + void ApplyLocalVelocityImpulse( const Vector &vecImpulse ); + void SetAbsVelocity( const Vector &vecVelocity ); + void ApplyAbsVelocityImpulse( const Vector &vecImpulse ); + void ApplyLocalAngularVelocityImpulse( const AngularImpulse &angImpulse ); + + const Vector& GetLocalVelocity( ) const; + const Vector& GetAbsVelocity( ) const; + + // NOTE: Setting the abs velocity in either space will cause a recomputation + // in the other space, so setting the abs velocity will also set the local vel + void SetLocalAngularVelocity( const QAngle &vecAngVelocity ); + const QAngle& GetLocalAngularVelocity( ) const; + + // FIXME: While we're using (dPitch, dYaw, dRoll) as our local angular velocity + // representation, we can't actually solve this problem +// void SetAbsAngularVelocity( const QAngle &vecAngVelocity ); +// const QAngle& GetAbsAngularVelocity( ) const; + + const Vector& GetBaseVelocity() const; + void SetBaseVelocity( const Vector& v ); + + virtual Vector GetSmoothedVelocity( void ); + + // FIXME: Figure out what to do about this + virtual void GetVelocity(Vector *vVelocity, AngularImpulse *vAngVelocity = NULL); + + float GetGravity( void ) const; + void SetGravity( float gravity ); + float GetFriction( void ) const; + void SetFriction( float flFriction ); +#ifdef MAPBASE_VSCRIPT + void SetMass(float mass); + float GetMass(); +#endif + + virtual bool FVisible ( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); + virtual bool FVisible( const Vector &vecTarget, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); + + virtual bool CanBeSeenBy( CAI_BaseNPC *pNPC ) { return true; } // allows entities to be 'invisible' to NPC senses. + + // This function returns a value that scales all damage done by this entity. + // Use CDamageModifier to hook in damage modifiers on a guy. + virtual float GetAttackDamageScale( CBaseEntity *pVictim ); + // This returns a value that scales all damage done to this entity + // Use CDamageModifier to hook in damage modifiers on a guy. + virtual float GetReceivedDamageScale( CBaseEntity *pAttacker ); + + void SetCheckUntouch( bool check ); + bool GetCheckUntouch() const; + + void SetGroundEntity( CBaseEntity *ground ); + CBaseEntity *GetGroundEntity( void ); + CBaseEntity *GetGroundEntity( void ) const { return const_cast(this)->GetGroundEntity(); } + + // Gets the velocity we impart to a player standing on us + virtual void GetGroundVelocityToApply( Vector &vecGroundVel ) { vecGroundVel = vec3_origin; } + + int GetWaterLevel() const; + void SetWaterLevel( int nLevel ); + int GetWaterType() const; + void SetWaterType( int nType ); + + virtual bool PhysicsSplash( const Vector ¢erPoint, const Vector &normal, float rawSpeed, float scaledSpeed ) { return false; } + virtual void Splash() {} + + void ClearSolidFlags( void ); + void RemoveSolidFlags( int flags ); + void AddSolidFlags( int flags ); + bool IsSolidFlagSet( int flagMask ) const; + void SetSolidFlags( int flags ); + bool IsSolid() const; + + void SetModelName( string_t name ); + + model_t *GetModel( void ); + + // These methods return a *world-aligned* box relative to the absorigin of the entity. + // This is used for collision purposes and is *not* guaranteed + // to surround the entire entity's visual representation + // NOTE: It is illegal to ask for the world-aligned bounds for + // SOLID_BSP objects + const Vector& WorldAlignMins( ) const; + const Vector& WorldAlignMaxs( ) const; + + // This defines collision bounds in OBB space + void SetCollisionBounds( const Vector& mins, const Vector &maxs ); + + // NOTE: The world space center *may* move when the entity rotates. + virtual const Vector& WorldSpaceCenter( ) const; + const Vector& WorldAlignSize( ) const; + + // Returns a radius of a sphere + // *centered at the world space center* bounding the collision representation + // of the entity. NOTE: The world space center *may* move when the entity rotates. + float BoundingRadius() const; + bool IsPointSized() const; + + // NOTE: Setting the abs origin or angles will cause the local origin + angles to be set also + void SetAbsOrigin( const Vector& origin ); + void SetAbsAngles( const QAngle& angles ); + + // Origin and angles in local space ( relative to parent ) + // NOTE: Setting the local origin or angles will cause the abs origin + angles to be set also + void SetLocalOrigin( const Vector& origin ); + const Vector& GetLocalOrigin( void ) const; + + void SetLocalAngles( const QAngle& angles ); + const QAngle& GetLocalAngles( void ) const; + + void SetElasticity( float flElasticity ); + float GetElasticity( void ) const; + + void SetShadowCastDistance( float flDistance ); + float GetShadowCastDistance( void ) const; + void SetShadowCastDistance( float flDesiredDistance, float flDelay ); + + float GetLocalTime( void ) const; + void IncrementLocalTime( float flTimeDelta ); + float GetMoveDoneTime( ) const; + void SetMoveDoneTime( float flTime ); + + // Used by the PAS filters to ask the entity where in world space the sounds it emits come from. + // This is used right now because if you have something sitting on an incline, using our axis-aligned + // bounding boxes can return a position in solid space, so you won't hear sounds emitted by the object. + // For now, we're hacking around it by moving the sound emission origin up on certain objects like vehicles. + // + // When OBBs get in, this can probably go away. + virtual Vector GetSoundEmissionOrigin() const; + + void AddFlag( int flags ); + void RemoveFlag( int flagsToRemove ); + void ToggleFlag( int flagToToggle ); + int GetFlags( void ) const; + void ClearFlags( void ); + + // Sets the local position from a transform + void SetLocalTransform( const matrix3x4_t &localTransform ); + + // See CSoundEmitterSystem + void EmitSound( const char *soundname, float soundtime = 0.0f, float *duration = NULL ); // Override for doing the general case of CPASAttenuationFilter filter( this ), and EmitSound( filter, entindex(), etc. ); + void EmitSound( const char *soundname, HSOUNDSCRIPTHANDLE& handle, float soundtime = 0.0f, float *duration = NULL ); // Override for doing the general case of CPASAttenuationFilter filter( this ), and EmitSound( filter, entindex(), etc. ); + void StopSound( const char *soundname ); + void StopSound( const char *soundname, HSOUNDSCRIPTHANDLE& handle ); + void GenderExpandString( char const *in, char *out, int maxlen ); + + virtual void ModifyEmitSoundParams( EmitSound_t ¶ms ); + + static float GetSoundDuration( const char *soundname, char const *actormodel ); + + static bool GetParametersForSound( const char *soundname, CSoundParameters ¶ms, char const *actormodel ); + static bool GetParametersForSound( const char *soundname, HSOUNDSCRIPTHANDLE& handle, CSoundParameters ¶ms, char const *actormodel ); + + static void EmitSound( IRecipientFilter& filter, int iEntIndex, const char *soundname, const Vector *pOrigin = NULL, float soundtime = 0.0f, float *duration = NULL ); + static void EmitSound( IRecipientFilter& filter, int iEntIndex, const char *soundname, HSOUNDSCRIPTHANDLE& handle, const Vector *pOrigin = NULL, float soundtime = 0.0f, float *duration = NULL ); + static void StopSound( int iEntIndex, const char *soundname ); + static soundlevel_t LookupSoundLevel( const char *soundname ); + static soundlevel_t LookupSoundLevel( const char *soundname, HSOUNDSCRIPTHANDLE& handle ); + + static void EmitSound( IRecipientFilter& filter, int iEntIndex, const EmitSound_t & params ); + static void EmitSound( IRecipientFilter& filter, int iEntIndex, const EmitSound_t & params, HSOUNDSCRIPTHANDLE& handle ); + + static void StopSound( int iEntIndex, int iChannel, const char *pSample ); + + static void EmitAmbientSound( int entindex, const Vector& origin, const char *soundname, int flags = 0, float soundtime = 0.0f, float *duration = NULL ); + + // These files need to be listed in scripts/game_sounds_manifest.txt + static HSOUNDSCRIPTHANDLE PrecacheScriptSound( const char *soundname ); + static void PrefetchScriptSound( const char *soundname ); + + // For each client who appears to be a valid recipient, checks the client has disabled CC and if so, removes them from + // the recipient list. + static void RemoveRecipientsIfNotCloseCaptioning( CRecipientFilter& filter ); + static void EmitCloseCaption( IRecipientFilter& filter, int entindex, char const *token, CUtlVector< Vector >& soundorigins, float duration, bool warnifmissing = false ); + static void EmitSentenceByIndex( IRecipientFilter& filter, int iEntIndex, int iChannel, int iSentenceIndex, + float flVolume, soundlevel_t iSoundlevel, int iFlags = 0, int iPitch = PITCH_NORM, + const Vector *pOrigin = NULL, const Vector *pDirection = NULL, bool bUpdatePositions = true, float soundtime = 0.0f ); + + static bool IsPrecacheAllowed(); + static void SetAllowPrecache( bool allow ); + + static bool m_bAllowPrecache; + + static bool IsSimulatingOnAlternateTicks(); + + virtual bool IsDeflectable() { return false; } + virtual void Deflected( CBaseEntity *pDeflectedBy, Vector &vecDir ) {} + +// void Relink() {} + +public: + + // VPHYSICS Integration ----------------------------------------------- + // + // -------------------------------------------------------------------- + // UNDONE: Move to IEntityVPhysics? or VPhysicsProp() ? + // Called after spawn, and in the case of self-managing objects, after load + virtual bool CreateVPhysics(); + + // Convenience routines to init the vphysics simulation for this object. + // This creates a static object. Something that behaves like world geometry - solid, but never moves + IPhysicsObject *VPhysicsInitStatic( void ); + + // This creates a normal vphysics simulated object - physics determines where it goes (gravity, friction, etc) + // and the entity receives updates from vphysics. SetAbsOrigin(), etc do not affect the object! + IPhysicsObject *VPhysicsInitNormal( SolidType_t solidType, int nSolidFlags, bool createAsleep, solid_t *pSolid = NULL ); + + // This creates a vphysics object with a shadow controller that follows the AI + // Move the object to where it should be and call UpdatePhysicsShadowToCurrentPosition() + IPhysicsObject *VPhysicsInitShadow( bool allowPhysicsMovement, bool allowPhysicsRotation, solid_t *pSolid = NULL ); + + // Force a non-solid (ie. solid_trigger) physics object to collide with other entities. + virtual bool ForceVPhysicsCollide( CBaseEntity *pEntity ) { return false; } + +private: + // called by all vphysics inits + bool VPhysicsInitSetup(); +public: + + void VPhysicsSetObject( IPhysicsObject *pPhysics ); + // destroy and remove the physics object for this entity + virtual void VPhysicsDestroyObject( void ); + void VPhysicsSwapObject( IPhysicsObject *pSwap ); + + inline IPhysicsObject *VPhysicsGetObject( void ) const { return m_pPhysicsObject; } + virtual void VPhysicsUpdate( IPhysicsObject *pPhysics ); + void VPhysicsUpdatePusher( IPhysicsObject *pPhysics ); + + // react physically to damage (called from CBaseEntity::OnTakeDamage() by default) + virtual int VPhysicsTakeDamage( const CTakeDamageInfo &info ); + virtual void VPhysicsShadowCollision( int index, gamevcollisionevent_t *pEvent ); + virtual void VPhysicsShadowUpdate( IPhysicsObject *pPhysics ) {} + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + virtual void VPhysicsFriction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit ); + + // update the shadow so it will coincide with the current AI position at some time + // in the future (or 0 for now) + virtual void UpdatePhysicsShadowToCurrentPosition( float deltaTime ); + virtual int VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ); + virtual bool VPhysicsIsFlesh( void ); + // -------------------------------------------------------------------- + +public: +#if !defined( NO_ENTITY_PREDICTION ) + // The player drives simulation of this entity + void SetPlayerSimulated( CBasePlayer *pOwner ); + void UnsetPlayerSimulated( void ); + bool IsPlayerSimulated( void ) const; + CBasePlayer *GetSimulatingPlayer( void ); +#endif + // FIXME: Make these private! + void PhysicsCheckForEntityUntouch( void ); + bool PhysicsRunThink( thinkmethods_t thinkMethod = THINK_FIRE_ALL_FUNCTIONS ); + bool PhysicsRunSpecificThink( int nContextIndex, BASEPTR thinkFunc ); + bool PhysicsTestEntityPosition( CBaseEntity **ppEntity = NULL ); + void PhysicsPushEntity( const Vector& push, trace_t *pTrace ); + bool PhysicsCheckWater( void ); + void PhysicsCheckWaterTransition( void ); + void PhysicsStepRecheckGround(); + // Computes the water level + type + void UpdateWaterState(); + bool IsEdictFree() const { return edict()->IsFree(); } + + // Callbacks for the physgun/cannon picking up an entity + virtual CBasePlayer *HasPhysicsAttacker( float dt ) { return NULL; } + +#ifdef MAPBASE + // This function needed to be extended to phys_magnet and I didn't like the dynamic_casts. + virtual bool CanBePickedUpByPhyscannon() { return false; } +#endif + + // UNDONE: Make this data? + virtual unsigned int PhysicsSolidMaskForEntity( void ) const; + + // Computes the abs position of a point specified in local space + void ComputeAbsPosition( const Vector &vecLocalPosition, Vector *pAbsPosition ); + + // Computes the abs position of a direction specified in local space + void ComputeAbsDirection( const Vector &vecLocalDirection, Vector *pAbsDirection ); + + void SetPredictionEligible( bool canpredict ); + +protected: + // Invalidates the abs state of all children + void InvalidatePhysicsRecursive( int nChangeFlags ); + + int PhysicsClipVelocity (const Vector& in, const Vector& normal, Vector& out, float overbounce ); + void PhysicsRelinkChildren( float dt ); + + // Performs the collision resolution for fliers. + void PerformFlyCollisionResolution( trace_t &trace, Vector &move ); + void ResolveFlyCollisionBounce( trace_t &trace, Vector &vecVelocity, float flMinTotalElasticity = 0.0f ); + void ResolveFlyCollisionSlide( trace_t &trace, Vector &vecVelocity ); + virtual void ResolveFlyCollisionCustom( trace_t &trace, Vector &vecVelocity ); + +private: + // Physics-related private methods + void PhysicsStep( void ); + void PhysicsPusher( void ); + void PhysicsNone( void ); + void PhysicsNoclip( void ); + void PhysicsStepRunTimestep( float timestep ); + void PhysicsToss( void ); + void PhysicsCustom( void ); + void PerformPush( float movetime ); + + // Simulation in local space of rigid children + void PhysicsRigidChild( void ); + + // Computes the base velocity + void UpdateBaseVelocity( void ); + + // Implement this if you use MOVETYPE_CUSTOM + virtual void PerformCustomPhysics( Vector *pNewPosition, Vector *pNewVelocity, QAngle *pNewAngles, QAngle *pNewAngVelocity ); + + void PhysicsDispatchThink( BASEPTR thinkFunc ); + + touchlink_t *PhysicsMarkEntityAsTouched( CBaseEntity *other ); + void PhysicsTouch( CBaseEntity *pentOther ); + void PhysicsStartTouch( CBaseEntity *pentOther ); + + CBaseEntity *PhysicsPushMove( float movetime ); + CBaseEntity *PhysicsPushRotate( float movetime ); + + CBaseEntity *PhysicsCheckRotateMove( rotatingpushmove_t &rotPushmove, CBaseEntity **pPusherList, int pusherListCount ); + CBaseEntity *PhysicsCheckPushMove( const Vector& move, CBaseEntity **pPusherList, int pusherListCount ); + int PhysicsTryMove( float flTime, trace_t *steptrace ); + + void PhysicsCheckVelocity( void ); + void PhysicsAddHalfGravity( float timestep ); + void PhysicsAddGravityMove( Vector &move ); + + void CalcAbsoluteVelocity(); + void CalcAbsoluteAngularVelocity(); + + // Checks a sweep without actually performing the move + void PhysicsCheckSweep( const Vector& vecAbsStart, const Vector &vecAbsDelta, trace_t *pTrace ); + + // Computes new angles based on the angular velocity + void SimulateAngles( float flFrameTime ); + + void CheckStepSimulationChanged(); + // Run regular think and latch off angle/origin changes so we can interpolate them on the server to fake simulation + void StepSimulationThink( float dt ); + + // Compute network origin +private: + void ComputeStepSimulationNetwork( StepSimulationData *step ); + +public: + bool UseStepSimulationNetworkOrigin( const Vector **out_v ); + bool UseStepSimulationNetworkAngles( const QAngle **out_a ); + +public: + // Add a discontinuity to a step + bool AddStepDiscontinuity( float flTime, const Vector &vecOrigin, const QAngle &vecAngles ); + int GetFirstThinkTick(); // get first tick thinking on any context +private: + // origin and angles to use in step calculations + virtual Vector GetStepOrigin( void ) const; + virtual QAngle GetStepAngles( void ) const; + + // These set entity flags (EFL_*) to help optimize queries + void CheckHasThinkFunction( bool isThinkingHint = false ); + void CheckHasGamePhysicsSimulation(); + bool WillThink(); + bool WillSimulateGamePhysics(); + + friend class CPushBlockerEnum; + + // Sets/Gets the next think based on context index + void SetNextThink( int nContextIndex, float thinkTime ); + void SetLastThink( int nContextIndex, float thinkTime ); + float GetNextThink( int nContextIndex ) const; + int GetNextThinkTick( int nContextIndex ) const; + + // Shot statistics + void UpdateShotStatistics( const trace_t &tr ); + + // Handle shot entering water + bool HandleShotImpactingWater( const FireBulletsInfo_t &info, const Vector &vecEnd, ITraceFilter *pTraceFilter, Vector *pVecTracerDest ); + + // Handle shot entering water + void HandleShotImpactingGlass( const FireBulletsInfo_t &info, const trace_t &tr, const Vector &vecDir, ITraceFilter *pTraceFilter ); + + // Should we draw bubbles underwater? + bool ShouldDrawUnderwaterBulletBubbles(); + + // Computes the tracer start position + void ComputeTracerStartPosition( const Vector &vecShotSrc, Vector *pVecTracerStart ); + + // Computes the tracer start position + void CreateBubbleTrailTracer( const Vector &vecShotSrc, const Vector &vecShotEnd, const Vector &vecShotDir ); + + virtual bool ShouldDrawWaterImpacts() { return true; } + + // Changes shadow cast distance over time + void ShadowCastDistThink( ); + + // Precache model sounds + particles + static void PrecacheModelComponents( int nModelIndex ); + static void PrecacheSoundHelper( const char *pName ); + +protected: + // Which frame did I simulate? + int m_nSimulationTick; + + // FIXME: Make this private! Still too many references to do so... + CNetworkVar( int, m_spawnflags ); + +private: + int m_iEFlags; // entity flags EFL_* + // was pev->flags + CNetworkVarForDerived( int, m_fFlags ); + + CNetworkVar( string_t, m_iName ); // name used to identify this entity + + // Damage modifiers + friend class CDamageModifier; + CUtlLinkedList m_DamageModifiers; + + EHANDLE m_pParent; // for movement hierarchy + byte m_nTransmitStateOwnedCounter; + CNetworkVar( unsigned char, m_iParentAttachment ); // 0 if we're relative to the parent's absorigin and absangles. + CNetworkVar( unsigned char, m_MoveType ); // One of the MOVETYPE_ defines. + CNetworkVar( unsigned char, m_MoveCollide ); + + // Our immediate parent in the movement hierarchy. + // FIXME: clarify m_pParent vs. m_pMoveParent + CNetworkHandle( CBaseEntity, m_hMoveParent ); + // cached child list + EHANDLE m_hMoveChild; + // generated from m_pMoveParent + EHANDLE m_hMovePeer; + + friend class CCollisionProperty; + friend class CServerNetworkProperty; + CNetworkVarEmbedded( CCollisionProperty, m_Collision ); + + CNetworkHandle( CBaseEntity, m_hOwnerEntity ); // only used to point to an edict it won't collide with + CNetworkHandle( CBaseEntity, m_hEffectEntity ); // Fire/Dissolve entity. + + CNetworkVar( int, m_CollisionGroup ); // used to cull collision tests + IPhysicsObject *m_pPhysicsObject; // pointer to the entity's physics object (vphysics.dll) + + CNetworkVar( float, m_flShadowCastDistance ); + float m_flDesiredShadowCastDistance; + + // Team handling + int m_iInitialTeamNum; // Team number of this entity's team read from file + CNetworkVar( int, m_iTeamNum ); // Team number of this entity's team. + + // Sets water type + level for physics objects + unsigned char m_nWaterTouch; + unsigned char m_nSlimeTouch; + unsigned char m_nWaterType; + CNetworkVarForDerived( unsigned char, m_nWaterLevel ); + float m_flNavIgnoreUntilTime; + + CNetworkHandleForDerived( CBaseEntity, m_hGroundEntity ); + float m_flGroundChangeTime; // Time that the ground entity changed + + string_t m_ModelName; + + // Velocity of the thing we're standing on (world space) + CNetworkVarForDerived( Vector, m_vecBaseVelocity ); + + // Global velocity + Vector m_vecAbsVelocity; + + // Local angular velocity + QAngle m_vecAngVelocity; + + // Global angular velocity +// QAngle m_vecAbsAngVelocity; + + // local coordinate frame of entity + matrix3x4_t m_rgflCoordinateFrame; + + // Physics state + EHANDLE m_pBlocker; + + // was pev->gravity; + float m_flGravity; // rename to m_flGravityScale; + // was pev->friction + CNetworkVarForDerived( float, m_flFriction ); + CNetworkVar( float, m_flElasticity ); + + // was pev->ltime + float m_flLocalTime; + // local time at the beginning of this frame + float m_flVPhysicsUpdateLocalTime; + // local time the movement has ended + float m_flMoveDoneTime; + + // A counter to help quickly build a list of potentially pushed objects for physics + int m_nPushEnumCount; + + Vector m_vecAbsOrigin; + CNetworkVectorForDerived( m_vecVelocity ); + + //Adrian + CNetworkVar( unsigned char, m_iTextureFrameIndex ); + + CNetworkVar( bool, m_bSimulatedEveryTick ); + CNetworkVar( bool, m_bAnimatedEveryTick ); + CNetworkVar( bool, m_bAlternateSorting ); + + // User outputs. Fired when the "FireInputX" input is triggered. +#ifdef MAPBASE + COutputVariant m_OutUser1; + COutputVariant m_OutUser2; + COutputVariant m_OutUser3; + COutputVariant m_OutUser4; +#endif + COutputEvent m_OnUser1; + COutputEvent m_OnUser2; + COutputEvent m_OnUser3; + COutputEvent m_OnUser4; + + QAngle m_angAbsRotation; + + CNetworkVector( m_vecOrigin ); + CNetworkQAngle( m_angRotation ); + CBaseHandle m_RefEHandle; + + // was pev->view_ofs ( FIXME: Move somewhere up the hierarch, CBaseAnimating, etc. ) + CNetworkVectorForDerived( m_vecViewOffset ); + +private: + // dynamic model state tracking + bool m_bDynamicModelAllowed; + bool m_bDynamicModelPending; + bool m_bDynamicModelSetBounds; + void OnModelLoadComplete( const model_t* model ); + friend class CBaseEntityModelLoadProxy; + +protected: + void EnableDynamicModels() { m_bDynamicModelAllowed = true; } + +public: + bool IsDynamicModelLoading() const { return m_bDynamicModelPending; } + void SetCollisionBoundsFromModel(); + +#if !defined( NO_ENTITY_PREDICTION ) + CNetworkVar( bool, m_bIsPlayerSimulated ); + // Player who is driving my simulation + CHandle< CBasePlayer > m_hPlayerSimulationOwner; +#endif + + int m_fDataObjectTypes; + + // So it can get at the physics methods + friend class CCollisionEvent; + +// Methods shared by client and server +public: + void SetSize( const Vector &vecMin, const Vector &vecMax ); // UTIL_SetSize( this, mins, maxs ); + static int PrecacheModel( const char *name, bool bPreload = true ); + static bool PrecacheSound( const char *name ); + static void PrefetchSound( const char *name ); + void Remove( ); // UTIL_Remove( this ); + +private: + + // This is a random seed used by the networking code to allow client - side prediction code + // randon number generators to spit out the same random numbers on both sides for a particular + // usercmd input. + static int m_nPredictionRandomSeed; + static CBasePlayer *m_pPredictionPlayer; + + // FIXME: Make hierarchy a member of CBaseEntity + // or a contained private class... + friend void UnlinkChild( CBaseEntity *pParent, CBaseEntity *pChild ); + friend void LinkChild( CBaseEntity *pParent, CBaseEntity *pChild ); + friend void ClearParent( CBaseEntity *pEntity ); + friend void UnlinkAllChildren( CBaseEntity *pParent ); + friend void UnlinkFromParent( CBaseEntity *pRemove ); + friend void TransferChildren( CBaseEntity *pOldParent, CBaseEntity *pNewParent ); + +public: + // Accessors for above + static int GetPredictionRandomSeed( void ); + static void SetPredictionRandomSeed( const CUserCmd *cmd ); + static CBasePlayer *GetPredictionPlayer( void ); + static void SetPredictionPlayer( CBasePlayer *player ); + + + // For debugging shared code + static bool IsServer( void ) + { + return true; + } + + static bool IsClient( void ) + { + return false; + } + + static char const *GetDLLType( void ) + { + return "server"; + } + + // Used to access m_vecAbsOrigin during restore when it's unsafe to call GetAbsOrigin. + friend class CPlayerRestoreHelper; + + static bool s_bAbsQueriesValid; + + // Call this when hierarchy is not completely set up (such as during Restore) to throw asserts + // when people call GetAbsAnything. + static inline void SetAbsQueriesValid( bool bValid ) + { + s_bAbsQueriesValid = bValid; + } + + static inline bool IsAbsQueriesValid() + { + return s_bAbsQueriesValid; + } + + + // VSCRIPT + HSCRIPT GetScriptInstance(); + bool ValidateScriptScope(); + virtual void RunVScripts(); + bool CallScriptFunction(const char* pFunctionName, ScriptVariant_t* pFunctionReturn); + void ConnectOutputToScript(const char* pszOutput, const char* pszScriptFunc); + void DisconnectOutputFromScript(const char* pszOutput, const char* pszScriptFunc); + void ScriptThink(); +#ifdef MAPBASE_VSCRIPT + void ScriptSetThinkFunction(const char *szFunc, float time); + void ScriptStopThinkFunction(); + void ScriptSetContextThink( const char* szContext, HSCRIPT hFunc, float time ); + void ScriptSetThink( HSCRIPT hFunc, float time ); + void ScriptStopThink(); + void ScriptContextThink(); +private: + CUtlVector< scriptthinkfunc_t* > m_ScriptThinkFuncs; +public: +#endif + const char* GetScriptId(); + HSCRIPT GetScriptScope(); +#ifdef MAPBASE_VSCRIPT + HSCRIPT GetOrCreatePrivateScriptScope(); +#endif + void RunPrecacheScripts(void); + void RunOnPostSpawnScripts(void); + +#ifdef MAPBASE_VSCRIPT + HSCRIPT LookupScriptFunction(const char* pFunctionName); + bool CallScriptFunctionHandle(HSCRIPT hFunc, ScriptVariant_t* pFunctionReturn); +#endif + + HSCRIPT ScriptGetMoveParent(void); + HSCRIPT ScriptGetRootMoveParent(); + HSCRIPT ScriptFirstMoveChild(void); + HSCRIPT ScriptNextMovePeer(void); + + const Vector& ScriptEyePosition(void) { static Vector vec; vec = EyePosition(); return vec; } +#ifdef MAPBASE_VSCRIPT + const QAngle& ScriptEyeAngles(void) { static QAngle ang; ang = EyeAngles(); return ang; } + void ScriptSetAngles(const QAngle angles) { Teleport(NULL, &angles, NULL); } + const QAngle& ScriptGetAngles(void) { return GetAbsAngles(); } +#else + void ScriptSetAngles(float fPitch, float fYaw, float fRoll) { QAngle angles(fPitch, fYaw, fRoll); Teleport(NULL, &angles, NULL); } + const Vector& ScriptGetAngles(void) { static Vector vec; QAngle qa = GetAbsAngles(); vec.x = qa.x; vec.y = qa.y; vec.z = qa.z; return vec; } +#endif + +#ifndef MAPBASE_VSCRIPT + void ScriptSetSize(const Vector& mins, const Vector& maxs) { UTIL_SetSize(this, mins, maxs); } + void ScriptUtilRemove(void) { UTIL_Remove(this); } +#endif + void ScriptSetOwner(HSCRIPT hEntity) { SetOwnerEntity(ToEnt(hEntity)); } + void ScriptSetOrigin(const Vector& v) { Teleport(&v, NULL, NULL); } + void ScriptSetForward(const Vector& v) { QAngle angles; VectorAngles(v, angles); Teleport(NULL, &angles, NULL); } + const Vector& ScriptGetForward(void) { static Vector vecForward; GetVectors(&vecForward, NULL, NULL); return vecForward; } +#ifdef MAPBASE_VSCRIPT + const Vector& ScriptGetRight(void) { static Vector vecRight; GetVectors(NULL, &vecRight, NULL); return vecRight; } +#endif + const Vector& ScriptGetLeft(void) { static Vector vecRight; GetVectors(NULL, &vecRight, NULL); return vecRight; } + + const Vector& ScriptGetUp(void) { static Vector vecUp; GetVectors(NULL, NULL, &vecUp); return vecUp; } + +#ifdef MAPBASE_VSCRIPT + void ScriptSetOriginAngles(const Vector &vecOrigin, const QAngle &angAngles) { Teleport(&vecOrigin, &angAngles, NULL); } + void ScriptSetOriginAnglesVelocity(const Vector &vecOrigin, const QAngle &angAngles, const Vector &vecVelocity) { Teleport(&vecOrigin, &angAngles, &vecVelocity); } + + HSCRIPT ScriptEntityToWorldTransform( void ); + + HSCRIPT ScriptGetPhysicsObject( void ); + + void ScriptSetParent(HSCRIPT hParent, const char *szAttachment); +#endif + + const char* ScriptGetModelName(void) const; + HSCRIPT ScriptGetModelKeyValues(void); + + void ScriptStopSound(const char* soundname); + void ScriptEmitSound(const char* soundname); + float ScriptSoundDuration(const char* soundname, const char* actormodel); + + void VScriptPrecacheScriptSound(const char* soundname); + + const Vector& ScriptGetLocalAngularVelocity( void ); + void ScriptSetLocalAngularVelocity( float pitchVel, float yawVel, float rollVel ); + + const Vector& ScriptGetBoundingMins(void); + const Vector& ScriptGetBoundingMaxs(void); + +#ifdef MAPBASE_VSCRIPT + bool ScriptIsVisible( const Vector &vecSpot ) { return FVisible( vecSpot ); } + bool ScriptIsEntVisible( HSCRIPT pEntity ) { return FVisible( ToEnt( pEntity ) ); } + bool ScriptIsVisibleWithMask( const Vector &vecSpot, int traceMask ) { return FVisible( vecSpot, traceMask ); } + + int ScriptTakeDamage( HSCRIPT pInfo ); + void ScriptFireBullets( HSCRIPT pInfo ); + + void ScriptAddContext( const char *name, const char *value, float duration = 0.0f ); + const char *ScriptGetContext( const char *name ); + HSCRIPT ScriptGetContextIndex( int index ); + + int ScriptClassify(void); + + bool ScriptAddOutput( const char *pszOutputName, const char *pszTarget, const char *pszAction, const char *pszParameter, float flDelay, int iMaxTimes ); + const char *ScriptGetKeyValue( const char *pszKeyName ); + + const Vector& ScriptGetColorVector(); + int ScriptGetColorR() { return m_clrRender.GetR(); } + int ScriptGetColorG() { return m_clrRender.GetG(); } + int ScriptGetColorB() { return m_clrRender.GetB(); } + int ScriptGetAlpha() { return m_clrRender.GetA(); } + void ScriptSetColorVector( const Vector& vecColor ); + void ScriptSetColor( int r, int g, int b ); + void ScriptSetColorR( int iVal ) { SetRenderColorR( iVal ); } + void ScriptSetColorG( int iVal ) { SetRenderColorG( iVal ); } + void ScriptSetColorB( int iVal ) { SetRenderColorB( iVal ); } + void ScriptSetAlpha( int iVal ) { SetRenderColorA( iVal ); } + + int ScriptGetRenderMode() { return GetRenderMode(); } + void ScriptSetRenderMode( int nRenderMode ) { SetRenderMode( (RenderMode_t)nRenderMode ); } + + int ScriptGetMoveType() { return GetMoveType(); } + void ScriptSetMoveType( int iMoveType ) { SetMoveType( (MoveType_t)iMoveType ); } + + bool ScriptDispatchInteraction( int interactionType, HSCRIPT data, HSCRIPT sourceEnt ); + + int ScriptGetTakeDamage() { return m_takedamage; } + void ScriptSetTakeDamage( int val ) { m_takedamage = val; } + + static ScriptHook_t g_Hook_UpdateOnRemove; + static ScriptHook_t g_Hook_VPhysicsCollision; + static ScriptHook_t g_Hook_FireBullets; + static ScriptHook_t g_Hook_OnDeath; + static ScriptHook_t g_Hook_HandleInteraction; +#endif + + string_t m_iszVScripts; + string_t m_iszScriptThinkFunction; + CScriptScope m_ScriptScope; + HSCRIPT m_hScriptInstance; + string_t m_iszScriptId; +#ifdef MAPBASE_VSCRIPT + HSCRIPT m_pScriptModelKeyValues; +#else + CScriptKeyValues* m_pScriptModelKeyValues; +#endif +}; + +// Send tables exposed in this module. +EXTERN_SEND_TABLE(DT_Edict); +EXTERN_SEND_TABLE(DT_BaseEntity); + + + +// Ugly technique to override base member functions +// Normally it's illegal to cast a pointer to a member function of a derived class to a pointer to a +// member function of a base class. static_cast is a sleezy way around that problem. + +#ifdef _DEBUG + +#define SetTouch( a ) TouchSet( static_cast (a), #a ) +#define SetUse( a ) UseSet( static_cast (a), #a ) +#define SetBlocked( a ) BlockedSet( static_cast (a), #a ) + +#else + +#define SetTouch( a ) m_pfnTouch = static_cast (a) +#define SetUse( a ) m_pfnUse = static_cast (a) +#define SetBlocked( a ) m_pfnBlocked = static_cast (a) + +#endif + +// handling entity/edict transforms +inline CBaseEntity *GetContainingEntity( edict_t *pent ) +{ + if ( pent && pent->GetUnknown() ) + { + return pent->GetUnknown()->GetBaseEntity(); + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Pauses or resumes entity i/o events. When paused, no outputs will +// fire unless Debug_SetSteps is called with a nonzero step value. +// Input : bPause - true to pause, false to resume. +//----------------------------------------------------------------------------- +inline void CBaseEntity::Debug_Pause(bool bPause) +{ + CBaseEntity::m_bDebugPause = bPause; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if entity i/o is paused, false if not. +//----------------------------------------------------------------------------- +inline bool CBaseEntity::Debug_IsPaused(void) +{ + return(CBaseEntity::m_bDebugPause); +} + + +//----------------------------------------------------------------------------- +// Purpose: Decrements the debug step counter. Used when the entity i/o system +// is in single step mode, this is called every time an output is fired. +// Output : Returns true on to continue firing outputs, false to stop. +//----------------------------------------------------------------------------- +inline bool CBaseEntity::Debug_Step(void) +{ + if (CBaseEntity::m_nDebugSteps > 0) + { + CBaseEntity::m_nDebugSteps--; + } + return(CBaseEntity::m_nDebugSteps > 0); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the number of entity outputs to allow to fire before pausing +// the entity i/o system. +// Input : nSteps - Number of steps to execute. +//----------------------------------------------------------------------------- +inline void CBaseEntity::Debug_SetSteps(int nSteps) +{ + CBaseEntity::m_nDebugSteps = nSteps; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if we should allow outputs to be fired, false if not. +//----------------------------------------------------------------------------- +inline bool CBaseEntity::Debug_ShouldStep(void) +{ + return(!CBaseEntity::m_bDebugPause || CBaseEntity::m_nDebugSteps > 0); +} + +//----------------------------------------------------------------------------- +// Methods relating to traversing hierarchy +//----------------------------------------------------------------------------- +inline CBaseEntity *CBaseEntity::GetMoveParent( void ) +{ + return m_hMoveParent.Get(); +} + +inline CBaseEntity *CBaseEntity::FirstMoveChild( void ) +{ + return m_hMoveChild.Get(); +} + +inline CBaseEntity *CBaseEntity::NextMovePeer( void ) +{ + return m_hMovePeer.Get(); +} + +// FIXME: Remove this! There shouldn't be a difference between moveparent + parent +inline CBaseEntity* CBaseEntity::GetParent() +{ + return m_pParent.Get(); +} + +inline int CBaseEntity::GetParentAttachment() +{ + return m_iParentAttachment; +} + +//----------------------------------------------------------------------------- +// Inline methods +//----------------------------------------------------------------------------- +inline string_t CBaseEntity::GetEntityName() +{ + return m_iName; +} + +inline const char *CBaseEntity::GetEntityNameAsCStr() +{ + return STRING(m_iName.Get()); +} + +inline const char *CBaseEntity::GetPreTemplateName() +{ + const char *pszDelimiter = V_strrchr( STRING(m_iName.Get()), '&' ); + if ( !pszDelimiter ) + return STRING( m_iName.Get() ); + static char szStrippedName[128]; + V_strncpy( szStrippedName, STRING( m_iName.Get() ), MIN( ARRAYSIZE(szStrippedName), pszDelimiter - STRING( m_iName.Get() ) + 1 ) ); + return szStrippedName; +} + +inline void CBaseEntity::SetName( string_t newName ) +{ + m_iName = newName; +} + +#ifdef MAPBASE_VSCRIPT +inline void CBaseEntity::SetNameAsCStr( const char *newName ) +{ + m_iName = AllocPooledString(newName); +} +#endif + +inline bool CBaseEntity::NameMatches( const char *pszNameOrWildcard ) +{ + if ( IDENT_STRINGS(m_iName, pszNameOrWildcard) ) + return true; + return NameMatchesComplex( pszNameOrWildcard ); +} + +inline bool CBaseEntity::NameMatches( string_t nameStr ) +{ + if ( IDENT_STRINGS(m_iName, nameStr) ) + return true; + return NameMatchesComplex( STRING(nameStr) ); +} + +inline bool CBaseEntity::ClassMatches( const char *pszClassOrWildcard ) +{ + if ( IDENT_STRINGS(m_iClassname, pszClassOrWildcard ) ) + return true; + return ClassMatchesComplex( pszClassOrWildcard ); +} + +inline const char* CBaseEntity::GetClassname() +{ + return STRING(m_iClassname); +} + + +inline bool CBaseEntity::ClassMatches( string_t nameStr ) +{ + if ( IDENT_STRINGS(m_iClassname, nameStr ) ) + return true; + return ClassMatchesComplex( STRING(nameStr) ); +} + +inline int CBaseEntity::GetSpawnFlags( void ) const +{ + return m_spawnflags; +} + +inline void CBaseEntity::AddSpawnFlags( int nFlags ) +{ + m_spawnflags |= nFlags; +} +inline void CBaseEntity::RemoveSpawnFlags( int nFlags ) +{ + m_spawnflags &= ~nFlags; +} + +inline void CBaseEntity::ClearSpawnFlags( void ) +{ + m_spawnflags = 0; +} + +inline bool CBaseEntity::HasSpawnFlags( int nFlags ) const +{ + return (m_spawnflags & nFlags) != 0; +} + +//----------------------------------------------------------------------------- +// checks to see if the entity is marked for deletion +//----------------------------------------------------------------------------- +inline bool CBaseEntity::IsMarkedForDeletion( void ) +{ + return (m_iEFlags & EFL_KILLME); +} + +//----------------------------------------------------------------------------- +// EFlags +//----------------------------------------------------------------------------- +inline int CBaseEntity::GetEFlags() const +{ + return m_iEFlags; +} + +inline void CBaseEntity::SetEFlags( int iEFlags ) +{ + m_iEFlags = iEFlags; + + if ( iEFlags & ( EFL_FORCE_CHECK_TRANSMIT | EFL_IN_SKYBOX ) ) + { + DispatchUpdateTransmitState(); + } +} + +inline void CBaseEntity::AddEFlags( int nEFlagMask ) +{ + m_iEFlags |= nEFlagMask; + + if ( nEFlagMask & ( EFL_FORCE_CHECK_TRANSMIT | EFL_IN_SKYBOX ) ) + { + DispatchUpdateTransmitState(); + } +} + +inline void CBaseEntity::RemoveEFlags( int nEFlagMask ) +{ + m_iEFlags &= ~nEFlagMask; + + if ( nEFlagMask & ( EFL_FORCE_CHECK_TRANSMIT | EFL_IN_SKYBOX ) ) + DispatchUpdateTransmitState(); +} + +inline bool CBaseEntity::IsEFlagSet( int nEFlagMask ) const +{ + return (m_iEFlags & nEFlagMask) != 0; +} + +inline void CBaseEntity::SetNavIgnore( float duration ) +{ + float flNavIgnoreUntilTime = ( duration == FLT_MAX ) ? FLT_MAX : gpGlobals->curtime + duration; + if ( flNavIgnoreUntilTime > m_flNavIgnoreUntilTime ) + m_flNavIgnoreUntilTime = flNavIgnoreUntilTime; +} + +inline void CBaseEntity::ClearNavIgnore() +{ + m_flNavIgnoreUntilTime = 0; +} + +inline bool CBaseEntity::IsNavIgnored() const +{ + return ( gpGlobals->curtime <= m_flNavIgnoreUntilTime ); +} + +inline bool CBaseEntity::GetCheckUntouch() const +{ + return IsEFlagSet( EFL_CHECK_UNTOUCH ); +} + +//----------------------------------------------------------------------------- +// Network state optimization +//----------------------------------------------------------------------------- +inline CBaseCombatCharacter *ToBaseCombatCharacter( CBaseEntity *pEntity ) +{ + if ( !pEntity ) + return NULL; + + return pEntity->MyCombatCharacterPointer(); +} + + +//----------------------------------------------------------------------------- +// Physics state accessor methods +//----------------------------------------------------------------------------- +inline const Vector& CBaseEntity::GetLocalOrigin( void ) const +{ + return m_vecOrigin.Get(); +} + +inline const QAngle& CBaseEntity::GetLocalAngles( void ) const +{ + return m_angRotation.Get(); +} + +inline const Vector& CBaseEntity::GetAbsOrigin( void ) const +{ + Assert( CBaseEntity::IsAbsQueriesValid() ); + + if (IsEFlagSet(EFL_DIRTY_ABSTRANSFORM)) + { + const_cast(this)->CalcAbsolutePosition(); + } + return m_vecAbsOrigin; +} + +inline const QAngle& CBaseEntity::GetAbsAngles( void ) const +{ + Assert( CBaseEntity::IsAbsQueriesValid() ); + + if (IsEFlagSet(EFL_DIRTY_ABSTRANSFORM)) + { + const_cast(this)->CalcAbsolutePosition(); + } + return m_angAbsRotation; +} + +#ifdef MAPBASE_VSCRIPT +inline float CBaseEntity::GetMass() +{ + IPhysicsObject *vPhys = VPhysicsGetObject(); + if (vPhys) + { + return vPhys->GetMass(); + } + else + { + Warning("Tried to call GetMass() on %s but it has no physics.\n", GetDebugName()); + return 0; + } +} + +inline void CBaseEntity::SetMass(float mass) +{ + mass = clamp(mass, VPHYSICS_MIN_MASS, VPHYSICS_MAX_MASS); + + IPhysicsObject *vPhys = VPhysicsGetObject(); + if (vPhys) + { + vPhys->SetMass(mass); + } + else + { + Warning("Tried to call SetMass() on %s but it has no physics.\n", GetDebugName()); + } +} +#endif + + +//----------------------------------------------------------------------------- +// Returns the entity-to-world transform +//----------------------------------------------------------------------------- +inline matrix3x4_t &CBaseEntity::EntityToWorldTransform() +{ + Assert( CBaseEntity::IsAbsQueriesValid() ); + + if (IsEFlagSet(EFL_DIRTY_ABSTRANSFORM)) + { + CalcAbsolutePosition(); + } + return m_rgflCoordinateFrame; +} + +inline const matrix3x4_t &CBaseEntity::EntityToWorldTransform() const +{ + Assert( CBaseEntity::IsAbsQueriesValid() ); + + if (IsEFlagSet(EFL_DIRTY_ABSTRANSFORM)) + { + const_cast(this)->CalcAbsolutePosition(); + } + return m_rgflCoordinateFrame; +} + + +//----------------------------------------------------------------------------- +// Some helper methods that transform a point from entity space to world space + back +//----------------------------------------------------------------------------- +inline void CBaseEntity::EntityToWorldSpace( const Vector &in, Vector *pOut ) const +{ + if ( GetAbsAngles() == vec3_angle ) + { + VectorAdd( in, GetAbsOrigin(), *pOut ); + } + else + { + VectorTransform( in, EntityToWorldTransform(), *pOut ); + } +} + +inline void CBaseEntity::WorldToEntitySpace( const Vector &in, Vector *pOut ) const +{ + if ( GetAbsAngles() == vec3_angle ) + { + VectorSubtract( in, GetAbsOrigin(), *pOut ); + } + else + { + VectorITransform( in, EntityToWorldTransform(), *pOut ); + } +} + + +//----------------------------------------------------------------------------- +// Velocity +//----------------------------------------------------------------------------- +inline Vector CBaseEntity::GetSmoothedVelocity( void ) +{ + Vector vel; + GetVelocity( &vel, NULL ); + return vel; +} + +inline const Vector &CBaseEntity::GetLocalVelocity( ) const +{ + return m_vecVelocity.Get(); +} + +inline const Vector &CBaseEntity::GetAbsVelocity( ) const +{ + Assert( CBaseEntity::IsAbsQueriesValid() ); + + if (IsEFlagSet(EFL_DIRTY_ABSVELOCITY)) + { + const_cast(this)->CalcAbsoluteVelocity(); + } + return m_vecAbsVelocity; +} + +inline const QAngle &CBaseEntity::GetLocalAngularVelocity( ) const +{ + return m_vecAngVelocity; +} + +/* +// FIXME: While we're using (dPitch, dYaw, dRoll) as our local angular velocity +// representation, we can't actually solve this problem +inline const QAngle &CBaseEntity::GetAbsAngularVelocity( ) const +{ + if (IsEFlagSet(EFL_DIRTY_ABSANGVELOCITY)) + { + const_cast(this)->CalcAbsoluteAngularVelocity(); + } + + return m_vecAbsAngVelocity; +} +*/ + +inline const Vector& CBaseEntity::GetBaseVelocity() const +{ + return m_vecBaseVelocity.Get(); +} + +inline void CBaseEntity::SetBaseVelocity( const Vector& v ) +{ + m_vecBaseVelocity = v; +} + +inline float CBaseEntity::GetGravity( void ) const +{ + return m_flGravity; +} + +inline void CBaseEntity::SetGravity( float gravity ) +{ + m_flGravity = gravity; +} + +inline float CBaseEntity::GetFriction( void ) const +{ + return m_flFriction; +} + +inline void CBaseEntity::SetFriction( float flFriction ) +{ + m_flFriction = flFriction; +} + +inline void CBaseEntity::SetElasticity( float flElasticity ) +{ + m_flElasticity = flElasticity; +} + +inline float CBaseEntity::GetElasticity( void ) const +{ + return m_flElasticity; +} + +inline void CBaseEntity::SetShadowCastDistance( float flDistance ) +{ + m_flShadowCastDistance = flDistance; +} + +inline float CBaseEntity::GetShadowCastDistance( void ) const +{ + return m_flShadowCastDistance; +} + +inline float CBaseEntity::GetLocalTime( void ) const +{ + return m_flLocalTime; +} + +inline void CBaseEntity::IncrementLocalTime( float flTimeDelta ) +{ + m_flLocalTime += flTimeDelta; +} + +inline float CBaseEntity::GetMoveDoneTime( ) const +{ + return (m_flMoveDoneTime >= 0) ? m_flMoveDoneTime - GetLocalTime() : -1; +} + +inline CBaseEntity *CBaseEntity::Instance( const edict_t *pent ) +{ + return GetContainingEntity( const_cast(pent) ); +} + +inline CBaseEntity *CBaseEntity::Instance( edict_t *pent ) +{ + if ( !pent ) + { + pent = INDEXENT(0); + } + return GetContainingEntity( pent ); +} + +inline CBaseEntity* CBaseEntity::Instance( int iEnt ) +{ + return Instance( INDEXENT( iEnt ) ); +} + +inline int CBaseEntity::GetWaterLevel() const +{ + return m_nWaterLevel; +} + +inline void CBaseEntity::SetWaterLevel( int nLevel ) +{ + m_nWaterLevel = nLevel; +} + +inline const color32 CBaseEntity::GetRenderColor() const +{ + return m_clrRender.Get(); +} + +inline void CBaseEntity::SetRenderColor( byte r, byte g, byte b ) +{ + m_clrRender.Init( r, g, b ); +} + +inline void CBaseEntity::SetRenderColor( byte r, byte g, byte b, byte a ) +{ + m_clrRender.Init( r, g, b, a ); +} + +inline void CBaseEntity::SetRenderColorR( byte r ) +{ + m_clrRender.SetR( r ); +} + +inline void CBaseEntity::SetRenderColorG( byte g ) +{ + m_clrRender.SetG( g ); +} + +inline void CBaseEntity::SetRenderColorB( byte b ) +{ + m_clrRender.SetB( b ); +} + +inline void CBaseEntity::SetRenderColorA( byte a ) +{ + m_clrRender.SetA( a ); +} + +inline void CBaseEntity::SetMoveCollide( MoveCollide_t val ) +{ + m_MoveCollide = val; +} + +inline bool CBaseEntity::IsTransparent() const +{ + return m_nRenderMode != kRenderNormal; +} + +inline int CBaseEntity::GetTextureFrameIndex( void ) +{ + return m_iTextureFrameIndex; +} + +inline void CBaseEntity::SetTextureFrameIndex( int iIndex ) +{ + m_iTextureFrameIndex = iIndex; +} + +//----------------------------------------------------------------------------- +// An inline version the game code can use +//----------------------------------------------------------------------------- +inline CCollisionProperty *CBaseEntity::CollisionProp() +{ + return &m_Collision; +} + +inline const CCollisionProperty *CBaseEntity::CollisionProp() const +{ + return &m_Collision; +} + +inline CServerNetworkProperty *CBaseEntity::NetworkProp() +{ + return &m_Network; +} + +inline const CServerNetworkProperty *CBaseEntity::NetworkProp() const +{ + return &m_Network; +} + +inline void CBaseEntity::ClearSolidFlags( void ) +{ + CollisionProp()->ClearSolidFlags(); +} + +inline void CBaseEntity::RemoveSolidFlags( int flags ) +{ + CollisionProp()->RemoveSolidFlags( flags ); +} + +inline void CBaseEntity::AddSolidFlags( int flags ) +{ + CollisionProp()->AddSolidFlags( flags ); +} + +inline int CBaseEntity::GetSolidFlags( void ) const +{ + return CollisionProp()->GetSolidFlags(); +} + +inline bool CBaseEntity::IsSolidFlagSet( int flagMask ) const +{ + return CollisionProp()->IsSolidFlagSet( flagMask ); +} + +inline bool CBaseEntity::IsSolid() const +{ + return CollisionProp()->IsSolid( ); +} + +inline void CBaseEntity::SetSolid( SolidType_t val ) +{ + CollisionProp()->SetSolid( val ); +} + +inline void CBaseEntity::SetSolidFlags( int flags ) +{ + CollisionProp()->SetSolidFlags( flags ); +} + +inline SolidType_t CBaseEntity::GetSolid() const +{ + return CollisionProp()->GetSolid(); +} + + +//----------------------------------------------------------------------------- +// Methods related to IServerUnknown +//----------------------------------------------------------------------------- +inline ICollideable *CBaseEntity::GetCollideable() +{ + return &m_Collision; +} + +inline IServerNetworkable *CBaseEntity::GetNetworkable() +{ + return &m_Network; +} + +inline CBaseEntity *CBaseEntity::GetBaseEntity() +{ + return this; +} + + +//----------------------------------------------------------------------------- +// Model related methods +//----------------------------------------------------------------------------- +inline void CBaseEntity::SetModelName( string_t name ) +{ + m_ModelName = name; + DispatchUpdateTransmitState(); +} + +inline string_t CBaseEntity::GetModelName( void ) const +{ + return m_ModelName; +} + +inline int CBaseEntity::GetModelIndex( void ) const +{ + return m_nModelIndex; +} + + + +//----------------------------------------------------------------------------- +// Methods relating to bounds +//----------------------------------------------------------------------------- +inline const Vector& CBaseEntity::WorldAlignMins( ) const +{ + Assert( !CollisionProp()->IsBoundsDefinedInEntitySpace() ); + Assert( CollisionProp()->GetCollisionAngles() == vec3_angle ); + return CollisionProp()->OBBMins(); +} + +inline const Vector& CBaseEntity::WorldAlignMaxs( ) const +{ + Assert( !CollisionProp()->IsBoundsDefinedInEntitySpace() ); + Assert( CollisionProp()->GetCollisionAngles() == vec3_angle ); + return CollisionProp()->OBBMaxs(); +} + +inline const Vector& CBaseEntity::WorldAlignSize( ) const +{ + Assert( !CollisionProp()->IsBoundsDefinedInEntitySpace() ); + Assert( CollisionProp()->GetCollisionAngles() == vec3_angle ); + return CollisionProp()->OBBSize(); +} + +// Returns a radius of a sphere *centered at the world space center* +// bounding the collision representation of the entity +inline float CBaseEntity::BoundingRadius() const +{ + return CollisionProp()->BoundingRadius(); +} + +inline bool CBaseEntity::IsPointSized() const +{ + return CollisionProp()->BoundingRadius() == 0.0f; +} + +inline void CBaseEntity::SetRenderMode( RenderMode_t nRenderMode ) +{ + m_nRenderMode = nRenderMode; +} + +inline RenderMode_t CBaseEntity::GetRenderMode() const +{ + return (RenderMode_t)m_nRenderMode.Get(); +} + + +//----------------------------------------------------------------------------- +// Methods to cast away const +//----------------------------------------------------------------------------- +inline Vector CBaseEntity::EyePosition( void ) const +{ + return const_cast(this)->EyePosition(); +} + +inline const QAngle &CBaseEntity::EyeAngles( void ) const // Direction of eyes in world space +{ + return const_cast(this)->EyeAngles(); +} + +inline const QAngle &CBaseEntity::LocalEyeAngles( void ) const // Direction of eyes +{ + return const_cast(this)->LocalEyeAngles(); +} + +inline Vector CBaseEntity::EarPosition( void ) const // position of ears +{ + return const_cast(this)->EarPosition(); +} + + +//----------------------------------------------------------------------------- +// Methods relating to networking +//----------------------------------------------------------------------------- +inline void CBaseEntity::NetworkStateChanged() +{ + NetworkProp()->NetworkStateChanged(); +} + + +inline void CBaseEntity::NetworkStateChanged( void *pVar ) +{ + // Make sure it's a semi-reasonable pointer. + Assert( (char*)pVar > (char*)this ); + Assert( (char*)pVar - (char*)this < 32768 ); + + // Good, they passed an offset so we can track this variable's change + // and avoid sending the whole entity. + NetworkProp()->NetworkStateChanged( (char*)pVar - (char*)this ); +} + + +//----------------------------------------------------------------------------- +// IHandleEntity overrides. +//----------------------------------------------------------------------------- +inline const CBaseHandle& CBaseEntity::GetRefEHandle() const +{ + return m_RefEHandle; +} + +inline void CBaseEntity::IncrementTransmitStateOwnedCounter() +{ + Assert( m_nTransmitStateOwnedCounter != 255 ); + m_nTransmitStateOwnedCounter++; +} + +inline void CBaseEntity::DecrementTransmitStateOwnedCounter() +{ + Assert( m_nTransmitStateOwnedCounter != 0 ); + m_nTransmitStateOwnedCounter--; +} + + +//----------------------------------------------------------------------------- +// Bullet firing (legacy)... +//----------------------------------------------------------------------------- +inline void CBaseEntity::FireBullets( int cShots, const Vector &vecSrc, + const Vector &vecDirShooting, const Vector &vecSpread, float flDistance, + int iAmmoType, int iTracerFreq, int firingEntID, int attachmentID, + int iDamage, CBaseEntity *pAttacker, bool bFirstShotAccurate, bool bPrimaryAttack ) +{ + FireBulletsInfo_t info; + info.m_iShots = cShots; + info.m_vecSrc = vecSrc; + info.m_vecDirShooting = vecDirShooting; + info.m_vecSpread = vecSpread; + info.m_flDistance = flDistance; + info.m_iAmmoType = iAmmoType; + info.m_iTracerFreq = iTracerFreq; + info.m_flDamage = iDamage; + info.m_pAttacker = pAttacker; + info.m_nFlags = bFirstShotAccurate ? FIRE_BULLETS_FIRST_SHOT_ACCURATE : 0; + info.m_bPrimaryAttack = bPrimaryAttack; + + FireBullets( info ); +} + +//----------------------------------------------------------------------------- +// VScript +//----------------------------------------------------------------------------- +inline const char* CBaseEntity::ScriptGetModelName(void) const +{ + return STRING(m_ModelName); +} + +// Ugly technique to override base member functions +// Normally it's illegal to cast a pointer to a member function of a derived class to a pointer to a +// member function of a base class. static_cast is a sleezy way around that problem. + +#define SetThink( a ) ThinkSet( static_cast (a), 0, NULL ) +#define SetContextThink( a, b, context ) ThinkSet( static_cast (a), (b), context ) + +#ifdef _DEBUG +#define SetMoveDone( a ) \ + do \ + { \ + m_pfnMoveDone = static_cast (a); \ + FunctionCheck( (void *)*((int *)((char *)this + ( offsetof(CBaseEntity,m_pfnMoveDone)))), "BaseMoveFunc" ); \ + } while ( 0 ) +#else +#define SetMoveDone( a ) \ + (void)(m_pfnMoveDone = static_cast (a)) +#endif + + +inline bool FClassnameIs(CBaseEntity *pEntity, const char *szClassname) +{ + return pEntity->ClassMatches(szClassname); +} + +class CPointEntity : public CBaseEntity +{ +public: + DECLARE_CLASS( CPointEntity, CBaseEntity ); + + void Spawn( void ); + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + virtual bool KeyValue( const char *szKeyName, const char *szValue ); +private: +}; + +// Has a position + size +class CServerOnlyEntity : public CBaseEntity +{ + DECLARE_CLASS( CServerOnlyEntity, CBaseEntity ); +public: + CServerOnlyEntity() : CBaseEntity( true ) {} + + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); } +}; + +// Has only a position, no size +class CServerOnlyPointEntity : public CServerOnlyEntity +{ + DECLARE_CLASS( CServerOnlyPointEntity, CServerOnlyEntity ); + +public: + virtual bool KeyValue( const char *szKeyName, const char *szValue ); +}; + +// Has no position or size +class CLogicalEntity : public CServerOnlyEntity +{ + DECLARE_CLASS( CLogicalEntity, CServerOnlyEntity ); + +public: + virtual bool KeyValue( const char *szKeyName, const char *szValue ); +}; + + +// Network proxy functions + +void SendProxy_Origin( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_OriginXY( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_OriginZ( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); + + +#endif // BASEENTITY_H diff --git a/sp/src/game/server/baseflex.cpp b/sp/src/game/server/baseflex.cpp new file mode 100644 index 00000000..d908b15c --- /dev/null +++ b/sp/src/game/server/baseflex.cpp @@ -0,0 +1,2967 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "animation.h" +#include "baseflex.h" +#include "filesystem.h" +#include "studio.h" +#include "choreoevent.h" +#include "choreoscene.h" +#include "choreoactor.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "tier1/strtools.h" +#include "KeyValues.h" +#include "ai_basenpc.h" +#include "ai_navigator.h" +#include "ai_moveprobe.h" +#include "sceneentity.h" +#include "ai_baseactor.h" +#include "datacache/imdlcache.h" +#include "tier1/byteswap.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static ConVar scene_showlook( "scene_showlook", "0", FCVAR_ARCHIVE, "When playing back, show the directions of look events." ); +static ConVar scene_showmoveto( "scene_showmoveto", "0", FCVAR_ARCHIVE, "When moving, show the end location." ); +static ConVar scene_showunlock( "scene_showunlock", "0", FCVAR_ARCHIVE, "Show when a vcd is playing but normal AI is running." ); + +// static ConVar scene_checktagposition( "scene_checktagposition", "0", FCVAR_ARCHIVE, "When playing back a choreographed scene, check the current position of the tags relative to where they were authored." ); + +// Fake layer # to force HandleProcessSceneEvent to actually allocate the layer during npc think time instead of in between. +#define REQUEST_DEFERRED_LAYER_ALLOCATION -2 + +extern bool g_bClientFlex; + +// --------------------------------------------------------------------- +// +// CBaseFlex -- physically simulated brush rectangular solid +// +// --------------------------------------------------------------------- + +void* SendProxy_FlexWeights( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ) +{ + // Don't any flexweights to client unless scene_clientflex.GetBool() is false + if ( !g_bClientFlex ) + return (void*)pVarData; + else + return NULL; +} + +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_FlexWeights ); + +// SendTable stuff. +IMPLEMENT_SERVERCLASS_ST(CBaseFlex, DT_BaseFlex) +// Note we can't totally disabled flexweights transmission since some things like blink and eye tracking are still done by the server + SendPropArray3 (SENDINFO_ARRAY3(m_flexWeight), SendPropFloat(SENDINFO_ARRAY(m_flexWeight), 12, SPROP_ROUNDDOWN, 0.0f, 1.0f ) /*, SendProxy_FlexWeights*/ ), + SendPropInt (SENDINFO(m_blinktoggle), 1, SPROP_UNSIGNED ), + SendPropVector (SENDINFO(m_viewtarget), -1, SPROP_COORD), +#ifdef HL2_DLL + SendPropFloat ( SENDINFO_VECTORELEM(m_vecViewOffset, 0), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecViewOffset, 1), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecViewOffset, 2), 0, SPROP_NOSCALE ), + + SendPropVector ( SENDINFO(m_vecLean), -1, SPROP_COORD ), + SendPropVector ( SENDINFO(m_vecShift), -1, SPROP_COORD ), +#endif + +END_SEND_TABLE() + + +BEGIN_DATADESC( CBaseFlex ) + + // m_blinktoggle + DEFINE_ARRAY( m_flexWeight, FIELD_FLOAT, MAXSTUDIOFLEXCTRL ), + DEFINE_FIELD( m_viewtarget, FIELD_POSITION_VECTOR ), + // m_SceneEvents + // m_FileList + DEFINE_FIELD( m_flAllowResponsesEndTime, FIELD_TIME ), + // m_ActiveChoreoScenes + // DEFINE_FIELD( m_LocalToGlobal, CUtlRBTree < FS_LocalToGlobal_t , unsigned short > ), + // m_bUpdateLayerPriorities + DEFINE_FIELD( m_flLastFlexAnimationTime, FIELD_TIME ), + +#ifdef HL2_DLL + //DEFINE_FIELD( m_vecPrevOrigin, FIELD_POSITION_VECTOR ), + //DEFINE_FIELD( m_vecPrevVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_vecLean, FIELD_VECTOR ), + DEFINE_FIELD( m_vecShift, FIELD_VECTOR ), +#endif + +END_DATADESC() + +BEGIN_ENT_SCRIPTDESC( CBaseFlex, CBaseAnimating, "Animated characters who have vertex flex capability." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetOldestScene, "GetCurrentScene", "Returns the instance of the oldest active scene entity (if any)." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetSceneByIndex, "GetSceneByIndex", "Returns the instance of the scene entity at the specified index." ) +END_SCRIPTDESC(); + + +LINK_ENTITY_TO_CLASS( funCBaseFlex, CBaseFlex ); // meaningless independant class!! + +CBaseFlex::CBaseFlex( void ) : + m_LocalToGlobal( 0, 0, FlexSettingLessFunc ) +{ +#ifdef _DEBUG + // default constructor sets the viewtarget to NAN + m_viewtarget.Init(); +#endif + m_bUpdateLayerPriorities = true; + m_flLastFlexAnimationTime = 0.0; +} + +CBaseFlex::~CBaseFlex( void ) +{ + m_LocalToGlobal.RemoveAll(); + Assert( m_SceneEvents.Count() == 0 ); +} + +void CBaseFlex::SetModel( const char *szModelName ) +{ + MDLCACHE_CRITICAL_SECTION(); + + BaseClass::SetModel( szModelName ); + + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + SetFlexWeight( i, 0.0f ); + } +} + + +void CBaseFlex::SetViewtarget( const Vector &viewtarget ) +{ + m_viewtarget = viewtarget; // bah +} + +void CBaseFlex::SetFlexWeight( LocalFlexController_t index, float value ) +{ + if (index >= 0 && index < GetNumFlexControllers()) + { + CStudioHdr *pstudiohdr = GetModelPtr( ); + if (! pstudiohdr) + return; + + mstudioflexcontroller_t *pflexcontroller = pstudiohdr->pFlexcontroller( index ); + + if (pflexcontroller->max != pflexcontroller->min) + { + value = (value - pflexcontroller->min) / (pflexcontroller->max - pflexcontroller->min); + value = clamp( value, 0.0f, 1.0f ); + } + + m_flexWeight.Set( index, value ); + } +} + +float CBaseFlex::GetFlexWeight( LocalFlexController_t index ) +{ + if (index >= 0 && index < GetNumFlexControllers()) + { + CStudioHdr *pstudiohdr = GetModelPtr( ); + if (! pstudiohdr) + return 0; + + mstudioflexcontroller_t *pflexcontroller = pstudiohdr->pFlexcontroller( index ); + + if (pflexcontroller->max != pflexcontroller->min) + { + return m_flexWeight[index] * (pflexcontroller->max - pflexcontroller->min) + pflexcontroller->min; + } + + return m_flexWeight[index]; + } + return 0.0; +} + +LocalFlexController_t CBaseFlex::FindFlexController( const char *szName ) +{ + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + if (stricmp( GetFlexControllerName( i ), szName ) == 0) + { + return i; + } + } + + // AssertMsg( 0, UTIL_VarArgs( "flexcontroller %s couldn't be mapped!!!\n", szName ) ); + return LocalFlexController_t(0); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseFlex::StartChoreoScene( CChoreoScene *scene ) +{ + if ( m_ActiveChoreoScenes.Find( scene ) != m_ActiveChoreoScenes.InvalidIndex() ) + { + return; + } + + m_ActiveChoreoScenes.AddToTail( scene ); + m_bUpdateLayerPriorities = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseFlex::RemoveChoreoScene( CChoreoScene *scene, bool canceled ) +{ + // Assert( m_ActiveChoreoScenes.Find( scene ) != m_ActiveChoreoScenes.InvalidIndex() ); + + m_ActiveChoreoScenes.FindAndRemove( scene ); + m_bUpdateLayerPriorities = true; + + if (canceled) + { + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if ( myNpc ) + { + myNpc->ClearSceneLock( ); + } + } +} + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- + +int CBaseFlex::GetScenePriority( CChoreoScene *scene ) +{ + int iPriority = 0; + int c = m_ActiveChoreoScenes.Count(); + // count number of channels in scenes older than current + for ( int i = 0; i < c; i++ ) + { + CChoreoScene *pScene = m_ActiveChoreoScenes[ i ]; + if ( !pScene ) + { + continue; + } + + if ( pScene == scene ) + { + break; + } + + iPriority += pScene->GetNumChannels( ); + } + return iPriority; +} + + + + +//----------------------------------------------------------------------------- +// Purpose: Remove all active SceneEvents +//----------------------------------------------------------------------------- +void CBaseFlex::ClearSceneEvents( CChoreoScene *scene, bool canceled ) +{ + if ( !scene ) + { + m_SceneEvents.RemoveAll(); + return; + } + + for ( int i = m_SceneEvents.Count() - 1; i >= 0; i-- ) + { + CSceneEventInfo *info = &m_SceneEvents[ i ]; + + Assert( info ); + Assert( info->m_pScene ); + Assert( info->m_pEvent ); + + if ( info->m_pScene != scene ) + continue; + + if ( !ClearSceneEvent( info, false, canceled )) + { + // unknown expression to clear!! + Assert( 0 ); + } + + // Free this slot + info->m_pEvent = NULL; + info->m_pScene = NULL; + info->m_bStarted = false; + + m_SceneEvents.Remove( i ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Stop specifics of expression +//----------------------------------------------------------------------------- + +bool CBaseFlex::ClearSceneEvent( CSceneEventInfo *info, bool fastKill, bool canceled ) +{ + Assert( info ); + Assert( info->m_pScene ); + Assert( info->m_pEvent ); + + // FIXME: this code looks duplicated + switch ( info->m_pEvent->GetType() ) + { + case CChoreoEvent::GESTURE: + case CChoreoEvent::SEQUENCE: + { + if (info->m_iLayer >= 0) + { + if ( fastKill ) + { + FastRemoveLayer( info->m_iLayer ); + } + else if (info->m_pEvent->GetType() == CChoreoEvent::GESTURE) + { + if (canceled) + { + // remove slower if interrupted + RemoveLayer( info->m_iLayer, 0.5 ); + } + else + { + RemoveLayer( info->m_iLayer, 0.1 ); + } + } + else + { + RemoveLayer( info->m_iLayer, 0.3 ); + } + } + } + return true; + + case CChoreoEvent::MOVETO: + { + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if (!myNpc) + return true; + + // cancel moveto if it's distance based, of if the event was part of a canceled vcd + if (IsMoving() && (canceled || info->m_pEvent->GetDistanceToTarget() > 0.0)) + { + if (!info->m_bHasArrived) + { + if (info->m_pScene) + { + Scene_Printf( "%s : %8.2f: MOVETO canceled but actor %s not at goal\n", info->m_pScene->GetFilename(), info->m_pScene->GetTime(), info->m_pEvent->GetActor()->GetName() ); + } + } + myNpc->GetNavigator()->StopMoving( false ); // Stop moving + } + } + return true; + case CChoreoEvent::FACE: + case CChoreoEvent::FLEXANIMATION: + case CChoreoEvent::EXPRESSION: + case CChoreoEvent::LOOKAT: + case CChoreoEvent::GENERIC: + { + // no special rules + } + return true; + case CChoreoEvent::SPEAK: + { + // Tracker 15420: Issue stopsound if we need to cut this short... + if ( canceled ) + { + StopSound( info->m_pEvent->GetParameters() ); + +#ifdef HL2_EPISODIC + // If we were holding the semaphore because of this speech, release it + CAI_BaseActor *pBaseActor = dynamic_cast(this); + if ( pBaseActor ) + { + pBaseActor->GetExpresser()->ForceNotSpeaking(); + } +#endif + } + } + return true; + } + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Add string indexed scene/expression/duration to list of active SceneEvents +// Input : scenefile - +// expression - +// duration - +//----------------------------------------------------------------------------- +#ifdef MAPBASE +void CBaseFlex::AddSceneEvent( CChoreoScene *scene, CChoreoEvent *event, CBaseEntity *pTarget, CSceneEntity *pSceneEnt ) +#else +void CBaseFlex::AddSceneEvent( CChoreoScene *scene, CChoreoEvent *event, CBaseEntity *pTarget ) +#endif +{ + if ( !scene || !event ) + { + Msg( "CBaseFlex::AddSceneEvent: scene or event was NULL!!!\n" ); + return; + } + + CChoreoActor *actor = event->GetActor(); + if ( !actor ) + { + Msg( "CBaseFlex::AddSceneEvent: event->GetActor() was NULL!!!\n" ); + return; + } + + + CSceneEventInfo info; + + memset( (void *)&info, 0, sizeof( info ) ); + + info.m_pEvent = event; + info.m_pScene = scene; + info.m_hTarget = pTarget; + info.m_bStarted = false; + info.m_hSceneEntity = pSceneEnt; + +#ifdef MAPBASE + if (StartSceneEvent( &info, scene, event, actor, pTarget, pSceneEnt )) +#else + if (StartSceneEvent( &info, scene, event, actor, pTarget )) +#endif + { + m_SceneEvents.AddToTail( info ); + } + else + { + Scene_Printf( "CBaseFlex::AddSceneEvent: event failed\n" ); + // Assert( 0 ); // expression failed to start + } +} + + +//----------------------------------------------------------------------------- +// Starting various expression types +//----------------------------------------------------------------------------- + +bool CBaseFlex::RequestStartSequenceSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ) +{ + info->m_nSequence = LookupSequence( event->GetParameters() ); + + // make sure sequence exists + if (info->m_nSequence < 0) + { + Warning( "CSceneEntity %s :\"%s\" unable to find sequence \"%s\"\n", STRING(GetEntityName()), actor->GetName(), event->GetParameters() ); + return false; + } + + // This is a bit of a hack, but we need to defer the actual allocation until Process which will sync the layer allocation + // to the NPCs think/m_flAnimTime instead of some arbitrary tick + info->m_iLayer = REQUEST_DEFERRED_LAYER_ALLOCATION; + info->m_pActor = actor; + return true; +} + +bool CBaseFlex::RequestStartGestureSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ) +{ + info->m_nSequence = LookupSequence( event->GetParameters() ); + + // make sure sequence exists + if (info->m_nSequence < 0) + { + Warning( "CSceneEntity %s :\"%s\" unable to find gesture \"%s\"\n", STRING(GetEntityName()), actor->GetName(), event->GetParameters() ); + return false; + } + + // This is a bit of a hack, but we need to defer the actual allocation until Process which will sync the layer allocation + // to the NPCs think/m_flAnimTime instead of some arbitrary tick + info->m_iLayer = REQUEST_DEFERRED_LAYER_ALLOCATION; + info->m_pActor = actor; + return true; +} + +bool CBaseFlex::HandleStartSequenceSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor ) +{ + Assert( info->m_iLayer == REQUEST_DEFERRED_LAYER_ALLOCATION ); + + info->m_nSequence = LookupSequence( event->GetParameters() ); + info->m_iLayer = -1; + + if (info->m_nSequence < 0) + { + Warning( "CSceneEntity %s :\"%s\" unable to find sequence \"%s\"\n", STRING(GetEntityName()), actor->GetName(), event->GetParameters() ); + return false; + } + + if (!EnterSceneSequence( scene, event )) + { + if (!event->GetPlayOverScript()) + { + // this has failed to start + Warning( "CSceneEntity %s :\"%s\" failed to start sequence \"%s\"\n", STRING(GetEntityName()), actor->GetName(), event->GetParameters() ); + return false; + } + // Start anyways, just use normal no-movement, must be in IDLE rules + } + + info->m_iPriority = actor->FindChannelIndex( event->GetChannel() ); + info->m_iLayer = AddLayeredSequence( info->m_nSequence, info->m_iPriority + GetScenePriority( scene ) ); + SetLayerNoRestore( info->m_iLayer, true ); + SetLayerWeight( info->m_iLayer, 0.0 ); + + bool looping = ((GetSequenceFlags( GetModelPtr(), info->m_nSequence ) & STUDIO_LOOPING) != 0); + if (!looping) + { + // figure out the animtime when this was frame 0 + float dt = scene->GetTime() - event->GetStartTime(); + float seq_duration = SequenceDuration( info->m_nSequence ); + float flCycle = dt / seq_duration; + flCycle = flCycle - (int)flCycle; // loop + SetLayerCycle( info->m_iLayer, flCycle, flCycle ); + + SetLayerPlaybackRate( info->m_iLayer, 0.0 ); + } + else + { + SetLayerPlaybackRate( info->m_iLayer, 1.0 ); + } + + if (IsMoving()) + { + info->m_flWeight = 0.0; + } + else + { + info->m_flWeight = 1.0; + } + + return true; +} + +bool CBaseFlex::HandleStartGestureSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor ) +{ + Assert( info->m_iLayer == REQUEST_DEFERRED_LAYER_ALLOCATION ); + + info->m_nSequence = LookupSequence( event->GetParameters() ); + info->m_iLayer = -1; + + if (info->m_nSequence < 0) + { + Warning( "CSceneEntity %s :\"%s\" unable to find gesture \"%s\"\n", STRING(GetEntityName()), actor->GetName(), event->GetParameters() ); + return false; + } + + // FIXME: this seems like way too much code + info->m_bIsGesture = false; + KeyValues *seqKeyValues = GetSequenceKeyValues( info->m_nSequence ); + if (seqKeyValues) + { + // Do we have a build point section? + KeyValues *pkvAllFaceposer = seqKeyValues->FindKey("faceposer"); + if ( pkvAllFaceposer ) + { + KeyValues *pkvType = pkvAllFaceposer->FindKey("type"); + + if (pkvType) + { + info->m_bIsGesture = (stricmp( pkvType->GetString(), "gesture" ) == 0) ? true : false; + } + } + + // FIXME: fixup tags that should be set as "linear", should be done in faceposer + char szStartLoop[CEventAbsoluteTag::MAX_EVENTTAG_LENGTH] = { "loop" }; + char szEndLoop[CEventAbsoluteTag::MAX_EVENTTAG_LENGTH] = { "end" }; + + // check in the tag indexes + KeyValues *pkvFaceposer; + for ( pkvFaceposer = pkvAllFaceposer->GetFirstSubKey(); pkvFaceposer; pkvFaceposer = pkvFaceposer->GetNextKey() ) + { + if (!stricmp( pkvFaceposer->GetName(), "startloop" )) + { + V_strcpy_safe( szStartLoop, pkvFaceposer->GetString() ); + } + else if (!stricmp( pkvFaceposer->GetName(), "endloop" )) + { + V_strcpy_safe( szEndLoop, pkvFaceposer->GetString() ); + } + } + + CEventAbsoluteTag *ptag; + ptag = event->FindAbsoluteTag( CChoreoEvent::ORIGINAL, szStartLoop ); + if (ptag) + { + ptag->SetLinear( true ); + } + ptag = event->FindAbsoluteTag( CChoreoEvent::PLAYBACK, szStartLoop ); + if (ptag) + { + ptag->SetLinear( true ); + } + ptag = event->FindAbsoluteTag( CChoreoEvent::ORIGINAL, szEndLoop ); + if (ptag) + { + ptag->SetLinear( true ); + } + ptag = event->FindAbsoluteTag( CChoreoEvent::PLAYBACK, szEndLoop ); + if (ptag) + { + ptag->SetLinear( true ); + } + + if ( pkvAllFaceposer ) + { + CStudioHdr *pstudiohdr = GetModelPtr(); + + mstudioseqdesc_t &seqdesc = pstudiohdr->pSeqdesc( info->m_nSequence ); + mstudioanimdesc_t &animdesc = pstudiohdr->pAnimdesc( pstudiohdr->iRelativeAnim( info->m_nSequence, seqdesc.anim(0,0) ) ); + + // check in the tag indexes + KeyValues *pkvFaceposer; + for ( pkvFaceposer = pkvAllFaceposer->GetFirstSubKey(); pkvFaceposer; pkvFaceposer = pkvFaceposer->GetNextKey() ) + { + if (!stricmp( pkvFaceposer->GetName(), "tags" )) + { + KeyValues *pkvTags; + for ( pkvTags = pkvFaceposer->GetFirstSubKey(); pkvTags; pkvTags = pkvTags->GetNextKey() ) + { + int maxFrame = animdesc.numframes - 2; // FIXME: this is off by one! + + if ( maxFrame > 0) + { + float percentage = (float)pkvTags->GetInt() / maxFrame; + + CEventAbsoluteTag *ptag = event->FindAbsoluteTag( CChoreoEvent::ORIGINAL, pkvTags->GetName() ); + if (ptag) + { + if (fabs(ptag->GetPercentage() - percentage) > 0.05) + { + DevWarning("%s repositioned tag: %s : %.3f -> %.3f (%s:%s:%s)\n", scene->GetFilename(), pkvTags->GetName(), ptag->GetPercentage(), percentage, scene->GetFilename(), actor->GetName(), event->GetParameters() ); + // reposition tag + ptag->SetPercentage( percentage ); + } + } + } + } + } + } + + if (!event->VerifyTagOrder()) + { + DevWarning("out of order tags : %s : (%s:%s:%s)\n", scene->GetFilename(), actor->GetName(), event->GetName(), event->GetParameters() ); + } + } + + seqKeyValues->deleteThis(); + } + + // initialize posture suppression + // FIXME: move priority of base animation so that layers can be inserted before + // FIXME: query stopping, post idle layer to figure out correct weight + // GetIdleLayerWeight()? + if (!info->m_bIsGesture && IsMoving()) + { + info->m_flWeight = 0.0; + } + else + { + info->m_flWeight = 1.0; + } + + // this happens before StudioFrameAdvance() + info->m_iPriority = actor->FindChannelIndex( event->GetChannel() ); + info->m_iLayer = AddLayeredSequence( info->m_nSequence, info->m_iPriority + GetScenePriority( scene ) ); + SetLayerNoRestore( info->m_iLayer, true ); + SetLayerDuration( info->m_iLayer, event->GetDuration() ); + SetLayerWeight( info->m_iLayer, 0.0 ); + + bool looping = ((GetSequenceFlags( GetModelPtr(), info->m_nSequence ) & STUDIO_LOOPING) != 0); + if ( looping ) + { + DevMsg( 1, "vcd error, gesture %s of model %s is marked as STUDIO_LOOPING!\n", + event->GetParameters(), STRING(GetModelName()) ); + } + + SetLayerLooping( info->m_iLayer, false ); // force to not loop + + float duration = event->GetDuration( ); + + // figure out the animtime when this was frame 0 + float flEventCycle = (scene->GetTime() - event->GetStartTime()) / duration; + float flCycle = event->GetOriginalPercentageFromPlaybackPercentage( flEventCycle ); + SetLayerCycle( info->m_iLayer, flCycle, 0.0 ); + SetLayerPlaybackRate( info->m_iLayer, 0.0 ); + + return true; +} + +bool CBaseFlex::StartFacingSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ) +{ + if ( pTarget ) + { + // Don't allow FACE commands while sitting in the vehicle + CAI_BaseNPC *myNpc = MyNPCPointer(); + if ( myNpc && myNpc->IsInAVehicle() ) + return false; + + info->m_bIsMoving = false; + return true; + } + return false; +} + + +bool CBaseFlex::StartMoveToSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ) +{ + if (pTarget) + { + info->m_bIsMoving = false; + info->m_bHasArrived = false; + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if (!myNpc) + { + return false; + } + + EnterSceneSequence( scene, event, true ); + + // If they're already moving, stop them + // + // Don't stop them during restore because that will set a stopping path very + // nearby, causing us to signal arrival prematurely in CheckSceneEventCompletion. + // BEWARE: the behavior of this bug depended on the order in which the entities were restored!! + if ( myNpc->IsMoving() && !scene->IsRestoring() ) + { + myNpc->GetNavigator()->StopMoving( false ); + } + + return true; + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +#ifdef MAPBASE +bool CBaseFlex::StartSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget, CSceneEntity *pSceneEnt ) +#else +bool CBaseFlex::StartSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ) +#endif +{ + switch ( event->GetType() ) + { + case CChoreoEvent::SEQUENCE: + return RequestStartSequenceSceneEvent( info, scene, event, actor, pTarget ); + + case CChoreoEvent::GESTURE: + return RequestStartGestureSceneEvent( info, scene, event, actor, pTarget ); + + case CChoreoEvent::FACE: + return StartFacingSceneEvent( info, scene, event, actor, pTarget ); + + // FIXME: move this to an CBaseActor + case CChoreoEvent::MOVETO: + return StartMoveToSceneEvent( info, scene, event, actor, pTarget ); + + case CChoreoEvent::LOOKAT: + info->m_hTarget = pTarget; + return true; + + case CChoreoEvent::FLEXANIMATION: + info->InitWeight( this ); + return true; + + case CChoreoEvent::SPEAK: + return true; + + case CChoreoEvent::EXPRESSION: // These are handled client-side + return true; + +#ifdef MAPBASE + case CChoreoEvent::GENERIC: + { + // This is handled in CBaseFlex so that any flex entity--including players--could use this text. + if (stricmp(event->GetParameters(), "AI_GAMETEXT") == 0) + { + // game_text-based lines, for placeholders + if ( event->GetParameters2() ) + { + info->m_nType = 12; // SCENE_AI_GAMETEXT + + hudtextparms_t textParams; + textParams.holdTime = event->GetDuration(); + textParams.fadeinTime = 0.5f; + textParams.fadeoutTime = 0.5f; + + if ( GetGameTextSpeechParams( textParams ) ) + { + CRecipientFilter filter; + filter.AddAllPlayers(); + filter.MakeReliable(); + + UserMessageBegin( filter, "HudMsg" ); + WRITE_BYTE ( textParams.channel & 0xFF ); + WRITE_FLOAT( textParams.x ); + WRITE_FLOAT( textParams.y ); + WRITE_BYTE ( textParams.r1 ); + WRITE_BYTE ( textParams.g1 ); + WRITE_BYTE ( textParams.b1 ); + WRITE_BYTE ( textParams.a1 ); + WRITE_BYTE ( textParams.r2 ); + WRITE_BYTE ( textParams.g2 ); + WRITE_BYTE ( textParams.b2 ); + WRITE_BYTE ( textParams.a2 ); + WRITE_BYTE ( textParams.effect ); + WRITE_FLOAT( textParams.fadeinTime ); + WRITE_FLOAT( textParams.fadeoutTime ); + WRITE_FLOAT( textParams.holdTime ); + WRITE_FLOAT( textParams.fxTime ); + WRITE_STRING( event->GetParameters2() ); + WRITE_STRING( "" ); // No custom font + WRITE_BYTE ( Q_strlen( event->GetParameters2() ) ); + MessageEnd(); + } + return true; + } + } + + return false; + } +#endif + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Remove expression +// Input : scenefile - +// expression - +//----------------------------------------------------------------------------- +void CBaseFlex::RemoveSceneEvent( CChoreoScene *scene, CChoreoEvent *event, bool fastKill ) +{ + Assert( event ); + + for ( int i = 0 ; i < m_SceneEvents.Count(); i++ ) + { + CSceneEventInfo *info = &m_SceneEvents[ i ]; + + Assert( info ); + Assert( info->m_pEvent ); + + if ( info->m_pScene != scene ) + continue; + + if ( info->m_pEvent != event) + continue; + + if (ClearSceneEvent( info, fastKill, false )) + { + // Free this slot + info->m_pEvent = NULL; + info->m_pScene = NULL; + info->m_bStarted = false; + + m_SceneEvents.Remove( i ); + return; + } + } + + // many events refuse to start due to bogus parameters +} + +//----------------------------------------------------------------------------- +// Purpose: Checks to see if the event should be considered "completed" +//----------------------------------------------------------------------------- +bool CBaseFlex::CheckSceneEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) +{ + for ( int i = 0 ; i < m_SceneEvents.Count(); i++ ) + { + CSceneEventInfo *info = &m_SceneEvents[ i ]; + + Assert( info ); + Assert( info->m_pEvent ); + + if ( info->m_pScene != scene ) + continue; + + if ( info->m_pEvent != event) + continue; + + return CheckSceneEventCompletion( info, currenttime, scene, event ); + } + return true; +} + + + +bool CBaseFlex::CheckSceneEventCompletion( CSceneEventInfo *info, float currenttime, CChoreoScene *scene, CChoreoEvent *event ) +{ + switch ( event->GetType() ) + { + case CChoreoEvent::MOVETO: + { + CAI_BaseNPC *npc = MyNPCPointer( ); + + if (npc) + { + // check movement, check arrival + if (npc->GetNavigator()->IsGoalActive()) + { + const Task_t *pCurTask = npc->GetTask(); + if ( pCurTask && (pCurTask->iTask == TASK_PLAY_SCENE || pCurTask->iTask == TASK_WAIT_FOR_MOVEMENT ) ) + { + float preload = event->GetEndTime() - currenttime; + if (preload < 0) + { + //Msg("%.1f: no preload\n", currenttime ); + return false; + } + float t = npc->GetTimeToNavGoal(); + + // Msg("%.1f: preload (%s:%.1f) %.1f %.1f\n", currenttime, event->GetName(), event->GetEndTime(), preload, t ); + + // FIXME: t is zero if no path can be built! + + if (t > 0.0f && t <= preload) + { + return true; + } + return false; + } + } + else if (info->m_bHasArrived) + { + return true; + } + else if (info->m_bStarted && !npc->IsCurSchedule( SCHED_SCENE_GENERIC )) + { + // FIXME: There's still a hole in the logic is the save happens immediately after the SS steals the npc but before their AI has run again + Warning( "%s : %8.2f: waiting for actor %s to complete MOVETO but actor not in SCHED_SCENE_GENERIC\n", scene->GetFilename(), scene->GetTime(), event->GetActor()->GetName() ); + // no longer in a scene :P + return true; + } + // still trying + return false; + } + } + break; + default: + break; + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Default implementation +//----------------------------------------------------------------------------- +void CBaseFlex::ProcessSceneEvents( void ) +{ + VPROF( "CBaseFlex::ProcessSceneEvents" ); + // slowly decay to netural expression + for ( LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + SetFlexWeight( i, GetFlexWeight( i ) * 0.95 ); + } + + bool bHasForegroundEvents = false; + // Iterate SceneEvents and look for active slots + for ( int i = 0; i < m_SceneEvents.Count(); i++ ) + { + CSceneEventInfo *info = &m_SceneEvents[ i ]; + Assert( info ); + + // FIXME: Need a safe handle to m_pEvent in case of memory deletion? + CChoreoEvent *event = info->m_pEvent; + Assert( event ); + + CChoreoScene *scene = info->m_pScene; + Assert( scene ); + + if ( scene && !scene->IsBackground() ) + { + bHasForegroundEvents = true; + } + + if (ProcessSceneEvent( info, scene, event )) + { + info->m_bStarted = true; + } + } + + if ( bHasForegroundEvents && scene_showunlock.GetBool()) + { + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if ( myNpc && !(myNpc->GetState() == NPC_STATE_SCRIPT || myNpc->IsCurSchedule( SCHED_SCENE_GENERIC )) ) + { + Vector p0 = myNpc->GetHullMins(); + Vector p1 = myNpc->GetHullMaxs(); + p0.z = p1.z + 2; + p1.z = p1.z + 2; + NDebugOverlay::Box( myNpc->GetAbsOrigin(), p0, p1, 255, 0, 0, 0, 0.12 ); + } + } + + + // any needed layer priorites have now been reset + m_bUpdateLayerPriorities = false; +} + +class CFlexSceneFileManager : CAutoGameSystem +{ +public: + + CFlexSceneFileManager( char const *name ) : CAutoGameSystem( name ) + { + } + + virtual bool Init() + { + // Trakcer 16692: Preload these at startup to avoid hitch first time we try to load them during actual gameplay + FindSceneFile( NULL, "phonemes", true ); + FindSceneFile( NULL, "phonemes_weak", true ); + FindSceneFile( NULL, "phonemes_strong", true ); +#if defined( HL2_DLL ) + FindSceneFile( NULL, "random", true ); + FindSceneFile( NULL, "randomAlert", true ); +#endif + return true; + } + + // Tracker 14992: We used to load 18K of .vfes for every CBaseFlex who lipsynced, but now we only load those files once globally. + // Note, we could wipe these between levels, but they don't ever load more than the weak/normal/strong phoneme classes that I can tell + // so I'll just leave them loaded forever for now + virtual void Shutdown() + { + DeleteSceneFiles(); + } + + //----------------------------------------------------------------------------- + // Purpose: Sets up translations + // Input : *instance - + // *pSettinghdr - + // Output : void + //----------------------------------------------------------------------------- + void EnsureTranslations( CBaseFlex *instance, const flexsettinghdr_t *pSettinghdr ) + { + // The only time instance is NULL is in Init() above, where we're just loading the .vfe files off of the hard disk. + if ( instance ) + { + instance->EnsureTranslations( pSettinghdr ); + } + } + + const void *FindSceneFile( CBaseFlex *instance, const char *filename, bool allowBlockingIO ) + { + // See if it's already loaded + int i; + for ( i = 0; i < m_FileList.Size(); i++ ) + { + CFlexSceneFile *file = m_FileList[ i ]; + if ( file && !stricmp( file->filename, filename ) ) + { + // Make sure translations (local to global flex controller) are set up for this instance + EnsureTranslations( instance, ( const flexsettinghdr_t * )file->buffer ); + return file->buffer; + } + } + + if ( !allowBlockingIO ) + { + return NULL; + } + + // Load file into memory + void *buffer = NULL; + int len = filesystem->ReadFileEx( UTIL_VarArgs( "expressions/%s.vfe", filename ), "GAME", &buffer, false, true ); + + if ( !len ) + return NULL; + + // Create scene entry + CFlexSceneFile *pfile = new CFlexSceneFile; + // Remember filename + Q_strncpy( pfile->filename, filename, sizeof( pfile->filename ) ); + // Remember data pointer + pfile->buffer = buffer; + // Add to list + m_FileList.AddToTail( pfile ); + + // Swap the entire file + if ( IsX360() ) + { + CByteswap swap; + swap.ActivateByteSwapping( true ); + byte *pData = (byte*)buffer; + flexsettinghdr_t *pHdr = (flexsettinghdr_t*)pData; + swap.SwapFieldsToTargetEndian( pHdr ); + + // Flex Settings + flexsetting_t *pFlexSetting = (flexsetting_t*)((byte*)pHdr + pHdr->flexsettingindex); + for ( int i = 0; i < pHdr->numflexsettings; ++i, ++pFlexSetting ) + { + swap.SwapFieldsToTargetEndian( pFlexSetting ); + + flexweight_t *pWeight = (flexweight_t*)(((byte*)pFlexSetting) + pFlexSetting->settingindex ); + for ( int j = 0; j < pFlexSetting->numsettings; ++j, ++pWeight ) + { + swap.SwapFieldsToTargetEndian( pWeight ); + } + } + + // indexes + pData = (byte*)pHdr + pHdr->indexindex; + swap.SwapBufferToTargetEndian( (int*)pData, (int*)pData, pHdr->numindexes ); + + // keymappings + pData = (byte*)pHdr + pHdr->keymappingindex; + swap.SwapBufferToTargetEndian( (int*)pData, (int*)pData, pHdr->numkeys ); + + // keyname indices + pData = (byte*)pHdr + pHdr->keynameindex; + swap.SwapBufferToTargetEndian( (int*)pData, (int*)pData, pHdr->numkeys ); + } + + // Fill in translation table + EnsureTranslations( instance, ( const flexsettinghdr_t * )pfile->buffer ); + + // Return data + return pfile->buffer; + } + +private: + + void DeleteSceneFiles() + { + while ( m_FileList.Size() > 0 ) + { + CFlexSceneFile *file = m_FileList[ 0 ]; + m_FileList.Remove( 0 ); + filesystem->FreeOptimalReadBuffer( file->buffer ); + delete file; + } + } + + CUtlVector< CFlexSceneFile * > m_FileList; +}; + +// Singleton manager +CFlexSceneFileManager g_FlexSceneFileManager( "CFlexSceneFileManager" ); + +//----------------------------------------------------------------------------- +// Purpose: Each CBaseFlex maintains a UtlRBTree of mappings, one for each loaded flex scene file it uses. This is used to +// sort the entries in the RBTree +// Input : lhs - +// rhs - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseFlex::FlexSettingLessFunc( const FS_LocalToGlobal_t& lhs, const FS_LocalToGlobal_t& rhs ) +{ + return lhs.m_Key < rhs.m_Key; +} + +//----------------------------------------------------------------------------- +// Purpose: Since everyone shared a pSettinghdr now, we need to set up the localtoglobal mapping per entity, but +// we just do this in memory with an array of integers (could be shorts, I suppose) +// Input : *pSettinghdr - +//----------------------------------------------------------------------------- +void CBaseFlex::EnsureTranslations( const flexsettinghdr_t *pSettinghdr ) +{ + Assert( pSettinghdr ); + + FS_LocalToGlobal_t entry( pSettinghdr ); + + unsigned short idx = m_LocalToGlobal.Find( entry ); + if ( idx != m_LocalToGlobal.InvalidIndex() ) + return; + + entry.SetCount( pSettinghdr->numkeys ); + + for ( int i = 0; i < pSettinghdr->numkeys; ++i ) + { + entry.m_Mapping[ i ] = FindFlexController( pSettinghdr->pLocalName( i ) ); + } + + m_LocalToGlobal.Insert( entry ); +} + +//----------------------------------------------------------------------------- +// Purpose: Look up instance specific mapping +// Input : *pSettinghdr - +// key - +// Output : int +//----------------------------------------------------------------------------- +LocalFlexController_t CBaseFlex::FlexControllerLocalToGlobal( const flexsettinghdr_t *pSettinghdr, int key ) +{ + FS_LocalToGlobal_t entry( pSettinghdr ); + + int idx = m_LocalToGlobal.Find( entry ); + if ( idx == m_LocalToGlobal.InvalidIndex() ) + { + // This should never happen!!! + Assert( 0 ); + Warning( "Unable to find mapping for flexcontroller %i, settings %p on %i/%s\n", key, pSettinghdr, entindex(), GetClassname() ); + EnsureTranslations( pSettinghdr ); + idx = m_LocalToGlobal.Find( entry ); + if ( idx == m_LocalToGlobal.InvalidIndex() ) + { + Error( "CBaseFlex::FlexControllerLocalToGlobal failed!\n" ); + } + } + + FS_LocalToGlobal_t& result = m_LocalToGlobal[ idx ]; + // Validate lookup + Assert( result.m_nCount != 0 && key < result.m_nCount ); + LocalFlexController_t index = result.m_Mapping[ key ]; + return index; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *filename - +//----------------------------------------------------------------------------- +const void *CBaseFlex::FindSceneFile( const char *filename ) +{ + // Ask manager to get the globally cached scene instead. + return g_FlexSceneFileManager.FindSceneFile( this, filename, false ); +} + +ConVar ai_expression_optimization( "ai_expression_optimization", "0", FCVAR_NONE, "Disable npc background expressions when you can't see them." ); +ConVar ai_expression_frametime( "ai_expression_frametime", "0.05", FCVAR_NONE, "Maximum frametime to still play background expressions." ); + +//----------------------------------------------------------------------------- +// Various methods to process facial SceneEvents: +//----------------------------------------------------------------------------- +bool CBaseFlex::ProcessFlexAnimationSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + VPROF( "CBaseFlex::ProcessFlexAnimationSceneEvent" ); + + if ( event->HasEndTime() ) + { + // don't bother with flex animation if the player can't see you + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if (myNpc) + { + if (!myNpc->HasCondition( COND_IN_PVS )) + return true; + + if (ai_expression_optimization.GetBool()) + { + if (scene->IsBackground()) + { + // if framerate too slow, disable + if (gpGlobals->frametime > ai_expression_frametime.GetFloat()) + { + info->m_bHasArrived = true; + info->m_flNext = gpGlobals->curtime + RandomFloat( 0.7, 1.2 ); + } + // only check occasionally + else if (info->m_flNext <= gpGlobals->curtime) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + // if not in view, disable + info->m_bHasArrived = (pPlayer && !pPlayer->FInViewCone( this ) ); + info->m_flNext = gpGlobals->curtime + RandomFloat( 0.7, 1.2 ); + } + + if (info->m_bHasArrived) + { + // NDebugOverlay::Box( myNpc->GetAbsOrigin(), myNpc->GetHullMins(), myNpc->GetHullMaxs(), 255, 0, 0, 0, 0.22 ); + return true; + } + // NDebugOverlay::Box( myNpc->GetAbsOrigin(), myNpc->GetHullMins(), myNpc->GetHullMaxs(), 0, 255, 0, 0, 0.22 ); + } + } + } + + AddFlexAnimation( info ); + } + return true; +} + +bool CBaseFlex::ProcessFlexSettingSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + // Flexanimations have to have an end time!!! + if ( !event->HasEndTime() ) + return true; + + VPROF( "CBaseFlex::ProcessFlexSettingSceneEvent" ); + + // Look up the actual strings + const char *scenefile = event->GetParameters(); + const char *name = event->GetParameters2(); + + // Have to find both strings + if ( scenefile && name ) + { + // Find the scene file + const flexsettinghdr_t *pExpHdr = ( const flexsettinghdr_t * )FindSceneFile( scenefile ); + if ( pExpHdr ) + { + float scenetime = scene->GetTime(); + + float scale = event->GetIntensity( scenetime ); + + // Add the named expression + AddFlexSetting( name, scale, pExpHdr, !info->m_bStarted ); + } + } + + return true; +} + +bool CBaseFlex::ProcessFacingSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + // make sure target exists + if (info->m_hTarget == NULL) + return false; + + VPROF( "CBaseFlex::ProcessFacingSceneEvent" ); + + // make sure we're still able to play this command + if (!EnterSceneSequence( scene, event, true )) + { + return false; + } + + if (!info->m_bStarted) + { + info->m_flInitialYaw = GetLocalAngles().y; + } + + // lock in place if aiming at self + if (info->m_hTarget == this) + { + return true; + } + + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if (myNpc) + { + if (info->m_bIsMoving != IsMoving()) + { + info->m_flInitialYaw = GetLocalAngles().y; + } + info->m_bIsMoving = IsMoving(); + + // Msg("%f : %f - %f\n", scene->GetTime(), event->GetStartTime(), event->GetEndTime() ); + // FIXME: why are the splines ill behaved at the end? + float intensity = event->GetIntensity( scene->GetTime() ); + if (info->m_bIsMoving) + { + myNpc->AddFacingTarget( info->m_hTarget, intensity, 0.2 ); + } + else + { + float goalYaw = myNpc->CalcIdealYaw( info->m_hTarget->EyePosition() ); + + float diff = UTIL_AngleDiff( goalYaw, info->m_flInitialYaw ); + + float idealYaw = UTIL_AngleMod( info->m_flInitialYaw + diff * intensity ); + + // Msg("yaw %.1f : %.1f (%.1f)\n", info->m_flInitialYaw, idealYaw, intensity ); + + myNpc->GetMotor()->SetIdealYawAndUpdate( idealYaw ); + } + + return true; + } + return false; +} + +static Activity DetermineExpressionMoveActivity( CChoreoEvent *event, CAI_BaseNPC *pNPC ) +{ + Activity activity = ACT_WALK; + const char *sParam2 = event->GetParameters2(); + if ( !sParam2 || !sParam2[0] ) + return activity; + + // Custom distance styles are appended to param2 with a space as a separator + const char *pszAct = Q_strstr( sParam2, " " ); + char szActName[256]; + if ( pszAct ) + { + Q_strncpy( szActName, sParam2, sizeof(szActName) ); + szActName[ (pszAct-sParam2) ] = '\0'; + pszAct = szActName; + } + else + { + pszAct = sParam2; + } + + if ( !Q_stricmp( pszAct, "Walk" ) ) + { + activity = ACT_WALK; + } + else if ( !Q_stricmp( pszAct, "Run" ) ) + { + activity = ACT_RUN; + } + else if ( !Q_stricmp( pszAct, "CrouchWalk" ) ) + { + activity = ACT_WALK_CROUCH; + } + else + { + // Try and resolve the activity name + activity = (Activity)ActivityList_IndexForName( pszAct ); + if ( activity == ACT_INVALID ) + { + // Assume it's a sequence name + pNPC->m_iszSceneCustomMoveSeq = AllocPooledString( pszAct ); + activity = ACT_SCRIPT_CUSTOM_MOVE; + } + } + + return activity; +} + +bool CBaseFlex::ProcessMoveToSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + // make sure target exists + if (info->m_hTarget == NULL) + return false; + + // FIXME: move to CBaseActor or BaseNPC + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if (!myNpc) + return false; + + VPROF( "CBaseFlex::ProcessMoveToSceneEvent" ); + + // make sure we're still able to play this command + if (!EnterSceneSequence( scene, event, true )) + { + return false; + } + + // lock in place if aiming at self + if (info->m_hTarget == this) + { + return true; + } + + // If we're in a vehicle, make us exit and *then* begin the run + if ( myNpc->IsInAVehicle() ) + { + // Make us exit and wait + myNpc->ExitVehicle(); + return false; + } + + const Task_t *pCurTask = myNpc->GetTask(); + if (!info->m_bIsMoving && (!IsMoving() || pCurTask->iTask == TASK_STOP_MOVING) ) + { + if ( pCurTask && (pCurTask->iTask == TASK_PLAY_SCENE || pCurTask->iTask == TASK_WAIT_FOR_MOVEMENT || pCurTask->iTask == TASK_STOP_MOVING ) ) + { + Activity moveActivity = DetermineExpressionMoveActivity( event, myNpc ); + // AI_NavGoal_t goal( info->m_hTarget->EyePosition(), moveActivity, AIN_HULL_TOLERANCE ); + myNpc->SetTarget( info->m_hTarget ); + + float flDistTolerance; + flDistTolerance = myNpc->GetHullWidth() / 2.0; + // flDistTolerance = AIN_HULL_TOLERANCE; + + if (event->m_bForceShortMovement) + { + flDistTolerance = 0.1f; + } + + AI_NavGoal_t goal( GOALTYPE_TARGETENT, moveActivity, flDistTolerance, AIN_UPDATE_TARGET_POS ); + + float flDist = (info->m_hTarget->EyePosition() - GetAbsOrigin()).Length2D(); + + if (flDist > MAX( MAX( flDistTolerance, 0.1 ), event->GetDistanceToTarget())) + { + // Msg("flDist %.1f\n", flDist ); + int result = false; + + if ( !myNpc->IsUnreachable( info->m_hTarget ) ) + { + result = myNpc->GetNavigator()->SetGoal( goal, AIN_CLEAR_TARGET ); + if ( !result ) + { + myNpc->RememberUnreachable( info->m_hTarget, 1.5 ); + } + } + + if (result) + { + myNpc->GetNavigator()->SetMovementActivity( moveActivity ); + myNpc->GetNavigator()->SetArrivalDistance( event->GetDistanceToTarget() ); + info->m_bIsMoving = true; + } + else + { + // need route build failure case + // Msg("actor %s unable to build route\n", STRING( myNpc->GetEntityName() ) ); + // Assert(0); + + if (developer.GetInt() > 0 && scene_showmoveto.GetBool()) + { + Vector vTestPoint; + myNpc->GetMoveProbe()->FloorPoint( info->m_hTarget->EyePosition(), MASK_NPCSOLID, 0, -64, &vTestPoint ); + NDebugOverlay::HorzArrow( GetAbsOrigin() + Vector( 0, 0, 1 ), vTestPoint + Vector( 0, 0, 1 ), 4, 255, 0, 255, 0, false, 0.12 ); + NDebugOverlay::Box( vTestPoint, myNpc->GetHullMins(), myNpc->GetHullMaxs(), 255, 0, 255, 0, 0.12 ); + } + } + } + else + { + info->m_bHasArrived = true; + } + } + } + else if (IsMoving()) + { + // float flDist = (myNpc->GetNavigator()->GetGoalPos() - GetAbsOrigin()).Length2D(); + float flDist = (info->m_hTarget->EyePosition() - GetAbsOrigin()).Length2D(); + + if (flDist <= event->GetDistanceToTarget()) + { + myNpc->GetNavigator()->StopMoving( false ); // Stop moving + info->m_bHasArrived = true; + } + } + else + { + info->m_bIsMoving = false; + } + + // show movement target + if (developer.GetInt() > 0 && scene_showmoveto.GetBool() && IsMoving()) + { + Vector vecStart, vTestPoint; + vecStart = myNpc->GetNavigator()->GetGoalPos(); + + myNpc->GetMoveProbe()->FloorPoint( vecStart, MASK_NPCSOLID, 0, -64, &vTestPoint ); + + int r, g, b; + r = b = g = 0; + if ( myNpc->GetNavigator()->CanFitAtPosition( vTestPoint, MASK_NPCSOLID ) ) + { + if ( myNpc->GetMoveProbe()->CheckStandPosition( vTestPoint, MASK_NPCSOLID ) ) + { + if (event->IsResumeCondition()) + { + g = 255; + } + else + { + r = 255; g = 255; + } + } + else + { + b = 255; g = 255; + } + } + else + { + r = 255; + } + + NDebugOverlay::HorzArrow( GetAbsOrigin() + Vector( 0, 0, 1 ), vTestPoint + Vector( 0, 0, 1 ), 4, r, g, b, 0, false, 0.12 ); + NDebugOverlay::Box( vTestPoint, myNpc->GetHullMins(), myNpc->GetHullMaxs(), r, g, b, 0, 0.12 ); + } + + // handled in task + return true; +} + +bool CBaseFlex::ProcessLookAtSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + VPROF( "CBaseFlex::ProcessLookAtSceneEvent" ); + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if (myNpc && info->m_hTarget != NULL) + { + float intensity = event->GetIntensity( scene->GetTime() ); + + // clamp in-ramp to 0.3 seconds + float flDuration = scene->GetTime() - event->GetStartTime(); + float flMaxIntensity = flDuration < 0.3f ? SimpleSpline( flDuration / 0.3f ) : 1.0f; + intensity = clamp( intensity, 0.0f, flMaxIntensity ); + + myNpc->AddLookTarget( info->m_hTarget, intensity, 0.1 ); + if (developer.GetInt() > 0 && scene_showlook.GetBool() && info->m_hTarget) + { + Vector tmp = info->m_hTarget->EyePosition() - myNpc->EyePosition(); + VectorNormalize( tmp ); + Vector p0 = myNpc->EyePosition(); + NDebugOverlay::VertArrow( p0, p0 + tmp * (4 + 16 * intensity ), 4, 255, 255, 255, 0, true, 0.12 ); + } + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseFlex::ProcessSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + VPROF( "CBaseFlex::ProcessSceneEvent" ); + switch ( event->GetType() ) + { + case CChoreoEvent::FLEXANIMATION: + return ProcessFlexAnimationSceneEvent( info, scene, event ); + + case CChoreoEvent::EXPRESSION: + return ProcessFlexSettingSceneEvent( info, scene, event ); + + case CChoreoEvent::SEQUENCE: + return ProcessSequenceSceneEvent( info, scene, event ); + + case CChoreoEvent::GESTURE: + return ProcessGestureSceneEvent( info, scene, event ); + + case CChoreoEvent::FACE: + return ProcessFacingSceneEvent( info, scene, event ); + + case CChoreoEvent::MOVETO: + return ProcessMoveToSceneEvent( info, scene, event ); + + case CChoreoEvent::LOOKAT: + return ProcessLookAtSceneEvent( info, scene, event ); + + case CChoreoEvent::SPEAK: + return true; + + default: + { + Msg( "unknown type %d in ProcessSceneEvent()\n", event->GetType() ); + Assert( 0 ); + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseFlex::IsRunningSceneMoveToEvent() +{ + for ( int i = m_SceneEvents.Count() - 1; i >= 0; i-- ) + { + CSceneEventInfo *info = &m_SceneEvents[ i ]; + CChoreoEvent *event = info->m_pEvent; + if ( event && event->GetType() == CChoreoEvent::MOVETO ) + return true; + } + + return false; +} + + +flexsetting_t const *CBaseFlex::FindNamedSetting( flexsettinghdr_t const *pSettinghdr, const char *expr ) +{ + int i; + const flexsetting_t *pSetting = NULL; + + for ( i = 0; i < pSettinghdr->numflexsettings; i++ ) + { + pSetting = pSettinghdr->pSetting( i ); + if ( !pSetting ) + continue; + + const char *name = pSetting->pszName(); + + if ( !stricmp( name, expr ) ) + break; + } + + if ( i>=pSettinghdr->numflexsettings ) + { + return NULL; + } + + return pSetting; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *event - +//----------------------------------------------------------------------------- +void CBaseFlex::AddFlexAnimation( CSceneEventInfo *info ) +{ + if ( !info ) + return; + + // don't bother with flex animation if the player can't see you + CAI_BaseNPC *myNpc = MyNPCPointer( ); + if (myNpc && !myNpc->HasCondition( COND_IN_PVS )) + return; + + CChoreoEvent *event = info->m_pEvent; + if ( !event ) + return; + + CChoreoScene *scene = info->m_pScene; + if ( !scene ) + return; + + if ( !event->GetTrackLookupSet() ) + { + // Create lookup data + for ( int i = 0; i < event->GetNumFlexAnimationTracks(); i++ ) + { + CFlexAnimationTrack *track = event->GetFlexAnimationTrack( i ); + if ( !track ) + continue; + + if ( track->IsComboType() ) + { + char name[ 512 ]; + Q_strncpy( name, "right_" ,sizeof(name)); + Q_strncat( name, track->GetFlexControllerName(),sizeof(name), COPY_ALL_CHARACTERS ); + + track->SetFlexControllerIndex( FindFlexController( name ), 0, 0 ); + + if ( CAI_BaseActor::IsServerSideFlexController( name ) ) + { + Assert( !"Should stereo controllers ever be server side only?" ); + track->SetServerSide( true ); + } + + Q_strncpy( name, "left_" ,sizeof(name)); + Q_strncat( name, track->GetFlexControllerName(),sizeof(name), COPY_ALL_CHARACTERS ); + + track->SetFlexControllerIndex( FindFlexController( name ), 0, 1 ); + + if ( CAI_BaseActor::IsServerSideFlexController( name ) ) + { + Assert( !"Should stereo controllers ever be server side only?" ); + track->SetServerSide( true ); + } + } + else + { + track->SetFlexControllerIndex( FindFlexController( (char *)track->GetFlexControllerName() ), 0 ); + + // Only non-combo tracks can be server side + track->SetServerSide( CAI_BaseActor::IsServerSideFlexController( track->GetFlexControllerName() ) ); + } + } + + event->SetTrackLookupSet( true ); + } + + float scenetime = scene->GetTime(); + // decay if this is a background scene and there's other flex animations playing + float weight = event->GetIntensity( scenetime ) * info->UpdateWeight( this ); + { + VPROF( "AddFlexAnimation_SetFlexWeight" ); + + // Compute intensity for each track in animation and apply + // Iterate animation tracks + for ( int i = 0; i < event->GetNumFlexAnimationTracks(); i++ ) + { + CFlexAnimationTrack *track = event->GetFlexAnimationTrack( i ); + if ( !track ) + continue; + + // Disabled + if ( !track->IsTrackActive() ) + continue; + + // If we are doing client side flexing, skip all tracks which are not server side + if ( g_bClientFlex && !track->IsServerSide() ) + continue; + + // Map track flex controller to global name + if ( track->IsComboType() ) + { + for ( int side = 0; side < 2; side++ ) + { + LocalFlexController_t controller = track->GetRawFlexControllerIndex( side ); + + // Get spline intensity for controller + float flIntensity = track->GetIntensity( scenetime, side ); + if ( controller >= LocalFlexController_t(0) ) + { + float orig = GetFlexWeight( controller ); + SetFlexWeight( controller, orig * (1 - weight) + flIntensity * weight ); + } + } + } + else + { + LocalFlexController_t controller = track->GetRawFlexControllerIndex( 0 ); + + // Get spline intensity for controller + float flIntensity = track->GetIntensity( scenetime, 0 ); + if ( controller >= LocalFlexController_t(0) ) + { + float orig = GetFlexWeight( controller ); + SetFlexWeight( controller, orig * (1 - weight) + flIntensity * weight ); + } + } + } + } + + info->m_bStarted = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *expr - +// scale - +// *pSettinghdr - +// newexpression - +//----------------------------------------------------------------------------- +void CBaseFlex::AddFlexSetting( const char *expr, float scale, + const flexsettinghdr_t *pSettinghdr, bool newexpression ) +{ + int i; + const flexsetting_t *pSetting = NULL; + + // Find the named setting in the base + for ( i = 0; i < pSettinghdr->numflexsettings; i++ ) + { + pSetting = pSettinghdr->pSetting( i ); + if ( !pSetting ) + continue; + + const char *name = pSetting->pszName(); + + if ( !stricmp( name, expr ) ) + break; + } + + if ( i>=pSettinghdr->numflexsettings ) + { + return; + } + + flexweight_t *pWeights = NULL; + int truecount = pSetting->psetting( (byte *)pSettinghdr, 0, &pWeights ); + if ( !pWeights ) + return; + + for (i = 0; i < truecount; i++, pWeights++) + { + // Translate to local flex controller + // this is translating from the settings's local index to the models local index + LocalFlexController_t index = FlexControllerLocalToGlobal( pSettinghdr, pWeights->key ); + + // blend scaled weighting in to total + float s = clamp( scale * pWeights->influence, 0.0f, 1.0f ); + float value = GetFlexWeight( index ) * (1.0f - s ) + pWeights->weight * s; + SetFlexWeight( index, value ); + } +} + + + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +bool CBaseFlex::ProcessGestureSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + if ( !info || !event || !scene ) + return false; + + if ( info->m_iLayer == REQUEST_DEFERRED_LAYER_ALLOCATION ) + { + HandleStartGestureSceneEvent( info, scene, event, info->m_pActor ); + } + + if (info->m_iLayer >= 0) + { + // this happens after StudioFrameAdvance() + // FIXME; this needs to be adjusted by npc offset to scene time? Known? + // FIXME: what should this do when the scene loops? + float duration = event->GetDuration( ); + float flEventCycle = (scene->GetTime() - event->GetStartTime()) / duration; + float flCycle = event->GetOriginalPercentageFromPlaybackPercentage( flEventCycle ); + + SetLayerCycle( info->m_iLayer, flCycle ); + + float flWeight = event->GetIntensity( scene->GetTime() ); + + /* + if (stricmp( event->GetParameters(), "m_g_arms_crossed" ) == 0) + { + Msg("%.2f (%.2f) : %s : %.3f (%.3f) %.2f\n", scene->GetTime(), scene->GetTime() - event->GetStartTime(), event->GetParameters(), flCycle, flEventCycle, flWeight ); + } + */ + + // fade out/in if npc is moving + if (!info->m_bIsGesture) + { + if (IsMoving()) + { + info->m_flWeight = MAX( info->m_flWeight - 0.2, 0.0 ); + } + else + { + info->m_flWeight = MIN( info->m_flWeight + 0.2, 1.0 ); + } + } + + // 3x^2-2x^3 + float spline = 3 * info->m_flWeight * info->m_flWeight - 2 * info->m_flWeight * info->m_flWeight * info->m_flWeight; + SetLayerWeight( info->m_iLayer, flWeight * spline ); + + // update layer priority + if (m_bUpdateLayerPriorities) + { + SetLayerPriority( info->m_iLayer, info->m_iPriority + GetScenePriority( scene ) ); + } + + /* + Msg( "%d : %.2f (%.2f) : %.3f %.3f : %.3f\n", + info->m_iLayer, + scene->GetTime(), + (scene->GetTime() - event->GetStartTime()) / duration, + flCycle, + flNextCycle, + rate ); + */ + } + + return true; +} + + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +bool CBaseFlex::ProcessSequenceSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) +{ + if ( !info || !event || !scene ) + return false; + + bool bNewlyAllocated = false; + if ( info->m_iLayer == REQUEST_DEFERRED_LAYER_ALLOCATION ) + { + bool result = HandleStartSequenceSceneEvent( info, scene, event, info->m_pActor ); + if (!result) + return false; + bNewlyAllocated = true; + } + + if (info->m_iLayer >= 0) + { + float flWeight = event->GetIntensity( scene->GetTime() ); + + // force layer to zero weight in newly allocated, fixed bug with inter-think spawned sequences blending in badly + if (bNewlyAllocated) + flWeight = 0.0; + + CAI_BaseNPC *myNpc = MyNPCPointer( ); + + // fade out/in if npc is moving + + bool bFadeOut = IsMoving(); + if (myNpc && !(myNpc->IsCurSchedule( SCHED_SCENE_GENERIC ) || myNpc->GetActivity() == ACT_IDLE_ANGRY || myNpc->GetActivity() == ACT_IDLE) ) + { + bFadeOut = true; + if (info->m_flWeight == 1.0) + { + Warning( "%s playing CChoreoEvent::SEQUENCE but AI has forced them to do something different\n", STRING(GetEntityName()) ); + } + } + + if (bFadeOut) + { + info->m_flWeight = MAX( info->m_flWeight - 0.2, 0.0 ); + } + else + { + info->m_flWeight = MIN( info->m_flWeight + 0.2, 1.0 ); + } + + float spline = 3 * info->m_flWeight * info->m_flWeight - 2 * info->m_flWeight * info->m_flWeight * info->m_flWeight; + SetLayerWeight( info->m_iLayer, flWeight * spline ); + + bool looping = ((GetSequenceFlags( GetModelPtr(), info->m_nSequence ) & STUDIO_LOOPING) != 0); + if (!looping) + { + float dt = scene->GetTime() - event->GetStartTime(); + float seq_duration = SequenceDuration( info->m_nSequence ); + float flCycle = dt / seq_duration; + flCycle = clamp( flCycle, 0.f, 1.0f ); + SetLayerCycle( info->m_iLayer, flCycle ); + } + + if (myNpc) + { + myNpc->AddSceneLock( 0.2 ); + } + + // update layer priority + if (m_bUpdateLayerPriorities) + { + SetLayerPriority( info->m_iLayer, info->m_iPriority + GetScenePriority( scene ) ); + } + } + + // FIXME: clean up cycle index from restart + return true; + +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the actor is not currently in a scene OR if the actor +// is in a scene (checked externally), but a PERMIT_RESPONSES event is active and +// the permit time period has enough time remaining to handle the response in full. +// Input : response_length - +//----------------------------------------------------------------------------- +bool CBaseFlex::PermitResponse( float response_length ) +{ + // Nothing set, disallow it + if ( m_flAllowResponsesEndTime <= 0.0f ) + { + return false; + } + + // If response ends before end of allow time, then that's okay + if ( gpGlobals->curtime + response_length <= m_flAllowResponsesEndTime ) + { + return true; + } + + // Disallow responses for now + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Set response end time (0 to clear response blocking) +// Input : endtime - +//----------------------------------------------------------------------------- +void CBaseFlex::SetPermitResponse( float endtime ) +{ + // Mark time after which we'll be occupied again (if in a scene) + // Note a value of <= 0.0f means unset (always allow actor to speak if not in a scene, + // and always disallow if in a scene) + m_flAllowResponsesEndTime = endtime; +} + +//----------------------------------------------------------------------------- +// Purpose: Play a one-shot scene +// Input : +// Output : +//----------------------------------------------------------------------------- +float CBaseFlex::PlayScene( const char *pszScene, float flDelay, AI_Response *response, IRecipientFilter *filter /* = NULL */ ) +{ + return InstancedScriptedScene( this, pszScene, NULL, flDelay, false, response, false, filter ); +} + +//----------------------------------------------------------------------------- +// Purpose: Generate a one-shot scene in memory with one track which is to play the named sound on the actor +// Input : *soundname - +// Output : float +//----------------------------------------------------------------------------- +#ifdef MAPBASE +float CBaseFlex::PlayAutoGeneratedSoundScene( const char *soundname, float flDelay, AI_Response *response, IRecipientFilter *filter ) +{ + return InstancedAutoGeneratedSoundScene( this, soundname, NULL, flDelay, false, response, false, filter ); +} +#else +float CBaseFlex::PlayAutoGeneratedSoundScene( const char *soundname ) +{ + return InstancedAutoGeneratedSoundScene( this, soundname ); +} +#endif + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Parameters for scene event AI_GameText +//----------------------------------------------------------------------------- +bool CBaseFlex::GetGameTextSpeechParams( hudtextparms_t ¶ms ) +{ + params.channel = 3; + params.x = -1; + params.y = 0.6; + params.effect = 0; + + params.r1 = 255; + params.g1 = 255; + params.b1 = 255; + + ScriptVariant_t varTable; + if (g_pScriptVM->GetValue(m_ScriptScope, "m_GameTextSpeechParams", &varTable) && varTable.m_type == FIELD_HSCRIPT) + { + int nIterator = -1; + ScriptVariant_t varKey, varValue; + while ((nIterator = g_pScriptVM->GetKeyValue( varTable.m_hScript, nIterator, &varKey, &varValue )) != -1) + { + if (FStrEq( varKey.m_pszString, "color" )) + { + params.r1 = varValue.m_pVector->x; + params.g1 = varValue.m_pVector->y; + params.b1 = varValue.m_pVector->z; + } + else if (FStrEq( varKey.m_pszString, "color2" )) + { + params.r2 = varValue.m_pVector->x; + params.g2 = varValue.m_pVector->y; + params.b2 = varValue.m_pVector->z; + } + else if (FStrEq( varKey.m_pszString, "channel" )) + { + params.channel = varValue.m_int; + } + else if (FStrEq( varKey.m_pszString, "x" )) + { + params.x = varValue.m_float; + } + else if (FStrEq( varKey.m_pszString, "y" )) + { + params.y = varValue.m_float; + } + else if (FStrEq( varKey.m_pszString, "effect" )) + { + params.effect = varValue.m_int; + } + else if (FStrEq( varKey.m_pszString, "fxtime" )) + { + params.fxTime = varValue.m_float; + } + + g_pScriptVM->ReleaseValue( varKey ); + g_pScriptVM->ReleaseValue( varValue ); + } + } + + return true; +} +#endif + + +//-------------------------------------------------------------------------------------------------- +// Returns the script instance of the scene entity associated with our oldest ("top level") scene event +//-------------------------------------------------------------------------------------------------- +HSCRIPT CBaseFlex::ScriptGetOldestScene( void ) +{ + if ( m_SceneEvents.Count() > 0 ) + { + CSceneEventInfo curScene = m_SceneEvents.Head(); + return ToHScript( (CBaseEntity*)(curScene.m_hSceneEntity.Get()) ); + } + else + { + return NULL; + } +} + +//-------------------------------------------------------------------------------------------------- +// Returns the script instance of the scene at the specified index, or null if index >= count +//-------------------------------------------------------------------------------------------------- +HSCRIPT CBaseFlex::ScriptGetSceneByIndex( int index ) +{ + if ( m_SceneEvents.IsValidIndex( index ) ) + { + CSceneEventInfo curScene = m_SceneEvents.Element( index ); + return ToHScript( (CBaseEntity*)(curScene.m_hSceneEntity.Get()) ); + } + else + { + return NULL; + } +} + + +// FIXME: move to CBaseActor +bool CBaseFlex::EnterSceneSequence( CChoreoScene *scene, CChoreoEvent *event, bool bRestart ) +{ + CAI_BaseNPC *myNpc = MyNPCPointer( ); + + if (!myNpc) + { + // In multiplayer, we allow players to play scenes + if ( IsPlayer() ) + return true; + + return false; + } + + // 2 seconds past current event, or 0.2 seconds past end of scene, whichever is shorter + float flDuration = MIN( 2.0, MIN( event->GetEndTime() - scene->GetTime() + 2.0, scene->FindStopTime() - scene->GetTime() + 0.2 ) ); + + if (myNpc->IsCurSchedule( SCHED_SCENE_GENERIC )) + { + myNpc->AddSceneLock( flDuration ); + return true; + } + + // for now, don't interrupt sequences that don't understand being interrupted + if (myNpc->GetCurSchedule()) + { + CAI_ScheduleBits testBits; + myNpc->GetCurSchedule()->GetInterruptMask( &testBits ); + + testBits.Clear( COND_PROVOKED ); + + if (testBits.IsAllClear()) + { + return false; + } + } + + if (myNpc->IsInterruptable()) + { + if (myNpc->m_hCine) + { + // Assert( !(myNpc->GetFlags() & FL_FLY ) ); + myNpc->ExitScriptedSequence( ); + } + + myNpc->OnStartScene(); + myNpc->SetSchedule( SCHED_SCENE_GENERIC ); + myNpc->AddSceneLock( flDuration ); + return true; + } + + return false; +} + +bool CBaseFlex::ExitSceneSequence( void ) +{ + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: keep track of last valid flex animation time and returns if the current info should play theirs +//----------------------------------------------------------------------------- + +bool CBaseFlex::IsSuppressedFlexAnimation( CSceneEventInfo *info ) +{ + // check for suppression if the current info is a background + if (info->m_pScene && info->m_pScene->IsBackground()) + { + // allow for slight jitter + return m_flLastFlexAnimationTime > gpGlobals->curtime - GetAnimTimeInterval() * 1.5; + } + // keep track of last non-suppressable flex animation + m_flLastFlexAnimationTime = gpGlobals->curtime; + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Clear out body lean states that are invalidated with Teleport +//----------------------------------------------------------------------------- + +void CBaseFlex::Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ) +{ + BaseClass::Teleport( newPosition, newAngles, newVelocity ); +#ifdef HL2_DLL + + // clear out Body Lean + m_vecPrevOrigin = vec3_origin; + +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: keep track of accel/decal and lean the body +//----------------------------------------------------------------------------- + +void CBaseFlex::DoBodyLean( void ) +{ +#ifdef HL2_DLL + CAI_BaseNPC *myNpc = MyNPCPointer( ); + + if (myNpc) + { + Vector vecDelta; + Vector vecPos; + Vector vecOrigin = GetAbsOrigin(); + + if (m_vecPrevOrigin == vec3_origin) + { + m_vecPrevOrigin = vecOrigin; + } + + vecDelta = vecOrigin - m_vecPrevOrigin; + vecDelta.x = clamp( vecDelta.x, -50, 50 ); + vecDelta.y = clamp( vecDelta.y, -50, 50 ); + vecDelta.z = clamp( vecDelta.z, -50, 50 ); + + float dt = gpGlobals->curtime - GetLastThink(); + bool bSkip = ((GetFlags() & (FL_FLY | FL_SWIM)) != 0) || (GetMoveParent() != NULL) || (GetGroundEntity() == NULL) || (GetGroundEntity()->IsMoving()); + bSkip |= myNpc->TaskRanAutomovement() || (myNpc->GetVehicleEntity() != NULL); + + if (!bSkip) + { + if (vecDelta.LengthSqr() > m_vecPrevVelocity.LengthSqr()) + { + float decay = ExponentialDecay( 0.6, 0.1, dt ); + m_vecPrevVelocity = m_vecPrevVelocity * (decay) + vecDelta * (1.f - decay); + } + else + { + float decay = ExponentialDecay( 0.4, 0.1, dt ); + m_vecPrevVelocity = m_vecPrevVelocity * (decay) + vecDelta * (1.f - decay); + } + + vecPos = m_vecPrevOrigin + m_vecPrevVelocity; + + float decay = ExponentialDecay( 0.5, 0.1, dt ); + m_vecShift = m_vecShift * (decay) + (vecOrigin - vecPos) * (1.f - decay); // FIXME: Scale this + m_vecLean = (vecOrigin - vecPos) * 1.0; // FIXME: Scale this + } + else + { + m_vecPrevVelocity = vecDelta; + float decay = ExponentialDecay( 0.5, 0.1, dt ); + m_vecShift = m_vecLean * decay; + m_vecLean = m_vecShift * decay; + } + + m_vecPrevOrigin = vecOrigin; + + /* + DevMsg( "%.2f %.2f %.2f (%.2f %.2f %.2f)\n", + m_vecLean.Get().x, m_vecLean.Get().y, m_vecLean.Get().z, + vecDelta.x, vecDelta.y, vecDelta.z ); + */ + } +#endif +} + + + + + + +//----------------------------------------------------------------------------- +// Purpose: initialize weight for background events +//----------------------------------------------------------------------------- + +void CSceneEventInfo::InitWeight( CBaseFlex *pActor ) +{ + if (pActor->IsSuppressedFlexAnimation( this )) + { + m_flWeight = 0.0; + } + else + { + m_flWeight = 1.0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: update weight for background events. Only call once per think +//----------------------------------------------------------------------------- + +float CSceneEventInfo::UpdateWeight( CBaseFlex *pActor ) +{ + // decay if this is a background scene and there's other flex animations playing + if (pActor->IsSuppressedFlexAnimation( this )) + { + m_flWeight = MAX( m_flWeight - 0.2, 0.0 ); + } + else + { + m_flWeight = MIN( m_flWeight + 0.1, 1.0 ); + } + return m_flWeight; +} + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- + +class CFlexCycler : public CBaseFlex +{ +private: + DECLARE_CLASS( CFlexCycler, CBaseFlex ); +public: + DECLARE_DATADESC(); + + CFlexCycler() { m_iszSentence = NULL_STRING; m_sentence = 0; } + void GenericCyclerSpawn(char *szModel, Vector vecMin, Vector vecMax); + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() | FCAP_IMPULSE_USE); } + int OnTakeDamage( const CTakeDamageInfo &info ); + void Spawn( void ); + void Think( void ); + + virtual void ProcessSceneEvents( void ); + + // Don't treat as a live target + virtual bool IsAlive( void ) { return FALSE; } + + float m_flextime; + LocalFlexController_t m_flexnum; + float m_flextarget[64]; + float m_blinktime; + float m_looktime; + Vector m_lookTarget; + float m_speaktime; + int m_istalking; + int m_phoneme; + + string_t m_iszSentence; + int m_sentence; + + void SetFlexTarget( LocalFlexController_t flexnum ); + LocalFlexController_t LookupFlex( const char *szTarget ); +}; + +BEGIN_DATADESC( CFlexCycler ) + + DEFINE_FIELD( m_flextime, FIELD_TIME ), + DEFINE_FIELD( m_flexnum, FIELD_INTEGER ), + DEFINE_ARRAY( m_flextarget, FIELD_FLOAT, 64 ), + DEFINE_FIELD( m_blinktime, FIELD_TIME ), + DEFINE_FIELD( m_looktime, FIELD_TIME ), + DEFINE_FIELD( m_lookTarget, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_speaktime, FIELD_TIME ), + DEFINE_FIELD( m_istalking, FIELD_INTEGER ), + DEFINE_FIELD( m_phoneme, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_iszSentence, FIELD_STRING, "Sentence" ), + DEFINE_FIELD( m_sentence, FIELD_INTEGER ), + +END_DATADESC() + + +// +// we should get rid of all the other cyclers and replace them with this. +// +class CGenericFlexCycler : public CFlexCycler +{ +public: + DECLARE_CLASS( CGenericFlexCycler, CFlexCycler ); + + void Spawn( void ) { GenericCyclerSpawn( (char *)STRING( GetModelName() ), Vector(-16, -16, 0), Vector(16, 16, 72) ); } +}; + +LINK_ENTITY_TO_CLASS( cycler_flex, CGenericFlexCycler ); + + + +ConVar flex_expression( "flex_expression","-" ); +ConVar flex_talk( "flex_talk","0" ); + +// Cycler member functions + +void CFlexCycler::GenericCyclerSpawn(char *szModel, Vector vecMin, Vector vecMax) +{ + if (!szModel || !*szModel) + { + Warning( "cycler at %.0f %.0f %0.f missing modelname\n", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + UTIL_Remove( this ); + return; + } + + PrecacheModel( szModel ); + SetModel( szModel ); + + CFlexCycler::Spawn( ); + + UTIL_SetSize(this, vecMin, vecMax); + + Vector vecEyeOffset; + GetEyePosition( GetModelPtr(), vecEyeOffset ); + SetViewOffset( vecEyeOffset ); + + InitBoneControllers(); + + if (GetNumFlexControllers() < 5) + Warning( "cycler_flex used on model %s without enough flexes.\n", szModel ); +} + +void CFlexCycler::Spawn( ) +{ + Precache(); + /* + if ( m_spawnflags & FCYCLER_NOTSOLID ) + { + SetSolid( SOLID_NOT ); + } + else + { + SetSolid( SOLID_SLIDEBOX ); + } + */ + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + + SetMoveType( MOVETYPE_NONE ); + m_takedamage = DAMAGE_YES; + m_iHealth = 80000;// no cycler should die + + m_flPlaybackRate = 1.0f; + m_flGroundSpeed = 0; + + + SetNextThink( gpGlobals->curtime + 1.0f ); + + ResetSequenceInfo( ); + + m_flCycle = random->RandomFloat( 0, 1.0 ); +} + +const char *predef_flexcontroller_names[] = { + "right_lid_raiser", + "left_lid_raiser", + "right_lid_tightener", + "left_lid_tightener", + "right_lid_droop", + "left_lid_droop", + "right_inner_raiser", + "left_inner_raiser", + "right_outer_raiser", + "left_outer_raiser", + "right_lowerer", + "left_lowerer", + "right_cheek_raiser", + "left_cheek_raiser", + "wrinkler", + "right_upper_raiser", + "left_upper_raiser", + "right_corner_puller", + "left_corner_puller", + "corner_depressor", + "chin_raiser", + "right_puckerer", + "left_puckerer", + "right_funneler", + "left_funneler", + "tightener", + "jaw_clencher", + "jaw_drop", + "right_mouth_drop", + "left_mouth_drop", + NULL }; + +float predef_flexcontroller_values[7][30] = { +/* 0 */ { 0.700,0.560,0.650,0.650,0.650,0.585,0.000,0.000,0.400,0.040,0.000,0.000,0.450,0.450,0.000,0.000,0.000,0.750,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.150,1.000,0.000,0.000,0.000 }, +/* 1 */ { 0.450,0.450,0.450,0.450,0.000,0.000,0.000,0.000,0.300,0.300,0.000,0.000,0.250,0.250,0.000,0.000,0.000,0.750,0.750,0.000,0.000,0.000,0.000,0.400,0.400,0.000,1.000,0.000,0.050,0.050 }, +/* 2 */ { 0.200,0.200,0.500,0.500,0.150,0.150,0.100,0.100,0.150,0.150,0.000,0.000,0.700,0.700,0.000,0.000,0.000,0.750,0.750,0.000,0.200,0.000,0.000,0.000,0.000,0.000,0.850,0.000,0.000,0.000 }, +/* 3 */ { 0.000,0.000,0.000,0.000,0.000,0.000,0.300,0.300,0.000,0.000,0.000,0.000,0.000,0.000,0.100,0.000,0.000,0.000,0.000,0.700,0.300,0.000,0.000,0.200,0.200,0.000,0.000,0.300,0.000,0.000 }, +/* 4 */ { 0.450,0.450,0.000,0.000,0.450,0.450,0.000,0.000,0.000,0.000,0.450,0.450,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.300,0.000,0.000,0.000,0.000 }, +/* 5 */ { 0.000,0.000,0.350,0.350,0.150,0.150,0.300,0.300,0.450,0.450,0.000,0.000,0.200,0.200,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.200,0.200,0.000,0.000,0.300,0.000,0.000,0.000,0.000 }, +/* 6 */ { 0.000,0.000,0.650,0.650,0.750,0.750,0.000,0.000,0.000,0.000,0.300,0.300,0.000,0.000,0.000,0.250,0.250,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000,0.000 } +}; + +//----------------------------------------------------------------------------- +// Purpose: Changes sequences when shot +//----------------------------------------------------------------------------- +int CFlexCycler::OnTakeDamage( const CTakeDamageInfo &info ) +{ + int nSequence = GetSequence() + 1; + if (!IsValidSequence( nSequence )) + { + nSequence = 0; + } + + ResetSequence( nSequence ); + m_flCycle = 0; + + return 0; +} + + +void CFlexCycler::SetFlexTarget( LocalFlexController_t flexnum ) +{ + m_flextarget[flexnum] = random->RandomFloat( 0.5, 1.0 ); + + const char *pszType = GetFlexControllerType( flexnum ); + + // zero out all other flexes of the same type + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + if (i != flexnum) + { + const char *pszOtherType = GetFlexControllerType( i ); + if (stricmp( pszType, pszOtherType ) == 0) + { + m_flextarget[i] = 0; + } + } + } + + // HACK, for now, consider then linked is named "right_" or "left_" + if (strncmp( "right_", GetFlexControllerName( flexnum ), 6 ) == 0) + { + m_flextarget[flexnum+1] = m_flextarget[flexnum]; + } + else if (strncmp( "left_", GetFlexControllerName( flexnum ), 5 ) == 0) + { + m_flextarget[flexnum-1] = m_flextarget[flexnum]; + } +} + + +LocalFlexController_t CFlexCycler::LookupFlex( const char *szTarget ) +{ + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + const char *pszFlex = GetFlexControllerName( i ); + if (stricmp( szTarget, pszFlex ) == 0) + { + return i; + } + } + return LocalFlexController_t(-1); +} + + +void CFlexCycler::Think( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + StudioFrameAdvance ( ); + + if (IsSequenceFinished() && !SequenceLoops()) + { + // ResetSequenceInfo(); + // hack to avoid reloading model every frame + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 1.0; + m_bSequenceFinished = false; + m_flLastEventCheck = 0; + m_flCycle = 0; + } + + // only do this if they have more than eyelid movement + if (GetNumFlexControllers() > 2) + { + const char *pszExpression = flex_expression.GetString(); + + if (pszExpression && pszExpression[0] == '+' && pszExpression[1] != '\0') + { + int i; + int j = atoi( &pszExpression[1] ); + for ( i = 0; i < GetNumFlexControllers(); i++) + { + m_flextarget[m_flexnum] = 0; + } + + for (i = 0; i < 35 && predef_flexcontroller_names[i]; i++) + { + m_flexnum = LookupFlex( predef_flexcontroller_names[i] ); + m_flextarget[m_flexnum] = predef_flexcontroller_values[j][i]; + // Msg( "%s %.3f\n", predef_flexcontroller_names[i], predef_flexcontroller_values[j][i] ); + } + } + else if ( pszExpression && (pszExpression[0] == '1') && (pszExpression[1] == '\0') ) // 1 for maxed controller values + { + for ( LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++ ) + { + // Max everything out... + m_flextarget[i] = 1.0f; + SetFlexWeight( i, m_flextarget[i] ); + } + } + else if ( pszExpression && (pszExpression[0] == '^') && (pszExpression[1] == '\0') ) // ^ for sine wave + { + for ( LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++ ) + { + // Throw a differently offset sine wave on all of the flex controllers + float fFlexTime = i * (1.0f / (float)GetNumFlexControllers()) + gpGlobals->curtime; + m_flextarget[i] = sinf( fFlexTime ) * 0.5f + 0.5f; + SetFlexWeight( i, m_flextarget[i] ); + } + } + else if (pszExpression && pszExpression[0] != '\0' && strcmp(pszExpression, "+") != 0) + { + char szExpression[128]; + char szTemp[32]; + + Q_strncpy( szExpression, pszExpression ,sizeof(szExpression)); + char *pszExpression = szExpression; + + while (*pszExpression != '\0') + { + if (*pszExpression == '+') + *pszExpression = ' '; + + pszExpression++; + } + + pszExpression = szExpression; + + while (*pszExpression) + { + if (*pszExpression != ' ') + { + if (*pszExpression == '-') + { + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + m_flextarget[i] = 0; + } + } + else if (*pszExpression == '?') + { + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + Msg( "\"%s\" ", GetFlexControllerName( i ) ); + } + Msg( "\n" ); + flex_expression.SetValue( "" ); + } + else + { + if (sscanf( pszExpression, "%31s", szTemp ) == 1) + { + m_flexnum = LookupFlex( szTemp ); + + if (m_flexnum != -1 && m_flextarget[m_flexnum] != 1) + { + m_flextarget[m_flexnum] = 1.0; + // SetFlexTarget( m_flexnum ); + } + pszExpression += strlen( szTemp ) - 1; + } + } + } + pszExpression++; + } + } + else if (m_flextime < gpGlobals->curtime) + { + // m_flextime = gpGlobals->curtime + 1.0; // RandomFloat( 0.1, 0.5 ); + m_flextime = gpGlobals->curtime + random->RandomFloat( 0.3, 0.5 ) * (30.0 / GetNumFlexControllers()); + m_flexnum = (LocalFlexController_t)random->RandomInt( 0, GetNumFlexControllers() - 1 ); + + // m_flexnum = (pflex->num + 1) % r_psubmodel->numflexes; + + if (m_flextarget[m_flexnum] == 1) + { + m_flextarget[m_flexnum] = 0; + // pflex->time = cl.time + 0.1; + } + else if (stricmp( GetFlexControllerType( m_flexnum ), "phoneme" ) != 0) + { + if (strstr( GetFlexControllerName( m_flexnum ), "upper_raiser" ) == NULL) + { + Msg( "%s:%s\n", GetFlexControllerType( m_flexnum ), GetFlexControllerName( m_flexnum ) ); + SetFlexTarget( m_flexnum ); + } + } + +#if 0 + char szWhat[256]; + szWhat[0] = '\0'; + for (int i = 0; i < GetNumFlexControllers(); i++) + { + if (m_flextarget[i] == 1.0) + { + if (stricmp( GetFlexFacs( i ), "upper") != 0 && stricmp( GetFlexFacs( i ), "lower") != 0) + { + if (szWhat[0] == '\0') + Q_strncat( szWhat, "-", sizeof( szWhat ), COPY_ALL_CHARACTERS ); + else + Q_strncat( szWhat, "+", sizeof( szWhat ), COPY_ALL_CHARACTERS ); + Q_strncat( szWhat, GetFlexFacs( i ), sizeof( szWhat ), COPY_ALL_CHARACTERS ); + } + } + } + Msg( "%s\n", szWhat ); +#endif + } + + // slide it up. + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + float weight = GetFlexWeight( i ); + + if (weight != m_flextarget[i]) + { + weight = weight + (m_flextarget[i] - weight) / random->RandomFloat( 2.0, 4.0 ); + } + weight = clamp( weight, 0.0f, 1.0f ); + SetFlexWeight( i, weight ); + } + +#if 1 + if (flex_talk.GetInt() == -1) + { + m_istalking = 1; + char pszSentence[256]; + Q_snprintf( pszSentence,sizeof(pszSentence), "%s%d", STRING(m_iszSentence), m_sentence++ ); + int sentenceIndex = engine->SentenceIndexFromName( pszSentence ); + if (sentenceIndex >= 0) + { + Msg( "%d : %s\n", sentenceIndex, pszSentence ); + CPASAttenuationFilter filter( this ); + CBaseEntity::EmitSentenceByIndex( filter, entindex(), CHAN_VOICE, sentenceIndex, 1, SNDLVL_TALKING, 0, PITCH_NORM ); + } + else + { + m_sentence = 0; + } + + flex_talk.SetValue( "0" ); + } + else if (!FStrEq( flex_talk.GetString(), "0") ) + { + int sentenceIndex = engine->SentenceIndexFromName( flex_talk.GetString() ); + if (sentenceIndex >= 0) + { + CPASAttenuationFilter filter( this ); + CBaseEntity::EmitSentenceByIndex( filter, entindex(), CHAN_VOICE, sentenceIndex, 1, SNDLVL_TALKING, 0, PITCH_NORM ); + } + flex_talk.SetValue( "0" ); + } +#else + if (flex_talk.GetInt()) + { + if (m_speaktime < gpGlobals->curtime) + { + if (m_phoneme == 0) + { + for (m_phoneme = 0; m_phoneme < GetNumFlexControllers(); m_phoneme++) + { + if (stricmp( GetFlexFacs( m_phoneme ), "27") == 0) + break; + } + } + m_istalking = !m_istalking; + if (m_istalking) + { + m_looktime = gpGlobals->curtime - 1.0; + m_speaktime = gpGlobals->curtime + random->RandomFloat( 0.5, 2.0 ); + } + else + { + m_speaktime = gpGlobals->curtime + random->RandomFloat( 1.0, 3.0 ); + } + } + + for (i = m_phoneme; i < GetNumFlexControllers(); i++) + { + SetFlexWeight( i, 0.0f ); + } + + if (m_istalking) + { + m_flextime = gpGlobals->curtime + random->RandomFloat( 0.0, 0.2 ); + m_flexWeight[random->RandomInt(m_phoneme, GetNumFlexControllers()-1)] = random->RandomFloat( 0.5, 1.0 ); + float mouth = random->RandomFloat( 0.0, 1.0 ); + float jaw = random->RandomFloat( 0.0, 1.0 ); + + m_flexWeight[m_phoneme - 2] = jaw * (mouth); + m_flexWeight[m_phoneme - 1] = jaw * (1.0 - mouth); + } + } + else + { + m_istalking = 0; + } +#endif + + // blink + if (m_blinktime < gpGlobals->curtime) + { + Blink(); + m_blinktime = gpGlobals->curtime + random->RandomFloat( 1.5, 4.5 ); + } + } + + + Vector forward, right, up; + GetVectors( &forward, &right, &up ); + + CBaseEntity *pPlayer = (CBaseEntity *)UTIL_GetLocalPlayer(); + if (pPlayer) + { + if (pPlayer->GetSmoothedVelocity().Length() != 0 && DotProduct( forward, pPlayer->EyePosition() - EyePosition()) > 0.5) + { + m_lookTarget = pPlayer->EyePosition(); + m_looktime = gpGlobals->curtime + random->RandomFloat(2.0,4.0); + } + else if (m_looktime < gpGlobals->curtime) + { + if ((!m_istalking) && random->RandomInt( 0, 1 ) == 0) + { + m_lookTarget = EyePosition() + forward * 128 + right * random->RandomFloat(-64,64) + up * random->RandomFloat(-32,32); + m_looktime = gpGlobals->curtime + random->RandomFloat(0.3,1.0); + + if (m_blinktime - 0.5 < gpGlobals->curtime) + { + Blink(); + } + } + else + { + m_lookTarget = pPlayer->EyePosition(); + m_looktime = gpGlobals->curtime + random->RandomFloat(1.0,4.0); + } + } + +#if 0 + float dt = acos( DotProduct( (m_lookTarget - EyePosition()).Normalize(), (m_viewtarget - EyePosition()).Normalize() ) ); + + if (dt > M_PI / 4) + { + dt = (M_PI / 4) * dt; + m_viewtarget = ((1 - dt) * m_viewtarget + dt * m_lookTarget); + } +#endif + + SetViewtarget( m_lookTarget ); + } + + // Handle any facial animation from scene playback + // FIXME: do we still actually need flex cyclers? + // AddSceneSceneEvents(); +} + + +void CFlexCycler::ProcessSceneEvents( void ) +{ + // Don't do anything since we handle facial stuff in Think() +} + + +BEGIN_BYTESWAP_DATADESC( flexsettinghdr_t ) + DEFINE_FIELD( id, FIELD_INTEGER ), + DEFINE_FIELD( version, FIELD_INTEGER ), + DEFINE_ARRAY( name, FIELD_CHARACTER, 64 ), + DEFINE_FIELD( length, FIELD_INTEGER ), + DEFINE_FIELD( numflexsettings, FIELD_INTEGER ), + DEFINE_FIELD( flexsettingindex, FIELD_INTEGER ), + DEFINE_FIELD( nameindex, FIELD_INTEGER ), + DEFINE_FIELD( numindexes, FIELD_INTEGER ), + DEFINE_FIELD( indexindex, FIELD_INTEGER ), + DEFINE_FIELD( numkeys, FIELD_INTEGER ), + DEFINE_FIELD( keynameindex, FIELD_INTEGER ), + DEFINE_FIELD( keymappingindex, FIELD_INTEGER ), +END_BYTESWAP_DATADESC() + +BEGIN_BYTESWAP_DATADESC( flexsetting_t ) + DEFINE_FIELD( nameindex, FIELD_INTEGER ), + DEFINE_FIELD( obsolete1, FIELD_INTEGER ), + DEFINE_FIELD( numsettings, FIELD_INTEGER ), + DEFINE_FIELD( index, FIELD_INTEGER ), + DEFINE_FIELD( obsolete2, FIELD_INTEGER ), + DEFINE_FIELD( settingindex, FIELD_INTEGER ), +END_BYTESWAP_DATADESC() + +BEGIN_BYTESWAP_DATADESC( flexweight_t ) + DEFINE_FIELD( key, FIELD_INTEGER ), + DEFINE_FIELD( weight, FIELD_FLOAT ), + DEFINE_FIELD( influence, FIELD_FLOAT ), +END_BYTESWAP_DATADESC() + diff --git a/sp/src/game/server/baseflex.h b/sp/src/game/server/baseflex.h new file mode 100644 index 00000000..215b13f5 --- /dev/null +++ b/sp/src/game/server/baseflex.h @@ -0,0 +1,324 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef BASEFLEX_H +#define BASEFLEX_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "BaseAnimatingOverlay.h" +#include "utlvector.h" +#include "utlrbtree.h" +#include "sceneentity_shared.h" + +struct flexsettinghdr_t; +struct flexsetting_t; +class AI_Response; + +//----------------------------------------------------------------------------- +// Purpose: A .vfe referenced by a scene during .vcd playback +//----------------------------------------------------------------------------- +class CFlexSceneFile +{ +public: + enum + { + MAX_FLEX_FILENAME = 128, + }; + + char filename[ MAX_FLEX_FILENAME ]; + void *buffer; +}; + +//----------------------------------------------------------------------------- +// Purpose: Animated characters who have vertex flex capability (e.g., facial expressions) +//----------------------------------------------------------------------------- +class CBaseFlex : public CBaseAnimatingOverlay +{ + DECLARE_CLASS( CBaseFlex, CBaseAnimatingOverlay ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + DECLARE_PREDICTABLE(); + // script description + DECLARE_ENT_SCRIPTDESC(); + + // Construction + CBaseFlex( void ); + ~CBaseFlex( void ); + + virtual void SetModel( const char *szModelName ); + + void Blink( ); + + virtual void SetViewtarget( const Vector &viewtarget ); + const Vector &GetViewtarget( void ) const; + + void SetFlexWeight( char *szName, float value ); + void SetFlexWeight( LocalFlexController_t index, float value ); + float GetFlexWeight( char *szName ); + float GetFlexWeight( LocalFlexController_t index ); + + // Look up flex controller index by global name + LocalFlexController_t FindFlexController( const char *szName ); + void EnsureTranslations( const flexsettinghdr_t *pSettinghdr ); + + // Keep track of what scenes are being played + void StartChoreoScene( CChoreoScene *scene ); + void RemoveChoreoScene( CChoreoScene *scene, bool canceled = false ); + + // Start the specifics of an scene event +#ifdef MAPBASE + virtual bool StartSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget, CSceneEntity *pSceneEnt = NULL ); +#else + virtual bool StartSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ); +#endif + + // Manipulation of events for the object + // Should be called by think function to process all scene events + // The default implementation resets m_flexWeight array and calls + // AddSceneEvents + virtual void ProcessSceneEvents( void ); + + // Assumes m_flexWeight array has been set up, this adds the actual currently playing + // expressions to the flex weights and adds other scene events as needed + virtual bool ProcessSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + + // Remove all playing events + void ClearSceneEvents( CChoreoScene *scene, bool canceled ); + + // Stop specifics of event + virtual bool ClearSceneEvent( CSceneEventInfo *info, bool fastKill, bool canceled ); + + // Add the event to the queue for this actor +#ifdef MAPBASE + void AddSceneEvent( CChoreoScene *scene, CChoreoEvent *event, CBaseEntity *pTarget = NULL, CSceneEntity *pSceneEnt = NULL ); +#else + void AddSceneEvent( CChoreoScene *scene, CChoreoEvent *event, CBaseEntity *pTarget = NULL ); +#endif + + // Remove the event from the queue for this actor + void RemoveSceneEvent( CChoreoScene *scene, CChoreoEvent *event, bool fastKill ); + + // Checks to see if the event should be considered "completed" + bool CheckSceneEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ); + + // Checks to see if a event should be considered "completed" + virtual bool CheckSceneEventCompletion( CSceneEventInfo *info, float currenttime, CChoreoScene *scene, CChoreoEvent *event ); + + // Finds the layer priority of the current scene + int GetScenePriority( CChoreoScene *scene ); + + // Returns true if the actor is not currently in a scene OR if the actor + // is in a scene, but a PERMIT_RESPONSES event is active and the permit time + // period has enough time remaining to handle the response in full. + bool PermitResponse( float response_length ); + + // Set response end time (0 to clear response blocking) + void SetPermitResponse( float endtime ); + + void SentenceStop( void ) { EmitSound( "AI_BaseNPC.SentenceStop" ); } + + virtual float PlayScene( const char *pszScene, float flDelay = 0.0f, AI_Response *response = NULL, IRecipientFilter *filter = NULL ); +#ifdef MAPBASE + virtual float PlayAutoGeneratedSoundScene( const char *soundname, float flDelay = 0.0f, AI_Response *response = NULL, IRecipientFilter *filter = NULL ); +#else + virtual float PlayAutoGeneratedSoundScene( const char *soundname ); +#endif + + // Returns the script instance of the scene entity associated with our oldest ("top level") scene event + virtual HSCRIPT ScriptGetOldestScene( void ); + virtual HSCRIPT ScriptGetSceneByIndex( int index ); + + virtual int GetSpecialDSP( void ) { return 0; } + +#ifdef MAPBASE + virtual bool GetGameTextSpeechParams( hudtextparms_t ¶ms ); +#endif + +protected: + // For handling .vfe files + // Search list, or add if not in list + const void *FindSceneFile( const char *filename ); + + // Find setting by name + const flexsetting_t *FindNamedSetting( const flexsettinghdr_t *pSettinghdr, const char *expr ); + + // Called at the lowest level to actually apply an expression + void AddFlexSetting( const char *expr, float scale, const flexsettinghdr_t *pSettinghdr, bool newexpression ); + + // Called at the lowest level to actually apply a flex animation + void AddFlexAnimation( CSceneEventInfo *info ); + + bool HasSceneEvents() const; + bool IsRunningSceneMoveToEvent(); + + LocalFlexController_t FlexControllerLocalToGlobal( const flexsettinghdr_t *pSettinghdr, int key ); + +private: + // Starting various expression types + + bool RequestStartSequenceSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ); + bool RequestStartGestureSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ); + + bool HandleStartSequenceSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor ); + bool HandleStartGestureSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor ); + bool StartFacingSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ); + bool StartMoveToSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ); + + // Processing various expression types + bool ProcessFlexAnimationSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + bool ProcessFlexSettingSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + bool ProcessSequenceSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + bool ProcessGestureSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + bool ProcessFacingSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + bool ProcessMoveToSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + bool ProcessLookAtSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ); + + // Set playing the scene sequence +public: + bool EnterSceneSequence( CChoreoScene *scene, CChoreoEvent *event, bool bRestart = false ); +private: + bool ExitSceneSequence( void ); + +private: + CNetworkArray( float, m_flexWeight, MAXSTUDIOFLEXCTRL ); // indexed by model local flexcontroller + + // Vector from actor to eye target + CNetworkVector( m_viewtarget ); + + // Blink state + CNetworkVar( int, m_blinktoggle ); + + // Array of active SceneEvents, in order oldest to newest + CUtlVector < CSceneEventInfo > m_SceneEvents; + + // Mapping for each loaded scene file used by this actor + struct FS_LocalToGlobal_t + { + explicit FS_LocalToGlobal_t() : + m_Key( 0 ), + m_nCount( 0 ), + m_Mapping( 0 ) + { + } + + explicit FS_LocalToGlobal_t( const flexsettinghdr_t *key ) : + m_Key( key ), + m_nCount( 0 ), + m_Mapping( 0 ) + { + } + + void SetCount( int count ) + { + Assert( !m_Mapping ); + Assert( count > 0 ); + m_nCount = count; + m_Mapping = new LocalFlexController_t[ m_nCount ]; + Q_memset( m_Mapping, 0, m_nCount * sizeof( int ) ); + } + + FS_LocalToGlobal_t( const FS_LocalToGlobal_t& src ) + { + m_Key = src.m_Key; + delete m_Mapping; + m_Mapping = new LocalFlexController_t[ src.m_nCount ]; + Q_memcpy( m_Mapping, src.m_Mapping, src.m_nCount * sizeof( int ) ); + + m_nCount = src.m_nCount; + } + + ~FS_LocalToGlobal_t() + { + delete m_Mapping; + m_nCount = 0; + m_Mapping = 0; + } + + const flexsettinghdr_t *m_Key; + int m_nCount; + LocalFlexController_t *m_Mapping; + }; + + static bool FlexSettingLessFunc( const FS_LocalToGlobal_t& lhs, const FS_LocalToGlobal_t& rhs ); + + CUtlRBTree< FS_LocalToGlobal_t, unsigned short > m_LocalToGlobal; + + // The NPC is in a scene, but another .vcd (such as a short wave to say in response to the player doing something ) + // can be layered on top of this actor (assuming duration matches, etc. + float m_flAllowResponsesEndTime; + + // List of actively playing scenes + CUtlVector < CChoreoScene * > m_ActiveChoreoScenes; + bool m_bUpdateLayerPriorities; + +public: + bool IsSuppressedFlexAnimation( CSceneEventInfo *info ); + +private: + // last time a foreground flex animation was played + float m_flLastFlexAnimationTime; + + +public: + void DoBodyLean( void ); + + virtual void Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ); + + +#ifdef HL2_DLL + Vector m_vecPrevOrigin; + Vector m_vecPrevVelocity; + CNetworkVector( m_vecLean ); + CNetworkVector( m_vecShift ); +#endif +}; + + +//----------------------------------------------------------------------------- +// For toggling blinking +//----------------------------------------------------------------------------- +inline void CBaseFlex::Blink() +{ + m_blinktoggle = !m_blinktoggle; +} + +//----------------------------------------------------------------------------- +// Do we have active expressions? +//----------------------------------------------------------------------------- +inline bool CBaseFlex::HasSceneEvents() const +{ + return m_SceneEvents.Count() != 0; +} + + +//----------------------------------------------------------------------------- +// Other inlines +//----------------------------------------------------------------------------- +inline const Vector &CBaseFlex::GetViewtarget( ) const +{ + return m_viewtarget.Get(); // bah +} + +inline void CBaseFlex::SetFlexWeight( char *szName, float value ) +{ + SetFlexWeight( FindFlexController( szName ), value ); +} + +inline float CBaseFlex::GetFlexWeight( char *szName ) +{ + return GetFlexWeight( FindFlexController( szName ) ); +} + + +EXTERN_SEND_TABLE(DT_BaseFlex); + + +#endif // BASEFLEX_H diff --git a/sp/src/game/server/basegrenade_concussion.cpp b/sp/src/game/server/basegrenade_concussion.cpp new file mode 100644 index 00000000..2954416e --- /dev/null +++ b/sp/src/game/server/basegrenade_concussion.cpp @@ -0,0 +1,131 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "baseentity.h" +#include "basegrenade_shared.h" +#include "soundent.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CBaseGrenadeConcussion : public CBaseGrenade +{ + DECLARE_DATADESC(); + +public: + DECLARE_CLASS( CBaseGrenadeConcussion, CBaseGrenade ); + + void Spawn( void ); + void Precache( void ); + void FallThink(void); + void ExplodeConcussion( CBaseEntity *pOther ); + +protected: + static int m_nTrailSprite; +}; + +int CBaseGrenadeConcussion::m_nTrailSprite = 0; + +LINK_ENTITY_TO_CLASS( npc_concussiongrenade, CBaseGrenadeConcussion ); + +BEGIN_DATADESC( CBaseGrenadeConcussion ) + + DEFINE_THINKFUNC( FallThink ), + DEFINE_ENTITYFUNC( ExplodeConcussion ), + +END_DATADESC() + + +void CBaseGrenadeConcussion::FallThink(void) +{ + if (!IsInWorld()) + { + Remove( ); + return; + } + CSoundEnt::InsertSound ( SOUND_DANGER, GetAbsOrigin() + GetAbsVelocity() * 0.5, GetAbsVelocity().Length( ), 0.2 ); + + SetNextThink( gpGlobals->curtime + random->RandomFloat(0.05, 0.1) ); + + if (GetWaterLevel() != 0) + { + SetAbsVelocity( GetAbsVelocity() * 0.5 ); + } + + Vector pos = GetAbsOrigin() + Vector(random->RandomFloat(-4, 4), random->RandomFloat(-4, 4), random->RandomFloat(-4, 4)); + + CPVSFilter filter( GetAbsOrigin() ); + + te->Sprite( filter, 0.0, + &pos, + m_nTrailSprite, + random->RandomFloat(0.5, 0.8), + 200 ); +} + + +// +// Contact grenade, explode when it touches something +// +void CBaseGrenadeConcussion::ExplodeConcussion( CBaseEntity *pOther ) +{ + trace_t tr; + Vector vecSpot;// trace starts here! + + Vector velDir = GetAbsVelocity(); + VectorNormalize( velDir ); + vecSpot = GetAbsOrigin() - velDir * 32; + UTIL_TraceLine( vecSpot, vecSpot + velDir * 64, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + Explode( &tr, DMG_BLAST ); +} + + +void CBaseGrenadeConcussion::Spawn( void ) +{ + // point sized, solid, bouncing + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + SetSolid( SOLID_BBOX ); + SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); + SetModel( "models/weapons/w_grenade.mdl" ); // BUG: wrong model + + UTIL_SetSize(this, vec3_origin, vec3_origin); + + // contact grenades arc lower + SetGravity( UTIL_ScaleForGravity( 400 ) ); // use a lower gravity for grenades to make them easier to see + QAngle angles; + VectorAngles(GetAbsVelocity(), angles ); + SetLocalAngles( angles ); + + m_nRenderFX = kRenderFxGlowShell; + SetRenderColor( 200, 200, 20, 255 ); + + // make NPCs afaid of it while in the air + SetThink( &CBaseGrenadeConcussion::FallThink ); + SetNextThink( gpGlobals->curtime ); + + // Tumble in air + QAngle vecAngVel( random->RandomFloat ( -100, -500 ), 0, 0 ); + SetLocalAngularVelocity( vecAngVel ); + + // Explode on contact + SetTouch( &CBaseGrenadeConcussion::ExplodeConcussion ); + + m_flDamage = 80; + + // Allow player to blow this puppy up in the air + m_takedamage = DAMAGE_YES; +} + + +void CBaseGrenadeConcussion::Precache( void ) +{ + BaseClass::Precache( ); + + PrecacheModel("models/weapons/w_grenade.mdl"); + m_nTrailSprite = PrecacheModel("sprites/twinkle01.vmt"); +} diff --git a/sp/src/game/server/basegrenade_contact.cpp b/sp/src/game/server/basegrenade_contact.cpp new file mode 100644 index 00000000..d0123ba0 --- /dev/null +++ b/sp/src/game/server/basegrenade_contact.cpp @@ -0,0 +1,71 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "basegrenade_shared.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar sk_plr_dmg_grenade; + +// ========================================================================================== + +class CBaseGrenadeContact : public CBaseGrenade +{ + DECLARE_CLASS( CBaseGrenadeContact, CBaseGrenade ); +public: + + void Spawn( void ); + void Precache( void ); +}; +LINK_ENTITY_TO_CLASS( npc_contactgrenade, CBaseGrenadeContact ); + + +void CBaseGrenadeContact::Spawn( void ) +{ + // point sized, solid, bouncing + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + SetSolid( SOLID_BBOX ); + SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); + SetModel( "models/weapons/w_grenade.mdl" ); // BUG: wrong model + + UTIL_SetSize(this, vec3_origin, vec3_origin); + + // contact grenades arc lower + SetGravity( UTIL_ScaleForGravity( 400 ) ); // use a lower gravity for grenades to make them easier to see + + QAngle angles; + VectorAngles(GetAbsVelocity(), angles); + SetLocalAngles( angles ); + + // make NPCs afaid of it while in the air + SetThink( &CBaseGrenadeContact::DangerSoundThink ); + SetNextThink( gpGlobals->curtime ); + + // Tumble in air + QAngle vecAngVelocity( random->RandomFloat ( -100, -500 ), 0, 0 ); + SetLocalAngularVelocity( vecAngVelocity ); + + // Explode on contact + SetTouch( &CBaseGrenadeContact::ExplodeTouch ); + + m_flDamage = sk_plr_dmg_grenade.GetFloat(); + + // Allow player to blow this puppy up in the air + m_takedamage = DAMAGE_YES; + + m_iszBounceSound = NULL_STRING; +} + + +void CBaseGrenadeContact::Precache( void ) +{ + BaseClass::Precache( ); + + PrecacheModel("models/weapons/w_grenade.mdl"); +} diff --git a/sp/src/game/server/basegrenade_timed.cpp b/sp/src/game/server/basegrenade_timed.cpp new file mode 100644 index 00000000..e22b47c4 --- /dev/null +++ b/sp/src/game/server/basegrenade_timed.cpp @@ -0,0 +1,73 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "basegrenade_shared.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CBaseGrenadeTimed : public CBaseGrenade +{ +public: + DECLARE_CLASS( CBaseGrenadeTimed, CBaseGrenade ); + + void Spawn( void ); + void Precache( void ); +}; +LINK_ENTITY_TO_CLASS( npc_handgrenade, CBaseGrenadeTimed ); + + +void CBaseGrenadeTimed::Spawn( void ) +{ + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + SetSolid( SOLID_BBOX ); + SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); + SetModel( "models/Weapons/w_grenade.mdl" ); + + UTIL_SetSize(this, Vector( -4, -4, -4), Vector(4, 4, 4)); + + QAngle angles; + Vector vel = GetAbsVelocity(); + + VectorAngles( vel, angles ); + SetLocalAngles( angles ); + + SetTouch( &CBaseGrenadeTimed::BounceTouch ); // Bounce if touched + + // Take one second off of the desired detonation time and set the think to PreDetonate. PreDetonate + // will insert a DANGER sound into the world sound list and delay detonation for one second so that + // the grenade explodes after the exact amount of time specified in the call to ShootTimed(). + + SetThink( &CBaseGrenadeTimed::TumbleThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + // if the delay is < 0.1 seconds, don't fly anywhere + if ((m_flDetonateTime - gpGlobals->curtime) < 0.1) + { + SetNextThink( gpGlobals->curtime ); + SetAbsVelocity( vec3_origin ); + } + + // Tumble through the air + // pGrenade->m_vecAngVelocity.x = -400; + SetGravity(1.0); // Don't change or throw calculations will be off! + SetFriction(0.8); + + m_flDamage = 100; // ???? + + m_takedamage = DAMAGE_NO; +} + + + +void CBaseGrenadeTimed::Precache( void ) +{ + BaseClass::Precache( ); + + PrecacheModel("models/weapons/w_grenade.mdl"); +} diff --git a/sp/src/game/server/basemultiplayerplayer.cpp b/sp/src/game/server/basemultiplayerplayer.cpp new file mode 100644 index 00000000..b6ecd44c --- /dev/null +++ b/sp/src/game/server/basemultiplayerplayer.cpp @@ -0,0 +1,349 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//============================================================================= + +#include "cbase.h" +#include "mp_shareddefs.h" +#include "basemultiplayerplayer.h" + +// Minimum interval between rate-limited commands that players can run. +#define COMMAND_MAX_RATE 0.3 + +CBaseMultiplayerPlayer::CBaseMultiplayerPlayer() +{ + m_iCurrentConcept = MP_CONCEPT_NONE; + m_flLastForcedChangeTeamTime = -1; + m_iBalanceScore = 0; + m_flConnectionTime = gpGlobals->curtime; + + // per life achievement counters + m_pAchievementKV = new KeyValues( "achievement_counts" ); + + m_flAreaCaptureScoreAccumulator = 0.0f; +} + +CBaseMultiplayerPlayer::~CBaseMultiplayerPlayer() +{ + m_pAchievementKV->deleteThis(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CAI_Expresser *CBaseMultiplayerPlayer::CreateExpresser( void ) +{ + m_pExpresser = new CMultiplayer_Expresser(this); + if ( !m_pExpresser) + return NULL; + + m_pExpresser->Connect(this); + return m_pExpresser; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseMultiplayerPlayer::PostConstructor( const char *szClassname ) +{ + BaseClass::PostConstructor( szClassname ); + CreateExpresser(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseMultiplayerPlayer::ModifyOrAppendCriteria( AI_CriteriaSet& criteriaSet ) +{ + BaseClass::ModifyOrAppendCriteria( criteriaSet ); + + ModifyOrAppendPlayerCriteria( criteriaSet ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseMultiplayerPlayer::SpeakIfAllowed( AIConcept_t concept, const char *modifiers, char *pszOutResponseChosen, size_t bufsize, IRecipientFilter *filter ) +{ + if ( !IsAlive() ) + return false; + + //if ( IsAllowedToSpeak( concept, bRespondingToPlayer ) ) + return Speak( concept, modifiers, pszOutResponseChosen, bufsize, filter ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +IResponseSystem *CBaseMultiplayerPlayer::GetResponseSystem() +{ + return BaseClass::GetResponseSystem(); + // NOTE: This is where you would hook your custom responses. +// return <*>GameRules()->m_ResponseRules[iIndex].m_ResponseSystems[m_iCurrentConcept]; +} + +//----------------------------------------------------------------------------- +// Purpose: Doesn't actually speak the concept. Just finds a response in the system. You then have to play it yourself. +//----------------------------------------------------------------------------- +AI_Response *CBaseMultiplayerPlayer::SpeakConcept( int iConcept ) +{ + m_iCurrentConcept = iConcept; + return SpeakFindResponse( g_pszMPConcepts[iConcept] ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseMultiplayerPlayer::SpeakConceptIfAllowed( int iConcept, const char *modifiers, char *pszOutResponseChosen, size_t bufsize, IRecipientFilter *filter ) +{ + // Save the current concept. + m_iCurrentConcept = iConcept; + return SpeakIfAllowed( g_pszMPConcepts[iConcept], modifiers, pszOutResponseChosen, bufsize, filter ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseMultiplayerPlayer::CanHearAndReadChatFrom( CBasePlayer *pPlayer ) +{ + // can always hear the console unless we're ignoring all chat + if ( !pPlayer ) + return m_iIgnoreGlobalChat != CHAT_IGNORE_ALL; + + // check if we're ignoring all chat + if ( m_iIgnoreGlobalChat == CHAT_IGNORE_ALL ) + return false; + + // check if we're ignoring all but teammates + if ( m_iIgnoreGlobalChat == CHAT_IGNORE_TEAM && g_pGameRules->PlayerRelationship( this, pPlayer ) != GR_TEAMMATE ) + return false; + + // can't hear dead players if we're alive + if ( pPlayer->m_lifeState != LIFE_ALIVE && m_lifeState == LIFE_ALIVE ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseMultiplayerPlayer::ShouldRunRateLimitedCommand( const CCommand &args ) +{ + return ShouldRunRateLimitedCommand( args[0] ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseMultiplayerPlayer::ShouldRunRateLimitedCommand( const char *pszCommand ) +{ + const char *pcmd = pszCommand; + + int i = m_RateLimitLastCommandTimes.Find( pcmd ); + if ( i == m_RateLimitLastCommandTimes.InvalidIndex() ) + { + m_RateLimitLastCommandTimes.Insert( pcmd, gpGlobals->curtime ); + return true; + } + else if ( (gpGlobals->curtime - m_RateLimitLastCommandTimes[i]) < COMMAND_MAX_RATE ) + { + // Too fast. + return false; + } + else + { + m_RateLimitLastCommandTimes[i] = gpGlobals->curtime; + return true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseMultiplayerPlayer::ClientCommand( const CCommand &args ) +{ + const char *pcmd = args[0]; + + if ( FStrEq( pcmd, "ignoremsg" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + m_iIgnoreGlobalChat = (m_iIgnoreGlobalChat + 1) % 3; + switch( m_iIgnoreGlobalChat ) + { + case CHAT_IGNORE_NONE: + ClientPrint( this, HUD_PRINTTALK, "#Accept_All_Messages" ); + break; + case CHAT_IGNORE_ALL: + ClientPrint( this, HUD_PRINTTALK, "#Ignore_Broadcast_Messages" ); + break; + case CHAT_IGNORE_TEAM: + ClientPrint( this, HUD_PRINTTALK, "#Ignore_Broadcast_Team_Messages" ); + break; + default: + break; + } + } + return true; + } + + return BaseClass::ClientCommand( args ); +} + +bool CBaseMultiplayerPlayer::ShouldShowVoiceSubtitleToEnemy( void ) +{ + return false; +} + +//----------------------------------------------------------------------------- +// calculate a score for this player. higher is more likely to be switched +//----------------------------------------------------------------------------- +int CBaseMultiplayerPlayer::CalculateTeamBalanceScore( void ) +{ + // base score is 0 - ( seconds on server ) + float flTimeConnected = gpGlobals->curtime - m_flConnectionTime; + int iScore = 0 - (int)flTimeConnected; + + // if we were switched recently, score us way down + float flLastSwitchedTime = GetLastForcedChangeTeamTime(); + if ( flLastSwitchedTime > 0 && ( gpGlobals->curtime - flLastSwitchedTime ) < 300 ) + { + iScore -= 10000; + } + return iScore; +} + +void CBaseMultiplayerPlayer::Spawn( void ) +{ + ResetPerLifeCounters(); + + StopScoringEscortPoints(); + + BaseClass::Spawn(); +} + +void CBaseMultiplayerPlayer::AwardAchievement( int iAchievement, int iCount ) +{ + Assert( iAchievement >= 0 && iAchievement < 0xFFFF ); // must fit in short + + CSingleUserRecipientFilter filter( this ); + + UserMessageBegin( filter, "AchievementEvent" ); + WRITE_SHORT( iAchievement ); + WRITE_SHORT( iCount ); + MessageEnd(); +} + +#ifdef _DEBUG + + #include "utlbuffer.h" + + void DumpAchievementCounters( const CCommand &args ) + { + int iPlayerIndex = 1; + + if ( args.ArgC() >= 2 ) + { + iPlayerIndex = atoi( args[1] ); + } + + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->GetPerLifeCounterKeys() ) + { + CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); + pPlayer->GetPerLifeCounterKeys()->RecursiveSaveToFile( buf, 0 ); + + char szBuf[1024]; + + // probably not the best way to print out a CUtlBuffer + int pos = 0; + while ( buf.PeekStringLength() ) + { + szBuf[pos] = buf.GetChar(); + pos++; + } + szBuf[pos] = '\0'; + + Msg( "%s\n", szBuf ); + } + } + ConCommand dump_achievement_counters( "dump_achievement_counters", DumpAchievementCounters, "Spew the per-life achievement counters for multiplayer players", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); + +#endif // _DEBUG + +int CBaseMultiplayerPlayer::GetPerLifeCounterKV( const char *name ) +{ + return m_pAchievementKV->GetInt( name, 0 ); +} + +void CBaseMultiplayerPlayer::SetPerLifeCounterKV( const char *name, int value ) +{ + m_pAchievementKV->SetInt( name, value ); +} + +void CBaseMultiplayerPlayer::ResetPerLifeCounters( void ) +{ + m_pAchievementKV->Clear(); +} + + +ConVar tf_escort_score_rate( "tf_escort_score_rate", "1", FCVAR_CHEAT, "Score for escorting the train, in points per second" ); + +#define ESCORT_SCORE_CONTEXT "AreaScoreContext" +#define ESCORT_SCORE_INTERVAL 0.1 + +//----------------------------------------------------------------------------- +// Purpose: think to accumulate and award points for escorting the train +//----------------------------------------------------------------------------- +void CBaseMultiplayerPlayer::EscortScoringThink( void ) +{ + m_flAreaCaptureScoreAccumulator += ESCORT_SCORE_INTERVAL; + + if ( m_flCapPointScoreRate > 0 ) + { + float flTimeForOnePoint = 1.0f / m_flCapPointScoreRate; + + int iPoints = 0; + + while ( m_flAreaCaptureScoreAccumulator >= flTimeForOnePoint ) + { + m_flAreaCaptureScoreAccumulator -= flTimeForOnePoint; + iPoints++; + } + + if ( iPoints > 0 ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_escort_score" ); + if ( event ) + { + event->SetInt( "player", entindex() ); + event->SetInt( "points", iPoints ); + gameeventmanager->FireEvent( event, true /* only to server */ ); + } + } + } + + SetContextThink( &CBaseMultiplayerPlayer::EscortScoringThink, gpGlobals->curtime + ESCORT_SCORE_INTERVAL, ESCORT_SCORE_CONTEXT ); +} + +//----------------------------------------------------------------------------- +// Purpose: We're escorting the train, start giving points +//----------------------------------------------------------------------------- +void CBaseMultiplayerPlayer::StartScoringEscortPoints( float flRate ) +{ + Assert( flRate > 0.0f ); + m_flCapPointScoreRate = flRate; + SetContextThink( &CBaseMultiplayerPlayer::EscortScoringThink, gpGlobals->curtime + ESCORT_SCORE_INTERVAL, ESCORT_SCORE_CONTEXT ); +} + +//----------------------------------------------------------------------------- +// Purpose: Stopped escorting the train +//----------------------------------------------------------------------------- +void CBaseMultiplayerPlayer::StopScoringEscortPoints( void ) +{ + SetContextThink( NULL, 0, ESCORT_SCORE_CONTEXT ); +} + diff --git a/sp/src/game/server/basemultiplayerplayer.h b/sp/src/game/server/basemultiplayerplayer.h new file mode 100644 index 00000000..06a0e00d --- /dev/null +++ b/sp/src/game/server/basemultiplayerplayer.h @@ -0,0 +1,127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//============================================================================= +#ifndef BASEMULTIPLAYERPLAYER_H +#define BASEMULTIPLAYERPLAYER_H +#pragma once + +#include "player.h" +#include "ai_speech.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CBaseMultiplayerPlayer : public CAI_ExpresserHost +{ + + DECLARE_CLASS( CBaseMultiplayerPlayer, CAI_ExpresserHost ); + +public: + + CBaseMultiplayerPlayer(); + ~CBaseMultiplayerPlayer(); + + virtual void Spawn( void ); + + virtual void PostConstructor( const char *szClassname ); + virtual void ModifyOrAppendCriteria( AI_CriteriaSet& criteriaSet ); + + virtual bool SpeakIfAllowed( AIConcept_t concept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); + virtual IResponseSystem *GetResponseSystem(); + AI_Response *SpeakConcept( int iConcept ); + virtual bool SpeakConceptIfAllowed( int iConcept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); + + virtual bool CanHearAndReadChatFrom( CBasePlayer *pPlayer ); + virtual bool CanSpeak( void ) { return true; } + virtual bool CanBeAutobalanced() { return true; } + + virtual void Precache( void ) + { + PrecacheParticleSystem( "achieved" ); + + BaseClass::Precache(); + } + + virtual bool ClientCommand( const CCommand &args ); + + virtual bool CanSpeakVoiceCommand( void ) { return true; } + virtual bool ShouldShowVoiceSubtitleToEnemy( void ); + virtual void NoteSpokeVoiceCommand( const char *pszScenePlayed ) {} + + virtual void OnAchievementEarned( int iAchievement ) {} + + enum + { + CHAT_IGNORE_NONE = 0, + CHAT_IGNORE_ALL, + CHAT_IGNORE_TEAM, + }; + + int m_iIgnoreGlobalChat; + + //--------------------------------- + // Speech support + virtual CAI_Expresser *GetExpresser() { return m_pExpresser; } + virtual CMultiplayer_Expresser *GetMultiplayerExpresser() { return m_pExpresser; } + + void SetLastForcedChangeTeamTimeToNow( void ) { m_flLastForcedChangeTeamTime = gpGlobals->curtime; } + float GetLastForcedChangeTeamTime( void ) { return m_flLastForcedChangeTeamTime; } + + void SetTeamBalanceScore( int iScore ) { m_iBalanceScore = iScore; } + int GetTeamBalanceScore( void ) { return m_iBalanceScore; } + + virtual int CalculateTeamBalanceScore( void ); + + void AwardAchievement( int iAchievement, int iCount = 1 ); + int GetPerLifeCounterKV( const char *name ); + void SetPerLifeCounterKV( const char *name, int value ); + void ResetPerLifeCounters( void ); + + KeyValues *GetPerLifeCounterKeys( void ) { return m_pAchievementKV; } + + void EscortScoringThink( void ); + void StartScoringEscortPoints( float flRate ); + void StopScoringEscortPoints( void ); + float m_flAreaCaptureScoreAccumulator; + float m_flCapPointScoreRate; + + float GetConnectionTime( void ) { return m_flConnectionTime; } + + // Command rate limiting. + bool ShouldRunRateLimitedCommand( const CCommand &args ); + bool ShouldRunRateLimitedCommand( const char *pszCommand ); + +protected: + virtual CAI_Expresser *CreateExpresser( void ); + + int m_iCurrentConcept; +private: + //--------------------------------- + CMultiplayer_Expresser *m_pExpresser; + + float m_flConnectionTime; + float m_flLastForcedChangeTeamTime; + + int m_iBalanceScore; // a score used to determine which players are switched to balance the teams + + KeyValues *m_pAchievementKV; + + // This lets us rate limit the commands the players can execute so they don't overflow things like reliable buffers. + CUtlDict m_RateLimitLastCommandTimes; +}; + +//----------------------------------------------------------------------------- +// Inline methods +//----------------------------------------------------------------------------- +inline CBaseMultiplayerPlayer *ToBaseMultiplayerPlayer( CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() ) + return NULL; +#if _DEBUG + return dynamic_cast( pEntity ); +#else + return static_cast( pEntity ); +#endif +} + +#endif // BASEMULTIPLAYERPLAYER_H diff --git a/sp/src/game/server/basetempentity.cpp b/sp/src/game/server/basetempentity.cpp new file mode 100644 index 00000000..a238397e --- /dev/null +++ b/sp/src/game/server/basetempentity.cpp @@ -0,0 +1,126 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "mathlib/mathlib.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CBaseTempEntity, DT_BaseTempEntity) +END_SEND_TABLE() + + + + +// Global list of temp entity event classes +CBaseTempEntity *CBaseTempEntity::s_pTempEntities = NULL; + +//----------------------------------------------------------------------------- +// Purpose: Returns head of list +// Output : CBaseTempEntity * -- head of list +//----------------------------------------------------------------------------- +CBaseTempEntity *CBaseTempEntity::GetList( void ) +{ + return s_pTempEntities; +} + +//----------------------------------------------------------------------------- +// Purpose: Creates temp entity, sets name, adds to global list +// Input : *name - +//----------------------------------------------------------------------------- +CBaseTempEntity::CBaseTempEntity( const char *name ) +{ + m_pszName = name; + Assert( m_pszName ); + + // Add to list + m_pNext = s_pTempEntities; + s_pTempEntities = this; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseTempEntity::~CBaseTempEntity( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Get the name of this temp entity +// Output : const char * +//----------------------------------------------------------------------------- +const char *CBaseTempEntity::GetName( void ) +{ + return m_pszName ? m_pszName : "Unnamed"; +} + +//----------------------------------------------------------------------------- +// Purpose: Get next temp ent in chain +// Output : CBaseTempEntity * +//----------------------------------------------------------------------------- +CBaseTempEntity *CBaseTempEntity::GetNext( void ) +{ + return m_pNext; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTempEntity::Precache( void ) +{ + // Nothing... +} + +//----------------------------------------------------------------------------- +// Purpose: Default test implementation. Should only be called by derived classes +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CBaseTempEntity::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + Vector origin, forward; + + Msg( "%s\n", m_pszName ); + AngleVectors( current_angles, &forward ); + + VectorMA( current_origin, 20, forward, origin ); + + CBroadcastRecipientFilter filter; + + Create( filter, 0.0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called at startup to allow temp entities to precache any models/sounds that they need +//----------------------------------------------------------------------------- +void CBaseTempEntity::PrecacheTempEnts( void ) +{ + CBaseTempEntity *te = GetList(); + while ( te ) + { + te->Precache(); + te = te->GetNext(); + } +} + + +void CBaseTempEntity::Create( IRecipientFilter& filter, float delay ) +{ + // temp entities can't be reliable or part of the signon message, use real entities instead + Assert( !filter.IsReliable() && !filter.IsInitMessage() ); + Assert( delay >= -1 && delay <= 1); // 1 second max delay + + engine->PlaybackTempEntity( filter, delay, + (void *)this, GetServerClass()->m_pTable, GetServerClass()->m_ClassID ); +} diff --git a/sp/src/game/server/basetempentity.h b/sp/src/game/server/basetempentity.h new file mode 100644 index 00000000..bef09105 --- /dev/null +++ b/sp/src/game/server/basetempentity.h @@ -0,0 +1,65 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#if !defined( BASETEMPENTITY_H ) +#define BASETEMPENTITY_H +#ifdef _WIN32 +#pragma once +#endif + +#include "edict.h" + +// This is the base class for TEMP ENTITIES that use the +// event system to propagate +class CBaseTempEntity +{ +public: + DECLARE_CLASS_NOBASE( CBaseTempEntity ); + DECLARE_SERVERCLASS(); + + CBaseTempEntity( const char *name ); + virtual ~CBaseTempEntity( void ); + + const char *GetName( void ); + + // Force all derived classes to implement a test + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + virtual void Create( IRecipientFilter& filter, float delay = 0.0 ); + + virtual void Precache( void ); + + CBaseTempEntity *GetNext( void ); + + // Get list of tempentities + static CBaseTempEntity *GetList( void ); + + // Called at startup to allow temp entities to precache any models/sounds that they need + static void PrecacheTempEnts( void ); + + void NetworkStateChanged() {} // TE's are sent out right away so we don't track whether state changes or not, + // but we want to allow CNetworkVars. + void NetworkStateChanged( void *pVar ) {} + +private: + // Descriptive name, for when running tests + const char *m_pszName; + + // Next in chain + CBaseTempEntity *m_pNext; + + // ConVars add themselves to this list for the executable. Then ConVarMgr::Init() runs through + // all the console variables and registers them. + static CBaseTempEntity *s_pTempEntities; +}; + +#endif // BASETEMPENTITY_H diff --git a/sp/src/game/server/basetoggle.h b/sp/src/game/server/basetoggle.h new file mode 100644 index 00000000..877e48db --- /dev/null +++ b/sp/src/game/server/basetoggle.h @@ -0,0 +1,69 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: For the slow removing of the CBaseToggle entity +// only old entities that need it for backwards-compatibility should +// include this file +//=============================================================================// + +#ifndef BASETOGGLE_H +#define BASETOGGLE_H +#pragma once + +#include "baseentity.h" + + +class CBaseToggle : public CBaseEntity +{ + DECLARE_CLASS( CBaseToggle, CBaseEntity ); +public: + CBaseToggle(); + + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual bool KeyValue( const char *szKeyName, Vector vec ) { return BaseClass::KeyValue( szKeyName, vec ); }; + virtual bool KeyValue( const char *szKeyName, float flValue ) { return BaseClass::KeyValue( szKeyName, flValue ); }; + + TOGGLE_STATE m_toggle_state; + float m_flMoveDistance;// how far a door should slide or rotate + float m_flWait; + float m_flLip; + + Vector m_vecPosition1; + Vector m_vecPosition2; + + QAngle m_vecMoveAng; + QAngle m_vecAngle1; + QAngle m_vecAngle2; + + float m_flHeight; + EHANDLE m_hActivator; + Vector m_vecFinalDest; + QAngle m_vecFinalAngle; + + int m_movementType; + + DECLARE_DATADESC(); + + virtual float GetDelay( void ) { return m_flWait; } + + // common member functions + void LinearMove( const Vector &vecDest, float flSpeed ); + void LinearMoveDone( void ); + void AngularMove( const QAngle &vecDestAngle, float flSpeed ); + void AngularMoveDone( void ); + bool IsLockedByMaster( void ); + virtual void MoveDone( void ); + + static float AxisValue( int flags, const QAngle &angles ); + void AxisDir( void ); + static float AxisDelta( int flags, const QAngle &angle1, const QAngle &angle2 ); + + string_t m_sMaster; // If this button has a master switch, this is the targetname. + // A master switch must be of the multisource type. If all + // of the switches in the multisource have been triggered, then + // the button will be allowed to operate. Otherwise, it will be + // deactivated. +}; + + + +#endif // BASETOGGLE_H diff --git a/sp/src/game/server/baseviewmodel.cpp b/sp/src/game/server/baseviewmodel.cpp new file mode 100644 index 00000000..0936b97d --- /dev/null +++ b/sp/src/game/server/baseviewmodel.cpp @@ -0,0 +1,116 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "animation.h" +#include "baseviewmodel.h" +#include "player.h" +#include +#include "studio.h" +#include "vguiscreen.h" +#include "saverestore_utlvector.h" +#include "hltvdirector.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void SendProxy_AnimTime( const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_SequenceChanged( const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ); + +//----------------------------------------------------------------------------- +// Purpose: Save Data for Base Weapon object +//-----------------------------------------------------------------------------// +BEGIN_DATADESC( CBaseViewModel ) + + DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ), + +// Client only +// DEFINE_FIELD( m_LagAnglesHistory, CInterpolatedVar < QAngle > ), +// DEFINE_FIELD( m_vLagAngles, FIELD_VECTOR ), + + DEFINE_FIELD( m_nViewModelIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_flTimeWeaponIdle, FIELD_FLOAT ), + DEFINE_FIELD( m_nAnimationParity, FIELD_INTEGER ), + + // Client only +// DEFINE_FIELD( m_nOldAnimationParity, FIELD_INTEGER ), + + DEFINE_FIELD( m_vecLastFacing, FIELD_VECTOR ), + DEFINE_FIELD( m_hWeapon, FIELD_EHANDLE ), + DEFINE_UTLVECTOR( m_hScreens, FIELD_EHANDLE ), + +// Read from weapons file +// DEFINE_FIELD( m_sVMName, FIELD_STRING ), +// DEFINE_FIELD( m_sAnimationPrefix, FIELD_STRING ), + +// --------------------------------------------------------------------- + +// Don't save these, init to 0 and regenerate +// DEFINE_FIELD( m_Activity, FIELD_INTEGER ), + +END_DATADESC() + +int CBaseViewModel::UpdateTransmitState() +{ + if ( IsEffectActive( EF_NODRAW ) ) + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } + + return SetTransmitState( FL_EDICT_FULLCHECK ); +} + +int CBaseViewModel::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + // check if receipient owns this weapon viewmodel + CBasePlayer *pOwner = ToBasePlayer( m_hOwner ); + + if ( pOwner && pOwner->edict() == pInfo->m_pClientEnt ) + { + return FL_EDICT_ALWAYS; + } + + // check if recipient spectates the own of this viewmodel + CBaseEntity *pRecipientEntity = CBaseEntity::Instance( pInfo->m_pClientEnt ); + + if ( pRecipientEntity->IsPlayer() ) + { + CBasePlayer *pPlayer = static_cast( pRecipientEntity ); +#ifndef _XBOX + if ( pPlayer->IsHLTV() || pPlayer->IsReplay() ) + { + // if this is the HLTV client, transmit all viewmodels in our PVS + return FL_EDICT_PVSCHECK; + } +#endif + if ( (pPlayer->GetObserverMode() == OBS_MODE_IN_EYE) && (pPlayer->GetObserverTarget() == pOwner) ) + { + return FL_EDICT_ALWAYS; + } + } + + // Don't send to anyone else except the local player or his spectators + return FL_EDICT_DONTSEND; +} + +void CBaseViewModel::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + // Are we already marked for transmission? + if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) + return; + + BaseClass::SetTransmit( pInfo, bAlways ); + + // Force our screens to be sent too. + for ( int i=0; i < m_hScreens.Count(); i++ ) + { + CVGuiScreen *pScreen = m_hScreens[i].Get(); + if ( pScreen ) + pScreen->SetTransmit( pInfo, bAlways ); + } +} diff --git a/sp/src/game/server/baseviewmodel.h b/sp/src/game/server/baseviewmodel.h new file mode 100644 index 00000000..6560bbd8 --- /dev/null +++ b/sp/src/game/server/baseviewmodel.h @@ -0,0 +1,17 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Server side view model object +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#if !defined( BASEVIEWMODEL_H ) +#define BASEVIEWMODEL_H +#ifdef _WIN32 +#pragma once +#endif + +#include "baseviewmodel_shared.h" + +#endif // BASEVIEWMODEL_H \ No newline at end of file diff --git a/sp/src/game/server/bitstring.cpp b/sp/src/game/server/bitstring.cpp new file mode 100644 index 00000000..21d424d3 --- /dev/null +++ b/sp/src/game/server/bitstring.cpp @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Arbitrary length bit string +// ** NOTE: This class does NOT override the bitwise operators +// as doing so would require overriding the operators +// to allocate memory for the returned bitstring. This method +// would be prone to memory leaks as the calling party +// would have to remember to delete the memory. Funtions +// are used instead to require the calling party to allocate +// and destroy their own memory +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +#include + +#include "bitstring.h" +#include "utlbuffer.h" +#include "tier0/dbg.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + diff --git a/sp/src/game/server/bitstring.h b/sp/src/game/server/bitstring.h new file mode 100644 index 00000000..f3c063f6 --- /dev/null +++ b/sp/src/game/server/bitstring.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Arbitrary length bit string +// ** NOTE: This class does NOT override the bitwise operators +// as doing so would require overriding the operators +// to allocate memory for the returned bitstring. This method +// would be prone to memory leaks as the calling party +// would have to remember to delete the memory. Funtions +// are used instead to require the calling party to allocate +// and destroy their own memory +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef BITSTRING_H +#define BITSTRING_H +#pragma once + +#include "bitvec.h" + + +#endif // BITSTRING_H diff --git a/sp/src/game/server/bmodels.cpp b/sp/src/game/server/bmodels.cpp new file mode 100644 index 00000000..e8291f8f --- /dev/null +++ b/sp/src/game/server/bmodels.cpp @@ -0,0 +1,1522 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Spawn, think, and use functions for common brush entities. +// +//=============================================================================// + +#include "cbase.h" +#include "doors.h" +#include "mathlib/mathlib.h" +#include "physics.h" +#include "ndebugoverlay.h" +#include "engine/IEngineSound.h" +#include "globals.h" +#include "filters.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//Tony; moved the spawnflags to util.h to prevent more mistakes in the future. + +// =================== FUNC_WALL ============================================== +class CFuncWall : public CBaseEntity +{ +public: + DECLARE_DATADESC(); + DECLARE_CLASS( CFuncWall, CBaseEntity ); + void Spawn( void ); + bool CreateVPhysics( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + int m_nState; +}; + +LINK_ENTITY_TO_CLASS( func_wall, CFuncWall ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CFuncWall ) + + DEFINE_FIELD( m_nState, FIELD_INTEGER ), + +END_DATADESC() + +void CFuncWall::Spawn( void ) +{ + SetLocalAngles( vec3_angle ); + SetMoveType( MOVETYPE_PUSH ); // so it doesn't get pushed by anything + SetModel( STRING( GetModelName() ) ); + + // If it can't move/go away, it's really part of the world + AddFlag( FL_WORLDBRUSH ); + + // set manual mode + CreateVPhysics(); +} + + +bool CFuncWall::CreateVPhysics( void ) +{ + SetSolid( SOLID_BSP ); + IPhysicsObject *pPhys = VPhysicsInitStatic(); + if ( pPhys ) + { + int contents = modelinfo->GetModelContents( GetModelIndex() ); + if ( ! (contents & (MASK_SOLID|MASK_PLAYERSOLID|MASK_NPCSOLID)) ) + { + // leave the physics shadow there in case it has crap constrained to it + // but disable collisions with it + pPhys->EnableCollisions( false ); + } + } + + return true; +} + + +void CFuncWall::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( ShouldToggle( useType, m_nState ) ) + { + m_nState = 1 - m_nState; + } +} + + +#define SF_WALL_START_OFF 0x0001 + +class CFuncWallToggle : public CFuncWall +{ +public: + DECLARE_CLASS( CFuncWallToggle, CFuncWall ); + + DECLARE_DATADESC(); + + void Spawn( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void InputToggle( inputdata_t &inputdata ); + + void TurnOff( void ); + void TurnOn( void ); + bool IsOn( void ); +}; + +BEGIN_DATADESC( CFuncWallToggle ) + + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( func_wall_toggle, CFuncWallToggle ); + +void CFuncWallToggle::Spawn( void ) +{ + BaseClass::Spawn(); + if ( HasSpawnFlags( SF_WALL_START_OFF ) ) + TurnOff(); + + SetMoveType( MOVETYPE_PUSH ); +} + + +void CFuncWallToggle::TurnOff( void ) +{ + IPhysicsObject *pPhys = VPhysicsGetObject(); + if ( pPhys ) + { + pPhys->EnableCollisions( false ); + } + AddSolidFlags( FSOLID_NOT_SOLID ); + AddEffects( EF_NODRAW ); +} + + +void CFuncWallToggle::TurnOn( void ) +{ + IPhysicsObject *pPhys = VPhysicsGetObject(); + if ( pPhys ) + { + pPhys->EnableCollisions( true ); + } + RemoveSolidFlags( FSOLID_NOT_SOLID ); + RemoveEffects( EF_NODRAW ); +} + + +bool CFuncWallToggle::IsOn( void ) +{ + if ( IsSolidFlagSet( FSOLID_NOT_SOLID ) ) + return false; + return true; +} + +void CFuncWallToggle::InputToggle( inputdata_t &inputdata ) +{ + int status = IsOn(); + + if ( ShouldToggle( USE_TOGGLE, status ) ) + { + if ( status ) + TurnOff(); + else + TurnOn(); + } +} + +//Adrian - Is this function needed at all? +void CFuncWallToggle::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + int status = IsOn(); + + if ( ShouldToggle( useType, status ) ) + { + if ( status ) + TurnOff(); + else + TurnOn(); + } +} + +//============================== FUNC_VEHICLECLIP ===================================== +class CFuncVehicleClip : public CBaseEntity +{ +public: + DECLARE_CLASS( CFuncVehicleClip, CBaseEntity ); + DECLARE_DATADESC(); + + void Spawn(); + bool CreateVPhysics( void ); + + void InputEnable( inputdata_t &data ); + void InputDisable( inputdata_t &data ); + +private: +}; + +BEGIN_DATADESC( CFuncVehicleClip ) + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_vehicleclip, CFuncVehicleClip ); + +void CFuncVehicleClip::Spawn() +{ + + SetLocalAngles( vec3_angle ); + SetMoveType( MOVETYPE_PUSH ); // so it doesn't get pushed by anything + SetModel( STRING( GetModelName() ) ); + + // It's part of the world + AddFlag( FL_WORLDBRUSH ); + + CreateVPhysics(); + + AddEffects( EF_NODRAW ); // make entity invisible + + SetCollisionGroup( COLLISION_GROUP_VEHICLE_CLIP ); +} + +bool CFuncVehicleClip::CreateVPhysics( void ) +{ + SetSolid( SOLID_BSP ); + VPhysicsInitStatic(); + + return true; +} + +void CFuncVehicleClip::InputEnable( inputdata_t &data ) +{ + IPhysicsObject *pPhys = VPhysicsGetObject(); + if ( pPhys ) + { + pPhys->EnableCollisions( true ); + } + RemoveSolidFlags( FSOLID_NOT_SOLID ); +} + +void CFuncVehicleClip::InputDisable( inputdata_t &data ) +{ + IPhysicsObject *pPhys = VPhysicsGetObject(); + if ( pPhys ) + { + pPhys->EnableCollisions( false ); + } + AddSolidFlags( FSOLID_NOT_SOLID ); +} + +//============================= FUNC_CONVEYOR ======================================= + +#define SF_CONVEYOR_VISUAL 0x0001 +#define SF_CONVEYOR_NOTSOLID 0x0002 + +class CFuncConveyor : public CFuncWall +{ +public: + DECLARE_CLASS( CFuncConveyor, CFuncWall ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CFuncConveyor(); + + void Spawn( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void UpdateSpeed( float flNewSpeed ); + + void GetGroundVelocityToApply( Vector &vecGroundVel ); + + // Input handlers. + void InputToggleDirection( inputdata_t &inputdata ); + void InputSetSpeed( inputdata_t &inputdata ); + +private: + + Vector m_vecMoveDir; + CNetworkVar( float, m_flConveyorSpeed ); +}; + +LINK_ENTITY_TO_CLASS( func_conveyor, CFuncConveyor ); + +BEGIN_DATADESC( CFuncConveyor ) + + DEFINE_INPUTFUNC( FIELD_VOID, "ToggleDirection", InputToggleDirection ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetSpeed", InputSetSpeed ), + + DEFINE_KEYFIELD( m_vecMoveDir, FIELD_VECTOR, "movedir" ), + DEFINE_FIELD( m_flConveyorSpeed, FIELD_FLOAT ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST(CFuncConveyor, DT_FuncConveyor) + SendPropFloat( SENDINFO(m_flConveyorSpeed), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + + +CFuncConveyor::CFuncConveyor() +{ + m_flConveyorSpeed = 0.0; +} + +void CFuncConveyor::Spawn( void ) +{ + // Convert movedir from angles to a vector + QAngle angMoveDir = QAngle( m_vecMoveDir.x, m_vecMoveDir.y, m_vecMoveDir.z ); + AngleVectors( angMoveDir, &m_vecMoveDir ); + + BaseClass::Spawn(); + + if ( !HasSpawnFlags(SF_CONVEYOR_VISUAL) ) + AddFlag( FL_CONVEYOR ); + + // HACKHACK - This is to allow for some special effects + if ( HasSpawnFlags( SF_CONVEYOR_NOTSOLID ) ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + if ( m_flSpeed == 0 ) + m_flSpeed = 100; + + UpdateSpeed( m_flSpeed ); +} + + +void CFuncConveyor::UpdateSpeed( float flNewSpeed ) +{ + m_flConveyorSpeed = flNewSpeed; +} + + +void CFuncConveyor::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + m_flSpeed = -m_flSpeed; + UpdateSpeed( m_flSpeed ); +} + + +void CFuncConveyor::InputToggleDirection( inputdata_t &inputdata ) +{ + Use( inputdata.pActivator, inputdata.pCaller, USE_TOGGLE, 0 ); +} + + +void CFuncConveyor::InputSetSpeed( inputdata_t &inputdata ) +{ + m_flSpeed = inputdata.value.Float(); + UpdateSpeed( m_flSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the velocity imparted to players standing on us. +//----------------------------------------------------------------------------- +void CFuncConveyor::GetGroundVelocityToApply( Vector &vecGroundVel ) +{ + vecGroundVel = m_vecMoveDir * m_flSpeed; +} + + +// =================== FUNC_ILLUSIONARY ============================================== +// A simple entity that looks solid but lets you walk through it. +class CFuncIllusionary : public CBaseEntity +{ + DECLARE_CLASS( CFuncIllusionary, CBaseEntity ); +public: + void Spawn( void ); +}; + +LINK_ENTITY_TO_CLASS( func_illusionary, CFuncIllusionary ); + +void CFuncIllusionary::Spawn( void ) +{ + SetLocalAngles( vec3_angle ); + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_NONE ); + SetModel( STRING( GetModelName() ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: A rotating brush entity. +// +// You need to have an origin brush as part of this entity. The +// center of that brush will be the point around which it is rotated. +// +// It will rotate around the Z axis by default. Spawnflags can be set +// to make it rotate around the X or Y axes. +// +// The direction of rotation is also controlled by a spawnflag. +//----------------------------------------------------------------------------- +class CFuncRotating : public CBaseEntity +{ + DECLARE_CLASS( CFuncRotating, CBaseEntity ); +public: + // basic functions + void Spawn( void ); + void Precache( void ); + bool CreateVPhysics( void ); + void SpinUpMove( void ); + void SpinDownMove( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void HurtTouch ( CBaseEntity *pOther ); + void RotatingUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void RotateMove( void ); + void ReverseMove( void ); + void RampPitchVol( void ); + void Blocked( CBaseEntity *pOther ); + void SetTargetSpeed( float flSpeed ); + void UpdateSpeed( float flNewSpeed ); + + int DrawDebugTextOverlays(void); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + +protected: + bool SpinDown( float flTargetSpeed ); + float GetMoveSpeed( float flSpeed ); + + float GetNextMoveInterval() const; + + // Input handlers + void InputSetSpeed( inputdata_t &inputdata ); + void InputStart( inputdata_t &inputdata ); + void InputStop( inputdata_t &inputdata ); + void InputStartForward( inputdata_t &inputdata ); + void InputStartBackward( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputReverse( inputdata_t &inputdata ); + void InputStopAtStartPos( inputdata_t &inputdata ); + + QAngle m_vecMoveAng; + + float m_flFanFriction; + float m_flAttenuation; + float m_flVolume; + float m_flTargetSpeed; // Target value for m_flSpeed, used for spinning up and down. + float m_flMaxSpeed; // Maximum value for m_flSpeed, used for ramping sound effects. + float m_flBlockDamage; // Damage inflicted when blocked. + string_t m_NoiseRunning; + bool m_bReversed; + + QAngle m_angStart; + bool m_bStopAtStartPos; + + bool m_bSolidBsp; // Brush is SOLID_BSP + +#ifdef MAPBASE + int m_iMinPitch = 30; // FANPITCHMIN + int m_iMaxPitch = 100; // FANPITCHMAX +#endif + +public: + Vector m_vecClientOrigin; + QAngle m_vecClientAngles; +}; + +LINK_ENTITY_TO_CLASS( func_rotating, CFuncRotating ); + + +BEGIN_DATADESC( CFuncRotating ) + + DEFINE_FIELD( m_vecMoveAng, FIELD_VECTOR ), + DEFINE_FIELD( m_flFanFriction, FIELD_FLOAT ), + DEFINE_FIELD( m_flAttenuation, FIELD_FLOAT ), + DEFINE_FIELD( m_flVolume, FIELD_FLOAT ), + DEFINE_FIELD( m_flTargetSpeed, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flMaxSpeed, FIELD_FLOAT, "maxspeed" ), + DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), + DEFINE_KEYFIELD( m_NoiseRunning, FIELD_SOUNDNAME, "message" ), + DEFINE_FIELD( m_bReversed, FIELD_BOOLEAN ), + DEFINE_FIELD( m_angStart, FIELD_VECTOR ), + DEFINE_FIELD( m_bStopAtStartPos, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_bSolidBsp, FIELD_BOOLEAN, "solidbsp" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iMinPitch, FIELD_INTEGER, "minpitch" ), + DEFINE_KEYFIELD( m_iMaxPitch, FIELD_INTEGER, "maxpitch" ), +#endif + + // Function Pointers + DEFINE_FUNCTION( SpinUpMove ), + DEFINE_FUNCTION( SpinDownMove ), + DEFINE_FUNCTION( HurtTouch ), + DEFINE_FUNCTION( RotatingUse ), + DEFINE_FUNCTION( RotateMove ), + DEFINE_FUNCTION( ReverseMove ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), + DEFINE_INPUTFUNC( FIELD_VOID, "Start", InputStart ), + DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Reverse", InputReverse ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartForward", InputStartForward ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartBackward", InputStartBackward ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopAtStartPos", InputStopAtStartPos ), + +END_DATADESC() + +extern void SendProxy_Origin( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_FuncRotatingOrigin( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ +#ifdef TF_DLL + CFuncRotating *entity = (CFuncRotating*)pStruct; + Assert( entity ); + + if ( entity->HasSpawnFlags(SF_BRUSH_ROTATE_CLIENTSIDE) ) + { + const Vector *v = &entity->m_vecClientOrigin; + pOut->m_Vector[ 0 ] = v->x; + pOut->m_Vector[ 1 ] = v->y; + pOut->m_Vector[ 2 ] = v->z; + return; + } +#endif + + SendProxy_Origin( pProp, pStruct, pData, pOut, iElement, objectID ); +} + +/* +extern void SendProxy_Angles( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_FuncRotatingAngles( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CFuncRotating *entity = (CFuncRotating*)pStruct; + Assert( entity ); + if ( entity->HasSpawnFlags(SF_BRUSH_ROTATE_CLIENTSIDE) ) + { + const QAngle *a = &entity->m_vecClientAngles; + pOut->m_Vector[ 0 ] = anglemod( a->x ); + pOut->m_Vector[ 1 ] = anglemod( a->y ); + pOut->m_Vector[ 2 ] = anglemod( a->z ); + return; + } + + SendProxy_Angles( pProp, pStruct, pData, pOut, iElement, objectID ); +} +*/ +void SendProxy_FuncRotatingAngle( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID) +{ + CFuncRotating *entity = (CFuncRotating*)pStruct; + Assert( entity ); + + vec_t const *qa = (vec_t *)pData; + vec_t const *ea = entity->GetLocalAngles().Base(); + NOTE_UNUSED(ea); + // Assert its actually an index into m_angRotation if not this won't work + + Assert( (uintp)qa >= (uintp)ea && (uintp)qa < (uintp)ea + sizeof( QAngle )); + +#ifdef TF_DLL + if ( entity->HasSpawnFlags(SF_BRUSH_ROTATE_CLIENTSIDE) ) + { + const QAngle *a = &entity->m_vecClientAngles; + + pOut->m_Float = anglemod( (*a)[ qa - ea ] ); + return; + } +#endif + + pOut->m_Float = anglemod( *qa ); + + Assert( IsFinite( pOut->m_Float ) ); +} + + +extern void SendProxy_SimulationTime( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_FuncRotatingSimulationTime( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ) +{ +#ifdef TF_DLL + CFuncRotating *entity = (CFuncRotating*)pStruct; + Assert( entity ); + + if ( entity->HasSpawnFlags(SF_BRUSH_ROTATE_CLIENTSIDE) ) + { + pOut->m_Int = 0; + return; + } +#endif + + SendProxy_SimulationTime( pProp, pStruct, pVarData, pOut, iElement, objectID ); +} + +IMPLEMENT_SERVERCLASS_ST(CFuncRotating, DT_FuncRotating) + SendPropExclude( "DT_BaseEntity", "m_angRotation" ), + SendPropExclude( "DT_BaseEntity", "m_vecOrigin" ), + SendPropExclude( "DT_BaseEntity", "m_flSimulationTime" ), + + SendPropVector(SENDINFO(m_vecOrigin), -1, SPROP_COORD|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_FuncRotatingOrigin ), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 0), 13, SPROP_CHANGES_OFTEN, SendProxy_FuncRotatingAngle ), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 1), 13, SPROP_CHANGES_OFTEN, SendProxy_FuncRotatingAngle ), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 2), 13, SPROP_CHANGES_OFTEN, SendProxy_FuncRotatingAngle ), + + SendPropInt(SENDINFO(m_flSimulationTime), SIMULATION_TIME_WINDOW_BITS, SPROP_UNSIGNED|SPROP_CHANGES_OFTEN|SPROP_ENCODED_AGAINST_TICKCOUNT, SendProxy_FuncRotatingSimulationTime), +END_SEND_TABLE() + + + +//----------------------------------------------------------------------------- +// Purpose: Handles keyvalues from the BSP. Called before spawning. +//----------------------------------------------------------------------------- +bool CFuncRotating::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "fanfriction")) + { + m_flFanFriction = atof(szValue)/100; + } + else if (FStrEq(szKeyName, "Volume")) + { + m_flVolume = atof(szValue) / 10.0; + m_flVolume = clamp(m_flVolume, 0.0f, 1.0f); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been set. +//----------------------------------------------------------------------------- +void CFuncRotating::Spawn( ) +{ +#ifdef TF_DLL + AddSpawnFlags( SF_BRUSH_ROTATE_CLIENTSIDE ); +#endif + + // + // Maintain compatibility with previous maps. + // + if (m_flVolume == 0.0) + { + m_flVolume = 1.0; + } + + // + // If the designer didn't set a sound attenuation, default to one. + // + if ( HasSpawnFlags(SF_BRUSH_ROTATE_SMALLRADIUS) ) + { + m_flAttenuation = ATTN_IDLE; + } + else if ( HasSpawnFlags(SF_BRUSH_ROTATE_MEDIUMRADIUS) ) + { + m_flAttenuation = ATTN_STATIC; + } + else if ( HasSpawnFlags(SF_BRUSH_ROTATE_LARGERADIUS) ) + { + m_flAttenuation = ATTN_NORM; + } + else + { + m_flAttenuation = ATTN_NORM; + } + + // + // Prevent divide by zero if level designer forgets friction! + // + if ( m_flFanFriction == 0 ) + { + m_flFanFriction = 1; + } + + // + // Build the axis of rotation based on spawnflags. + // + if ( HasSpawnFlags(SF_BRUSH_ROTATE_Z_AXIS) ) + { + m_vecMoveAng = QAngle(0,0,1); + } + else if ( HasSpawnFlags(SF_BRUSH_ROTATE_X_AXIS) ) + { + m_vecMoveAng = QAngle(1,0,0); + } + else + { + m_vecMoveAng = QAngle(0,1,0); // y-axis + } + + // + // Check for reverse rotation. + // + if ( HasSpawnFlags(SF_BRUSH_ROTATE_BACKWARDS) ) + { + m_vecMoveAng = m_vecMoveAng * -1; + } + + SetSolid( SOLID_VPHYSICS ); + + // + // Some rotating objects like fake volumetric lights will not be solid. + // + if ( HasSpawnFlags(SF_ROTATING_NOT_SOLID) ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_PUSH ); + } + else + { + RemoveSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_PUSH ); + } + + SetModel( STRING( GetModelName() ) ); + + SetUse( &CFuncRotating::RotatingUse ); + + // + // Did level designer forget to assign a maximum speed? Prevent a divide by + // zero in RampPitchVol as well as allowing the rotator to work. + // + m_flMaxSpeed = fabs( m_flMaxSpeed ); + if (m_flMaxSpeed == 0) + { + m_flMaxSpeed = 100; + } + + // + // If the brush should be initially rotating, use it in a little while. + // + if ( HasSpawnFlags(SF_BRUSH_ROTATE_START_ON) ) + { + SetThink( &CFuncRotating::SUB_CallUseToggle ); + SetNextThink( gpGlobals->curtime + .2 ); // leave a magic delay for client to start up + } + + // + // Can this brush inflict pain? + // + if ( HasSpawnFlags(SF_BRUSH_HURT) ) + { + SetTouch( &CFuncRotating::HurtTouch ); + } + + // + // Set speed to 0 in case there's an old "speed" key lying around. + // + m_flSpeed = 0; + + Precache( ); + CreateVPhysics(); + + m_angStart = GetLocalAngles(); + + // Slam the object back to solid - if we really want it to be solid. + if ( m_bSolidBsp ) + { + SetSolid( SOLID_BSP ); + } + +#ifdef TF_DLL + if ( HasSpawnFlags(SF_BRUSH_ROTATE_CLIENTSIDE) ) + { + m_vecClientOrigin = GetLocalOrigin(); + m_vecClientAngles = GetLocalAngles(); + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFuncRotating::CreateVPhysics( void ) +{ + if ( !IsSolidFlagSet( FSOLID_NOT_SOLID )) + { + VPhysicsInitShadow( false, false ); + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncRotating::Precache( void ) +{ + // + // Set up rotation sound. + // + char *szSoundFile = ( char * )STRING( m_NoiseRunning ); + if ( !m_NoiseRunning || strlen( szSoundFile ) == 0 ) + { + // No sound set up, use the null sound. + m_NoiseRunning = AllocPooledString("DoorSound.Null"); + } + PrecacheScriptSound( STRING( m_NoiseRunning ) ); + + if (GetLocalAngularVelocity() != vec3_angle ) + { + // + // If fan was spinning, and we went through transition or save/restore, + // make sure we restart the sound. 1.5 sec delay is a magic number. + // + SetMoveDone( &CFuncRotating::SpinUpMove ); + SetMoveDoneTime( 1.5 ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Will hurt others based on how fast the brush is spinning. +// Input : pOther - +//----------------------------------------------------------------------------- +void CFuncRotating::HurtTouch ( CBaseEntity *pOther ) +{ + // we can't hurt this thing, so we're not concerned with it + if ( !pOther->m_takedamage ) + return; + + // calculate damage based on rotation speed + m_flBlockDamage = GetLocalAngularVelocity().Length() / 10; + +#ifdef HL1_DLL + if( m_flBlockDamage > 0 ) +#endif + { + pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); + + Vector vecNewVelocity = pOther->GetAbsOrigin() - WorldSpaceCenter(); + VectorNormalize(vecNewVelocity); + vecNewVelocity *= m_flBlockDamage; + pOther->SetAbsVelocity( vecNewVelocity ); + } +} + + +#ifdef MAPBASE +// In Mapbase, use the keyvalues instead +#define FANPITCHMIN m_iMinPitch +#define FANPITCHMAX m_iMaxPitch +#else +#define FANPITCHMIN 30 +#define FANPITCHMAX 100 +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Ramp pitch and volume up to maximum values, based on the difference +// between how fast we're going vs how fast we can go. +//----------------------------------------------------------------------------- +void CFuncRotating::RampPitchVol( void ) +{ + // + // Calc volume and pitch as % of maximum vol and pitch. + // + float fpct = fabs(m_flSpeed) / m_flMaxSpeed; + float fvol = clamp(m_flVolume * fpct, 0.f, 1.f); // slowdown volume ramps down to 0 + + float fpitch = FANPITCHMIN + (FANPITCHMAX - FANPITCHMIN) * fpct; + + int pitch = clamp(FastFloatToSmallInt(fpitch), 0, 255); + if (pitch == PITCH_NORM) + { + pitch = PITCH_NORM - 1; + } + + // + // Update the fan's volume and pitch. + // + CPASAttenuationFilter filter( GetAbsOrigin(), m_flAttenuation ); + filter.MakeReliable(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = STRING(m_NoiseRunning); + ep.m_flVolume = fvol; + ep.m_SoundLevel = ATTN_TO_SNDLVL( m_flAttenuation ); + ep.m_nFlags = SND_CHANGE_PITCH | SND_CHANGE_VOL; + ep.m_nPitch = pitch; + + EmitSound( filter, entindex(), ep ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : float +//----------------------------------------------------------------------------- +float CFuncRotating::GetNextMoveInterval() const +{ + if ( m_bStopAtStartPos ) + { + return TICK_INTERVAL; + } + return 0.1f; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the current speed to the given value and manages the sound effects. +// Input : flNewSpeed - New speed in degrees per second. +//----------------------------------------------------------------------------- +void CFuncRotating::UpdateSpeed( float flNewSpeed ) +{ + float flOldSpeed = m_flSpeed; + m_flSpeed = clamp( flNewSpeed, -m_flMaxSpeed, m_flMaxSpeed ); + + if ( m_bStopAtStartPos ) + { + int checkAxis = 2; + // See if we got close to the starting orientation + if ( m_vecMoveAng[0] != 0 ) + { + checkAxis = 0; + } + else if ( m_vecMoveAng[1] != 0 ) + { + checkAxis = 1; + } + + float angDelta = anglemod( GetLocalAngles()[ checkAxis ] - m_angStart[ checkAxis ] ); + if ( angDelta > 180.0f ) + { + angDelta -= 360.0f; + } + + if ( flNewSpeed < 100 ) + { + if ( flNewSpeed <= 25 && fabs( angDelta ) < 1.0f ) + { + m_flTargetSpeed = 0; + m_bStopAtStartPos = false; + m_flSpeed = 0.0f; + + SetLocalAngles( m_angStart ); + } + else if ( fabs( angDelta ) > 90.0f ) + { + // Keep rotating at same speed for now + m_flSpeed = flOldSpeed; + } + else + { + float minSpeed = fabs( angDelta ); + if ( minSpeed < 20 ) + minSpeed = 20; + + m_flSpeed = flOldSpeed > 0.0f ? minSpeed : -minSpeed; + } + } + } + + + if ( ( flOldSpeed == 0 ) && ( m_flSpeed != 0 ) ) + { + // Starting to move - emit the sound. + CPASAttenuationFilter filter( GetAbsOrigin(), m_flAttenuation ); + filter.MakeReliable(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = STRING(m_NoiseRunning); + ep.m_flVolume = 0.01; + ep.m_SoundLevel = ATTN_TO_SNDLVL( m_flAttenuation ); + ep.m_nPitch = FANPITCHMIN; + + EmitSound( filter, entindex(), ep ); + RampPitchVol(); + } + else if ( ( flOldSpeed != 0 ) && ( m_flSpeed == 0 ) ) + { + // Stopping - stop the sound. + StopSound( entindex(), CHAN_STATIC, STRING(m_NoiseRunning) ); + + } + else + { + // Changing speed - adjust the pitch and volume. + RampPitchVol(); + } + + SetLocalAngularVelocity( m_vecMoveAng * m_flSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function. Accelerates a func_rotating to a higher angular velocity. +//----------------------------------------------------------------------------- +void CFuncRotating::SpinUpMove( void ) +{ + // + // Calculate our new speed. + // + bool bSpinUpDone = false; + float flNewSpeed = fabs( m_flSpeed ) + 0.2 * m_flMaxSpeed * m_flFanFriction; + if ( fabs( flNewSpeed ) >= fabs( m_flTargetSpeed ) ) + { + // Reached our target speed. + flNewSpeed = m_flTargetSpeed; + bSpinUpDone = !m_bStopAtStartPos; + } + else if ( m_flTargetSpeed < 0 ) + { + // Spinning up in reverse - negate the speed. + flNewSpeed *= -1; + } + + // + // Apply the new speed, adjust sound pitch and volume. + // + UpdateSpeed( flNewSpeed ); + + // + // If we've met or exceeded target speed, stop spinning up. + // + if ( bSpinUpDone ) + { + SetMoveDone( &CFuncRotating::RotateMove ); + RotateMove(); + } + + SetMoveDoneTime( GetNextMoveInterval() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Decelerates the rotator from a higher speed to a lower one. +// Input : flTargetSpeed - Speed to spin down to. +// Output : Returns true if we reached the target speed, false otherwise. +//----------------------------------------------------------------------------- +bool CFuncRotating::SpinDown( float flTargetSpeed ) +{ + // + // Bleed off a little speed due to friction. + // + bool bSpinDownDone = false; + float flNewSpeed = fabs( m_flSpeed ) - 0.1 * m_flMaxSpeed * m_flFanFriction; + if ( flNewSpeed < 0 ) + { + flNewSpeed = 0; + } + + if ( fabs( flNewSpeed ) <= fabs( flTargetSpeed ) ) + { + // Reached our target speed. + flNewSpeed = flTargetSpeed; + bSpinDownDone = !m_bStopAtStartPos; + } + else if ( m_flSpeed < 0 ) + { + // Spinning down in reverse - negate the speed. + flNewSpeed *= -1; + } + + // + // Apply the new speed, adjust sound pitch and volume. + // + UpdateSpeed( flNewSpeed ); + + // + // If we've met or exceeded target speed, stop spinning down. + // + return bSpinDownDone; +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function. Decelerates a func_rotating to a lower angular velocity. +//----------------------------------------------------------------------------- +void CFuncRotating::SpinDownMove( void ) +{ + // + // If we've met or exceeded target speed, stop spinning down. + // + if ( SpinDown( m_flTargetSpeed ) ) + { + SetMoveDone( &CFuncRotating::RotateMove ); + RotateMove(); + } + else + { + SetMoveDoneTime( GetNextMoveInterval() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function for reversing directions. Spins down to zero, then +// starts spinning up to the target speed. +//----------------------------------------------------------------------------- +void CFuncRotating::ReverseMove( void ) +{ + if ( SpinDown( 0 ) ) + { + // We've reached zero - spin back up to the target speed. + SetTargetSpeed( m_flTargetSpeed ); + } + else + { + SetMoveDoneTime( GetNextMoveInterval() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function. Called while rotating at a constant angular velocity. +//----------------------------------------------------------------------------- +void CFuncRotating::RotateMove( void ) +{ + SetMoveDoneTime( 10 ); + +#ifdef MAPBASE + QAngle angNormalizedAngles = GetLocalAngles(); + if (m_vecMoveAng.x) + angNormalizedAngles.x = AngleNormalize( angNormalizedAngles.x ); + if (m_vecMoveAng.y) + angNormalizedAngles.y = AngleNormalize( angNormalizedAngles.y ); + if (m_vecMoveAng.z) + angNormalizedAngles.z = AngleNormalize( angNormalizedAngles.z ); + + SetLocalAngles(angNormalizedAngles); +#endif + + if ( m_bStopAtStartPos ) + { + SetMoveDoneTime( GetNextMoveInterval() ); + int checkAxis = 2; + + // See if we got close to the starting orientation + if ( m_vecMoveAng[0] != 0 ) + { + checkAxis = 0; + } + else if ( m_vecMoveAng[1] != 0 ) + { + checkAxis = 1; + } + + float angDelta = anglemod( GetLocalAngles()[ checkAxis ] - m_angStart[ checkAxis ] ); + if ( angDelta > 180.0f ) + angDelta -= 360.0f; + + QAngle avel = GetLocalAngularVelocity(); + // Delta per tick + QAngle avelpertick = avel * TICK_INTERVAL; + + if ( fabs( angDelta ) < fabs( avelpertick[ checkAxis ] ) ) + { + SetTargetSpeed( 0 ); + SetLocalAngles( m_angStart ); + m_bStopAtStartPos = false; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Used for debug output. Returns the given speed considering our current +// direction of rotation, so that positive values are forward and negative +// values are backward. +// Input : flSpeed - Angular speed in degrees per second. +//----------------------------------------------------------------------------- +float CFuncRotating::GetMoveSpeed( float flSpeed ) +{ + if ( m_vecMoveAng[0] != 0 ) + { + return flSpeed * m_vecMoveAng[0]; + } + + if ( m_vecMoveAng[1] != 0 ) + { + return flSpeed * m_vecMoveAng[1]; + } + + return flSpeed * m_vecMoveAng[2]; +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets a new angular velocity to achieve. +// Input : flSpeed - Target angular velocity in degrees per second. +//----------------------------------------------------------------------------- +void CFuncRotating::SetTargetSpeed( float flSpeed ) +{ + // + // Make sure the sign is correct - positive for forward rotation, + // negative for reverse rotation. + // + flSpeed = fabs( flSpeed ); + if ( m_bReversed ) + { + flSpeed *= -1; + } + + m_flTargetSpeed = flSpeed; + + // + // If we don't accelerate, change to the new speed instantly. + // + if ( !HasSpawnFlags(SF_BRUSH_ACCDCC ) ) + { + UpdateSpeed( m_flTargetSpeed ); + SetMoveDone( &CFuncRotating::RotateMove ); + } + // + // Otherwise deal with acceleration/deceleration: + // + else + { + // + // Check for reversing directions. + // + if ((( m_flSpeed > 0 ) && ( m_flTargetSpeed < 0 )) || + (( m_flSpeed < 0 ) && ( m_flTargetSpeed > 0 ))) + { + SetMoveDone( &CFuncRotating::ReverseMove ); + } + // + // If we are below the new target speed, spin up to the target speed. + // + else if ( fabs( m_flSpeed ) < fabs( m_flTargetSpeed ) ) + { + SetMoveDone( &CFuncRotating::SpinUpMove ); + } + // + // If we are above the new target speed, spin down to the target speed. + // + else if ( fabs( m_flSpeed ) > fabs( m_flTargetSpeed ) ) + { + SetMoveDone( &CFuncRotating::SpinDownMove ); + } + // + // We are already at the new target speed. Just keep rotating. + // + else + { + SetMoveDone( &CFuncRotating::RotateMove ); + } + } + + SetMoveDoneTime( GetNextMoveInterval() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when a rotating brush is used by the player. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CFuncRotating::RotatingUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // + // If the rotator is spinning, stop it. + // + if ( m_flSpeed != 0 ) + { + SetTargetSpeed( 0 ); + } + // + // Rotator is not moving, so start it. + // + else + { + SetTargetSpeed( m_flMaxSpeed ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that reverses the direction of rotation. +//----------------------------------------------------------------------------- +void CFuncRotating::InputReverse( inputdata_t &inputdata ) +{ + m_bStopAtStartPos = false; + m_bReversed = !m_bReversed; + SetTargetSpeed( m_flSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the speed of the rotator. +// Input : Float target angular velocity as a ratio of maximum speed [0, 1]. +//----------------------------------------------------------------------------- +void CFuncRotating::InputSetSpeed( inputdata_t &inputdata ) +{ + m_bStopAtStartPos = false; + float flSpeed = inputdata.value.Float(); + m_bReversed = flSpeed < 0 ? true : false; + flSpeed = fabs(flSpeed); + SetTargetSpeed( clamp( flSpeed, 0.f, 1.f ) * m_flMaxSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to start the rotator spinning. +//----------------------------------------------------------------------------- +void CFuncRotating::InputStart( inputdata_t &inputdata ) +{ + m_bStopAtStartPos = false; + SetTargetSpeed( m_flMaxSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to start the rotator spinning. +//----------------------------------------------------------------------------- +void CFuncRotating::InputStartForward( inputdata_t &inputdata ) +{ + m_bReversed = false; + SetTargetSpeed( m_flMaxSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to start the rotator spinning. +//----------------------------------------------------------------------------- +void CFuncRotating::InputStartBackward( inputdata_t &inputdata ) +{ + m_bStopAtStartPos = false; + m_bReversed = true; + SetTargetSpeed( m_flMaxSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to stop the rotator from spinning. +//----------------------------------------------------------------------------- +void CFuncRotating::InputStop( inputdata_t &inputdata ) +{ + m_bStopAtStartPos = false; + SetTargetSpeed( 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CFuncRotating::InputStopAtStartPos( inputdata_t &inputdata ) +{ + m_bStopAtStartPos = true; + SetTargetSpeed( 0 ); + SetMoveDoneTime( GetNextMoveInterval() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Starts the rotator if it is still, stops it if it is spinning. +//----------------------------------------------------------------------------- +void CFuncRotating::InputToggle( inputdata_t &inputdata ) +{ + if (m_flSpeed > 0) + { + SetTargetSpeed( 0 ); + } + else + { + SetTargetSpeed( m_flMaxSpeed ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: An entity has blocked the brush. +// Input : pOther - +//----------------------------------------------------------------------------- +void CFuncRotating::Blocked( CBaseEntity *pOther ) +{ +#ifdef HL1_DLL + if( m_flBlockDamage > 0 ) +#endif + pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CFuncRotating::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf( tempstr, sizeof( tempstr ),"Speed cur (target): %3.2f (%3.2f)", GetMoveSpeed( m_flSpeed ), GetMoveSpeed( m_flTargetSpeed ) ); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; + +} + + +class CFuncVPhysicsClip : public CBaseEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CFuncVPhysicsClip, CBaseEntity ); + +public: + void Spawn(); + void Activate(); + bool CreateVPhysics( void ); + + bool EntityPassesFilter( CBaseEntity *pOther ); + bool ForceVPhysicsCollide( CBaseEntity *pEntity ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetFilter( inputdata_t &inputdata ); +#endif + +private: + + string_t m_iFilterName; + CHandle m_hFilter; + bool m_bDisabled; +}; + +// Global Savedata for base trigger +BEGIN_DATADESC( CFuncVPhysicsClip ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + DEFINE_FIELD( m_hFilter, FIELD_EHANDLE ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), +#else + DEFINE_FIELD( m_bDisabled, FIELD_BOOLEAN ), +#endif + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_EHANDLE, "SetFilter", InputSetFilter ), +#endif + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( func_clip_vphysics, CFuncVPhysicsClip ); + +void CFuncVPhysicsClip::Spawn( void ) +{ + SetMoveType( MOVETYPE_PUSH ); // so it doesn't get pushed by anything + SetSolid( SOLID_VPHYSICS ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetModel( STRING( GetModelName() ) ); + AddEffects( EF_NODRAW ); + CreateVPhysics(); + VPhysicsGetObject()->EnableCollisions( !m_bDisabled ); +} + + +bool CFuncVPhysicsClip::CreateVPhysics( void ) +{ + VPhysicsInitStatic(); + return true; +} + + +void CFuncVPhysicsClip::Activate( void ) +{ + // Get a handle to my filter entity if there is one + if (m_iFilterName != NULL_STRING) + { + m_hFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iFilterName )); + } + BaseClass::Activate(); +} + +bool CFuncVPhysicsClip::EntityPassesFilter( CBaseEntity *pOther ) +{ + CBaseFilter* pFilter = (CBaseFilter*)(m_hFilter.Get()); + + if ( pFilter ) + return pFilter->PassesFilter( this, pOther ); + +#ifdef MAPBASE + // I couldn't figure out what else made this crash. The entity shouldn't be NULL. + if ( !pOther->VPhysicsGetObject() ) + return false; +#endif + + if ( pOther->GetMoveType() == MOVETYPE_VPHYSICS && pOther->VPhysicsGetObject()->IsMoveable() ) + return true; + + return false; +} + + +bool CFuncVPhysicsClip::ForceVPhysicsCollide( CBaseEntity *pEntity ) +{ + return EntityPassesFilter(pEntity); +} + +void CFuncVPhysicsClip::InputEnable( inputdata_t &inputdata ) +{ + VPhysicsGetObject()->EnableCollisions(true); + m_bDisabled = false; +} + +void CFuncVPhysicsClip::InputDisable( inputdata_t &inputdata ) +{ + VPhysicsGetObject()->EnableCollisions(false); + m_bDisabled = true; +} + +#ifdef MAPBASE +void CFuncVPhysicsClip::InputSetFilter( inputdata_t &inputdata ) +{ + if (inputdata.value.Entity()) + { + m_iFilterName = inputdata.value.Entity()->GetEntityName(); + m_hFilter = dynamic_cast(inputdata.value.Entity().Get()); + } + else + { + m_iFilterName = NULL_STRING; + m_hFilter = NULL; + } +} +#endif diff --git a/sp/src/game/server/buttons.cpp b/sp/src/game/server/buttons.cpp new file mode 100644 index 00000000..47472d43 --- /dev/null +++ b/sp/src/game/server/buttons.cpp @@ -0,0 +1,1594 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements buttons. +// +//============================================================================= + +#include "cbase.h" +#include "doors.h" +#include "ndebugoverlay.h" +#include "spark.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "tier1/strtools.h" +#include "buttons.h" +#include "eventqueue.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void PlayLockSounds( CBaseEntity *pEdict, locksound_t *pls, int flocked, int fbutton ); +string_t MakeButtonSound( int sound ); // get string of button sound number + + +#define SF_BUTTON_DONTMOVE 1 +#define SF_ROTBUTTON_NOTSOLID 1 +#define SF_BUTTON_TOGGLE 32 // button stays pushed until reactivated +#define SF_BUTTON_TOUCH_ACTIVATES 256 // Button fires when touched. +#define SF_BUTTON_DAMAGE_ACTIVATES 512 // Button fires when damaged. +#define SF_BUTTON_USE_ACTIVATES 1024 // Button fires when used. +#define SF_BUTTON_LOCKED 2048 // Whether the button is initially locked. +#define SF_BUTTON_SPARK_IF_OFF 4096 // button sparks in OFF state +#define SF_BUTTON_JIGGLE_ON_USE_LOCKED 8192 // whether to jiggle if someone uses us when we're locked + +BEGIN_DATADESC( CBaseButton ) + + DEFINE_KEYFIELD( m_vecMoveDir, FIELD_VECTOR, "movedir" ), + DEFINE_FIELD( m_fStayPushed, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fRotating, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_bLockedSound, FIELD_CHARACTER ), + DEFINE_FIELD( m_bLockedSentence, FIELD_CHARACTER ), + DEFINE_FIELD( m_bUnlockedSound, FIELD_CHARACTER ), + DEFINE_FIELD( m_bUnlockedSentence, FIELD_CHARACTER ), + DEFINE_FIELD( m_bLocked, FIELD_BOOLEAN ), + DEFINE_FIELD( m_sNoise, FIELD_SOUNDNAME ), + DEFINE_FIELD( m_flUseLockedTime, FIELD_TIME ), + DEFINE_FIELD( m_bSolidBsp, FIELD_BOOLEAN ), + + DEFINE_KEYFIELD( m_sounds, FIELD_INTEGER, "sounds" ), + +// DEFINE_FIELD( m_ls, FIELD_SOUNDNAME ), // This is restored in Precache() +// DEFINE_FIELD( m_nState, FIELD_INTEGER ), + + // Function Pointers + DEFINE_FUNCTION( ButtonTouch ), + DEFINE_FUNCTION( ButtonSpark ), + DEFINE_FUNCTION( TriggerAndWait ), + DEFINE_FUNCTION( ButtonReturn ), + DEFINE_FUNCTION( ButtonBackHome ), + DEFINE_FUNCTION( ButtonUse ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Lock", InputLock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Unlock", InputUnlock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Press", InputPress ), + DEFINE_INPUTFUNC( FIELD_VOID, "PressIn", InputPressIn ), + DEFINE_INPUTFUNC( FIELD_VOID, "PressOut", InputPressOut ), + + // Outputs + DEFINE_OUTPUT( m_OnDamaged, "OnDamaged" ), + DEFINE_OUTPUT( m_OnPressed, "OnPressed" ), + DEFINE_OUTPUT( m_OnUseLocked, "OnUseLocked" ), + DEFINE_OUTPUT( m_OnIn, "OnIn" ), + DEFINE_OUTPUT( m_OnOut, "OnOut" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( func_button, CBaseButton ); + + + +void CBaseButton::Precache( void ) +{ + // get door button sounds, for doors which require buttons to open + if (m_bLockedSound) + { + m_ls.sLockedSound = MakeButtonSound( (int)m_bLockedSound ); + PrecacheScriptSound(m_ls.sLockedSound.ToCStr()); + } + + if (m_bUnlockedSound) + { + m_ls.sUnlockedSound = MakeButtonSound( (int)m_bUnlockedSound ); + PrecacheScriptSound(m_ls.sUnlockedSound.ToCStr()); + } + + // get sentence group names, for doors which are directly 'touched' to open + + switch (m_bLockedSentence) + { + case 1: m_ls.sLockedSentence = MAKE_STRING("NA"); break; // access denied + case 2: m_ls.sLockedSentence = MAKE_STRING("ND"); break; // security lockout + case 3: m_ls.sLockedSentence = MAKE_STRING("NF"); break; // blast door + case 4: m_ls.sLockedSentence = MAKE_STRING("NFIRE"); break; // fire door + case 5: m_ls.sLockedSentence = MAKE_STRING("NCHEM"); break; // chemical door + case 6: m_ls.sLockedSentence = MAKE_STRING("NRAD"); break; // radiation door + case 7: m_ls.sLockedSentence = MAKE_STRING("NCON"); break; // gen containment + case 8: m_ls.sLockedSentence = MAKE_STRING("NH"); break; // maintenance door + case 9: m_ls.sLockedSentence = MAKE_STRING("NG"); break; // broken door + + default: m_ls.sLockedSentence = NULL_STRING; break; + } + + switch (m_bUnlockedSentence) + { + case 1: m_ls.sUnlockedSentence = MAKE_STRING("EA"); break; // access granted + case 2: m_ls.sUnlockedSentence = MAKE_STRING("ED"); break; // security door + case 3: m_ls.sUnlockedSentence = MAKE_STRING("EF"); break; // blast door + case 4: m_ls.sUnlockedSentence = MAKE_STRING("EFIRE"); break; // fire door + case 5: m_ls.sUnlockedSentence = MAKE_STRING("ECHEM"); break; // chemical door + case 6: m_ls.sUnlockedSentence = MAKE_STRING("ERAD"); break; // radiation door + case 7: m_ls.sUnlockedSentence = MAKE_STRING("ECON"); break; // gen containment + case 8: m_ls.sUnlockedSentence = MAKE_STRING("EH"); break; // maintenance door + + default: m_ls.sUnlockedSentence = NULL_STRING; break; + } + + if ( m_sNoise != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_sNoise ) ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Cache user-entity-field values until spawn is called. +// Input : szKeyName - +// szValue - +// Output : Returns true if handled, false if not. +//----------------------------------------------------------------------------- +bool CBaseButton::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "locked_sound")) + { + m_bLockedSound = atof(szValue); + } + else if (FStrEq(szKeyName, "locked_sentence")) + { + m_bLockedSentence = atof(szValue); + } + else if (FStrEq(szKeyName, "unlocked_sound")) + { + m_bUnlockedSound = atof(szValue); + } + else if (FStrEq(szKeyName, "unlocked_sentence")) + { + m_bUnlockedSentence = atof(szValue); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Locks the button. If locked, the button will play the locked sound +// when the player tries to use it. +//----------------------------------------------------------------------------- +void CBaseButton::Lock() +{ + m_bLocked = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Unlocks the button, making it able to be pressed again. +//----------------------------------------------------------------------------- +void CBaseButton::Unlock() +{ + m_bLocked = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Locks the button. If locked, the button will play the locked sound +// when the player tries to use it. +//----------------------------------------------------------------------------- +void CBaseButton::InputLock( inputdata_t &inputdata ) +{ + Lock(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Unlocks the button, making it able to be pressed again. +//----------------------------------------------------------------------------- +void CBaseButton::InputUnlock( inputdata_t &inputdata ) +{ + Unlock(); +} + + +//----------------------------------------------------------------------------- +// Presses or unpresses the button. +//----------------------------------------------------------------------------- +void CBaseButton::Press( CBaseEntity *pActivator, BUTTON_CODE eCode ) +{ + if ( ( eCode == BUTTON_PRESS ) && ( m_toggle_state == TS_GOING_UP || m_toggle_state == TS_GOING_DOWN ) ) + { + return; + } + + if ( ( eCode == BUTTON_ACTIVATE ) && ( m_toggle_state == TS_GOING_UP || m_toggle_state == TS_AT_TOP ) ) + { + return; + } + + if ( ( eCode == BUTTON_RETURN ) && ( m_toggle_state == TS_GOING_DOWN || m_toggle_state == TS_AT_BOTTOM ) ) + { + return; + } + + // FIXME: consolidate all the button press code into one place! + if (m_bLocked) + { + // play button locked sound + PlayLockSounds(this, &m_ls, TRUE, TRUE); + return; + } + + // Temporarily disable the touch function, until movement is finished. + SetTouch( NULL ); + + if ( ( ( eCode == BUTTON_PRESS ) && ( m_toggle_state == TS_AT_TOP ) ) || + ( ( eCode == BUTTON_RETURN ) && ( m_toggle_state == TS_AT_TOP || m_toggle_state == TS_GOING_UP ) ) ) + { + if ( m_sNoise != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = (char*)STRING(m_sNoise); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + m_OnPressed.FireOutput(pActivator, this); + ButtonReturn(); + } + else if ( ( eCode == BUTTON_PRESS ) || + ( ( eCode == BUTTON_ACTIVATE ) && ( m_toggle_state == TS_AT_BOTTOM || m_toggle_state == TS_GOING_DOWN ) ) ) + { + m_OnPressed.FireOutput(pActivator, this); + ButtonActivate(); + } +} + + +//----------------------------------------------------------------------------- +// Presses the button. +//----------------------------------------------------------------------------- +void CBaseButton::InputPress( inputdata_t &inputdata ) +{ + Press( inputdata.pActivator, BUTTON_PRESS ); +} + + +//----------------------------------------------------------------------------- +// Presses the button, sending it to the top/pressed position. +//----------------------------------------------------------------------------- +void CBaseButton::InputPressIn( inputdata_t &inputdata ) +{ + Press( inputdata.pActivator, BUTTON_ACTIVATE ); +} + + +//----------------------------------------------------------------------------- +// Unpresses the button, sending it to the unpressed/bottom position. +//----------------------------------------------------------------------------- +void CBaseButton::InputPressOut( inputdata_t &inputdata ) +{ + Press( inputdata.pActivator, BUTTON_RETURN ); +} + + +//----------------------------------------------------------------------------- +// Purpose: We have been damaged. Possibly activate, depending on our flags. +// Input : pInflictor - +// pAttacker - +// flDamage - +// bitsDamageType - +// Output : +//----------------------------------------------------------------------------- +int CBaseButton::OnTakeDamage( const CTakeDamageInfo &info ) +{ + m_OnDamaged.FireOutput(m_hActivator, this); + + // dvsents2: remove obselete health keyvalue from func_button + if (!HasSpawnFlags(SF_BUTTON_DAMAGE_ACTIVATES) && (m_iHealth == 0)) + { + return(0); + } + + BUTTON_CODE code = ButtonResponseToTouch(); + + if ( code == BUTTON_NOTHING ) + return 0; + + m_hActivator = info.GetAttacker(); + + // dvsents2: why would activator be NULL here? + if ( m_hActivator == NULL ) + return 0; + + if (m_bLocked) + { + return(0); + } + + // Temporarily disable the touch function, until movement is finished. + SetTouch( NULL ); + + if ( code == BUTTON_RETURN ) + { + if ( m_sNoise != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = (char*)STRING(m_sNoise); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + m_OnPressed.FireOutput(m_hActivator, this); + ButtonReturn(); + } + else + { + // code == BUTTON_ACTIVATE + m_OnPressed.FireOutput(m_hActivator, this); + ButtonActivate( ); + } + + return 0; +} + + +void CBaseButton::Spawn( ) +{ + //---------------------------------------------------- + //determine sounds for buttons + //a sound of 0 should not make a sound + //---------------------------------------------------- + if ( m_sounds ) + { + m_sNoise = MakeButtonSound( m_sounds ); + PrecacheScriptSound(m_sNoise.ToCStr()); + } + else + { + m_sNoise = NULL_STRING; + } + + Precache(); + + if ( HasSpawnFlags( SF_BUTTON_SPARK_IF_OFF ) )// this button should spark in OFF state + { + SetThink ( &CBaseButton::ButtonSpark ); + SetNextThink( gpGlobals->curtime + 0.5f );// no hurry, make sure everything else spawns + } + + // Convert movedir from angles to a vector + QAngle angMoveDir = QAngle( m_vecMoveDir.x, m_vecMoveDir.y, m_vecMoveDir.z ); + AngleVectors( angMoveDir, &m_vecMoveDir ); + + SetMoveType( MOVETYPE_PUSH ); + SetSolid( SOLID_BSP ); + SetModel( STRING( GetModelName() ) ); + + if (m_flSpeed == 0) + { + m_flSpeed = 40; + } + + m_takedamage = DAMAGE_YES; + + if (m_flWait == 0) + { + m_flWait = 1; + } + + if (m_flLip == 0) + { + m_flLip = 4; + } + + m_toggle_state = TS_AT_BOTTOM; + m_vecPosition1 = GetLocalOrigin(); + + // Subtract 2 from size because the engine expands bboxes by 1 in all directions making the size too big + Vector vecButtonOBB = CollisionProp()->OBBSize(); + vecButtonOBB -= Vector( 2, 2, 2 ); + m_vecPosition2 = m_vecPosition1 + (m_vecMoveDir * (DotProductAbs( m_vecMoveDir, vecButtonOBB ) - m_flLip)); + + // Is this a non-moving button? + if ( ((m_vecPosition2 - m_vecPosition1).Length() < 1) || HasSpawnFlags(SF_BUTTON_DONTMOVE) ) + { + m_vecPosition2 = m_vecPosition1; + } + + m_fStayPushed = (m_flWait == -1 ? TRUE : FALSE); + m_fRotating = FALSE; + + if (HasSpawnFlags(SF_BUTTON_LOCKED)) + { + m_bLocked = true; + } + + // + // If using activates the button, set its use function. + // + if (HasSpawnFlags(SF_BUTTON_USE_ACTIVATES)) + { + SetUse(&CBaseButton::ButtonUse); + } + else + { + SetUse(NULL); + } + + // + // If touching activates the button, set its touch function. + // + if (HasSpawnFlags(SF_BUTTON_TOUCH_ACTIVATES)) + { + SetTouch( &CBaseButton::ButtonTouch ); + } + else + { + SetTouch ( NULL ); + } + + CreateVPhysics(); +} + +//----------------------------------------------------------------------------- + +bool CBaseButton::CreateVPhysics() +{ + VPhysicsInitShadow( false, false ); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Button sound table. +// Also used by CBaseDoor to get 'touched' door lock/unlock sounds +// Input : sound - index of sound to look up. +// Output : Returns a pointer to the corresponding sound file. +//----------------------------------------------------------------------------- +string_t MakeButtonSound( int sound ) +{ + char tmp[1024]; + Q_snprintf( tmp, sizeof(tmp), "Buttons.snd%d", sound ); + return AllocPooledString(tmp); +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function that emits sparks at random intervals. +//----------------------------------------------------------------------------- +void CBaseButton::ButtonSpark ( void ) +{ + SetThink ( &CBaseButton::ButtonSpark ); + SetNextThink( gpGlobals->curtime + 0.1 + random->RandomFloat ( 0, 1.5 ) );// spark again at random interval + + DoSpark( this, WorldSpaceCenter(), 1, 1, true, vec3_origin ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when someone uses us whilst we are locked. +//----------------------------------------------------------------------------- +bool CBaseButton::OnUseLocked( CBaseEntity *pActivator ) +{ + PlayLockSounds(this, &m_ls, TRUE, TRUE); + + if ( gpGlobals->curtime > m_flUseLockedTime ) + { + m_OnUseLocked.FireOutput( pActivator, this ); + m_flUseLockedTime = gpGlobals->curtime + 0.5; + return true; + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Use function that starts the button moving. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CBaseButton::ButtonUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // Ignore touches if button is moving, or pushed-in and waiting to auto-come-out. + // UNDONE: Should this use ButtonResponseToTouch() too? + if (m_toggle_state == TS_GOING_UP || m_toggle_state == TS_GOING_DOWN ) + return; + + if (m_bLocked) + { + OnUseLocked( pActivator ); + return; + } + + m_hActivator = pActivator; + + if ( m_toggle_state == TS_AT_TOP) + { + // + // If it's a toggle button it can return now. Otherwise, it will either + // return on its own or will stay pressed indefinitely. + // + if ( HasSpawnFlags(SF_BUTTON_TOGGLE)) + { + if ( m_sNoise != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = (char*)STRING(m_sNoise); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + m_OnPressed.FireOutput(m_hActivator, this); + ButtonReturn(); + } + } + else + { + m_OnPressed.FireOutput(m_hActivator, this); + ButtonActivate( ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns a code indicating how the button should respond to being touched. +// Output : Returns one of the following: +// BUTTON_NOTHING - do nothing +// BUTTON_RETURN - +// BUTTON_ACTIVATE - act as if pressed +//----------------------------------------------------------------------------- +CBaseButton::BUTTON_CODE CBaseButton::ButtonResponseToTouch( void ) +{ + // Ignore touches if button is moving, or pushed-in and waiting to auto-come-out. + if (m_toggle_state == TS_GOING_UP || + m_toggle_state == TS_GOING_DOWN || + (m_toggle_state == TS_AT_TOP && !m_fStayPushed && !HasSpawnFlags(SF_BUTTON_TOGGLE) ) ) + return BUTTON_NOTHING; + + if (m_toggle_state == TS_AT_TOP) + { + if ( HasSpawnFlags(SF_BUTTON_TOGGLE) && !m_fStayPushed) + { + return BUTTON_RETURN; + } + } + else + return BUTTON_ACTIVATE; + + return BUTTON_NOTHING; +} + + +//----------------------------------------------------------------------------- +// Purpose: Touch function that activates the button if it responds to touch. +// Input : pOther - The entity that touched us. +//----------------------------------------------------------------------------- +void CBaseButton::ButtonTouch( CBaseEntity *pOther ) +{ + // Ignore touches by anything but players + if ( !pOther->IsPlayer() ) + return; + + m_hActivator = pOther; + + BUTTON_CODE code = ButtonResponseToTouch(); + + if ( code == BUTTON_NOTHING ) + return; + + if (!UTIL_IsMasterTriggered(m_sMaster, pOther) || m_bLocked) + { + // play button locked sound + PlayLockSounds(this, &m_ls, TRUE, TRUE); + return; + } + + // Temporarily disable the touch function, until movement is finished. + SetTouch( NULL ); + + if ( code == BUTTON_RETURN ) + { + if ( m_sNoise != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = (char*)STRING(m_sNoise); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + m_OnPressed.FireOutput(m_hActivator, this); + ButtonReturn(); + } + else + { + // code == BUTTON_ACTIVATE + m_OnPressed.FireOutput(m_hActivator, this); + ButtonActivate( ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the button moving "in/up". +// Input : *pOther - +//----------------------------------------------------------------------------- +void CBaseButton::ButtonActivate( void ) +{ + if ( m_sNoise != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = (char*)STRING(m_sNoise); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + if (!UTIL_IsMasterTriggered(m_sMaster, m_hActivator) || m_bLocked) + { + // button is locked, play locked sound + PlayLockSounds(this, &m_ls, TRUE, TRUE); + return; + } + else + { + // button is unlocked, play unlocked sound + PlayLockSounds(this, &m_ls, FALSE, TRUE); + } + + ASSERT(m_toggle_state == TS_AT_BOTTOM); + m_toggle_state = TS_GOING_UP; + + SetMoveDone( &CBaseButton::TriggerAndWait ); + if (!m_fRotating) + LinearMove( m_vecPosition2, m_flSpeed); + else + AngularMove( m_vecAngle2, m_flSpeed); +} + + +//----------------------------------------------------------------------------- +// Purpose: Enables or disables the use capability based on our spawnflags. +//----------------------------------------------------------------------------- +int CBaseButton::ObjectCaps(void) +{ + return((BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION) | + (HasSpawnFlags(SF_BUTTON_USE_ACTIVATES) ? (FCAP_IMPULSE_USE | FCAP_USE_IN_RADIUS) : 0)); +} + + +//----------------------------------------------------------------------------- +// Purpose: Button has reached the "pressed/top" position. Fire its OnIn output, +// and pause before returning to "unpressed/bottom". +//----------------------------------------------------------------------------- +void CBaseButton::TriggerAndWait( void ) +{ + ASSERT(m_toggle_state == TS_GOING_UP); + + if (!UTIL_IsMasterTriggered(m_sMaster, m_hActivator) || m_bLocked) + { + return; + } + + m_toggle_state = TS_AT_TOP; + + // + // Re-instate touches if the button is of the toggle variety. + // + if (m_fStayPushed || HasSpawnFlags(SF_BUTTON_TOGGLE ) ) + { + if (HasSpawnFlags(SF_BUTTON_TOUCH_ACTIVATES)) + { + SetTouch(&CBaseButton::ButtonTouch); + } + else + { + // BUGBUG: ALL buttons no longer respond to touch + SetTouch (NULL); + } + } + + // + // If button automatically comes back out, start it moving out. + // + else + { + SetNextThink( gpGlobals->curtime + m_flWait ); + SetThink( &CBaseButton::ButtonReturn ); + } + + m_nState = 1; // use alternate textures + + m_OnIn.FireOutput(m_hActivator, this); +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the button moving "out/down". +//----------------------------------------------------------------------------- +void CBaseButton::ButtonReturn( void ) +{ + ASSERT(m_toggle_state == TS_AT_TOP); + m_toggle_state = TS_GOING_DOWN; + + SetMoveDone( &CBaseButton::ButtonBackHome ); + if (!m_fRotating) + LinearMove( m_vecPosition1, m_flSpeed); + else + AngularMove( m_vecAngle1, m_flSpeed); + + m_nState = 0; // use normal textures +} + + +//----------------------------------------------------------------------------- +// Purpose: Button has returned to the "unpressed/bottom" position. Fire its +// OnOut output and stop moving. +//----------------------------------------------------------------------------- +void CBaseButton::ButtonBackHome( void ) +{ + ASSERT(m_toggle_state == TS_GOING_DOWN); + m_toggle_state = TS_AT_BOTTOM; + + m_OnOut.FireOutput(m_hActivator, this); + + // + // Re-instate touch method, movement cycle is complete. + // + if (HasSpawnFlags(SF_BUTTON_TOUCH_ACTIVATES)) + { + SetTouch( &CBaseButton::ButtonTouch ); + } + else + { + // BUGBUG: ALL buttons no longer respond to touch + SetTouch ( NULL ); + } + + // reset think for a sparking button + if (HasSpawnFlags( SF_BUTTON_SPARK_IF_OFF ) ) + { + SetThink ( &CBaseButton::ButtonSpark ); + SetNextThink( gpGlobals->curtime + 0.5f );// no hurry + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CBaseButton::DrawDebugTextOverlays() +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + static const char *pszStates[] = + { + "Pressed", + "Unpressed", + "Pressing...", + "Unpressing...", + "", + }; + + char tempstr[255]; + + int nState = m_toggle_state; + if ( ( nState < 0 ) || ( nState > 3 ) ) + { + nState = 4; + } + + Q_snprintf( tempstr, sizeof(tempstr), "State: %s", pszStates[nState] ); + EntityText( text_offset, tempstr, 0 ); + text_offset++; + + Q_snprintf( tempstr, sizeof(tempstr), "%s", m_bLocked ? "Locked" : "Unlocked" ); + EntityText( text_offset, tempstr, 0 ); + text_offset++; + } + return text_offset; +} + + +// +// Rotating button (aka "lever") +// +LINK_ENTITY_TO_CLASS( func_rot_button, CRotButton ); + + +void CRotButton::Spawn( void ) +{ + //---------------------------------------------------- + //determine sounds for buttons + //a sound of 0 should not make a sound + //---------------------------------------------------- + if ( m_sounds ) + { + m_sNoise = MakeButtonSound( m_sounds ); + PrecacheScriptSound(m_sNoise.ToCStr()); + } + else + { + m_sNoise = NULL_STRING; + } + + // set the axis of rotation + CBaseToggle::AxisDir(); + + // check for clockwise rotation + if ( HasSpawnFlags( SF_DOOR_ROTATE_BACKWARDS) ) + { + m_vecMoveAng = m_vecMoveAng * -1; + } + + SetMoveType( MOVETYPE_PUSH ); + +#ifdef HL1_DLL + SetSolid( SOLID_BSP ); +#else + SetSolid( SOLID_VPHYSICS ); +#endif + if ( HasSpawnFlags( SF_ROTBUTTON_NOTSOLID ) ) + { + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + SetModel( STRING( GetModelName() ) ); + + if (m_flSpeed == 0) + m_flSpeed = 40; + + if (m_flWait == 0) + m_flWait = 1; + + if (m_iHealth > 0) + { + m_takedamage = DAMAGE_YES; + } + + m_toggle_state = TS_AT_BOTTOM; + m_vecAngle1 = GetLocalAngles(); + m_vecAngle2 = GetLocalAngles() + m_vecMoveAng * m_flMoveDistance; + ASSERTSZ(m_vecAngle1 != m_vecAngle2, "rotating button start/end positions are equal\n"); + + m_fStayPushed = (m_flWait == -1 ? TRUE : FALSE); + m_fRotating = TRUE; + + SetUse(&CRotButton::ButtonUse); + +#ifdef MAPBASE + if (HasSpawnFlags(SF_BUTTON_LOCKED)) + { + m_bLocked = true; + } +#endif + + // + // If touching activates the button, set its touch function. + // + if (!HasSpawnFlags(SF_BUTTON_TOUCH_ACTIVATES)) + { + SetTouch ( NULL ); + } + else + { + SetTouch( &CRotButton::ButtonTouch ); + } + + CreateVPhysics(); +} + + +bool CRotButton::CreateVPhysics( void ) +{ + VPhysicsInitShadow( false, false ); + return true; +} + + +//----------------------------------------------------------------------------- +// CMomentaryRotButton spawnflags +//----------------------------------------------------------------------------- +#define SF_MOMENTARY_DOOR 1 +#define SF_MOMENTARY_NOT_USABLE 2 +#define SF_MOMENTARY_AUTO_RETURN 16 + + +BEGIN_DATADESC( CMomentaryRotButton ) + + DEFINE_FIELD( m_lastUsed, FIELD_INTEGER ), + DEFINE_FIELD( m_start, FIELD_VECTOR ), + DEFINE_FIELD( m_end, FIELD_VECTOR ), + DEFINE_FIELD( m_IdealYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_sNoise, FIELD_SOUNDNAME ), + DEFINE_FIELD( m_bUpdateTarget, FIELD_BOOLEAN ), + + DEFINE_KEYFIELD( m_direction, FIELD_INTEGER, "StartDirection" ), + DEFINE_KEYFIELD( m_returnSpeed, FIELD_FLOAT, "returnspeed" ), + DEFINE_KEYFIELD( m_flStartPosition, FIELD_FLOAT, "StartPosition"), + DEFINE_KEYFIELD( m_bSolidBsp, FIELD_BOOLEAN, "solidbsp" ), + + // Function Pointers + DEFINE_FUNCTION( UseMoveDone ), + DEFINE_FUNCTION( ReturnMoveDone ), + DEFINE_FUNCTION( SetPositionMoveDone ), + DEFINE_FUNCTION( UpdateThink ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPosition", InputSetPosition ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPositionImmediately", InputSetPositionImmediately ), + DEFINE_INPUTFUNC( FIELD_VOID, "_DisableUpdateTarget", InputDisableUpdateTarget ), + DEFINE_INPUTFUNC( FIELD_VOID, "_EnableUpdateTarget", InputEnableUpdateTarget ), + + // Outputs + DEFINE_OUTPUT( m_Position, "Position" ), + DEFINE_OUTPUT( m_OnUnpressed, "OnUnpressed" ), + DEFINE_OUTPUT( m_OnFullyClosed, "OnFullyClosed" ), + DEFINE_OUTPUT( m_OnFullyOpen, "OnFullyOpen" ), + DEFINE_OUTPUT( m_OnReachedPosition, "OnReachedPosition" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_FIELD( m_bDisabled, FIELD_BOOLEAN ) + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( momentary_rot_button, CMomentaryRotButton ); + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been handled. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::Spawn( void ) +{ + CBaseToggle::AxisDir(); + + m_bUpdateTarget = true; + + if ( m_flSpeed == 0 ) + { + m_flSpeed = 100; + } + + // Clamp start position and issue bounds warning + if (m_flStartPosition < 0.0f || m_flStartPosition > 1.0f) + { + Warning("WARNING: Momentary door (%s) start position not between 0 and 1. Clamping.\n",GetDebugName()); + m_flStartPosition = clamp(m_IdealYaw, 0.f, 1.f); + } + + // Check direction fields (for backward compatibility) + if (m_direction != 1 && m_direction != -1) + { + m_direction = 1; + } + + if (m_flMoveDistance < 0) + { + m_vecMoveAng = m_vecMoveAng * -1; + m_flMoveDistance = -m_flMoveDistance; + } + + m_start = GetLocalAngles() - m_vecMoveAng * m_flMoveDistance * m_flStartPosition; + m_end = GetLocalAngles() + m_vecMoveAng * m_flMoveDistance * (1-m_flStartPosition); + + m_IdealYaw = m_flStartPosition; + + // Force start direction at end points + if (m_flStartPosition == 0.0) + { + m_direction = -1; + } + else if (m_flStartPosition == 1.0) + { + m_direction = 1; + } + + if (HasSpawnFlags(SF_BUTTON_LOCKED)) + { + m_bLocked = true; + } + + if ( HasSpawnFlags( SF_BUTTON_USE_ACTIVATES ) ) + { + if ( m_sounds ) + { + m_sNoise = MakeButtonSound( m_sounds ); + PrecacheScriptSound(m_sNoise.ToCStr()); + } + else + { + m_sNoise = NULL_STRING; + } + + m_lastUsed = 0; + UpdateTarget(0,this); + } + +#ifdef HL1_DLL + SetSolid( SOLID_BSP ); +#else + SetSolid( SOLID_VPHYSICS ); +#endif + if (HasSpawnFlags(SF_ROTBUTTON_NOTSOLID)) + { + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + SetMoveType( MOVETYPE_PUSH ); + SetModel( STRING( GetModelName() ) ); + + CreateVPhysics(); + + // Slam the object back to solid - if we really want it to be solid. + if ( m_bSolidBsp ) + { + SetSolid( SOLID_BSP ); + } + + m_bDisabled = false; +} + +int CMomentaryRotButton::ObjectCaps( void ) +{ + int flags = BaseClass::ObjectCaps(); + if (!HasSpawnFlags(SF_BUTTON_USE_ACTIVATES)) + { + return flags; + } + else + { + return (flags | FCAP_CONTINUOUS_USE | FCAP_USE_IN_RADIUS); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CMomentaryRotButton::CreateVPhysics( void ) +{ + VPhysicsInitShadow( false, false ); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMomentaryRotButton::PlaySound( void ) +{ + if ( m_sNoise == NULL_STRING ) + return; + + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = (char*)STRING(m_sNoise); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns a given angular position as a value along our motion from 0 to 1. +// Input : vecAngles - +//----------------------------------------------------------------------------- +float CMomentaryRotButton::GetPos( const QAngle &vecAngles ) +{ + float flScale = 1; + if (( m_vecMoveAng[0] < 0 ) || ( m_vecMoveAng[1] < 0 ) || ( m_vecMoveAng[2] < 0 )) + { + flScale = -1; + } + + float flPos = flScale * CBaseToggle::AxisDelta( m_spawnflags, vecAngles, m_start ) / m_flMoveDistance; + return( clamp( flPos, 0.f, 1.f )); +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : flPosition +//------------------------------------------------------------------------------ +void CMomentaryRotButton::InputSetPosition( inputdata_t &inputdata ) +{ + m_IdealYaw = clamp( inputdata.value.Float(), 0.f, 1.f ); + + float flCurPos = GetPos( GetLocalAngles() ); + if ( flCurPos < m_IdealYaw ) + { + // Moving forward (from start to end). + SetLocalAngularVelocity( m_flSpeed * m_vecMoveAng ); + m_direction = 1; + } + else if ( flCurPos > m_IdealYaw ) + { + // Moving backward (from end to start). + SetLocalAngularVelocity( -m_flSpeed * m_vecMoveAng ); + m_direction = -1; + } + else + { + // We're there already; nothing to do. + SetLocalAngularVelocity( vec3_angle ); + return; + } + + SetMoveDone( &CMomentaryRotButton::SetPositionMoveDone ); + + SetThink( &CMomentaryRotButton::UpdateThink ); + SetNextThink( gpGlobals->curtime ); + + // + // Think again in 0.1 seconds or the time that it will take us to reach our movement goal, + // whichever is the shorter interval. This prevents us from overshooting and stuttering when we + // are told to change position in very small increments. + // + QAngle vecNewAngles = m_start + m_vecMoveAng * ( m_IdealYaw * m_flMoveDistance ); + float flAngleDelta = fabs( AxisDelta( m_spawnflags, vecNewAngles, GetLocalAngles() )); + float dt = flAngleDelta / m_flSpeed; + if ( dt < TICK_INTERVAL ) + { + dt = TICK_INTERVAL; + float speed = flAngleDelta / TICK_INTERVAL; + SetLocalAngularVelocity( speed * m_vecMoveAng * m_direction ); + } + dt = clamp( dt, TICK_INTERVAL, TICK_INTERVAL * 6); + + SetMoveDoneTime( dt ); +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : flPosition +//------------------------------------------------------------------------------ +void CMomentaryRotButton::InputSetPositionImmediately( inputdata_t &inputdata ) +{ + m_IdealYaw = clamp( inputdata.value.Float(), 0.f, 1.f ); + SetLocalAngles( m_start + m_vecMoveAng * ( m_IdealYaw * m_flMoveDistance ) ); +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns off target updates so that we can change the wheel's position +// without changing the target's position. Used for jiggling when locked. +//------------------------------------------------------------------------------ +void CMomentaryRotButton::InputDisableUpdateTarget( inputdata_t &inputdata ) +{ + m_bUpdateTarget = false; +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns target updates back on (after jiggling). +//------------------------------------------------------------------------------ +void CMomentaryRotButton::InputEnableUpdateTarget( inputdata_t &inputdata ) +{ + m_bUpdateTarget = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Locks the button. If locked, the button will play the locked sound +// when the player tries to use it. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::Lock() +{ + BaseClass::Lock(); + + SetLocalAngularVelocity( vec3_angle ); + SetMoveDoneTime( -1 ); + SetMoveDone( NULL ); + + SetNextThink( TICK_NEVER_THINK ); + SetThink( NULL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Unlocks the button, making it able to be pressed again. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::Unlock() +{ + BaseClass::Unlock(); + + SetMoveDone( &CMomentaryRotButton::ReturnMoveDone ); + + // Delay before autoreturn. + SetMoveDoneTime( 0.1f ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Fires the appropriate outputs at the extremes of motion. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::OutputMovementComplete( void ) +{ + if (m_IdealYaw == 1.0) + { + m_OnFullyClosed.FireOutput(this, this); + } + else if (m_IdealYaw == 0.0) + { + m_OnFullyOpen.FireOutput(this, this); + } + + m_OnReachedPosition.FireOutput( this, this ); +} + + +//------------------------------------------------------------------------------ +// Purpose: MoveDone function for the SetPosition input handler. Tracks our +// progress toward a movement goal and updates our outputs. +//------------------------------------------------------------------------------ +void CMomentaryRotButton::SetPositionMoveDone(void) +{ + float flCurPos = GetPos( GetLocalAngles() ); + + if ((( flCurPos >= m_IdealYaw ) && ( m_direction == 1 )) || + (( flCurPos <= m_IdealYaw ) && ( m_direction == -1 ))) + { + // + // We reached or surpassed our movement goal. + // + SetLocalAngularVelocity( vec3_angle ); + // BUGBUG: Won't this get the player stuck? + SetLocalAngles( m_start + m_vecMoveAng * ( m_IdealYaw * m_flMoveDistance ) ); + SetNextThink( TICK_NEVER_THINK ); + SetMoveDoneTime( -1 ); + UpdateTarget( m_IdealYaw, this ); + OutputMovementComplete(); + return; + } + + // TODO: change this to use a Think function like ReturnThink. + QAngle vecNewAngles = m_start + m_vecMoveAng * ( m_IdealYaw * m_flMoveDistance ); + float flAngleDelta = fabs( AxisDelta( m_spawnflags, vecNewAngles, GetLocalAngles() )); + float dt = flAngleDelta / m_flSpeed; + if ( dt < TICK_INTERVAL ) + { + dt = TICK_INTERVAL; + float speed = flAngleDelta / TICK_INTERVAL; + SetLocalAngularVelocity( speed * m_vecMoveAng * m_direction ); + } + dt = clamp( dt, TICK_INTERVAL, TICK_INTERVAL * 6); + + SetMoveDoneTime( dt ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CMomentaryRotButton::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( m_bDisabled == true ) + return; + + if (m_bLocked) + { + if ( OnUseLocked( pActivator ) && HasSpawnFlags( SF_BUTTON_JIGGLE_ON_USE_LOCKED ) ) + { + // Jiggle two degrees. + float flDist = 2.0 / m_flMoveDistance; + + // Must be first! + g_EventQueue.AddEvent( this, "_DisableUpdateTarget", 0, this, this ); + + variant_t value; + value.SetFloat( flDist ); + g_EventQueue.AddEvent( this, "SetPosition", value, 0.01, this, this ); + + value.SetFloat( 0.0 ); + g_EventQueue.AddEvent( this, "SetPosition", value, 0.1, this, this ); + + value.SetFloat( 0.5 * flDist ); + g_EventQueue.AddEvent( this, "SetPosition", value, 0.2, this, this ); + + value.SetFloat( 0.0 ); + g_EventQueue.AddEvent( this, "SetPosition", value, 0.3, this, this ); + + // Must be last! And must be late enough to cover the settling time. + g_EventQueue.AddEvent( this, "_EnableUpdateTarget", 0.5, this, this ); + } + + return; + } + + // + // Reverse our direction and play movement sound every time the player + // pauses between uses. + // + bool bPlaySound = false; + + if ( !m_lastUsed ) + { + bPlaySound = true; + m_direction = -m_direction; + + //Alert that we've been pressed + m_OnPressed.FireOutput( m_hActivator, this ); + } + + m_lastUsed = 1; + + float flPos = GetPos( GetLocalAngles() ); + UpdateSelf( flPos, bPlaySound ); + + // + // Think every frame while we are moving. + // HACK: Don't reset the think time if we already have a pending think. + // This works around an issue with host_thread_mode > 0 when the player's + // clock runs ahead of the server. + // + if ( !m_pfnThink ) + { + SetThink( &CMomentaryRotButton::UpdateThink ); + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Handles changing direction at the extremes of our range of motion +// and updating our avelocity while being used by the player. +// Input : value - Number from 0 to 1 indicating our desired position within +// our range of motion, 0 = start, 1 = end. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::UpdateSelf( float value, bool bPlaySound ) +{ + // + // Set our move clock to 0.1 seconds in the future so we stop spinning unless we are + // used again before then. + // + SetMoveDoneTime( 0.1 ); + + // + // If we hit the end, zero our avelocity and snap to the end angles. + // + if ( m_direction > 0 && value >= 1.0 ) + { + SetLocalAngularVelocity( vec3_angle ); + SetLocalAngles( m_end ); + + m_OnFullyClosed.FireOutput(this, this); + return; + } + // + // If we returned to the start, zero our avelocity and snap to the start angles. + // + else if ( m_direction < 0 && value <= 0 ) + { + SetLocalAngularVelocity( vec3_angle ); + SetLocalAngles( m_start ); + + m_OnFullyOpen.FireOutput(this, this); + return; + } + + if ( bPlaySound ) + { + PlaySound(); + } + + SetLocalAngularVelocity( ( m_direction * m_flSpeed ) * m_vecMoveAng ); + SetMoveDone( &CMomentaryRotButton::UseMoveDone ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Updates the value of our position, firing any targets. +// Input : value - New position, from 0 - 1. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::UpdateTarget( float value, CBaseEntity *pActivator ) +{ + if ( !m_bUpdateTarget ) + return; + + if (m_Position.Get() != value) + { + m_Position.Set(value, pActivator, this); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Handles the end of motion caused by player use. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::UseMoveDone( void ) +{ + SetLocalAngularVelocity( vec3_angle ); + + // Make sure our targets stop where we stopped. + float flPos = GetPos( GetLocalAngles() ); + UpdateTarget( flPos, this ); + + // Alert that we've been unpressed + m_OnUnpressed.FireOutput( m_hActivator, this ); + + m_lastUsed = 0; + + if ( !HasSpawnFlags( SF_BUTTON_TOGGLE ) && m_returnSpeed > 0 ) + { + SetMoveDone( &CMomentaryRotButton::ReturnMoveDone ); + m_direction = -1; + + // Delay before autoreturn. + SetMoveDoneTime( 0.1f ); + } + else + { + SetThink( NULL ); + SetMoveDone( NULL ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: MoveDone function for rotating back to the start position. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::ReturnMoveDone( void ) +{ + float value = GetPos( GetLocalAngles() ); + if ( value <= 0 ) + { + // + // Got back to the start, stop spinning. + // + SetLocalAngularVelocity( vec3_angle ); + SetLocalAngles( m_start ); + + UpdateTarget( 0, NULL ); + + SetMoveDoneTime( -1 ); + SetMoveDone( NULL ); + + SetNextThink( TICK_NEVER_THINK ); + SetThink( NULL ); + } + else + { + SetLocalAngularVelocity( -m_returnSpeed * m_vecMoveAng ); + SetMoveDoneTime( 0.1f ); + + SetThink( &CMomentaryRotButton::UpdateThink ); + SetNextThink( gpGlobals->curtime + 0.01f ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function for updating target as we move. +//----------------------------------------------------------------------------- +void CMomentaryRotButton::UpdateThink( void ) +{ + float value = GetPos( GetLocalAngles() ); + UpdateTarget( value, NULL ); + SetNextThink( gpGlobals->curtime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CMomentaryRotButton::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[255]; + + Q_snprintf(tempstr,sizeof(tempstr),"QAngle: %.2f %.2f %.2f", GetLocalAngles()[0], GetLocalAngles()[1], GetLocalAngles()[2]); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"AVelocity: %.2f %.2f %.2f", GetLocalAngularVelocity()[0], GetLocalAngularVelocity()[1], GetLocalAngularVelocity()[2]); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Target Pos: %3.3f",m_IdealYaw); + EntityText(text_offset,tempstr,0); + text_offset++; + + float flCurPos = GetPos(GetLocalAngles()); + Q_snprintf(tempstr,sizeof(tempstr),"Current Pos: %3.3f",flCurPos); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Direction: %s",(m_direction == 1) ? "Forward" : "Backward"); + EntityText(text_offset,tempstr,0); + text_offset++; + + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: Input hander that starts the spawner +//----------------------------------------------------------------------------- +void CMomentaryRotButton::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input hander that stops the spawner +//----------------------------------------------------------------------------- +void CMomentaryRotButton::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +//----------------------------------------------------------------------------- +// Purpose: Start the spawner +//----------------------------------------------------------------------------- +void CMomentaryRotButton::Enable( void ) +{ + m_bDisabled = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Stop the spawner +//----------------------------------------------------------------------------- +void CMomentaryRotButton::Disable( void ) +{ + m_bDisabled = true; +} \ No newline at end of file diff --git a/sp/src/game/server/buttons.h b/sp/src/game/server/buttons.h new file mode 100644 index 00000000..c78a8652 --- /dev/null +++ b/sp/src/game/server/buttons.h @@ -0,0 +1,172 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef BUTTONS_H +#define BUTTONS_H +#ifdef _WIN32 +#pragma once +#endif + + +class CBaseButton : public CBaseToggle +{ +public: + + DECLARE_CLASS( CBaseButton, CBaseToggle ); + + void Spawn( void ); + virtual void Precache( void ); + bool CreateVPhysics(); + void RotSpawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + int DrawDebugTextOverlays(); + +protected: + + void ButtonActivate( ); + void SparkSoundCache( void ); + + void ButtonTouch( ::CBaseEntity *pOther ); + void ButtonSpark ( void ); + void TriggerAndWait( void ); + void ButtonReturn( void ); + void ButtonBackHome( void ); + void ButtonUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + bool OnUseLocked( CBaseEntity *pActivator ); + + virtual void Lock(); + virtual void Unlock(); + + // Input handlers + void InputLock( inputdata_t &inputdata ); + void InputUnlock( inputdata_t &inputdata ); + void InputPress( inputdata_t &inputdata ); + void InputPressIn( inputdata_t &inputdata ); + void InputPressOut( inputdata_t &inputdata ); + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + enum BUTTON_CODE { BUTTON_NOTHING, BUTTON_ACTIVATE, BUTTON_RETURN, BUTTON_PRESS }; + + BUTTON_CODE ButtonResponseToTouch( void ); + void Press( CBaseEntity *pActivator, BUTTON_CODE eCode ); + + DECLARE_DATADESC(); + + virtual int ObjectCaps(void); + + Vector m_vecMoveDir; + + bool m_fStayPushed; // button stays pushed in until touched again? + bool m_fRotating; // a rotating button? default is a sliding button. + + locksound_t m_ls; // door lock sounds + + byte m_bLockedSound; // ordinals from entity selection + byte m_bLockedSentence; + byte m_bUnlockedSound; + byte m_bUnlockedSentence; + bool m_bLocked; + int m_sounds; + float m_flUseLockedTime; // Controls how often we fire the OnUseLocked output. + + bool m_bSolidBsp; + + string_t m_sNoise; // The actual WAV file name of the sound. + + COutputEvent m_OnDamaged; + COutputEvent m_OnPressed; + COutputEvent m_OnUseLocked; + COutputEvent m_OnIn; + COutputEvent m_OnOut; + + int m_nState; +}; + + +// +// Rotating button (aka "lever") +// +class CRotButton : public CBaseButton +{ +public: + DECLARE_CLASS( CRotButton, CBaseButton ); + + void Spawn( void ); + bool CreateVPhysics( void ); + +}; + + +class CMomentaryRotButton : public CRotButton +{ + DECLARE_CLASS( CMomentaryRotButton, CRotButton ); + +public: + void Spawn ( void ); + bool CreateVPhysics( void ); + virtual int ObjectCaps( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void UseMoveDone( void ); + void ReturnMoveDone( void ); + void OutputMovementComplete(void); + void SetPositionMoveDone(void); + void UpdateSelf( float value, bool bPlaySound ); + + void PlaySound( void ); + void UpdateTarget( float value, CBaseEntity *pActivator ); + + int DrawDebugTextOverlays(void); + + static CMomentaryRotButton *Instance( edict_t *pent ) { return (CMomentaryRotButton *)GetContainingEntity(pent); } + + float GetPos(const QAngle &vecAngles); + + DECLARE_DATADESC(); + + virtual void Lock(); + virtual void Unlock(); + + // Input handlers + void InputSetPosition( inputdata_t &inputdata ); + void InputSetPositionImmediately( inputdata_t &inputdata ); + void InputDisableUpdateTarget( inputdata_t &inputdata ); + void InputEnableUpdateTarget( inputdata_t &inputdata ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + virtual void Enable( void ); + virtual void Disable( void ); + + bool m_bDisabled; + + COutputFloat m_Position; + COutputEvent m_OnUnpressed; + COutputEvent m_OnFullyOpen; + COutputEvent m_OnFullyClosed; + COutputEvent m_OnReachedPosition; + + int m_lastUsed; + QAngle m_start; + QAngle m_end; + float m_IdealYaw; + string_t m_sNoise; + + bool m_bUpdateTarget; // Used when jiggling so that we don't jiggle the target (door, etc) + + int m_direction; + float m_returnSpeed; + float m_flStartPosition; + +protected: + + void UpdateThink( void ); +}; + + +#endif // BUTTONS_H diff --git a/sp/src/game/server/cbase.cpp b/sp/src/game/server/cbase.cpp new file mode 100644 index 00000000..d98ea9cd --- /dev/null +++ b/sp/src/game/server/cbase.cpp @@ -0,0 +1,2199 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* +Entity Data Descriptions + +Each entity has an array which defines it's data in way that is useful for +entity communication, parsing initial values from the map, and save/restore. + +each entity has to have the following line in it's class declaration: + + DECLARE_DATADESC(); + +this line defines that it has an m_DataDesc[] array, and declares functions through +which various subsystems can iterate the data. + +In it's implementation, each entity has to have: + + typedescription_t CBaseEntity::m_DataDesc[] = { ... } + +in which all it's data is defined (see below), followed by: + + +which implements the functions necessary for iterating through an entities data desc. + +There are several types of data: + + FIELD : this is a variable which gets saved & loaded from disk + KEY : this variable can be read in from the map file + GLOBAL : a global field is actually local; it is saved/restored, but is actually + unique to the entity on each level. + CUSTOM : the save/restore parsing functions are described by the user. + ARRAY : an array of values + OUTPUT : a variable or event that can be connected to other entities (see below) + INPUTFUNC : maps a string input to a function pointer. Outputs connected to this input + will call the notify function when fired. + INPUT : maps a string input to a member variable. Outputs connected to this input + will update the input data value when fired. + INPUTNOTIFY : maps a string input to a member variable/function pointer combo. Outputs + connected to this input will update the data value and call the notify + function when fired. + +some of these can overlap. all the data descriptions usable are: + + DEFINE_FIELD( name, fieldtype ) + DEFINE_KEYFIELD( name, fieldtype, mapname ) + DEFINE_KEYFIELD_NOTSAVED( name, fieldtype, mapname ) + DEFINE_ARRAY( name, fieldtype, count ) + DEFINE_GLOBAL_FIELD(name, fieldtype ) + DEFINE_CUSTOM_FIELD(name, datafuncs, mapname ) + DEFINE_GLOBAL_KEYFIELD(name, fieldtype, mapname ) + +where: + type is the name of the class (eg. CBaseEntity) + name is the name of the variable in the class (eg. m_iHealth) + fieldtype is the type of data (FIELD_STRING, FIELD_INTEGER, etc) + mapname is the string by which this variable is associated with map file data + count is the number of items in the array + datafuncs is a struct containing function pointers for a custom-defined save/restore/parse + +OUTPUTS: + + DEFINE_OUTPUT( outputvar, outputname ) + + This maps the string 'outputname' to the COutput-derived member variable outputvar. In the VMF + file these outputs can be hooked up to inputs (see above). Whenever the internal state + of an entity changes it will often fire off outputs so that map makers can hook up behaviors. + e.g. A door entity would have OnDoorOpen, OnDoorClose, OnTouched, etc outputs. +*/ + + +#include "cbase.h" +#include "entitylist.h" +#include "mapentities_shared.h" +#include "isaverestore.h" +#include "eventqueue.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "mempool.h" +#include "tier1/strtools.h" +#include "datacache/imdlcache.h" +#include "env_debughistory.h" +#ifdef MAPBASE +#include "mapbase/variant_tools.h" +#include "mapbase/matchers.h" +#endif + +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ISaveRestoreOps *variantFuncs; // function pointer set for save/restoring variants + +BEGIN_SIMPLE_DATADESC( CEventAction ) + DEFINE_FIELD( m_iTarget, FIELD_STRING ), + DEFINE_FIELD( m_iTargetInput, FIELD_STRING ), + DEFINE_FIELD( m_iParameter, FIELD_STRING ), + DEFINE_FIELD( m_flDelay, FIELD_FLOAT ), + DEFINE_FIELD( m_nTimesToFire, FIELD_INTEGER ), + DEFINE_FIELD( m_iIDStamp, FIELD_INTEGER ), + + // This is dealt with by the Restore method + // DEFINE_FIELD( m_pNext, CEventAction ), +END_DATADESC() + + +// ID Stamp used to uniquely identify every output +int CEventAction::s_iNextIDStamp = 0; + + +//----------------------------------------------------------------------------- +// Purpose: Creates an event action and assigns it an unique ID stamp. +// Input : ActionData - the map file data block descibing the event action. +//----------------------------------------------------------------------------- +CEventAction::CEventAction( const char *ActionData ) +{ + m_pNext = NULL; + m_iIDStamp = ++s_iNextIDStamp; + + m_flDelay = 0; + m_iTarget = NULL_STRING; + m_iParameter = NULL_STRING; + m_iTargetInput = NULL_STRING; + m_nTimesToFire = EVENT_FIRE_ALWAYS; + + if (ActionData == NULL) + return; + + char szToken[256]; + + // + // Parse the target name. + // + const char *psz = nexttoken(szToken, ActionData, ',', sizeof(szToken)); + if (szToken[0] != '\0') + { + m_iTarget = AllocPooledString(szToken); + } + + // + // Parse the input name. + // + psz = nexttoken(szToken, psz, ',', sizeof(szToken)); + if (szToken[0] != '\0') + { + m_iTargetInput = AllocPooledString(szToken); + } + else + { + m_iTargetInput = AllocPooledString("Use"); + } + + // + // Parse the parameter override. + // + psz = nexttoken(szToken, psz, ',', sizeof(szToken)); + if (szToken[0] != '\0') + { + m_iParameter = AllocPooledString(szToken); + } + + // + // Parse the delay. + // + psz = nexttoken(szToken, psz, ',', sizeof(szToken)); + if (szToken[0] != '\0') + { + m_flDelay = atof(szToken); + } + + // + // Parse the number of times to fire. + // + nexttoken(szToken, psz, ',', sizeof(szToken)); + if (szToken[0] != '\0') + { + m_nTimesToFire = atoi(szToken); + if (m_nTimesToFire == 0) + { + m_nTimesToFire = EVENT_FIRE_ALWAYS; + } + } +} + + +// this memory pool stores blocks around the size of CEventAction/inputitem_t structs +// can be used for other blocks; will error if to big a block is tried to be allocated +CUtlMemoryPool g_EntityListPool( MAX(sizeof(CEventAction),sizeof(CMultiInputVar::inputitem_t)), 512, CUtlMemoryPool::GROW_FAST, "g_EntityListPool" ); + +#include "tier0/memdbgoff.h" + +void *CEventAction::operator new( size_t stAllocateBlock ) +{ + return g_EntityListPool.Alloc( stAllocateBlock ); +} + +void *CEventAction::operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ) +{ + return g_EntityListPool.Alloc( stAllocateBlock ); +} + +void CEventAction::operator delete( void *pMem ) +{ + g_EntityListPool.Free( pMem ); +} + +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Returns the highest-valued delay in our list of event actions. +//----------------------------------------------------------------------------- +float CBaseEntityOutput::GetMaxDelay(void) +{ + float flMaxDelay = 0; + CEventAction *ev = m_ActionList; + + while (ev != NULL) + { + if (ev->m_flDelay > flMaxDelay) + { + flMaxDelay = ev->m_flDelay; + } + ev = ev->m_pNext; + } + + return(flMaxDelay); +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor. +//----------------------------------------------------------------------------- +CBaseEntityOutput::~CBaseEntityOutput() +{ + CEventAction *ev = m_ActionList; + while (ev != NULL) + { + CEventAction *pNext = ev->m_pNext; + delete ev; + ev = pNext; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Fires the event, causing a sequence of action to occur in other ents. +// Input : pActivator - Entity that initiated this sequence of actions. +// pCaller - Entity that is actually causing the event. +//----------------------------------------------------------------------------- +void CBaseEntityOutput::FireOutput(variant_t Value, CBaseEntity *pActivator, CBaseEntity *pCaller, float fDelay) +{ + // + // Iterate through all eventactions and fire them off. + // + CEventAction *ev = m_ActionList; + CEventAction *prev = NULL; + + while (ev != NULL) + { + if (ev->m_iParameter == NULL_STRING) + { + // + // Post the event with the default parameter. + // + g_EventQueue.AddEvent( STRING(ev->m_iTarget), STRING(ev->m_iTargetInput), Value, ev->m_flDelay + fDelay, pActivator, pCaller, ev->m_iIDStamp ); + } + else + { + // + // Post the event with a parameter override. + // + variant_t ValueOverride; + ValueOverride.SetString( ev->m_iParameter ); +#ifdef MAPBASE + // I found this while making point_advanced_finder. FireOutput()'s own delay parameter doesn't work with...uh...parameters. + g_EventQueue.AddEvent( STRING(ev->m_iTarget), STRING(ev->m_iTargetInput), ValueOverride, ev->m_flDelay + fDelay, pActivator, pCaller, ev->m_iIDStamp ); +#else + g_EventQueue.AddEvent( STRING(ev->m_iTarget), STRING(ev->m_iTargetInput), ValueOverride, ev->m_flDelay, pActivator, pCaller, ev->m_iIDStamp ); +#endif + } + + if ( ev->m_flDelay ) + { + char szBuffer[256]; + Q_snprintf( szBuffer, + sizeof(szBuffer), + "(%0.2f) output: (%s,%s) -> (%s,%s,%.1f)(%s)\n", +#ifdef TF_DLL + engine->GetServerTime(), +#else + gpGlobals->curtime, +#endif + pCaller ? STRING(pCaller->m_iClassname) : "NULL", + pCaller ? STRING(pCaller->GetEntityName()) : "NULL", + STRING(ev->m_iTarget), + STRING(ev->m_iTargetInput), + ev->m_flDelay, + STRING(ev->m_iParameter) ); + +#ifdef MAPBASE + CGMsg( 2, CON_GROUP_IO_SYSTEM, "%s", szBuffer ); +#else + DevMsg( 2, "%s", szBuffer ); +#endif + ADD_DEBUG_HISTORY( HISTORY_ENTITY_IO, szBuffer ); + } + else + { + char szBuffer[256]; + Q_snprintf( szBuffer, + sizeof(szBuffer), + "(%0.2f) output: (%s,%s) -> (%s,%s)(%s)\n", +#ifdef TF_DLL + engine->GetServerTime(), +#else + gpGlobals->curtime, +#endif + pCaller ? STRING(pCaller->m_iClassname) : "NULL", + pCaller ? STRING(pCaller->GetEntityName()) : "NULL", STRING(ev->m_iTarget), + STRING(ev->m_iTargetInput), + STRING(ev->m_iParameter) ); + +#ifdef MAPBASE + CGMsg( 2, CON_GROUP_IO_SYSTEM, "%s", szBuffer ); +#else + DevMsg( 2, "%s", szBuffer ); +#endif + ADD_DEBUG_HISTORY( HISTORY_ENTITY_IO, szBuffer ); + } + + if ( pCaller && pCaller->m_debugOverlays & OVERLAY_MESSAGE_BIT) + { + pCaller->DrawOutputOverlay(ev); + } + + // + // Remove the event action from the list if it was set to be fired a finite + // number of times (and has been). + // + bool bRemove = false; + if (ev->m_nTimesToFire != EVENT_FIRE_ALWAYS) + { + ev->m_nTimesToFire--; + if (ev->m_nTimesToFire == 0) + { + char szBuffer[256]; + Q_snprintf( szBuffer, sizeof(szBuffer), "Removing from action list: (%s,%s) -> (%s,%s)\n", pCaller ? STRING(pCaller->m_iClassname) : "NULL", pCaller ? STRING(pCaller->GetEntityName()) : "NULL", STRING(ev->m_iTarget), STRING(ev->m_iTargetInput)); + +#ifdef MAPBASE + CGMsg( 2, CON_GROUP_IO_SYSTEM, "%s", szBuffer ); +#else + DevMsg( 2, "%s", szBuffer ); +#endif + ADD_DEBUG_HISTORY( HISTORY_ENTITY_IO, szBuffer ); + bRemove = true; + } + } + + if (!bRemove) + { + prev = ev; + ev = ev->m_pNext; + } + else + { + if (prev != NULL) + { + prev->m_pNext = ev->m_pNext; + } + else + { + m_ActionList = ev->m_pNext; + } + + CEventAction *next = ev->m_pNext; + delete ev; + ev = next; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Parameterless firing of an event +// Input : pActivator - +// pCaller - +//----------------------------------------------------------------------------- +void COutputEvent::FireOutput(CBaseEntity *pActivator, CBaseEntity *pCaller, float fDelay) +{ + variant_t Val; + Val.Set( FIELD_VOID, NULL ); + CBaseEntityOutput::FireOutput(Val, pActivator, pCaller, fDelay); +} + + +void CBaseEntityOutput::ParseEventAction( const char *EventData ) +{ + AddEventAction( new CEventAction( EventData ) ); +} + +void CBaseEntityOutput::AddEventAction( CEventAction *pEventAction ) +{ + pEventAction->m_pNext = m_ActionList; + m_ActionList = pEventAction; +} + +void CBaseEntityOutput::RemoveEventAction( CEventAction *pEventAction ) +{ + CEventAction *pAction = GetActionList(); + CEventAction *pPrevAction = NULL; + while ( pAction ) + { + if ( pAction == pEventAction ) + { + if ( !pPrevAction ) + { + m_ActionList = NULL; + } + else + { + pPrevAction->m_pNext = pAction->m_pNext; + } + return; + } + pAction = pAction->m_pNext; + } +} + + +// save data description for the event queue +BEGIN_SIMPLE_DATADESC( CBaseEntityOutput ) + + DEFINE_CUSTOM_FIELD( m_Value, variantFuncs ), + + // This is saved manually by CBaseEntityOutput::Save + // DEFINE_FIELD( m_ActionList, CEventAction ), +END_DATADESC() + + +int CBaseEntityOutput::Save( ISave &save ) +{ + // save that value out to disk, so we know how many to restore + if ( !save.WriteFields( "Value", this, NULL, m_DataMap.dataDesc, m_DataMap.dataNumFields ) ) + return 0; + + for ( CEventAction *ev = m_ActionList; ev != NULL; ev = ev->m_pNext ) + { + if ( !save.WriteFields( "EntityOutput", ev, NULL, ev->m_DataMap.dataDesc, ev->m_DataMap.dataNumFields ) ) + return 0; + } + + return 1; +} + +int CBaseEntityOutput::Restore( IRestore &restore, int elementCount ) +{ + // load the number of items saved + if ( !restore.ReadFields( "Value", this, NULL, m_DataMap.dataDesc, m_DataMap.dataNumFields ) ) + return 0; + + m_ActionList = NULL; + + // read in all the fields + CEventAction *lastEv = NULL; + for ( int i = 0; i < elementCount; i++ ) + { + CEventAction *ev = new CEventAction(NULL); + + if ( !restore.ReadFields( "EntityOutput", ev, NULL, ev->m_DataMap.dataDesc, ev->m_DataMap.dataNumFields ) ) + return 0; + + // add it to the list in the same order it was saved in + if ( lastEv ) + { + lastEv->m_pNext = ev; + } + else + { + m_ActionList = ev; + } + ev->m_pNext = NULL; + lastEv = ev; + } + + return 1; +} + +int CBaseEntityOutput::NumberOfElements( void ) +{ + int count = 0; + for ( CEventAction *ev = m_ActionList; ev != NULL; ev = ev->m_pNext ) + { + count++; + } + return count; +} + +/// Delete every single action in the action list. +void CBaseEntityOutput::DeleteAllElements( void ) +{ + // walk front to back, deleting as we go. We needn't fix up pointers because + // EVERYTHING will die. + + CEventAction *pNext = m_ActionList; + // wipe out the head + m_ActionList = NULL; + while (pNext) + { + register CEventAction *strikeThis = pNext; + pNext = pNext->m_pNext; + delete strikeThis; + } + +} + +/// EVENTS save/restore parsing wrapper + +class CEventsSaveDataOps : public ISaveRestoreOps +{ + virtual void Save( const SaveRestoreFieldInfo_t &fieldInfo, ISave *pSave ) + { + AssertMsg( fieldInfo.pTypeDesc->fieldSize == 1, "CEventsSaveDataOps does not support arrays"); + + CBaseEntityOutput *ev = (CBaseEntityOutput*)fieldInfo.pField; + const int fieldSize = fieldInfo.pTypeDesc->fieldSize; + for ( int i = 0; i < fieldSize; i++, ev++ ) + { + // save out the number of fields + int numElements = ev->NumberOfElements(); + pSave->WriteInt( &numElements, 1 ); + + // save the event data + ev->Save( *pSave ); + } + } + + virtual void Restore( const SaveRestoreFieldInfo_t &fieldInfo, IRestore *pRestore ) + { + AssertMsg( fieldInfo.pTypeDesc->fieldSize == 1, "CEventsSaveDataOps does not support arrays"); + + CBaseEntityOutput *ev = (CBaseEntityOutput*)fieldInfo.pField; + const int fieldSize = fieldInfo.pTypeDesc->fieldSize; + for ( int i = 0; i < fieldSize; i++, ev++ ) + { + int nElements = pRestore->ReadInt(); + + Assert( nElements < 100 ); + + ev->Restore( *pRestore, nElements ); + } + } + + virtual bool IsEmpty( const SaveRestoreFieldInfo_t &fieldInfo ) + { + AssertMsg( fieldInfo.pTypeDesc->fieldSize == 1, "CEventsSaveDataOps does not support arrays"); + + // check all the elements of the array (usually only 1) + CBaseEntityOutput *ev = (CBaseEntityOutput*)fieldInfo.pField; + const int fieldSize = fieldInfo.pTypeDesc->fieldSize; + for ( int i = 0; i < fieldSize; i++, ev++ ) + { + // It's not empty if it has events or if it has a non-void variant value + if (( ev->NumberOfElements() != 0 ) || ( ev->ValueFieldType() != FIELD_VOID )) + return 0; + } + + // variant has no data + return 1; + } + + virtual void MakeEmpty( const SaveRestoreFieldInfo_t &fieldInfo ) + { + // Don't no how to. This is okay, since objects of this type + // are always born clean before restore, and not reused + } + + virtual bool Parse( const SaveRestoreFieldInfo_t &fieldInfo, char const* szValue ) + { + CBaseEntityOutput *ev = (CBaseEntityOutput*)fieldInfo.pField; + ev->ParseEventAction( szValue ); + return true; + } +}; + +CEventsSaveDataOps g_EventsSaveDataOps; +ISaveRestoreOps *eventFuncs = &g_EventsSaveDataOps; + +//----------------------------------------------------------------------------- +// CMultiInputVar implementation +// +// Purpose: holds a list of inputs and their ID tags +// used for entities that hold inputs from a set of other entities +//----------------------------------------------------------------------------- + + +//----------------------------------------------------------------------------- +// Purpose: destructor, frees the data list +//----------------------------------------------------------------------------- +CMultiInputVar::~CMultiInputVar() +{ + if ( m_InputList ) + { + while ( m_InputList->next != NULL ) + { + inputitem_t *input = m_InputList->next; + m_InputList->next = input->next; + delete input; + } + delete m_InputList; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Updates the data set with a new value +// Input : newVal - the new value to add to or update in the list +// outputID - the source of the value +//----------------------------------------------------------------------------- +void CMultiInputVar::AddValue( variant_t newVal, int outputID ) +{ + // see if it's already in the list + inputitem_t *inp; + for ( inp = m_InputList; inp != NULL; inp = inp->next ) + { + // already in list, so just update this link + if ( inp->outputID == outputID ) + { + inp->value = newVal; + return; + } + } + + // add to start of list + inp = new inputitem_t; + inp->value = newVal; + inp->outputID = outputID; + if ( !m_InputList ) + { + m_InputList = inp; + inp->next = NULL; + } + else + { + inp->next = m_InputList; + m_InputList = inp; + } +} + + +#include "tier0/memdbgoff.h" + +//----------------------------------------------------------------------------- +// Purpose: allocates memory from the entitylist pool +//----------------------------------------------------------------------------- +void *CMultiInputVar::inputitem_t::operator new( size_t stAllocateBlock ) +{ + return g_EntityListPool.Alloc( stAllocateBlock ); +} + +void *CMultiInputVar::inputitem_t::operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ) +{ + return g_EntityListPool.Alloc( stAllocateBlock ); +} + +//----------------------------------------------------------------------------- +// Purpose: frees memory from the entitylist pool +//----------------------------------------------------------------------------- +void CMultiInputVar::inputitem_t::operator delete( void *pMem ) +{ + g_EntityListPool.Free( pMem ); +} + +#include "tier0/memdbgon.h" + + +//----------------------------------------------------------------------------- +// CEventQueue implementation +// +// Purpose: holds and executes a global prioritized queue of entity actions +//----------------------------------------------------------------------------- +DEFINE_FIXEDSIZE_ALLOCATOR( EventQueuePrioritizedEvent_t, 128, CUtlMemoryPool::GROW_SLOW ); + +CEventQueue g_EventQueue; + +CEventQueue::CEventQueue() +{ + m_Events.m_flFireTime = -FLT_MAX; + m_Events.m_pNext = NULL; + + Init(); +} + +CEventQueue::~CEventQueue() +{ + Clear(); +} + +// Robin: Left here for backwards compatability. +class CEventQueueSaveLoadProxy : public CLogicalEntity +{ + DECLARE_CLASS( CEventQueueSaveLoadProxy, CLogicalEntity ); + + int Save( ISave &save ) + { + if ( !BaseClass::Save(save) ) + return 0; + + // save out the message queue + return g_EventQueue.Save( save ); + } + + + int Restore( IRestore &restore ) + { + if ( !BaseClass::Restore(restore) ) + return 0; + + // restore the event queue + int iReturn = g_EventQueue.Restore( restore ); + + // Now remove myself, because the CEventQueue_SaveRestoreBlockHandler + // will handle future saves. + UTIL_Remove( this ); + + return iReturn; + } +}; + +LINK_ENTITY_TO_CLASS(event_queue_saveload_proxy, CEventQueueSaveLoadProxy); + +//----------------------------------------------------------------------------- +// EVENT QUEUE SAVE / RESTORE +//----------------------------------------------------------------------------- +static short EVENTQUEUE_SAVE_RESTORE_VERSION = 1; + +class CEventQueue_SaveRestoreBlockHandler : public CDefSaveRestoreBlockHandler +{ +public: + const char *GetBlockName() + { + return "EventQueue"; + } + + //--------------------------------- + + void Save( ISave *pSave ) + { + g_EventQueue.Save( *pSave ); + } + + //--------------------------------- + + void WriteSaveHeaders( ISave *pSave ) + { + pSave->WriteShort( &EVENTQUEUE_SAVE_RESTORE_VERSION ); + } + + //--------------------------------- + + void ReadRestoreHeaders( IRestore *pRestore ) + { + // No reason why any future version shouldn't try to retain backward compatability. The default here is to not do so. + short version; + pRestore->ReadShort( &version ); + m_fDoLoad = ( version == EVENTQUEUE_SAVE_RESTORE_VERSION ); + } + + //--------------------------------- + + void Restore( IRestore *pRestore, bool createPlayers ) + { + if ( m_fDoLoad ) + { + g_EventQueue.Restore( *pRestore ); + } + } + +private: + bool m_fDoLoad; +}; + +//----------------------------------------------------------------------------- + +CEventQueue_SaveRestoreBlockHandler g_EventQueue_SaveRestoreBlockHandler; + +//------------------------------------- + +ISaveRestoreBlockHandler *GetEventQueueSaveRestoreBlockHandler() +{ + return &g_EventQueue_SaveRestoreBlockHandler; +} + + +void CEventQueue::Init( void ) +{ + Clear(); +} + +void CEventQueue::Clear( void ) +{ + // delete all the events in the queue + EventQueuePrioritizedEvent_t *pe = m_Events.m_pNext; + + while ( pe != NULL ) + { + EventQueuePrioritizedEvent_t *next = pe->m_pNext; + delete pe; + pe = next; + } + + m_Events.m_pNext = NULL; +} + +void CEventQueue::Dump( void ) +{ + EventQueuePrioritizedEvent_t *pe = m_Events.m_pNext; + + Msg("Dumping event queue. Current time is: %.2f\n", +#ifdef TF_DLL + engine->GetServerTime() +#else + gpGlobals->curtime +#endif + ); + + while ( pe != NULL ) + { + EventQueuePrioritizedEvent_t *next = pe->m_pNext; + + Msg(" (%.2f) Target: '%s', Input: '%s', Parameter '%s'. Activator: '%s', Caller '%s'. \n", + pe->m_flFireTime, + STRING(pe->m_iTarget), + STRING(pe->m_iTargetInput), + pe->m_VariantValue.String(), + pe->m_pActivator ? pe->m_pActivator->GetDebugName() : "None", + pe->m_pCaller ? pe->m_pCaller->GetDebugName() : "None" ); + + pe = next; + } + + Msg("Finished dump.\n"); +} + + +//----------------------------------------------------------------------------- +// Purpose: adds the action into the correct spot in the priority queue, targeting entity via string name +//----------------------------------------------------------------------------- +#ifdef MAPBASE_VSCRIPT +int +#else +void +#endif +CEventQueue::AddEvent( const char *target, const char *targetInput, variant_t Value, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID ) +{ + // build the new event + EventQueuePrioritizedEvent_t *newEvent = new EventQueuePrioritizedEvent_t; +#ifdef TF_DLL + newEvent->m_flFireTime = engine->GetServerTime() + fireDelay; // priority key in the priority queue +#else + newEvent->m_flFireTime = gpGlobals->curtime + fireDelay; // priority key in the priority queue +#endif + newEvent->m_iTarget = MAKE_STRING( target ); + newEvent->m_pEntTarget = NULL; + newEvent->m_iTargetInput = MAKE_STRING( targetInput ); + newEvent->m_pActivator = pActivator; + newEvent->m_pCaller = pCaller; + newEvent->m_VariantValue = Value; + newEvent->m_iOutputID = outputID; + + AddEvent( newEvent ); + +#ifdef MAPBASE_VSCRIPT + Assert( sizeof(EventQueuePrioritizedEvent_t*) == sizeof(int) ); + return reinterpret_cast(newEvent); // POINTER_TO_INT +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: adds the action into the correct spot in the priority queue, targeting entity via pointer +//----------------------------------------------------------------------------- +#ifdef MAPBASE_VSCRIPT +int +#else +void +#endif +CEventQueue::AddEvent( CBaseEntity *target, const char *targetInput, variant_t Value, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID ) +{ + // build the new event + EventQueuePrioritizedEvent_t *newEvent = new EventQueuePrioritizedEvent_t; +#ifdef TF_DLL + newEvent->m_flFireTime = engine->GetServerTime() + fireDelay; // primary priority key in the priority queue +#else + newEvent->m_flFireTime = gpGlobals->curtime + fireDelay; // primary priority key in the priority queue +#endif + newEvent->m_iTarget = NULL_STRING; + newEvent->m_pEntTarget = target; + newEvent->m_iTargetInput = MAKE_STRING( targetInput ); + newEvent->m_pActivator = pActivator; + newEvent->m_pCaller = pCaller; + newEvent->m_VariantValue = Value; + newEvent->m_iOutputID = outputID; + + AddEvent( newEvent ); + +#ifdef MAPBASE_VSCRIPT + Assert( sizeof(EventQueuePrioritizedEvent_t*) == sizeof(int) ); + return reinterpret_cast(newEvent); // POINTER_TO_INT +#endif +} + +void CEventQueue::AddEvent( CBaseEntity *target, const char *action, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID ) +{ + variant_t Value; + Value.Set( FIELD_VOID, NULL ); + AddEvent( target, action, Value, fireDelay, pActivator, pCaller, outputID ); +} + + +//----------------------------------------------------------------------------- +// Purpose: private function, adds an event into the list +// Input : *newEvent - the (already built) event to add +//----------------------------------------------------------------------------- +void CEventQueue::AddEvent( EventQueuePrioritizedEvent_t *newEvent ) +{ + // loop through the actions looking for a place to insert + EventQueuePrioritizedEvent_t *pe; + for ( pe = &m_Events; pe->m_pNext != NULL; pe = pe->m_pNext ) + { + if ( pe->m_pNext->m_flFireTime > newEvent->m_flFireTime ) + { + break; + } + } + + Assert( pe ); + + // insert + newEvent->m_pNext = pe->m_pNext; + newEvent->m_pPrev = pe; + pe->m_pNext = newEvent; + if ( newEvent->m_pNext ) + { + newEvent->m_pNext->m_pPrev = newEvent; + } +} + +void CEventQueue::RemoveEvent( EventQueuePrioritizedEvent_t *pe ) +{ + Assert( pe->m_pPrev ); + pe->m_pPrev->m_pNext = pe->m_pNext; + if ( pe->m_pNext ) + { + pe->m_pNext->m_pPrev = pe->m_pPrev; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: fires off any events in the queue who's fire time is (or before) the present time +//----------------------------------------------------------------------------- +void CEventQueue::ServiceEvents( void ) +{ + if (!CBaseEntity::Debug_ShouldStep()) + { + return; + } + + EventQueuePrioritizedEvent_t *pe = m_Events.m_pNext; + +#ifdef TF_DLL + while ( pe != NULL && pe->m_flFireTime <= engine->GetServerTime() ) +#else + while ( pe != NULL && pe->m_flFireTime <= gpGlobals->curtime ) +#endif + { + MDLCACHE_CRITICAL_SECTION(); + + bool targetFound = false; + + // find the targets + if ( pe->m_iTarget != NULL_STRING ) + { + // In the context the event, the searching entity is also the caller + CBaseEntity *pSearchingEntity = pe->m_pCaller; +#ifdef MAPBASE + //=============================================================== + // + // This is the code that the I/O system uses to look for its targets. + // + // I wanted a good way to access a COutputEHANDLE's handle parameter. + // Sure, you could do it through logic_register_activator, but what if that's not good enough? + // + // Basic gEntList searches, which this originally used, would require extra implementation for another entity pointer to be passed. + // Without changing the way entity searching works, I just created a custom version of it here. + // + // Yes, all of this for mere "!output" support. + // + // I don't think there's much harm in this anyway. It's functionally identical and might even run a few nanoseconds faster + // since we don't need to check for filters or call the same loop over and over again. + // + //=============================================================== + const char *szName = STRING(pe->m_iTarget); + if ( szName[0] == '!' ) + { + CBaseEntity *target = gEntList.FindEntityProcedural( szName, pSearchingEntity, pe->m_pActivator, pe->m_pCaller ); + + if (!target) + { + // Here's the !output I was talking about. + // It only checks for it if we're looking for a procedural entity ('!' confirmed) + // and we didn't find one from FindEntityProcedural. + if (FStrEq(szName, "!output")) + { + pe->m_VariantValue.Convert( FIELD_EHANDLE ); + target = pe->m_VariantValue.Entity(); + } + } + + if (target) + { + // pump the action into the target + target->AcceptInput( STRING(pe->m_iTargetInput), pe->m_pActivator, pe->m_pCaller, pe->m_VariantValue, pe->m_iOutputID ); + targetFound = true; + } + } + else + { + const CEntInfo *pInfo = gEntList.FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if ( !ent ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + + if ( !ent->GetEntityName() ) + continue; + + if ( ent->NameMatches( szName ) ) + { + // pump the action into the target + ent->AcceptInput( STRING(pe->m_iTargetInput), pe->m_pActivator, pe->m_pCaller, pe->m_VariantValue, pe->m_iOutputID ); + targetFound = true; + } + } + } +#else + CBaseEntity *target = NULL; + while ( 1 ) + { + target = gEntList.FindEntityByName( target, pe->m_iTarget, pSearchingEntity, pe->m_pActivator, pe->m_pCaller ); + if ( !target ) + break; + + // pump the action into the target + target->AcceptInput( STRING(pe->m_iTargetInput), pe->m_pActivator, pe->m_pCaller, pe->m_VariantValue, pe->m_iOutputID ); + targetFound = true; + } +#endif + } + + // direct pointer + if ( pe->m_pEntTarget != NULL ) + { + pe->m_pEntTarget->AcceptInput( STRING(pe->m_iTargetInput), pe->m_pActivator, pe->m_pCaller, pe->m_VariantValue, pe->m_iOutputID ); + targetFound = true; + } + + if ( !targetFound ) + { + // See if we can find a target if we treat the target as a classname + if ( pe->m_iTarget != NULL_STRING ) + { + CBaseEntity *target = NULL; + while ( 1 ) + { + target = gEntList.FindEntityByClassname( target, STRING(pe->m_iTarget) ); + if ( !target ) + break; + + // pump the action into the target + target->AcceptInput( STRING(pe->m_iTargetInput), pe->m_pActivator, pe->m_pCaller, pe->m_VariantValue, pe->m_iOutputID ); + targetFound = true; + } + } + } + + if ( !targetFound ) + { + const char *pClass ="", *pName = ""; + + // might be NULL + if ( pe->m_pCaller ) + { + pClass = STRING(pe->m_pCaller->m_iClassname); + pName = STRING(pe->m_pCaller->GetEntityName()); + } + + char szBuffer[256]; + Q_snprintf( szBuffer, sizeof(szBuffer), "unhandled input: (%s) -> (%s), from (%s,%s); target entity not found\n", STRING(pe->m_iTargetInput), STRING(pe->m_iTarget), pClass, pName ); +#ifdef MAPBASE + CGMsg( 2, CON_GROUP_IO_SYSTEM, "%s", szBuffer ); +#else + DevMsg( 2, "%s", szBuffer ); +#endif + ADD_DEBUG_HISTORY( HISTORY_ENTITY_IO, szBuffer ); + } + + // remove the event from the list (remembering that the queue may have been added to) + RemoveEvent( pe ); + delete pe; + + // + // If we are in debug mode, exit the loop if we have fired the correct number of events. + // + if (CBaseEntity::Debug_IsPaused()) + { + if (!CBaseEntity::Debug_Step()) + { + break; + } + } + + // restart the list (to catch any new items have probably been added to the queue) + pe = m_Events.m_pNext; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Dumps the contents of the Entity I/O event queue to the console. +//----------------------------------------------------------------------------- +void CC_DumpEventQueue() +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + g_EventQueue.Dump(); +} +static ConCommand dumpeventqueue( "dumpeventqueue", CC_DumpEventQueue, "Dump the contents of the Entity I/O event queue to the console." ); + +//----------------------------------------------------------------------------- +// Purpose: Removes all pending events from the I/O queue that were added by the +// given caller. +// +// TODO: This is only as reliable as callers are in passing the correct +// caller pointer when they fire the outputs. Make more foolproof. +//----------------------------------------------------------------------------- +void CEventQueue::CancelEvents( CBaseEntity *pCaller ) +{ + if (!pCaller) + return; + + EventQueuePrioritizedEvent_t *pCur = m_Events.m_pNext; + + while (pCur != NULL) + { + bool bDelete = false; + if (pCur->m_pCaller == pCaller) + { + // Pointers match; make sure everything else matches. + if (!stricmp(STRING(pCur->m_pCaller->GetEntityName()), STRING(pCaller->GetEntityName())) && + !stricmp(pCur->m_pCaller->GetClassname(), pCaller->GetClassname())) + { + // Found a matching event; delete it from the queue. + bDelete = true; + } + } + + EventQueuePrioritizedEvent_t *pCurSave = pCur; + pCur = pCur->m_pNext; + + if (bDelete) + { + RemoveEvent( pCurSave ); + delete pCurSave; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Removes all pending events of the specified type from the I/O queue of the specified target +// +// TODO: This is only as reliable as callers are in passing the correct +// caller pointer when they fire the outputs. Make more foolproof. +//----------------------------------------------------------------------------- +void CEventQueue::CancelEventOn( CBaseEntity *pTarget, const char *sInputName ) +{ + if (!pTarget) + return; + + EventQueuePrioritizedEvent_t *pCur = m_Events.m_pNext; + + while (pCur != NULL) + { + bool bDelete = false; + if (pCur->m_pEntTarget == pTarget) + { + if ( !Q_strncmp( STRING(pCur->m_iTargetInput), sInputName, strlen(sInputName) ) ) + { + // Found a matching event; delete it from the queue. + bDelete = true; + } + } + + EventQueuePrioritizedEvent_t *pCurSave = pCur; + pCur = pCur->m_pNext; + + if (bDelete) + { + RemoveEvent( pCurSave ); + delete pCurSave; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if the target has any pending inputs. +// Input : *pTarget - +// *sInputName - NULL for any input, or a specified one +//----------------------------------------------------------------------------- +bool CEventQueue::HasEventPending( CBaseEntity *pTarget, const char *sInputName ) +{ + if (!pTarget) + return false; + + EventQueuePrioritizedEvent_t *pCur = m_Events.m_pNext; + + while (pCur != NULL) + { + if (pCur->m_pEntTarget == pTarget) + { + if ( !sInputName ) + return true; + + if ( !Q_strncmp( STRING(pCur->m_iTargetInput), sInputName, strlen(sInputName) ) ) + return true; + } + + pCur = pCur->m_pNext; + } + + return false; +} + +void ServiceEventQueue( void ) +{ + VPROF("ServiceEventQueue()"); + + g_EventQueue.ServiceEvents(); +} + + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +// Remove pending events on entity by input. +// +// Also removes events that were targeted with their debug name (classname when unnamed). +// E.g. CancelEventsByInput( pRelay, "Trigger" ) removes all pending logic_relay "Trigger" events. +//----------------------------------------------------------------------------- +void CEventQueue::CancelEventsByInput( CBaseEntity *pTarget, const char *szInput ) +{ + if ( !pTarget ) + return; + + string_t iszDebugName = MAKE_STRING( pTarget->GetDebugName() ); + EventQueuePrioritizedEvent_t *pCur = m_Events.m_pNext; + + while ( pCur ) + { + bool bRemove = false; + + if ( pTarget == pCur->m_pEntTarget || pCur->m_iTarget == iszDebugName ) + { + if ( !V_strncmp( STRING(pCur->m_iTargetInput), szInput, strlen(szInput) ) ) + { + bRemove = true; + } + } + + EventQueuePrioritizedEvent_t *pPrev = pCur; + pCur = pCur->m_pNext; + + if ( bRemove ) + { + RemoveEvent(pPrev); + delete pPrev; + } + } +} + +bool CEventQueue::RemoveEvent( int event ) +{ + EventQueuePrioritizedEvent_t *pe = reinterpret_cast(event); // INT_TO_POINTER + + for ( EventQueuePrioritizedEvent_t *pCur = m_Events.m_pNext; pCur; pCur = pCur->m_pNext ) + { + if ( pCur == pe ) + { + RemoveEvent(pCur); + delete pCur; + return true; + } + } + + return false; +} + +float CEventQueue::GetTimeLeft( int event ) +{ + EventQueuePrioritizedEvent_t *pe = reinterpret_cast(event); // INT_TO_POINTER + + for ( EventQueuePrioritizedEvent_t *pCur = m_Events.m_pNext; pCur; pCur = pCur->m_pNext ) + { + if ( pCur == pe ) + { + return (pCur->m_flFireTime - gpGlobals->curtime); + } + } + + return 0.f; +} +#endif // MAPBASE_VSCRIPT + + +// save data description for the event queue +BEGIN_SIMPLE_DATADESC( CEventQueue ) + // These are saved explicitly in CEventQueue::Save below + // DEFINE_FIELD( m_Events, EventQueuePrioritizedEvent_t ), + + DEFINE_FIELD( m_iListCount, FIELD_INTEGER ), // this value is only used during save/restore +END_DATADESC() + + +// save data for a single event in the queue +BEGIN_SIMPLE_DATADESC( EventQueuePrioritizedEvent_t ) + DEFINE_FIELD( m_flFireTime, FIELD_TIME ), + DEFINE_FIELD( m_iTarget, FIELD_STRING ), + DEFINE_FIELD( m_iTargetInput, FIELD_STRING ), + DEFINE_FIELD( m_pActivator, FIELD_EHANDLE ), + DEFINE_FIELD( m_pCaller, FIELD_EHANDLE ), + DEFINE_FIELD( m_pEntTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_iOutputID, FIELD_INTEGER ), + DEFINE_CUSTOM_FIELD( m_VariantValue, variantFuncs ), + +// DEFINE_FIELD( m_pNext, FIELD_??? ), +// DEFINE_FIELD( m_pPrev, FIELD_??? ), +END_DATADESC() + + +int CEventQueue::Save( ISave &save ) +{ + // count the number of items in the queue + EventQueuePrioritizedEvent_t *pe; + + m_iListCount = 0; + for ( pe = m_Events.m_pNext; pe != NULL; pe = pe->m_pNext ) + { + m_iListCount++; + } + + // save that value out to disk, so we know how many to restore + if ( !save.WriteFields( "EventQueue", this, NULL, m_DataMap.dataDesc, m_DataMap.dataNumFields ) ) + return 0; + + // cycle through all the events, saving them all + for ( pe = m_Events.m_pNext; pe != NULL; pe = pe->m_pNext ) + { + if ( !save.WriteFields( "PEvent", pe, NULL, pe->m_DataMap.dataDesc, pe->m_DataMap.dataNumFields ) ) + return 0; + } + + return 1; +} + + +int CEventQueue::Restore( IRestore &restore ) +{ + // clear the event queue + Clear(); + + // rebuild the event queue by restoring all the queue items + EventQueuePrioritizedEvent_t tmpEvent; + + // load the number of items saved + if ( !restore.ReadFields( "EventQueue", this, NULL, m_DataMap.dataDesc, m_DataMap.dataNumFields ) ) + return 0; + + for ( int i = 0; i < m_iListCount; i++ ) + { + if ( !restore.ReadFields( "PEvent", &tmpEvent, NULL, tmpEvent.m_DataMap.dataDesc, tmpEvent.m_DataMap.dataNumFields ) ) + return 0; + + // add the restored event into the list + if ( tmpEvent.m_pEntTarget ) + { + AddEvent( tmpEvent.m_pEntTarget, + STRING(tmpEvent.m_iTargetInput), + tmpEvent.m_VariantValue, +#ifdef TF_DLL + tmpEvent.m_flFireTime - engine->GetServerTime(), +#else + tmpEvent.m_flFireTime - gpGlobals->curtime, +#endif + tmpEvent.m_pActivator, + tmpEvent.m_pCaller, + tmpEvent.m_iOutputID ); + } + else + { + AddEvent( STRING(tmpEvent.m_iTarget), + STRING(tmpEvent.m_iTargetInput), + tmpEvent.m_VariantValue, +#ifdef TF_DLL + tmpEvent.m_flFireTime - engine->GetServerTime(), +#else + tmpEvent.m_flFireTime - gpGlobals->curtime, +#endif + tmpEvent.m_pActivator, + tmpEvent.m_pCaller, + tmpEvent.m_iOutputID ); + } + } + + return 1; +} + +////////////////////////// variant_t implementation ////////////////////////// + +// BUGBUG: Add support for function pointer save/restore to variants +// BUGBUG: Must pass datamap_t to read/write fields +void variant_t::Set( fieldtype_t ftype, void *data ) +{ + fieldType = ftype; + + switch ( ftype ) + { + case FIELD_BOOLEAN: bVal = *((bool *)data); break; + case FIELD_CHARACTER: iVal = *((char *)data); break; + case FIELD_SHORT: iVal = *((short *)data); break; + case FIELD_INTEGER: iVal = *((int *)data); break; + case FIELD_STRING: iszVal = *((string_t *)data); break; + case FIELD_FLOAT: flVal = *((float *)data); break; + case FIELD_COLOR32: rgbaVal = *((color32 *)data); break; + + case FIELD_VECTOR: + case FIELD_POSITION_VECTOR: + { + vecVal[0] = ((float *)data)[0]; + vecVal[1] = ((float *)data)[1]; + vecVal[2] = ((float *)data)[2]; + break; + } + +#ifdef MAPBASE + // There's this output class called COutputVariant which could output any data type, like a FIELD_INPUT input function. + // Well...nobody added support for it. It was there, but it wasn't functional. + // Mapbase adds support for it so you could variant your outputs as you please. + case FIELD_INPUT: + { + variant_t *variant = (variant_t*)data; + + // Pretty much just copying over its stored value. + fieldType = variant->FieldType(); + variant->SetOther(data); + + Set(fieldType, data); + break; + } +#endif + + case FIELD_EHANDLE: eVal = *((EHANDLE *)data); break; + case FIELD_CLASSPTR: eVal = *((CBaseEntity **)data); break; + case FIELD_VOID: + default: + iVal = 0; fieldType = FIELD_VOID; + break; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Copies the value in the variant into a block of memory +// Input : *data - the block to write into +//----------------------------------------------------------------------------- +void variant_t::SetOther( void *data ) +{ + switch ( fieldType ) + { + case FIELD_BOOLEAN: *((bool *)data) = bVal != 0; break; + case FIELD_CHARACTER: *((char *)data) = iVal; break; + case FIELD_SHORT: *((short *)data) = iVal; break; + case FIELD_INTEGER: *((int *)data) = iVal; break; + case FIELD_STRING: *((string_t *)data) = iszVal; break; + case FIELD_FLOAT: *((float *)data) = flVal; break; + case FIELD_COLOR32: *((color32 *)data) = rgbaVal; break; + + case FIELD_VECTOR: + case FIELD_POSITION_VECTOR: + { + ((float *)data)[0] = vecVal[0]; + ((float *)data)[1] = vecVal[1]; + ((float *)data)[2] = vecVal[2]; + break; + } + + case FIELD_EHANDLE: *((EHANDLE *)data) = eVal; break; + case FIELD_CLASSPTR: *((CBaseEntity **)data) = eVal; break; + } +} + +#ifdef MAPBASE +// This way we don't have to use string comparisons when reading failed conversions +static const char *g_szNoConversion = "No conversion to string"; +#endif + +//----------------------------------------------------------------------------- +// Purpose: Converts the variant to a new type. This function defines which I/O +// types can be automatically converted between. Connections that require +// an unsupported conversion will cause an error message at runtime. +// Input : newType - the type to convert to +// Output : Returns true on success, false if the conversion is not legal +//----------------------------------------------------------------------------- +bool variant_t::Convert( fieldtype_t newType ) +{ + if ( newType == fieldType ) + { + return true; + } + + // + // Converting to a null value is easy. + // + if ( newType == FIELD_VOID ) + { + Set( FIELD_VOID, NULL ); + return true; + } + + // + // FIELD_INPUT accepts the variant type directly. + // + if ( newType == FIELD_INPUT ) + { + return true; + } + +#ifdef MAPBASE + if (newType == FIELD_STRING) + { + // I got a conversion error when I tried to convert int to string. I'm actually quite baffled. + // Was that case really not handled before? Did I do something that overrode something that already did this? + const char *szString = ToString(); + + // g_szNoConversion is returned in ToString() when we can't convert to a string, + // so this is safe and it lets us get away with a pointer comparison. + if (szString != g_szNoConversion) + { + SetString(AllocPooledString(szString)); + return true; + } + return false; + } +#endif + + switch ( fieldType ) + { + case FIELD_INTEGER: + { + switch ( newType ) + { + case FIELD_FLOAT: + { + SetFloat( (float) iVal ); + return true; + } + + case FIELD_BOOLEAN: + { + SetBool( iVal != 0 ); + return true; + } + } + break; + } + + case FIELD_FLOAT: + { + switch ( newType ) + { + case FIELD_INTEGER: + { + SetInt( (int) flVal ); + return true; + } + + case FIELD_BOOLEAN: + { + SetBool( flVal != 0 ); + return true; + } + } + break; + } + + // + // Everyone must convert from FIELD_STRING if possible, since + // parameter overrides are always passed as strings. + // + case FIELD_STRING: + { + switch ( newType ) + { + case FIELD_INTEGER: + { + if (iszVal != NULL_STRING) + { + SetInt(atoi(STRING(iszVal))); + } + else + { + SetInt(0); + } + return true; + } + + case FIELD_FLOAT: + { + if (iszVal != NULL_STRING) + { + SetFloat(atof(STRING(iszVal))); + } + else + { + SetFloat(0); + } + return true; + } + + case FIELD_BOOLEAN: + { + if (iszVal != NULL_STRING) + { + SetBool( atoi(STRING(iszVal)) != 0 ); + } + else + { + SetBool(false); + } + return true; + } + + case FIELD_VECTOR: + { + Vector tmpVec = vec3_origin; + if (sscanf(STRING(iszVal), "[%f %f %f]", &tmpVec[0], &tmpVec[1], &tmpVec[2]) == 0) + { + // Try sucking out 3 floats with no []s + sscanf(STRING(iszVal), "%f %f %f", &tmpVec[0], &tmpVec[1], &tmpVec[2]); + } + SetVector3D( tmpVec ); + return true; + } + + case FIELD_COLOR32: + { + int nRed = 0; + int nGreen = 0; + int nBlue = 0; + int nAlpha = 255; + + sscanf(STRING(iszVal), "%d %d %d %d", &nRed, &nGreen, &nBlue, &nAlpha); + SetColor32( nRed, nGreen, nBlue, nAlpha ); + return true; + } + + case FIELD_EHANDLE: + { + // convert the string to an entity by locating it by classname + CBaseEntity *ent = NULL; + if ( iszVal != NULL_STRING ) + { +#ifdef MAPBASE + // We search by both entity name and class name now. + // We also have an entirely new version of Convert specifically for !activators on FIELD_EHANDLE. + ent = gEntList.FindEntityGeneric( NULL, STRING(iszVal) ); +#else + // FIXME: do we need to pass an activator in here? + ent = gEntList.FindEntityByName( NULL, iszVal ); +#endif + } + SetEntity( ent ); + return true; + } + } + + break; + } + +#ifndef MAPBASE // ToString() above handles this + case FIELD_EHANDLE: + { + switch ( newType ) + { + case FIELD_STRING: + { + // take the entities targetname as the string + string_t iszStr = NULL_STRING; + if ( eVal != NULL ) + { + SetString( eVal->GetEntityName() ); + } + return true; + } + } + break; + } +#endif + +#ifdef MAPBASE + case FIELD_VOID: + { + // Many fields already turn into some equivalent of "NULL" when given a null string_t. + // This takes advantage of that and allows FIELD_VOID to be converted to more than just empty strings. + SetString(NULL_STRING); + return Convert(newType); + } +#endif + } + + // invalid conversion + return false; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Only for when something like !activator needs to become a FIELD_EHANDLE, or when that's a possibility. +//----------------------------------------------------------------------------- +bool variant_t::Convert( fieldtype_t newType, CBaseEntity *pSelf, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + // Support for turning !activator, !caller, and !self into a FIELD_EHANDLE. + // Extremely necessary. + if (newType == FIELD_EHANDLE) + { + if (newType == fieldType) + return true; + + CBaseEntity *ent = NULL; + if (iszVal != NULL_STRING) + { + ent = gEntList.FindEntityGeneric(NULL, STRING(iszVal), pSelf, pActivator, pCaller); + } + SetEntity(ent); + return true; + } + +#if 0 // This was scrapped almost immediately. See the Trello card for details. + // Serves as a way of converting the name of the !activator, !caller, or !self into a string + // without passing the text "!activator" and stuff. + else if (fieldType == FIELD_STRING && STRING(iszVal)[0] == '&') + { + const char *val = STRING(iszVal) + 1; + + #define GetRealName(string, ent) if (FStrEq(val, string)) { if (ent) {SetString(ent->GetEntityName());} return true; } + + GetRealName("!activator", pActivator) + else GetRealName("!caller", pCaller) + else GetRealName("!self", pSelf) + } +#endif + + return Convert(newType); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: All types must be able to display as strings for debugging purposes. +// Output : Returns a pointer to the string that represents this value. +// +// NOTE: The returned pointer should not be stored by the caller as +// subsequent calls to this function will overwrite the contents +// of the buffer! +//----------------------------------------------------------------------------- +const char *variant_t::ToString( void ) const +{ + COMPILE_TIME_ASSERT( sizeof(string_t) == sizeof(int) ); + + static char szBuf[512]; + + switch (fieldType) + { + case FIELD_STRING: + { + return(STRING(iszVal)); + } + + case FIELD_BOOLEAN: + { + if (bVal == 0) + { + Q_strncpy(szBuf, "false",sizeof(szBuf)); + } + else + { + Q_strncpy(szBuf, "true",sizeof(szBuf)); + } + return(szBuf); + } + + case FIELD_INTEGER: + { + Q_snprintf( szBuf, sizeof( szBuf ), "%i", iVal ); + return(szBuf); + } + + case FIELD_FLOAT: + { + Q_snprintf(szBuf,sizeof(szBuf), "%g", flVal); + return(szBuf); + } + + case FIELD_COLOR32: + { + Q_snprintf(szBuf,sizeof(szBuf), "%d %d %d %d", (int)rgbaVal.r, (int)rgbaVal.g, (int)rgbaVal.b, (int)rgbaVal.a); + return(szBuf); + } + + case FIELD_VECTOR: + { + Q_snprintf(szBuf,sizeof(szBuf), "[%g %g %g]", (double)vecVal[0], (double)vecVal[1], (double)vecVal[2]); + return(szBuf); + } + + case FIELD_VOID: + { + szBuf[0] = '\0'; + return(szBuf); + } + + case FIELD_EHANDLE: + { +#ifdef MAPBASE + // This is a really bad idea. + const char *pszName = (Entity()) ? Entity()->GetDebugName() : "<>"; +#else + const char *pszName = (Entity()) ? STRING(Entity()->GetEntityName()) : "<>"; +#endif + Q_strncpy( szBuf, pszName, 512 ); + return (szBuf); + } + } + +#ifdef MAPBASE + return g_szNoConversion; +#else + return("No conversion to string"); +#endif +} + +#define classNameTypedef variant_t // to satisfy DEFINE... macros + +typedescription_t variant_t::m_SaveBool[] = +{ + DEFINE_FIELD( bVal, FIELD_BOOLEAN ), +}; +typedescription_t variant_t::m_SaveInt[] = +{ + DEFINE_FIELD( iVal, FIELD_INTEGER ), +}; +typedescription_t variant_t::m_SaveFloat[] = +{ + DEFINE_FIELD( flVal, FIELD_FLOAT ), +}; +typedescription_t variant_t::m_SaveEHandle[] = +{ + DEFINE_FIELD( eVal, FIELD_EHANDLE ), +}; +typedescription_t variant_t::m_SaveString[] = +{ + DEFINE_FIELD( iszVal, FIELD_STRING ), +}; +typedescription_t variant_t::m_SaveColor[] = +{ + DEFINE_FIELD( rgbaVal, FIELD_COLOR32 ), +}; + +#undef classNameTypedef + +// +// Struct for saving and restoring vector variants, since they are +// stored as float[3] and we want to take advantage of position vector +// fixup across level transitions. +// +#define classNameTypedef variant_savevector_t // to satisfy DEFINE... macros + +struct variant_savevector_t +{ + Vector vecSave; +}; +typedescription_t variant_t::m_SaveVector[] = +{ + // Just here to shut up ClassCheck +// DEFINE_ARRAY( vecVal, FIELD_FLOAT, 3 ), + + DEFINE_FIELD( vecSave, FIELD_VECTOR ), +}; +typedescription_t variant_t::m_SavePositionVector[] = +{ + DEFINE_FIELD( vecSave, FIELD_POSITION_VECTOR ), +}; +#undef classNameTypedef + +#define classNameTypedef variant_savevmatrix_t // to satisfy DEFINE... macros +struct variant_savevmatrix_t +{ + VMatrix matSave; +}; +typedescription_t variant_t::m_SaveVMatrix[] = +{ + DEFINE_FIELD( matSave, FIELD_VMATRIX ), +}; +typedescription_t variant_t::m_SaveVMatrixWorldspace[] = +{ + DEFINE_FIELD( matSave, FIELD_VMATRIX_WORLDSPACE ), +}; +#undef classNameTypedef + +#define classNameTypedef variant_savevmatrix3x4_t // to satisfy DEFINE... macros +struct variant_savevmatrix3x4_t +{ + matrix3x4_t matSave; +}; +typedescription_t variant_t::m_SaveMatrix3x4Worldspace[] = +{ + DEFINE_FIELD( matSave, FIELD_MATRIX3X4_WORLDSPACE ), +}; +#undef classNameTypedef + +class CVariantSaveDataOps : public CDefSaveRestoreOps +{ + // saves the entire array of variables + virtual void Save( const SaveRestoreFieldInfo_t &fieldInfo, ISave *pSave ) + { + variant_t *var = (variant_t*)fieldInfo.pField; + + int type = var->FieldType(); + pSave->WriteInt( &type, 1 ); + + switch ( var->FieldType() ) + { + case FIELD_VOID: + break; + case FIELD_BOOLEAN: + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveBool, 1 ); + break; + case FIELD_INTEGER: + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveInt, 1 ); + break; + case FIELD_FLOAT: + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveFloat, 1 ); + break; + case FIELD_EHANDLE: + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveEHandle, 1 ); + break; + case FIELD_STRING: + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveString, 1 ); + break; + case FIELD_COLOR32: + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveColor, 1 ); + break; + case FIELD_VECTOR: + { + variant_savevector_t Temp; + var->Vector3D(Temp.vecSave); + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, &Temp, NULL, variant_t::m_SaveVector, 1 ); + break; + } + + case FIELD_POSITION_VECTOR: + { + variant_savevector_t Temp; + var->Vector3D(Temp.vecSave); + pSave->WriteFields( fieldInfo.pTypeDesc->fieldName, &Temp, NULL, variant_t::m_SavePositionVector, 1 ); + break; + } + + default: + Warning( "Bad type %d in saved variant_t\n", var->FieldType() ); + Assert(0); + } + } + + // restores a single instance of the variable + virtual void Restore( const SaveRestoreFieldInfo_t &fieldInfo, IRestore *pRestore ) + { + variant_t *var = (variant_t*)fieldInfo.pField; + + *var = variant_t(); + + var->fieldType = (_fieldtypes)pRestore->ReadInt(); + + switch ( var->fieldType ) + { + case FIELD_VOID: + break; + case FIELD_BOOLEAN: + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveBool, 1 ); + break; + case FIELD_INTEGER: + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveInt, 1 ); + break; + case FIELD_FLOAT: + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveFloat, 1 ); + break; + case FIELD_EHANDLE: + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveEHandle, 1 ); + break; + case FIELD_STRING: + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveString, 1 ); + break; + case FIELD_COLOR32: + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, var, NULL, variant_t::m_SaveColor, 1 ); + break; + case FIELD_VECTOR: + { + variant_savevector_t Temp; + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, &Temp, NULL, variant_t::m_SaveVector, 1 ); + var->SetVector3D(Temp.vecSave); + break; + } + case FIELD_POSITION_VECTOR: + { + variant_savevector_t Temp; + pRestore->ReadFields( fieldInfo.pTypeDesc->fieldName, &Temp, NULL, variant_t::m_SavePositionVector, 1 ); + var->SetPositionVector3D(Temp.vecSave); + break; + } + default: + Warning( "Bad type %d in saved variant_t\n", var->FieldType() ); + Assert(0); + break; + } + } + + + virtual bool IsEmpty( const SaveRestoreFieldInfo_t &fieldInfo ) + { + // check all the elements of the array (usually only 1) + variant_t *var = (variant_t*)fieldInfo.pField; + for ( int i = 0; i < fieldInfo.pTypeDesc->fieldSize; i++, var++ ) + { + if ( var->FieldType() != FIELD_VOID ) + return 0; + } + + // variant has no data + return 1; + } + + virtual void MakeEmpty( const SaveRestoreFieldInfo_t &fieldInfo ) + { + // Don't no how to. This is okay, since objects of this type + // are always born clean before restore, and not reused + } + +#ifdef MAPBASE + // Parses a keyvalue string into a variant_t. + // We could just turn it into a string since variant_t can convert it later, but this keyvalue is probably a variant_t for a reason, + // meaning it might use strings and numbers completely differently without converting them. + // As a result, we try to read it to figure out what type it is. + virtual bool Parse( const SaveRestoreFieldInfo_t &fieldInfo, char const* szValue ) + { + variant_t *var = (variant_t*)fieldInfo.pField; + + *var = Variant_Parse(szValue); + + return true; + } +#endif +}; + +CVariantSaveDataOps g_VariantSaveDataOps; +ISaveRestoreOps *variantFuncs = &g_VariantSaveDataOps; + +/////////////////////// entitylist ///////////////////// + +CUtlMemoryPool g_EntListMemPool( sizeof(entitem_t), 256, CUtlMemoryPool::GROW_NONE, "g_EntListMemPool" ); + +#include "tier0/memdbgoff.h" + +void *entitem_t::operator new( size_t stAllocateBlock ) +{ + return g_EntListMemPool.Alloc( stAllocateBlock ); +} + +void *entitem_t::operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ) +{ + return g_EntListMemPool.Alloc( stAllocateBlock ); +} + +void entitem_t::operator delete( void *pMem ) +{ + g_EntListMemPool.Free( pMem ); +} + +#include "tier0/memdbgon.h" + + +CEntityList::CEntityList() +{ + m_pItemList = NULL; + m_iNumItems = 0; +} + +CEntityList::~CEntityList() +{ + // remove all items from the list + entitem_t *next, *e = m_pItemList; + while ( e != NULL ) + { + next = e->pNext; + delete e; + e = next; + } + m_pItemList = NULL; +} + +void CEntityList::AddEntity( CBaseEntity *pEnt ) +{ + // check if it's already in the list; if not, add it + entitem_t *e = m_pItemList; + while ( e != NULL ) + { + if ( e->hEnt == pEnt ) + { + // it's already in the list + return; + } + + if ( e->pNext == NULL ) + { + // we've hit the end of the list, so tack it on + e->pNext = new entitem_t; + e->pNext->hEnt = pEnt; + e->pNext->pNext = NULL; + m_iNumItems++; + return; + } + + e = e->pNext; + } + + // empty list + m_pItemList = new entitem_t; + m_pItemList->hEnt = pEnt; + m_pItemList->pNext = NULL; + m_iNumItems = 1; +} + +void CEntityList::DeleteEntity( CBaseEntity *pEnt ) +{ + // find the entry in the list and delete it + entitem_t *prev = NULL, *e = m_pItemList; + while ( e != NULL ) + { + // delete the link if it's the matching entity OR if the link is NULL + if ( e->hEnt == pEnt || e->hEnt == NULL ) + { + if ( prev ) + { + prev->pNext = e->pNext; + } + else + { + m_pItemList = e->pNext; + } + + delete e; + m_iNumItems--; + + // REVISIT: Is this correct? Is this just here to clean out dead EHANDLEs? + // restart the loop + e = m_pItemList; + prev = NULL; + continue; + } + + prev = e; + e = e->pNext; + } +} + diff --git a/sp/src/game/server/cbase.h b/sp/src/game/server/cbase.h new file mode 100644 index 00000000..290e3b25 --- /dev/null +++ b/sp/src/game/server/cbase.h @@ -0,0 +1,157 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef CBASE_H +#define CBASE_H +#ifdef _WIN32 +#pragma once +#endif + +#ifdef _WIN32 +// Silence certain warnings +#pragma warning(disable : 4244) // int or float down-conversion +#pragma warning(disable : 4305) // int or float data truncation +#pragma warning(disable : 4201) // nameless struct/union +#pragma warning(disable : 4511) // copy constructor could not be generated +#pragma warning(disable : 4675) // resolved overload was found by argument dependent lookup +#endif + +#ifdef _DEBUG +#define DEBUG 1 +#endif + +// Misc C-runtime library headers +#include + +#include + +// tier 0 +#include "tier0/dbg.h" +#include "tier0/platform.h" +#include "basetypes.h" + +// tier 1 +#include "tier1/strtools.h" +#include "utlvector.h" +#include "mathlib/vmatrix.h" + +// tier 2 +#include "string_t.h" + +// tier 3 +#include "vphysics_interface.h" + +// Shared engine/DLL constants +#include "const.h" +#include "edict.h" + +// Shared header describing protocol between engine and DLLs +#include "eiface.h" +#include "iserverentity.h" + +#include "dt_send.h" + +// Shared header between the client DLL and the game DLLs +#include "shareddefs.h" +#include "ehandle.h" + +// app +#if defined(_X360) +#define DISABLE_DEBUG_HISTORY 1 +#endif + + +#include "datamap.h" +#include "util.h" +#include "predictable_entity.h" +#include "predictableid.h" +#include "variant_t.h" +#include "takedamageinfo.h" +#include "utllinkedlist.h" +#include "touchlink.h" +#include "groundlink.h" +#include "base_transmit_proxy.h" +#include "soundflags.h" +#include "networkvar.h" +#include "baseentity_shared.h" +#include "basetoggle.h" +#include "igameevents.h" +#ifdef MAPBASE +#include "tier1/mapbase_con_groups.h" +#endif + +// saverestore.h declarations +class ISave; +class IRestore; + +// maximum number of targets a single multi_manager entity may be assigned. +#define MAX_MULTI_TARGETS 16 + +// NPCEvent.h declarations +struct animevent_t; + +struct studiohdr_t; +class CStudioHdr; + +extern void FireTargets( const char *targetName, CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + +// people gib if their health is <= this at the time of death +#define GIB_HEALTH_VALUE -30 + +#define MAX_OLD_ENEMIES 4 // how many old enemies to remember + +// used by suit voice to indicate damage sustained and repaired type to player + +enum +{ + itbd_Paralyze = 0, + itbd_NerveGas, + itbd_PoisonRecover, + itbd_Radiation, + itbd_DrownRecover, + itbd_Acid, + itbd_SlowBurn, + itbd_SlowFreeze, + + // Must be last! + CDMG_TIMEBASED +}; + +// when calling KILLED(), a value that governs gib behavior is expected to be +// one of these three values +#define GIB_NORMAL 0// gib if entity was overkilled +#define GIB_NEVER 1// never gib, no matter how much death damage is done ( freezing, etc ) +#define GIB_ALWAYS 2// always gib + +class CAI_BaseNPC; +class CAI_ScriptedSequence; +class CSound; + +#ifdef _XBOX +//#define FUNCTANK_AUTOUSE We haven't made the decision to use this yet (sjb) +#else +#undef FUNCTANK_AUTOUSE +#endif//_XBOX + +// This is a precompiled header. Include a bunch of common stuff. +// This is kind of ugly in that it adds a bunch of dependency where it isn't needed. +// But on balance, the compile time is much lower (even incrementally) once the precompiled +// headers contain these headers. +#include "precache_register.h" +#include "baseanimating.h" +#include "basecombatweapon.h" +#include "basecombatcharacter.h" +#include "gamerules.h" +#include "entitylist.h" +#include "basetempentity.h" +#include "player.h" +#include "te.h" +#include "physics.h" +#include "ndebugoverlay.h" +#include "recipientfilter.h" + +#endif // CBASE_H diff --git a/sp/src/game/server/client.cpp b/sp/src/game/server/client.cpp new file mode 100644 index 00000000..087d023f --- /dev/null +++ b/sp/src/game/server/client.cpp @@ -0,0 +1,1575 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* + +===== client.cpp ======================================================== + + client/server game specific stuff + +*/ + +#include "cbase.h" +#include "player.h" +#include "client.h" +#include "soundent.h" +#include "gamerules.h" +#include "game.h" +#include "physics.h" +#include "entitylist.h" +#include "shake.h" +#include "globalstate.h" +#include "event_tempentity_tester.h" +#include "ndebugoverlay.h" +#include "engine/IEngineSound.h" +#include +#include "tier1/strtools.h" +#include "te_effect_dispatch.h" +#include "globals.h" +#include "nav_mesh.h" +#include "team.h" +#include "datacache/imdlcache.h" +#include "basemultiplayerplayer.h" +#include "voice_gamemgr.h" + +#ifdef TF_DLL +#include "tf_player.h" +#include "tf_gamerules.h" +#endif + +#ifdef HL2_DLL +#include "weapon_physcannon.h" +#endif + +#ifdef MAPBASE +#include "fmtstr.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern int giPrecacheGrunt; + +// For not just using one big ai net +extern CBaseEntity* FindPickerEntity( CBasePlayer* pPlayer ); + +extern bool IsInCommentaryMode( void ); + +ConVar *sv_cheats = NULL; + +void ClientKill( edict_t *pEdict, const Vector &vecForce, bool bExplode = false ) +{ + CBasePlayer *pPlayer = static_cast( GetContainingEntity( pEdict ) ); + pPlayer->CommitSuicide( vecForce, bExplode ); +} + +char * CheckChatText( CBasePlayer *pPlayer, char *text ) +{ + char *p = text; + + // invalid if NULL or empty + if ( !text || !text[0] ) + return NULL; + + int length = Q_strlen( text ); + + // remove quotes (leading & trailing) if present + if (*p == '"') + { + p++; + length -=2; + p[length] = 0; + } + + // cut off after 127 chars + if ( length > 127 ) + text[127] = 0; + + GameRules()->CheckChatText( pPlayer, p ); + + return p; +} + +//// HOST_SAY +// String comes in as +// say blah blah blah +// or as +// blah blah blah +// +void Host_Say( edict_t *pEdict, const CCommand &args, bool teamonly ) +{ + CBasePlayer *client; + int j; + char *p; + char text[256]; + char szTemp[256]; + const char *cpSay = "say"; + const char *cpSayTeam = "say_team"; + const char *pcmd = args[0]; + bool bSenderDead = false; + + // We can get a raw string now, without the "say " prepended + if ( args.ArgC() == 0 ) + return; + + if ( !stricmp( pcmd, cpSay) || !stricmp( pcmd, cpSayTeam ) ) + { + if ( args.ArgC() >= 2 ) + { + p = (char *)args.ArgS(); + } + else + { + // say with a blank message, nothing to do + return; + } + } + else // Raw text, need to prepend argv[0] + { + if ( args.ArgC() >= 2 ) + { + Q_snprintf( szTemp,sizeof(szTemp), "%s %s", ( char * )pcmd, (char *)args.ArgS() ); + } + else + { + // Just a one word command, use the first word...sigh + Q_snprintf( szTemp,sizeof(szTemp), "%s", ( char * )pcmd ); + } + p = szTemp; + } + + CBasePlayer *pPlayer = NULL; + if ( pEdict ) + { + pPlayer = ((CBasePlayer *)CBaseEntity::Instance( pEdict )); + Assert( pPlayer ); + + // make sure the text has valid content + p = CheckChatText( pPlayer, p ); + } + + if ( !p ) + return; + + if ( pEdict ) + { + if ( !pPlayer->CanSpeak() ) + return; + + // See if the player wants to modify of check the text + pPlayer->CheckChatText( p, 127 ); // though the buffer szTemp that p points to is 256, + // chat text is capped to 127 in CheckChatText above + + Assert( strlen( pPlayer->GetPlayerName() ) > 0 ); + + bSenderDead = ( pPlayer->m_lifeState != LIFE_ALIVE ); + } + else + { + bSenderDead = false; + } + + const char *pszFormat = NULL; + const char *pszPrefix = NULL; + const char *pszLocation = NULL; + if ( g_pGameRules ) + { + pszFormat = g_pGameRules->GetChatFormat( teamonly, pPlayer ); + pszPrefix = g_pGameRules->GetChatPrefix( teamonly, pPlayer ); + pszLocation = g_pGameRules->GetChatLocation( teamonly, pPlayer ); + } + + const char *pszPlayerName = pPlayer ? pPlayer->GetPlayerName():"Console"; + + if ( pszPrefix && strlen( pszPrefix ) > 0 ) + { + if ( pszLocation && strlen( pszLocation ) ) + { + Q_snprintf( text, sizeof(text), "%s %s @ %s: ", pszPrefix, pszPlayerName, pszLocation ); + } + else + { + Q_snprintf( text, sizeof(text), "%s %s: ", pszPrefix, pszPlayerName ); + } + } + else + { + Q_snprintf( text, sizeof(text), "%s: ", pszPlayerName ); + } + + j = sizeof(text) - 2 - strlen(text); // -2 for /n and null terminator + if ( (int)strlen(p) > j ) + p[j] = 0; + + Q_strncat( text, p, sizeof( text ), COPY_ALL_CHARACTERS ); + Q_strncat( text, "\n", sizeof( text ), COPY_ALL_CHARACTERS ); + + // loop through all players + // Start with the first player. + // This may return the world in single player if the client types something between levels or during spawn + // so check it, or it will infinite loop + + client = NULL; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + client = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( i ) ); + if ( !client || !client->edict() ) + continue; + + if ( client->edict() == pEdict ) + continue; + + if ( !(client->IsNetClient()) ) // Not a client ? (should never be true) + continue; + + if ( teamonly && g_pGameRules->PlayerCanHearChat( client, pPlayer ) != GR_TEAMMATE ) + continue; + + if ( pPlayer && !client->CanHearAndReadChatFrom( pPlayer ) ) + continue; + + if ( pPlayer && GetVoiceGameMgr() && GetVoiceGameMgr()->IsPlayerIgnoringPlayer( pPlayer->entindex(), i ) ) + continue; + + CSingleUserRecipientFilter user( client ); + user.MakeReliable(); + + if ( pszFormat ) + { + UTIL_SayText2Filter( user, pPlayer, true, pszFormat, pszPlayerName, p, pszLocation ); + } + else + { + UTIL_SayTextFilter( user, text, pPlayer, true ); + } + } + + if ( pPlayer ) + { + // print to the sending client + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + if ( pszFormat ) + { + UTIL_SayText2Filter( user, pPlayer, true, pszFormat, pszPlayerName, p, pszLocation ); + } + else + { + UTIL_SayTextFilter( user, text, pPlayer, true ); + } + } + + // echo to server console + // Adrian: Only do this if we're running a dedicated server since we already print to console on the client. + if ( engine->IsDedicatedServer() ) + Msg( "%s", text ); + + Assert( p ); + + int userid = 0; + const char *networkID = "Console"; + const char *playerName = "Console"; + const char *playerTeam = "Console"; + if ( pPlayer ) + { + userid = pPlayer->GetUserID(); + networkID = pPlayer->GetNetworkIDString(); + playerName = pPlayer->GetPlayerName(); + CTeam *team = pPlayer->GetTeam(); + if ( team ) + { + playerTeam = team->GetName(); + } + } + + if ( teamonly ) + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" say_team \"%s\"\n", playerName, userid, networkID, playerTeam, p ); + else + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" say \"%s\"\n", playerName, userid, networkID, playerTeam, p ); + + IGameEvent * event = gameeventmanager->CreateEvent( "player_say", true ); + + if ( event ) + { + event->SetInt("userid", userid ); + event->SetString("text", p ); + event->SetInt("priority", 1 ); // HLTV event priority, not transmitted + gameeventmanager->FireEvent( event, true ); + } +} + + +void ClientPrecache( void ) +{ + // Precache cable textures. + CBaseEntity::PrecacheModel( "cable/cable.vmt" ); + CBaseEntity::PrecacheModel( "cable/cable_lit.vmt" ); + CBaseEntity::PrecacheModel( "cable/chain.vmt" ); + CBaseEntity::PrecacheModel( "cable/rope.vmt" ); + CBaseEntity::PrecacheModel( "sprites/blueglow1.vmt" ); + CBaseEntity::PrecacheModel( "sprites/purpleglow1.vmt" ); + CBaseEntity::PrecacheModel( "sprites/purplelaser1.vmt" ); + +#ifndef HL2MP + CBaseEntity::PrecacheScriptSound( "Hud.Hint" ); +#endif // HL2MP + CBaseEntity::PrecacheScriptSound( "Player.FallDamage" ); + CBaseEntity::PrecacheScriptSound( "Player.Swim" ); + + // General HUD sounds + CBaseEntity::PrecacheScriptSound( "Player.PickupWeapon" ); + CBaseEntity::PrecacheScriptSound( "Player.DenyWeaponSelection" ); + CBaseEntity::PrecacheScriptSound( "Player.WeaponSelected" ); + CBaseEntity::PrecacheScriptSound( "Player.WeaponSelectionClose" ); + CBaseEntity::PrecacheScriptSound( "Player.WeaponSelectionMoveSlot" ); + + // General legacy temp ents sounds + CBaseEntity::PrecacheScriptSound( "Bounce.Glass" ); + CBaseEntity::PrecacheScriptSound( "Bounce.Metal" ); + CBaseEntity::PrecacheScriptSound( "Bounce.Flesh" ); + CBaseEntity::PrecacheScriptSound( "Bounce.Wood" ); + CBaseEntity::PrecacheScriptSound( "Bounce.Shrapnel" ); + CBaseEntity::PrecacheScriptSound( "Bounce.ShotgunShell" ); + CBaseEntity::PrecacheScriptSound( "Bounce.Shell" ); + CBaseEntity::PrecacheScriptSound( "Bounce.Concrete" ); + +#ifdef MAPBASE + // Game Instructor sounds + CBaseEntity::PrecacheScriptSound( "Instructor.LessonStart" ); + CBaseEntity::PrecacheScriptSound( "Instructor.ImportantLessonStart" ); + + // TODO: Does sv_pure cover this? This is from the ASW SDK to prevent people from making simple scripted wall hacks + //engine->ForceExactFile( "scripts/instructor_lessons.txt" ); + //engine->ForceExactFile( "scripts/mod_lessons.txt" ); +#endif + + ClientGamePrecache(); +} + +CON_COMMAND_F( cast_ray, "Tests collision detection", FCVAR_CHEAT ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + + Vector forward; + trace_t tr; + + pPlayer->EyeVectors( &forward ); + Vector start = pPlayer->EyePosition(); + UTIL_TraceLine(start, start + forward * MAX_COORD_RANGE, MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.DidHit() ) + { + DevMsg(1, "Hit %s\nposition %.2f, %.2f, %.2f\nangles %.2f, %.2f, %.2f\n", tr.m_pEnt->GetClassname(), + tr.m_pEnt->GetAbsOrigin().x, tr.m_pEnt->GetAbsOrigin().y, tr.m_pEnt->GetAbsOrigin().z, + tr.m_pEnt->GetAbsAngles().x, tr.m_pEnt->GetAbsAngles().y, tr.m_pEnt->GetAbsAngles().z ); + DevMsg(1, "Hit: hitbox %d, hitgroup %d, physics bone %d, solid %d, surface %s, surfaceprop %s, contents %08x\n", tr.hitbox, tr.hitgroup, tr.physicsbone, tr.m_pEnt->GetSolid(), tr.surface.name, physprops->GetPropName( tr.surface.surfaceProps ), tr.contents ); + NDebugOverlay::Line( start, tr.endpos, 0, 255, 0, false, 10 ); + NDebugOverlay::Line( tr.endpos, tr.endpos + tr.plane.normal * 12, 255, 255, 0, false, 10 ); + } +} + +CON_COMMAND_F( cast_hull, "Tests hull collision detection", FCVAR_CHEAT ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + + Vector forward; + trace_t tr; + + Vector extents; + extents.Init(16,16,16); + pPlayer->EyeVectors( &forward ); + Vector start = pPlayer->EyePosition(); + UTIL_TraceHull(start, start + forward * MAX_COORD_RANGE, -extents, extents, MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); + if ( tr.DidHit() ) + { + DevMsg(1, "Hit %s\nposition %.2f, %.2f, %.2f\nangles %.2f, %.2f, %.2f\n", tr.m_pEnt->GetClassname(), + tr.m_pEnt->GetAbsOrigin().x, tr.m_pEnt->GetAbsOrigin().y, tr.m_pEnt->GetAbsOrigin().z, + tr.m_pEnt->GetAbsAngles().x, tr.m_pEnt->GetAbsAngles().y, tr.m_pEnt->GetAbsAngles().z ); + DevMsg(1, "Hit: hitbox %d, hitgroup %d, physics bone %d, solid %d, surface %s, surfaceprop %s\n", tr.hitbox, tr.hitgroup, tr.physicsbone, tr.m_pEnt->GetSolid(), tr.surface.name, physprops->GetPropName( tr.surface.surfaceProps ) ); + NDebugOverlay::SweptBox( start, tr.endpos, -extents, extents, vec3_angle, 0, 0, 255, 0, 10 ); + Vector end = tr.endpos;// - tr.plane.normal * DotProductAbs( tr.plane.normal, extents ); + NDebugOverlay::Line( end, end + tr.plane.normal * 24, 255, 255, 64, false, 10 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Used to find targets for ent_* commands +// Without a name, returns the entity under the player's crosshair. +// With a name it finds entities via name/classname/index +//----------------------------------------------------------------------------- +CBaseEntity *GetNextCommandEntity( CBasePlayer *pPlayer, const char *name, CBaseEntity *ent ) +{ + if ( !pPlayer ) + return NULL; + + // If no name was given set bits based on the picked + if (FStrEq(name,"")) + { + // If we've already found an entity, return NULL. + // Makes it easier to write code using this func. + if ( ent ) + return NULL; + + return FindPickerEntity( pPlayer ); + } + + int index = atoi( name ); + if ( index ) + { + // If we've already found an entity, return NULL. + // Makes it easier to write code using this func. + if ( ent ) + return NULL; + + return CBaseEntity::Instance( index ); + } + + // Loop through all entities matching, starting from the specified previous + while ( (ent = gEntList.NextEnt(ent)) != NULL ) + { + if ( (ent->GetEntityName() != NULL_STRING && ent->NameMatches(name)) || + (ent->m_iClassname != NULL_STRING && ent->ClassMatches(name)) ) + { + return ent; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: called each time a player uses a "cmd" command +// Input : pPlayer - the player who issued the command +//----------------------------------------------------------------------------- +void SetDebugBits( CBasePlayer* pPlayer, const char *name, int bit ) +{ + if ( !pPlayer ) + return; + + CBaseEntity *pEntity = NULL; + while ( (pEntity = GetNextCommandEntity( pPlayer, name, pEntity )) != NULL ) + { + if (pEntity->m_debugOverlays & bit) + { + pEntity->m_debugOverlays &= ~bit; + } + else + { + pEntity->m_debugOverlays |= bit; + +#ifdef AI_MONITOR_FOR_OSCILLATION + if( pEntity->IsNPC() ) + { + pEntity->MyNPCPointer()->m_ScheduleHistory.RemoveAll(); + } +#endif//AI_MONITOR_FOR_OSCILLATION + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pKillTargetName - +//----------------------------------------------------------------------------- +void KillTargets( const char *pKillTargetName ) +{ + CBaseEntity *pentKillTarget = NULL; + + DevMsg( 2, "KillTarget: %s\n", pKillTargetName ); + pentKillTarget = gEntList.FindEntityByName( NULL, pKillTargetName ); + while ( pentKillTarget ) + { + UTIL_Remove( pentKillTarget ); + + DevMsg( 2, "killing %s\n", STRING( pentKillTarget->m_iClassname ) ); + pentKillTarget = gEntList.FindEntityByName( pentKillTarget, pKillTargetName ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void ConsoleKillTarget( CBasePlayer *pPlayer, const char *name ) +{ + // If no name was given use the picker + if (FStrEq(name,"")) + { + CBaseEntity *pEntity = FindPickerEntity( pPlayer ); + if ( pEntity ) + { + UTIL_Remove( pEntity ); + Msg( "killing %s\n", pEntity->GetDebugName() ); + return; + } + } + // Otherwise use name or classname + KillTargets( name ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPointClientCommand : public CPointEntity +{ +public: + DECLARE_CLASS( CPointClientCommand, CPointEntity ); + DECLARE_DATADESC(); + + void InputCommand( inputdata_t& inputdata ); +}; + +void CPointClientCommand::InputCommand( inputdata_t& inputdata ) +{ + if ( !inputdata.value.String()[0] ) + return; + + edict_t *pClient = NULL; + if ( gpGlobals->maxClients == 1 ) + { + pClient = engine->PEntityOfEntIndex( 1 ); + } + else + { + // In multiplayer, send it back to the activator + CBasePlayer *player = dynamic_cast< CBasePlayer * >( inputdata.pActivator ); + if ( player ) + { + pClient = player->edict(); + } + + if ( IsInCommentaryMode() && !pClient ) + { + // Commentary is stuffing a command in. We'll pretend it came from the first player. + pClient = engine->PEntityOfEntIndex( 1 ); + } + } + + if ( !pClient || !pClient->GetUnknown() ) + return; + + engine->ClientCommand( pClient, "%s\n", inputdata.value.String() ); +} + +BEGIN_DATADESC( CPointClientCommand ) + DEFINE_INPUTFUNC( FIELD_STRING, "Command", InputCommand ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( point_clientcommand, CPointClientCommand ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPointServerCommand : public CPointEntity +{ +public: + DECLARE_CLASS( CPointServerCommand, CPointEntity ); + DECLARE_DATADESC(); + void InputCommand( inputdata_t& inputdata ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : inputdata - +//----------------------------------------------------------------------------- +void CPointServerCommand::InputCommand( inputdata_t& inputdata ) +{ + if ( !inputdata.value.String()[0] ) + return; + + engine->ServerCommand( UTIL_VarArgs( "%s\n", inputdata.value.String() ) ); +} + +BEGIN_DATADESC( CPointServerCommand ) + DEFINE_INPUTFUNC( FIELD_STRING, "Command", InputCommand ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( point_servercommand, CPointServerCommand ); + +//------------------------------------------------------------------------------ +// Purpose : Draw a line betwen two points. White if no world collisions, red if collisions +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_DrawLine( const CCommand &args ) +{ + Vector startPos; + Vector endPos; + + startPos.x = atof(args[1]); + startPos.y = atof(args[2]); + startPos.z = atof(args[3]); + endPos.x = atof(args[4]); + endPos.y = atof(args[5]); + endPos.z = atof(args[6]); + + UTIL_AddDebugLine(startPos,endPos,true,true); +} +static ConCommand drawline("drawline", CC_DrawLine, "Draws line between two 3D Points.\n\tGreen if no collision\n\tRed is collides with something\n\tArguments: x1 y1 z1 x2 y2 z2", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : Draw a cross at a points. +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_DrawCross( const CCommand &args ) +{ + Vector vPosition; + + vPosition.x = atof(args[1]); + vPosition.y = atof(args[2]); + vPosition.z = atof(args[3]); + + // Offset since min and max z in not about center + Vector mins = Vector(-5,-5,-5); + Vector maxs = Vector(5,5,5); + + Vector start = mins + vPosition; + Vector end = maxs + vPosition; + UTIL_AddDebugLine(start,end,true,true); + + start.x += (maxs.x - mins.x); + end.x -= (maxs.x - mins.x); + UTIL_AddDebugLine(start,end,true,true); + + start.y += (maxs.y - mins.y); + end.y -= (maxs.y - mins.y); + UTIL_AddDebugLine(start,end,true,true); + + start.x -= (maxs.x - mins.x); + end.x += (maxs.x - mins.x); + UTIL_AddDebugLine(start,end,true,true); +} +static ConCommand drawcross("drawcross", CC_DrawCross, "Draws a cross at the given location\n\tArguments: x y z", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +// helper function for kill and explode +//------------------------------------------------------------------------------ +void kill_helper( const CCommand &args, bool bExplode ) +{ + if ( args.ArgC() > 1 && sv_cheats->GetBool() ) + { + // Find the matching netname + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = ToBasePlayer( UTIL_PlayerByIndex(i) ); + if ( pPlayer ) + { + if ( Q_strstr( pPlayer->GetPlayerName(), args[1] ) ) + { + pPlayer->CommitSuicide( bExplode ); + } + } + } + } + else + { + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( pPlayer ) + { + pPlayer->CommitSuicide( bExplode ); + } + } +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND( kill, "Kills the player with generic damage" ) +{ + kill_helper( args, false ); +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND( explode, "Kills the player with explosive damage" ) +{ + kill_helper( args, true ); +} + +//------------------------------------------------------------------------------ +// helper function for killvector and explodevector +//------------------------------------------------------------------------------ +void killvector_helper( const CCommand &args, bool bExplode ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( pPlayer && args.ArgC() == 5 ) + { + // Find the matching netname. + for ( int iClient = 1; iClient <= gpGlobals->maxClients; iClient++ ) + { + CBasePlayer *pPlayer = ToBasePlayer( UTIL_PlayerByIndex( iClient ) ); + if ( pPlayer ) + { + if ( Q_strstr( pPlayer->GetPlayerName(), args[1] ) ) + { + // Build world-space force vector. + Vector vecForce; + vecForce.x = atof( args[2] ); + vecForce.y = atof( args[3] ); + vecForce.z = atof( args[4] ); + + ClientKill( pPlayer->edict(), vecForce, bExplode ); + } + } + } + } +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND_F( killvector, "Kills a player applying force. Usage: killvector ", FCVAR_CHEAT ) +{ + killvector_helper( args, false ); +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND_F( explodevector, "Kills a player applying an explosive force. Usage: explodevector ", FCVAR_CHEAT ) +{ + killvector_helper( args, false ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND_F( buddha, "Toggle. Player takes damage but won't die. (Shows red cross when health is zero)", FCVAR_CHEAT ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( pPlayer ) + { + if (pPlayer->m_debugOverlays & OVERLAY_BUDDHA_MODE) + { + pPlayer->m_debugOverlays &= ~OVERLAY_BUDDHA_MODE; + Msg("Buddha Mode off...\n"); + } + else + { + pPlayer->m_debugOverlays |= OVERLAY_BUDDHA_MODE; + Msg("Buddha Mode on...\n"); + } + } +} + + +#define TALK_INTERVAL 0.66 // min time between say commands from a client +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND( say, "Display player message" ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( pPlayer ) + { + if (( pPlayer->LastTimePlayerTalked() + TALK_INTERVAL ) < gpGlobals->curtime) + { + Host_Say( pPlayer->edict(), args, 0 ); + pPlayer->NotePlayerTalked(); + } + } + // This will result in a "console" say. Ignore anything from + // an index greater than 0 when we don't have a player pointer, + // as would be the case when a client that's connecting generates + // text via a script. This can be exploited to flood everyone off. + else if ( UTIL_GetCommandClientIndex() == 0 ) + { + Host_Say( NULL, args, 0 ); + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND( say_team, "Display player message to team" ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if (pPlayer) + { + if (( pPlayer->LastTimePlayerTalked() + TALK_INTERVAL ) < gpGlobals->curtime) + { + Host_Say( pPlayer->edict(), args, 1 ); + pPlayer->NotePlayerTalked(); + } + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND( give, "Give item to player.\n\tArguments: " ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( pPlayer + && (gpGlobals->maxClients == 1 || sv_cheats->GetBool()) + && args.ArgC() >= 2 ) + { + char item_to_give[ 256 ]; + Q_strncpy( item_to_give, args[1], sizeof( item_to_give ) ); + Q_strlower( item_to_give ); + + // Don't allow regular users to create point_servercommand entities for the same reason as blocking ent_fire + if ( !Q_stricmp( item_to_give, "point_servercommand" ) ) + { + if ( engine->IsDedicatedServer() ) + { + // We allow people with disabled autokick to do it, because they already have rcon. + if ( pPlayer->IsAutoKickDisabled() == false ) + return; + } + else if ( gpGlobals->maxClients > 1 ) + { + // On listen servers with more than 1 player, only allow the host to create point_servercommand. + CBasePlayer *pHostPlayer = UTIL_GetListenServerHost(); + if ( pPlayer != pHostPlayer ) + return; + } + } + + // Dirty hack to avoid suit playing it's pickup sound + if ( !Q_stricmp( item_to_give, "item_suit" ) ) + { + pPlayer->EquipSuit( false ); + return; + } + + string_t iszItem = AllocPooledString( item_to_give ); // Make a copy of the classname + pPlayer->GiveNamedItem( STRING(iszItem) ); + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +CON_COMMAND( fov, "Change players FOV" ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( pPlayer && sv_cheats->GetBool() ) + { + if ( args.ArgC() > 1 ) + { + int nFOV = atoi( args[1] ); + pPlayer->SetDefaultFOV( nFOV ); + } + else + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs( "\"fov\" is \"%d\"\n", pPlayer->GetFOV() ) ); + } + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CC_Player_SetModel( const CCommand &args ) +{ + if ( gpGlobals->deathmatch ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( pPlayer && args.ArgC() == 2) + { + static char szName[256]; + Q_snprintf( szName, sizeof( szName ), "models/%s.mdl", args[1] ); + pPlayer->SetModel( szName ); + UTIL_SetSize(pPlayer, VEC_HULL_MIN, VEC_HULL_MAX); + } +} +static ConCommand setmodel("setmodel", CC_Player_SetModel, "Changes's player's model", FCVAR_CHEAT ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CC_Player_TestDispatchEffect( const CCommand &args ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer) + return; + + if ( args.ArgC() < 2 ) + { + Msg(" Usage: test_dispatcheffect \n " ); + Msg(" defaults are: \n" ); + return; + } + + // Optional distance + float flDistance = 1024; + if ( args.ArgC() >= 3 ) + { + flDistance = atoi( args[ 2 ] ); + } + + // Optional flags + float flags = 0; + if ( args.ArgC() >= 4 ) + { + flags = atoi( args[ 3 ] ); + } + + // Optional magnitude + float magnitude = 0; + if ( args.ArgC() >= 5 ) + { + magnitude = atof( args[ 4 ] ); + } + + // Optional scale + float scale = 0; + if ( args.ArgC() >= 6 ) + { + scale = atof( args[ 5 ] ); + } + + Vector vecForward; + QAngle vecAngles = pPlayer->EyeAngles(); + AngleVectors( vecAngles, &vecForward ); + + // Trace forward + trace_t tr; + Vector vecSrc = pPlayer->EyePosition(); + Vector vecEnd = vecSrc + (vecForward * flDistance); + UTIL_TraceLine( vecSrc, vecEnd, MASK_ALL, pPlayer, COLLISION_GROUP_NONE, &tr ); + + // Fill out the generic data + CEffectData data; + // If we hit something, use that data + if ( tr.fraction < 1.0 ) + { + data.m_vOrigin = tr.endpos; + VectorAngles( tr.plane.normal, data.m_vAngles ); + data.m_vNormal = tr.plane.normal; + } + else + { + data.m_vOrigin = vecEnd; + data.m_vAngles = vecAngles; + AngleVectors( vecAngles, &data.m_vNormal ); + } + data.m_nEntIndex = pPlayer->entindex(); + data.m_fFlags = flags; + data.m_flMagnitude = magnitude; + data.m_flScale = scale; + DispatchEffect( (char *)args[1], data ); +} + +static ConCommand test_dispatcheffect("test_dispatcheffect", CC_Player_TestDispatchEffect, "Test a clientside dispatch effect.\n\tUsage: test_dispatcheffect \n\tDefaults are: \n", FCVAR_CHEAT); + +#ifdef HL2_DLL +//----------------------------------------------------------------------------- +// Purpose: Quickly switch to the physics cannon, or back to previous item +//----------------------------------------------------------------------------- +void CC_Player_PhysSwap( void ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + + if ( pPlayer ) + { + CBaseCombatWeapon *pWeapon = pPlayer->GetActiveWeapon(); + + if ( pWeapon ) + { + // Tell the client to stop selecting weapons + engine->ClientCommand( UTIL_GetCommandClient()->edict(), "cancelselect" ); + + const char *strWeaponName = pWeapon->GetName(); + + if ( !Q_stricmp( strWeaponName, "weapon_physcannon" ) ) + { + PhysCannonForceDrop( pWeapon, NULL ); + pPlayer->SelectLastItem(); + } + else + { + pPlayer->SelectItem( "weapon_physcannon" ); + } + } + } +} +static ConCommand physswap("phys_swap", CC_Player_PhysSwap, "Automatically swaps the current weapon for the physcannon and back again." ); +#endif + +//----------------------------------------------------------------------------- +// Purpose: Quickly switch to the bug bait, or back to previous item +//----------------------------------------------------------------------------- +void CC_Player_BugBaitSwap( void ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + + if ( pPlayer ) + { + CBaseCombatWeapon *pWeapon = pPlayer->GetActiveWeapon(); + + if ( pWeapon ) + { + // Tell the client to stop selecting weapons + engine->ClientCommand( UTIL_GetCommandClient()->edict(), "cancelselect" ); + + const char *strWeaponName = pWeapon->GetName(); + + if ( !Q_stricmp( strWeaponName, "weapon_bugbait" ) ) + { + pPlayer->SelectLastItem(); + } + else + { + pPlayer->SelectItem( "weapon_bugbait" ); + } + } + } +} +static ConCommand bugswap("bug_swap", CC_Player_BugBaitSwap, "Automatically swaps the current weapon for the bug bait and back again.", FCVAR_CHEAT ); + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CC_Player_Use( const CCommand &args ) +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( pPlayer) + { + pPlayer->SelectItem((char *)args[1]); + } +} +static ConCommand use("use", CC_Player_Use, "Use a particular weapon\t\nArguments: "); + + +//------------------------------------------------------------------------------ +// A small wrapper around SV_Move that never clips against the supplied entity. +//------------------------------------------------------------------------------ +static bool TestEntityPosition ( CBasePlayer *pPlayer ) +{ + trace_t trace; + UTIL_TraceEntity( pPlayer, pPlayer->GetAbsOrigin(), pPlayer->GetAbsOrigin(), MASK_PLAYERSOLID, &trace ); + return (trace.startsolid == 0); +} + + +//------------------------------------------------------------------------------ +// Searches along the direction ray in steps of "step" to see if +// the entity position is passible. +// Used for putting the player in valid space when toggling off noclip mode. +//------------------------------------------------------------------------------ +static int FindPassableSpace( CBasePlayer *pPlayer, const Vector& direction, float step, Vector& oldorigin ) +{ + int i; + for ( i = 0; i < 100; i++ ) + { + Vector origin = pPlayer->GetAbsOrigin(); + VectorMA( origin, step, direction, origin ); + pPlayer->SetAbsOrigin( origin ); + if ( TestEntityPosition( pPlayer ) ) + { + VectorCopy( pPlayer->GetAbsOrigin(), oldorigin ); + return 1; + } + } + return 0; +} + + +//------------------------------------------------------------------------------ +// Noclip +//------------------------------------------------------------------------------ +void EnableNoClip( CBasePlayer *pPlayer ) +{ + // Disengage from hierarchy + pPlayer->SetParent( NULL ); + pPlayer->SetMoveType( MOVETYPE_NOCLIP ); + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "noclip ON\n"); + pPlayer->AddEFlags( EFL_NOCLIP_ACTIVE ); +} + +void CC_Player_NoClip( void ) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + CPlayerState *pl = pPlayer->PlayerData(); + Assert( pl ); + + if (pPlayer->GetMoveType() != MOVETYPE_NOCLIP) + { + EnableNoClip( pPlayer ); + return; + } + + pPlayer->RemoveEFlags( EFL_NOCLIP_ACTIVE ); + pPlayer->SetMoveType( MOVETYPE_WALK ); + + Vector oldorigin = pPlayer->GetAbsOrigin(); + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "noclip OFF\n"); + if ( !TestEntityPosition( pPlayer ) ) + { + Vector forward, right, up; + + AngleVectors ( pl->v_angle, &forward, &right, &up); + + // Try to move into the world + if ( !FindPassableSpace( pPlayer, forward, 1, oldorigin ) ) + { + if ( !FindPassableSpace( pPlayer, right, 1, oldorigin ) ) + { + if ( !FindPassableSpace( pPlayer, right, -1, oldorigin ) ) // left + { + if ( !FindPassableSpace( pPlayer, up, 1, oldorigin ) ) // up + { + if ( !FindPassableSpace( pPlayer, up, -1, oldorigin ) ) // down + { + if ( !FindPassableSpace( pPlayer, forward, -1, oldorigin ) ) // back + { + Msg( "Can't find the world\n" ); + } + } + } + } + } + } + + pPlayer->SetAbsOrigin( oldorigin ); + } +} + +static ConCommand noclip("noclip", CC_Player_NoClip, "Toggle. Player becomes non-solid and flies.", FCVAR_CHEAT); + + +//------------------------------------------------------------------------------ +// Sets client to godmode +//------------------------------------------------------------------------------ +void CC_God_f (void) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + +#ifdef TF_DLL + if ( TFGameRules() && ( TFGameRules()->IsPVEModeActive() == false ) ) + { + if ( gpGlobals->deathmatch ) + return; + } +#else + if ( gpGlobals->deathmatch ) + return; +#endif + + pPlayer->ToggleFlag( FL_GODMODE ); + if (!(pPlayer->GetFlags() & FL_GODMODE ) ) + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "godmode OFF\n"); + else + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "godmode ON\n"); +} + +static ConCommand god("god", CC_God_f, "Toggle. Player becomes invulnerable.", FCVAR_CHEAT ); + + +//------------------------------------------------------------------------------ +// Sets client to godmode +//------------------------------------------------------------------------------ +CON_COMMAND_F( setpos, "Move player to specified origin (must have sv_cheats).", FCVAR_CHEAT ) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + if ( args.ArgC() < 3 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage: setpos x y \n"); + return; + } + + Vector oldorigin = pPlayer->GetAbsOrigin(); + + Vector newpos; + newpos.x = atof( args[1] ); + newpos.y = atof( args[2] ); + newpos.z = args.ArgC() == 4 ? atof( args[3] ) : oldorigin.z; + + pPlayer->SetAbsOrigin( newpos ); + + if ( !TestEntityPosition( pPlayer ) ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "setpos into world, use noclip to unstick yourself!\n"); + } +} + + +//------------------------------------------------------------------------------ +// Sets client to godmode +//------------------------------------------------------------------------------ +void CC_setang_f (const CCommand &args) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + if ( args.ArgC() < 3 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage: setang pitch yaw \n"); + return; + } + + QAngle oldang = pPlayer->GetAbsAngles(); + + QAngle newang; + newang.x = atof( args[1] ); + newang.y = atof( args[2] ); + newang.z = args.ArgC() == 4 ? atof( args[3] ) : oldang.z; + + pPlayer->SnapEyeAngles( newang ); +} + +static ConCommand setang("setang", CC_setang_f, "Snap player eyes to specified pitch yaw (must have sv_cheats).", FCVAR_CHEAT ); + +static float GetHexFloat( const char *pStr ) +{ + if ( ( pStr[0] == '0' ) && ( pStr[1] == 'x' ) ) + { + uint32 f = (uint32)V_atoi64( pStr ); + return *reinterpret_cast< const float * >( &f ); + } + + return atof( pStr ); +} + +//------------------------------------------------------------------------------ +// Move position +//------------------------------------------------------------------------------ +CON_COMMAND_F( setpos_exact, "Move player to an exact specified origin (must have sv_cheats).", FCVAR_CHEAT ) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + if ( args.ArgC() < 3 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage: setpos_exact x y \n"); + return; + } + + Vector oldorigin = pPlayer->GetAbsOrigin(); + + Vector newpos; + newpos.x = GetHexFloat( args[1] ); + newpos.y = GetHexFloat( args[2] ); + newpos.z = args.ArgC() == 4 ? GetHexFloat( args[3] ) : oldorigin.z; + + pPlayer->Teleport( &newpos, NULL, NULL ); + + if ( !TestEntityPosition( pPlayer ) ) + { + if ( pPlayer->GetMoveType() != MOVETYPE_NOCLIP ) + { + EnableNoClip( pPlayer ); + return; + } + } +} + +CON_COMMAND_F( setang_exact, "Snap player eyes and orientation to specified pitch yaw (must have sv_cheats).", FCVAR_CHEAT ) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + if ( args.ArgC() < 3 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Usage: setang_exact pitch yaw \n"); + return; + } + + QAngle oldang = pPlayer->GetAbsAngles(); + + QAngle newang; + newang.x = GetHexFloat( args[1] ); + newang.y = GetHexFloat( args[2] ); + newang.z = args.ArgC() == 4 ? GetHexFloat( args[3] ) : oldang.z; + + pPlayer->Teleport( NULL, &newang, NULL ); + pPlayer->SnapEyeAngles( newang ); + +#ifdef TF_DLL + static_cast( pPlayer )->DoAnimationEvent( PLAYERANIMEVENT_SNAP_YAW ); +#endif +} + + +//------------------------------------------------------------------------------ +// Sets client to notarget mode. +//------------------------------------------------------------------------------ +void CC_Notarget_f (void) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + if ( gpGlobals->deathmatch ) + return; + + pPlayer->ToggleFlag( FL_NOTARGET ); + if ( !(pPlayer->GetFlags() & FL_NOTARGET ) ) + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "notarget OFF\n"); + else + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "notarget ON\n"); +} + +ConCommand notarget("notarget", CC_Notarget_f, "Toggle. Player becomes hidden to NPCs.", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Damage the client the specified amount +//------------------------------------------------------------------------------ +void CC_HurtMe_f(const CCommand &args) +{ + if ( !sv_cheats->GetBool() ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( !pPlayer ) + return; + + int iDamage = 10; + if ( args.ArgC() >= 2 ) + { + iDamage = atoi( args[ 1 ] ); + } + + pPlayer->TakeDamage( CTakeDamageInfo( pPlayer, pPlayer, iDamage, DMG_PREVENT_PHYSICS_FORCE ) ); +} + +static ConCommand hurtme("hurtme", CC_HurtMe_f, "Hurts the player.\n\tArguments: ", FCVAR_CHEAT); + +static bool IsInGroundList( CBaseEntity *ent, CBaseEntity *ground ) +{ + if ( !ground || !ent ) + return false; + + groundlink_t *root = ( groundlink_t * )ground->GetDataObject( GROUNDLINK ); + if ( root ) + { + groundlink_t *link = root->nextLink; + while ( link != root ) + { + CBaseEntity *other = link->entity; + if ( other == ent ) + return true; + link = link->nextLink; + } + } + + return false; + +} + +static int DescribeGroundList( CBaseEntity *ent ) +{ + if ( !ent ) + return 0; + + int c = 1; + + Msg( "%i : %s (ground %i %s)\n", ent->entindex(), ent->GetClassname(), + ent->GetGroundEntity() ? ent->GetGroundEntity()->entindex() : -1, + ent->GetGroundEntity() ? ent->GetGroundEntity()->GetClassname() : "NULL" ); + groundlink_t *root = ( groundlink_t * )ent->GetDataObject( GROUNDLINK ); + if ( root ) + { + groundlink_t *link = root->nextLink; + while ( link != root ) + { + CBaseEntity *other = link->entity; + if ( other ) + { + Msg( " %02i: %i %s\n", c++, other->entindex(), other->GetClassname() ); + + if ( other->GetGroundEntity() != ent ) + { + Assert( 0 ); + Msg( " mismatched!!!\n" ); + } + } + else + { + Assert( 0 ); + Msg( " %02i: NULL link\n", c++ ); + } + link = link->nextLink; + } + } + + if ( ent->GetGroundEntity() != NULL ) + { + Assert( IsInGroundList( ent, ent->GetGroundEntity() ) ); + } + + return c - 1; +} + +void CC_GroundList_f(const CCommand &args) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() == 2 ) + { + int idx = atoi( args[1] ); + + CBaseEntity *ground = CBaseEntity::Instance( idx ); + if ( ground ) + { + DescribeGroundList( ground ); + } + } + else + { + CBaseEntity *ent = NULL; + int linkCount = 0; + while ( (ent = gEntList.NextEnt(ent)) != NULL ) + { + linkCount += DescribeGroundList( ent ); + } + + extern int groundlinksallocated; + Assert( linkCount == groundlinksallocated ); + + Msg( "--- %i links\n", groundlinksallocated ); + } +} + +static ConCommand groundlist("groundlist", CC_GroundList_f, "Display ground entity list " ); + +//----------------------------------------------------------------------------- +// Purpose: called each time a player uses a "cmd" command +// Input : *pEdict - the player who issued the command +//----------------------------------------------------------------------------- +void ClientCommand( CBasePlayer *pPlayer, const CCommand &args ) +{ + const char *pCmd = args[0]; + + // Is the client spawned yet? + if ( !pPlayer ) + return; + + MDLCACHE_CRITICAL_SECTION(); + + /* + const char *pstr; + + if (((pstr = strstr(pcmd, "weapon_")) != NULL) && (pstr == pcmd)) + { + // Subtype may be specified + if ( args.ArgC() == 2 ) + { + pPlayer->SelectItem( pcmd, atoi( args[1] ) ); + } + else + { + pPlayer->SelectItem(pcmd); + } + } + */ + + if ( FStrEq( pCmd, "killtarget" ) ) + { + if ( g_pDeveloper->GetBool() && sv_cheats->GetBool() && UTIL_IsCommandIssuedByServerAdmin() ) + { + ConsoleKillTarget( pPlayer, args[1] ); + } + } + else if ( FStrEq( pCmd, "demorestart" ) ) + { + pPlayer->ForceClientDllUpdate(); + } + else if ( FStrEq( pCmd, "fade" ) ) + { + color32 black = {32,63,100,200}; + UTIL_ScreenFade( pPlayer, black, 3, 3, FFADE_OUT ); + } + else if ( FStrEq( pCmd, "te" ) ) + { + if ( sv_cheats->GetBool() && UTIL_IsCommandIssuedByServerAdmin() ) + { + if ( FStrEq( args[1], "stop" ) ) + { + // Destroy it + // + CBaseEntity *ent = gEntList.FindEntityByClassname( NULL, "te_tester" ); + while ( ent ) + { + CBaseEntity *next = gEntList.FindEntityByClassname( ent, "te_tester" ); + UTIL_Remove( ent ); + ent = next; + } + } + else + { + CTempEntTester::Create( pPlayer->WorldSpaceCenter(), pPlayer->EyeAngles(), args[1], args[2] ); + } + } + } + else + { + if ( !g_pGameRules->ClientCommand( pPlayer, args ) ) + { +#ifdef MAPBASE_VSCRIPT + // Console command hook for VScript + if ( pPlayer->m_ScriptScope.IsInitialized() ) + { + ScriptVariant_t functionReturn; + g_pScriptVM->SetValue( "command", ScriptVariant_t( pCmd ) ); + + ScriptVariant_t varTable; + g_pScriptVM->CreateTable( varTable ); + HSCRIPT hTable = varTable.m_hScript; + for ( int i = 0; i < args.ArgC(); i++ ) + { + g_pScriptVM->SetValue( hTable, CNumStr( i ), ScriptVariant_t( args[i] ) ); + } + g_pScriptVM->SetValue( "args", varTable ); + + pPlayer->CallScriptFunction( "ClientCommand", &functionReturn ); + + g_pScriptVM->ClearValue( "command" ); + g_pScriptVM->ClearValue( "args" ); + g_pScriptVM->ReleaseValue( varTable ); + + if (functionReturn.m_bool) + return; + } +#endif + if ( Q_strlen( pCmd ) > 128 ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "Console command too long.\n" ); + } + else + { + // tell the user they entered an unknown command + ClientPrint( pPlayer, HUD_PRINTCONSOLE, UTIL_VarArgs( "Unknown command: %s\n", pCmd ) ); + } + } + } +} diff --git a/sp/src/game/server/client.h b/sp/src/game/server/client.h new file mode 100644 index 00000000..076d6cac --- /dev/null +++ b/sp/src/game/server/client.h @@ -0,0 +1,32 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef CLIENT_H +#define CLIENT_H + +#ifdef _WIN32 +#pragma once +#endif + + +class CCommand; +class CUserCmd; +class CBasePlayer; + + +void ClientActive( edict_t *pEdict, bool bLoadGame ); +void ClientPutInServer( edict_t *pEdict, const char *playername ); +void ClientCommand( CBasePlayer *pSender, const CCommand &args ); +void ClientPrecache( void ); +// Game specific precaches +void ClientGamePrecache( void ); +const char *GetGameDescription( void ); +void Host_Say( edict_t *pEdict, bool teamonly ); + + + +#endif // CLIENT_H diff --git a/sp/src/game/server/colorcorrection.cpp b/sp/src/game/server/colorcorrection.cpp new file mode 100644 index 00000000..16d01222 --- /dev/null +++ b/sp/src/game/server/colorcorrection.cpp @@ -0,0 +1,314 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Color correction entity. +// +// $NoKeywords: $ +//===========================================================================// + +#include + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define COLOR_CORRECTION_ENT_THINK_RATE TICK_INTERVAL + +static const char *s_pFadeInContextThink = "ColorCorrectionFadeInThink"; +static const char *s_pFadeOutContextThink = "ColorCorrectionFadeOutThink"; + +//------------------------------------------------------------------------------ +// FIXME: This really should inherit from something more lightweight +//------------------------------------------------------------------------------ + + +//------------------------------------------------------------------------------ +// Purpose : Shadow control entity +//------------------------------------------------------------------------------ +class CColorCorrection : public CBaseEntity +{ + DECLARE_CLASS( CColorCorrection, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CColorCorrection(); + + void Spawn( void ); + int UpdateTransmitState(); + void Activate( void ); + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + // Inputs + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputSetFadeInDuration ( inputdata_t &inputdata ); + void InputSetFadeOutDuration ( inputdata_t &inputdata ); + +private: + void FadeIn ( void ); + void FadeOut ( void ); + + void FadeInThink( void ); // Fades lookup weight from Cur->MaxWeight + void FadeOutThink( void ); // Fades lookup weight from CurWeight->0.0 + + + + float m_flFadeInDuration; // Duration for a full 0->MaxWeight transition + float m_flFadeOutDuration; // Duration for a full Max->0 transition + float m_flStartFadeInWeight; + float m_flStartFadeOutWeight; + float m_flTimeStartFadeIn; + float m_flTimeStartFadeOut; + + float m_flMaxWeight; + + bool m_bStartDisabled; + CNetworkVar( bool, m_bEnabled ); + + CNetworkVar( float, m_MinFalloff ); + CNetworkVar( float, m_MaxFalloff ); + CNetworkVar( float, m_flCurWeight ); + CNetworkString( m_netlookupFilename, MAX_PATH ); + + string_t m_lookupFilename; +}; + +LINK_ENTITY_TO_CLASS(color_correction, CColorCorrection); + +BEGIN_DATADESC( CColorCorrection ) + + DEFINE_THINKFUNC( FadeInThink ), + DEFINE_THINKFUNC( FadeOutThink ), + + DEFINE_FIELD( m_flCurWeight, FIELD_FLOAT ), + DEFINE_FIELD( m_flTimeStartFadeIn, FIELD_FLOAT ), + DEFINE_FIELD( m_flTimeStartFadeOut, FIELD_FLOAT ), + DEFINE_FIELD( m_flStartFadeInWeight, FIELD_FLOAT ), + DEFINE_FIELD( m_flStartFadeOutWeight, FIELD_FLOAT ), + + DEFINE_KEYFIELD( m_MinFalloff, FIELD_FLOAT, "minfalloff" ), + DEFINE_KEYFIELD( m_MaxFalloff, FIELD_FLOAT, "maxfalloff" ), + DEFINE_KEYFIELD( m_flMaxWeight, FIELD_FLOAT, "maxweight" ), + DEFINE_KEYFIELD( m_flFadeInDuration, FIELD_FLOAT, "fadeInDuration" ), + DEFINE_KEYFIELD( m_flFadeOutDuration, FIELD_FLOAT, "fadeOutDuration" ), + DEFINE_KEYFIELD( m_lookupFilename, FIELD_STRING, "filename" ), + + DEFINE_KEYFIELD( m_bEnabled, FIELD_BOOLEAN, "enabled" ), + DEFINE_KEYFIELD( m_bStartDisabled, FIELD_BOOLEAN, "StartDisabled" ), +// DEFINE_ARRAY( m_netlookupFilename, FIELD_CHARACTER, MAX_PATH ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFadeInDuration", InputSetFadeInDuration ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFadeOutDuration", InputSetFadeOutDuration ), + +END_DATADESC() + +extern void SendProxy_Origin( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); +IMPLEMENT_SERVERCLASS_ST_NOBASE(CColorCorrection, DT_ColorCorrection) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_NOSCALE, 0.0f, HIGH_DEFAULT, SendProxy_Origin ), + SendPropFloat( SENDINFO(m_MinFalloff) ), + SendPropFloat( SENDINFO(m_MaxFalloff) ), + SendPropFloat( SENDINFO(m_flCurWeight) ), + SendPropString( SENDINFO(m_netlookupFilename) ), + SendPropBool( SENDINFO(m_bEnabled) ), +END_SEND_TABLE() + + +CColorCorrection::CColorCorrection() : BaseClass() +{ + m_bEnabled = true; + m_MinFalloff = 0.0f; + m_MaxFalloff = 1000.0f; + m_flMaxWeight = 1.0f; + m_flCurWeight.Set( 0.0f ); + m_flFadeInDuration = 0.0f; + m_flFadeOutDuration = 0.0f; + m_flStartFadeInWeight = 0.0f; + m_flStartFadeOutWeight = 0.0f; + m_flTimeStartFadeIn = 0.0f; + m_flTimeStartFadeOut = 0.0f; + m_netlookupFilename.GetForModify()[0] = 0; + m_lookupFilename = NULL_STRING; +} + + +//------------------------------------------------------------------------------ +// Purpose : Send even though we don't have a model +//------------------------------------------------------------------------------ +int CColorCorrection::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CColorCorrection::Spawn( void ) +{ + AddEFlags( EFL_FORCE_CHECK_TRANSMIT | EFL_DIRTY_ABSTRANSFORM ); + Precache(); + SetSolid( SOLID_NONE ); + + // To fade in/out the weight. + SetContextThink( &CColorCorrection::FadeInThink, TICK_NEVER_THINK, s_pFadeInContextThink ); + SetContextThink( &CColorCorrection::FadeOutThink, TICK_NEVER_THINK, s_pFadeOutContextThink ); + + if( m_bStartDisabled ) + { + m_bEnabled = false; + m_flCurWeight.Set ( 0.0f ); + } + else + { + m_bEnabled = true; + m_flCurWeight.Set ( 1.0f ); + } + + BaseClass::Spawn(); +} + +void CColorCorrection::Activate( void ) +{ + BaseClass::Activate(); + + Q_strncpy( m_netlookupFilename.GetForModify(), STRING( m_lookupFilename ), MAX_PATH ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets up internal vars needed for fade in lerping +//----------------------------------------------------------------------------- +void CColorCorrection::FadeIn ( void ) +{ + m_bEnabled = true; + m_flTimeStartFadeIn = gpGlobals->curtime; + m_flStartFadeInWeight = m_flCurWeight; + SetNextThink ( gpGlobals->curtime + COLOR_CORRECTION_ENT_THINK_RATE, s_pFadeInContextThink ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets up internal vars needed for fade out lerping +//----------------------------------------------------------------------------- +void CColorCorrection::FadeOut ( void ) +{ + m_bEnabled = false; + m_flTimeStartFadeOut = gpGlobals->curtime; + m_flStartFadeOutWeight = m_flCurWeight; + SetNextThink ( gpGlobals->curtime + COLOR_CORRECTION_ENT_THINK_RATE, s_pFadeOutContextThink ); +} + +//----------------------------------------------------------------------------- +// Purpose: Fades lookup weight from CurWeight->MaxWeight +//----------------------------------------------------------------------------- +void CColorCorrection::FadeInThink( void ) +{ + // Check for conditions where we shouldnt fade in + if ( m_flFadeInDuration <= 0 || // not set to fade in + m_flCurWeight >= m_flMaxWeight || // already past max weight + !m_bEnabled || // fade in/out mutex + m_flMaxWeight == 0.0f || // min==max + m_flStartFadeInWeight >= m_flMaxWeight ) // already at max weight + { + SetNextThink ( TICK_NEVER_THINK, s_pFadeInContextThink ); + return; + } + + // If we started fading in without fully fading out, use a truncated duration + float flTimeToFade = m_flFadeInDuration; + if ( m_flStartFadeInWeight > 0.0f ) + { + float flWeightRatio = m_flStartFadeInWeight / m_flMaxWeight; + flWeightRatio = clamp ( flWeightRatio, 0.0f, 0.99f ); + flTimeToFade = m_flFadeInDuration * (1.0 - flWeightRatio); + } + + Assert ( flTimeToFade > 0.0f ); + float flFadeRatio = (gpGlobals->curtime - m_flTimeStartFadeIn) / flTimeToFade; + flFadeRatio = clamp ( flFadeRatio, 0.0f, 1.0f ); + m_flStartFadeInWeight = clamp ( m_flStartFadeInWeight, 0.0f, 1.0f ); + + m_flCurWeight = Lerp( flFadeRatio, m_flStartFadeInWeight, m_flMaxWeight ); + + SetNextThink( gpGlobals->curtime + COLOR_CORRECTION_ENT_THINK_RATE, s_pFadeInContextThink ); +} + +//----------------------------------------------------------------------------- +// Purpose: Fades lookup weight from CurWeight->0.0 +//----------------------------------------------------------------------------- +void CColorCorrection::FadeOutThink( void ) +{ + // Check for conditions where we shouldn't fade out + if ( m_flFadeOutDuration <= 0 || // not set to fade out + m_flCurWeight <= 0.0f || // already faded out + m_bEnabled || // fade in/out mutex + m_flMaxWeight == 0.0f || // min==max + m_flStartFadeOutWeight <= 0.0f )// already at min weight + { + SetNextThink ( TICK_NEVER_THINK, s_pFadeOutContextThink ); + return; + } + + // If we started fading out without fully fading in, use a truncated duration + float flTimeToFade = m_flFadeOutDuration; + if ( m_flStartFadeOutWeight < m_flMaxWeight ) + { + float flWeightRatio = m_flStartFadeOutWeight / m_flMaxWeight; + flWeightRatio = clamp ( flWeightRatio, 0.01f, 1.0f ); + flTimeToFade = m_flFadeOutDuration * flWeightRatio; + } + + Assert ( flTimeToFade > 0.0f ); + float flFadeRatio = (gpGlobals->curtime - m_flTimeStartFadeOut) / flTimeToFade; + flFadeRatio = clamp ( flFadeRatio, 0.0f, 1.0f ); + m_flStartFadeOutWeight = clamp ( m_flStartFadeOutWeight, 0.0f, 1.0f ); + + m_flCurWeight = Lerp( 1.0f - flFadeRatio, 0.0f, m_flStartFadeOutWeight ); + + SetNextThink( gpGlobals->curtime + COLOR_CORRECTION_ENT_THINK_RATE, s_pFadeOutContextThink ); +} + +//------------------------------------------------------------------------------ +// Purpose : Input handlers +//------------------------------------------------------------------------------ +void CColorCorrection::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; + + if ( m_flFadeInDuration > 0.0f ) + { + FadeIn(); + } + else + { + m_flCurWeight = m_flMaxWeight; + } + +} + +void CColorCorrection::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; + + if ( m_flFadeOutDuration > 0.0f ) + { + FadeOut(); + } + else + { + m_flCurWeight = 0.0f; + } + +} + +void CColorCorrection::InputSetFadeInDuration( inputdata_t& inputdata ) +{ + m_flFadeInDuration = inputdata.value.Float(); +} + +void CColorCorrection::InputSetFadeOutDuration( inputdata_t& inputdata ) +{ + m_flFadeOutDuration = inputdata.value.Float(); +} diff --git a/sp/src/game/server/colorcorrectionvolume.cpp b/sp/src/game/server/colorcorrectionvolume.cpp new file mode 100644 index 00000000..a56cd533 --- /dev/null +++ b/sp/src/game/server/colorcorrectionvolume.cpp @@ -0,0 +1,236 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Color correction entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include + +#include "cbase.h" +#include "triggers.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//------------------------------------------------------------------------------ +// FIXME: This really should inherit from something more lightweight +//------------------------------------------------------------------------------ + + +//------------------------------------------------------------------------------ +// Purpose : Shadow control entity +//------------------------------------------------------------------------------ +class CColorCorrectionVolume : public CBaseTrigger +{ + DECLARE_CLASS( CColorCorrectionVolume, CBaseTrigger ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CColorCorrectionVolume(); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + int UpdateTransmitState(); + + void ThinkFunc(); + + virtual bool PassesTriggerFilters(CBaseEntity *pOther); + virtual void StartTouch( CBaseEntity *pEntity ); + virtual void EndTouch( CBaseEntity *pEntity ); + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + // Inputs + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +private: + + bool m_bEnabled; + bool m_bStartDisabled; + + CNetworkVar( float, m_Weight ); + CNetworkVar( float, m_MaxWeight ); + CNetworkString( m_lookupFilename, MAX_PATH ); + + float m_LastEnterWeight; + float m_LastEnterTime; + + float m_LastExitWeight; + float m_LastExitTime; + + float m_FadeDuration; +}; + +LINK_ENTITY_TO_CLASS(color_correction_volume, CColorCorrectionVolume); + +BEGIN_DATADESC( CColorCorrectionVolume ) + + DEFINE_THINKFUNC( ThinkFunc ), + + DEFINE_KEYFIELD( m_FadeDuration, FIELD_FLOAT, "fadeDuration" ), + DEFINE_KEYFIELD( m_MaxWeight, FIELD_FLOAT, "maxweight" ), + DEFINE_AUTO_ARRAY_KEYFIELD( m_lookupFilename, FIELD_CHARACTER, "filename" ), + + DEFINE_KEYFIELD( m_bEnabled, FIELD_BOOLEAN, "enabled" ), + DEFINE_KEYFIELD( m_bStartDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_FIELD( m_Weight, FIELD_FLOAT ), + DEFINE_FIELD( m_LastEnterWeight, FIELD_FLOAT ), + DEFINE_FIELD( m_LastEnterTime, FIELD_FLOAT ), + DEFINE_FIELD( m_LastExitWeight, FIELD_FLOAT ), + DEFINE_FIELD( m_LastExitTime, FIELD_FLOAT ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CColorCorrectionVolume, DT_ColorCorrectionVolume) + SendPropFloat( SENDINFO(m_Weight) ), + SendPropString( SENDINFO(m_lookupFilename) ), +END_SEND_TABLE() + + +CColorCorrectionVolume::CColorCorrectionVolume() : BaseClass() +{ + m_bEnabled = true; + m_MaxWeight = 1.0f; + m_lookupFilename.GetForModify()[0] = 0; +} + + +//------------------------------------------------------------------------------ +// Purpose : Send even though we don't have a model +//------------------------------------------------------------------------------ +int CColorCorrectionVolume::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +bool CColorCorrectionVolume::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "filename" ) ) + { + Q_strncpy( m_lookupFilename.GetForModify(), szValue, MAX_PATH ); + + return true; + } + else if ( FStrEq( szKeyName, "maxweight" ) ) + { + float max_weight; + sscanf( szValue, "%f", &max_weight ); + m_MaxWeight = max_weight; + + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CColorCorrectionVolume::Spawn( void ) +{ + BaseClass::Spawn(); + + AddEFlags( EFL_FORCE_CHECK_TRANSMIT | EFL_DIRTY_ABSTRANSFORM ); + Precache(); + + SetSolid( SOLID_BSP ); + SetSolidFlags( FSOLID_TRIGGER | FSOLID_NOT_SOLID ); + SetModel( STRING( GetModelName() ) ); + + SetThink( &CColorCorrectionVolume::ThinkFunc ); + SetNextThink( gpGlobals->curtime + 0.01f ); + + if( m_bStartDisabled ) + { + m_bEnabled = false; + } + else + { + m_bEnabled = true; + } +} + +bool CColorCorrectionVolume::PassesTriggerFilters( CBaseEntity *pEntity ) +{ + if( pEntity == UTIL_GetLocalPlayer() ) + return true; + + return false; +} + +void CColorCorrectionVolume::StartTouch( CBaseEntity *pEntity ) +{ + m_LastEnterTime = gpGlobals->curtime; + m_LastEnterWeight = m_Weight; +} + +void CColorCorrectionVolume::EndTouch( CBaseEntity *pEntity ) +{ + m_LastExitTime = gpGlobals->curtime; + m_LastExitWeight = m_Weight; +} + +void CColorCorrectionVolume::ThinkFunc( ) +{ + if( !m_bEnabled ) + { + m_Weight.Set( 0.0f ); + } + else + { + if( m_LastEnterTime > m_LastExitTime ) + { + // we most recently entered the volume + + if( m_Weight < 1.0f ) + { + float dt = gpGlobals->curtime - m_LastEnterTime; + float weight = m_LastEnterWeight + dt / ((1.0f-m_LastEnterWeight)*m_FadeDuration); + if( weight>1.0f ) + weight = 1.0f; + + m_Weight = weight; + } + } + else + { + // we most recently exitted the volume + + if( m_Weight > 0.0f ) + { + float dt = gpGlobals->curtime - m_LastExitTime; + float weight = (1.0f-m_LastExitWeight) + dt / (m_LastExitWeight*m_FadeDuration); + if( weight>1.0f ) + weight = 1.0f; + + m_Weight = 1.0f - weight; + } + } + } + + SetNextThink( gpGlobals->curtime + 0.01f ); +} + + +//------------------------------------------------------------------------------ +// Purpose : Input handlers +//------------------------------------------------------------------------------ +void CColorCorrectionVolume::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; +} + +void CColorCorrectionVolume::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; +} diff --git a/sp/src/game/server/controlentities.cpp b/sp/src/game/server/controlentities.cpp new file mode 100644 index 00000000..e67b8f8e --- /dev/null +++ b/sp/src/game/server/controlentities.cpp @@ -0,0 +1,146 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: contains entities who have no physical representation in the game, and who +// must be triggered by other entities to cause their effects. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "tier1/strtools.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: when fired, it changes which track the CD is playing +//----------------------------------------------------------------------------- +class CTargetCDAudioRep : public CPointEntity +{ +public: + DECLARE_CLASS( CTargetCDAudioRep, CPointEntity ); + + void InputChangeCDTrack( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + int m_iTrack; // CD track to change to when fired +}; + +LINK_ENTITY_TO_CLASS( target_cdaudio, CTargetCDAudioRep ); + +BEGIN_DATADESC( CTargetCDAudioRep ) + + DEFINE_KEYFIELD( m_iTrack, FIELD_INTEGER, "track" ), + DEFINE_INPUTFUNC( FIELD_VOID, "ChangeCDTrack", InputChangeCDTrack ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Changes the current playing CD track +//----------------------------------------------------------------------------- +void CTargetCDAudioRep::InputChangeCDTrack( inputdata_t &inputdata ) +{ + int iTrack = m_iTrack; + + + edict_t *pClient = NULL; + if ( gpGlobals->maxClients == 1 ) + { + pClient = engine->PEntityOfEntIndex( 1 ); + } + else + { + // In multiplayer, send it back to the activator + CBasePlayer *player = dynamic_cast< CBasePlayer * >( inputdata.pActivator ); + if ( player ) + { + pClient = player->edict(); + } + } + + // Can't play if the client is not connected! + if ( !pClient ) + return; + + if ( iTrack < -1 || iTrack > 30 ) + { + Warning( "TargetCDAudio - Track %d out of range\n", iTrack ); + return; + } + + if ( iTrack == -1 ) + { + engine->ClientCommand( pClient, "cd pause\n" ); + } + else + { + engine->ClientCommand ( pClient, "cd play %3d\n", iTrack ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: changes the gravity of the player who activates this entity +//----------------------------------------------------------------------------- +class CTargetChangeGravity : public CPointEntity +{ +public: + DECLARE_CLASS( CTargetChangeGravity, CPointEntity ); + + DECLARE_DATADESC(); + + void InputChangeGrav( inputdata_t &inputdata ); + void InputResetGrav( inputdata_t &inputdata ); + + int m_iGravity; + + int m_iOldGrav; +}; + +LINK_ENTITY_TO_CLASS( target_changegravity, CTargetChangeGravity ); + +BEGIN_DATADESC( CTargetChangeGravity ) + + DEFINE_KEYFIELD( m_iGravity, FIELD_INTEGER, "gravity" ), + DEFINE_FIELD( m_iOldGrav, FIELD_INTEGER ), + DEFINE_INPUTFUNC( FIELD_VOID, "ChangeGrav", InputChangeGrav ), + DEFINE_INPUTFUNC( FIELD_VOID, "ResetGrav", InputResetGrav ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for changing the activator's gravity. +//----------------------------------------------------------------------------- +void CTargetChangeGravity::InputChangeGrav( inputdata_t &inputdata ) +{ + CBasePlayer *pl = ToBasePlayer( inputdata.pActivator ); + if ( !pl ) + return; + + // Save the gravity to restore it in InputResetGrav + if ( m_iOldGrav ) + m_iOldGrav = pl->GetGravity(); + + pl->SetGravity(m_iGravity); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for resetting the activator's gravity. +//----------------------------------------------------------------------------- +void CTargetChangeGravity::InputResetGrav( inputdata_t &inputdata ) +{ + CBasePlayer *pl = ToBasePlayer( inputdata.pActivator ); + if ( !pl ) + return; + + pl->SetGravity(m_iOldGrav); +} + + diff --git a/sp/src/game/server/cplane.cpp b/sp/src/game/server/cplane.cpp new file mode 100644 index 00000000..ccea553e --- /dev/null +++ b/sp/src/game/server/cplane.cpp @@ -0,0 +1,71 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "game.h" +#include "cplane.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//========================================================= +// Plane +//========================================================= +CPlane::CPlane ( void ) +{ + m_fInitialized = FALSE; +} + +//========================================================= +// InitializePlane - Takes a normal for the plane and a +// point on the plane and +//========================================================= +void CPlane::InitializePlane ( const Vector &vecNormal, const Vector &vecPoint ) +{ + m_vecNormal = vecNormal; + m_flDist = DotProduct ( m_vecNormal, vecPoint ); + m_fInitialized = TRUE; +} + + +//========================================================= +// PointInFront - determines whether the given vector is +// in front of the plane. +//========================================================= +bool CPlane::PointInFront ( const Vector &vecPoint ) +{ + float flFace; + + if ( !m_fInitialized ) + { + return FALSE; + } + + flFace = DotProduct ( m_vecNormal, vecPoint ) - m_flDist; + + if ( flFace >= 0 ) + { + return TRUE; + } + + return FALSE; +} + +//========================================================= +//========================================================= +float CPlane::PointDist ( const Vector &vecPoint ) +{ + float flDist; + + if ( !m_fInitialized ) + { + return FALSE; + } + + flDist = DotProduct ( m_vecNormal, vecPoint ) - m_flDist; + + return flDist; +} \ No newline at end of file diff --git a/sp/src/game/server/cplane.h b/sp/src/game/server/cplane.h new file mode 100644 index 00000000..98d5b458 --- /dev/null +++ b/sp/src/game/server/cplane.h @@ -0,0 +1,44 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#pragma once + +#ifndef CPLANE_H +#define CPLANE_H + +//========================================================= +// Plane +//========================================================= +class CPlane +{ +public: + CPlane ( void ); + + //========================================================= + // InitializePlane - Takes a normal for the plane and a + // point on the plane and + //========================================================= + void InitializePlane ( const Vector &vecNormal, const Vector &vecPoint ); + + //========================================================= + // PointInFront - determines whether the given vector is + // in front of the plane. + //========================================================= + bool PointInFront ( const Vector &vecPoint ); + + //========================================================= + // How far off the plane is this point? + //========================================================= + float PointDist( const Vector &vecPoint ); + +private: + Vector m_vecNormal; + float m_flDist; + bool m_fInitialized; +}; + +#endif //CPLANE_H diff --git a/sp/src/game/server/csm.vpc b/sp/src/game/server/csm.vpc new file mode 100644 index 00000000..45300a60 --- /dev/null +++ b/sp/src/game/server/csm.vpc @@ -0,0 +1,9 @@ + + +$Project +{ + $Folder "Source Files" + { + $File "env_cascade_light.cpp" + } +} diff --git a/sp/src/game/server/damagemodifier.cpp b/sp/src/game/server/damagemodifier.cpp new file mode 100644 index 00000000..12c9a595 --- /dev/null +++ b/sp/src/game/server/damagemodifier.cpp @@ -0,0 +1,85 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "damagemodifier.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CDamageModifier::CDamageModifier() +{ + m_flModifier = 1; + m_bDoneToMe = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDamageModifier::AddModifierToEntity( CBaseEntity *pEntity ) +{ + RemoveModifier(); + + pEntity->m_DamageModifiers.AddToTail( this ); + m_hEnt = pEntity; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDamageModifier::RemoveModifier() +{ + if ( m_hEnt.Get() ) + { + m_hEnt->m_DamageModifiers.FindAndRemove( this ); + m_hEnt = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDamageModifier::SetModifier( float flScale ) +{ + m_flModifier = flScale; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CDamageModifier::GetModifier() const +{ + return m_flModifier; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity* CDamageModifier::GetCharacter() const +{ + return m_hEnt.Get(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDamageModifier::SetDoneToMe( bool bDoneToMe ) +{ + m_bDoneToMe = bDoneToMe; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CDamageModifier::IsDamageDoneToMe() const +{ + return m_bDoneToMe; +} + diff --git a/sp/src/game/server/damagemodifier.h b/sp/src/game/server/damagemodifier.h new file mode 100644 index 00000000..5123f636 --- /dev/null +++ b/sp/src/game/server/damagemodifier.h @@ -0,0 +1,46 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef DAMAGEMODIFIER_H +#define DAMAGEMODIFIER_H +#ifdef _WIN32 +#pragma once +#endif + + +class CBaseEntity; + + +#include "ehandle.h" + +//----------------------------------------------------------------------------- +// Purpose: Class handling generic damage modification to & from a player +//----------------------------------------------------------------------------- +class CDamageModifier +{ +public: + CDamageModifier(); + + void AddModifierToEntity( CBaseEntity *pChar ); + void RemoveModifier(); + + void SetModifier( float flDamageScale ); + float GetModifier() const; + + void SetDoneToMe( bool bDoneToMe ); + bool IsDamageDoneToMe() const; + + CBaseEntity *GetCharacter() const; + +private: + float m_flModifier; + CHandle m_hEnt; + bool m_bDoneToMe; // True = modifies damage done to the entity, false = damage done by the entity +}; + + +#endif // DAMAGEMODIFIER_H diff --git a/sp/src/game/server/data_collector.cpp b/sp/src/game/server/data_collector.cpp new file mode 100644 index 00000000..d2323b86 --- /dev/null +++ b/sp/src/game/server/data_collector.cpp @@ -0,0 +1,66 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// data_collector.cpp +// Data collection system +// Author: Michael S. Booth, June 2004 + +#include "cbase.h" +#include "data_collector.h" + +static CDataCollector *collector = NULL; + +//---------------------------------------------------------------------------------------------------------------------- +void StartDataCollection( void ) +{ + if (collector) + { + // already collecting + return; + } + + collector = new CDataCollector; + Msg( "Data colletion started.\n" ); +} +ConCommand data_collection_start( "data_collection_start", StartDataCollection, "Start collecting game event data." ); + + +//---------------------------------------------------------------------------------------------------------------------- +void StopDataCollection( void ) +{ + if (collector) + { + delete collector; + collector = NULL; + + Msg( "Data collection stopped.\n" ); + } +} +ConCommand data_collection_stop( "data_collection_stop", StopDataCollection, "Stop collecting game event data." ); + + +//---------------------------------------------------------------------------------------------------------------------- +CDataCollector::CDataCollector( void ) +{ + // register for all events + gameeventmanager->AddListener( this, true ); +} + +//---------------------------------------------------------------------------------------------------------------------- +CDataCollector::~CDataCollector() +{ + gameeventmanager->RemoveListener( this ); +} + +//---------------------------------------------------------------------------------------------------------------------- +/** + * This is invoked for each event that occurs in the game + */ +void CDataCollector::FireGameEvent( KeyValues *event ) +{ + DevMsg( "Collected event '%s'\n", event->GetName() ); +} diff --git a/sp/src/game/server/data_collector.h b/sp/src/game/server/data_collector.h new file mode 100644 index 00000000..ba1b9fd6 --- /dev/null +++ b/sp/src/game/server/data_collector.h @@ -0,0 +1,37 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// data_collector.h +// Data collection system +// Author: Michael S. Booth, June 2004 + +#ifndef _DATA_COLLECTOR_H_ +#define _DATA_COLLECTOR_H_ + +#include +#include + +/** + * This class is used to monitor the event stream and + * store interesting events to disk for later analysis. + */ +class CDataCollector : public IGameEventListener +{ +public: + CDataCollector( void ); + ~CDataCollector(); + + // IGameEventListener + virtual void FireGameEvent( KeyValues *event ); +}; + + +extern void StartDataCollection( void ); +extern void StopDataCollection( void ); + + +#endif // _DATA_COLLECTOR_H_ diff --git a/sp/src/game/server/doors.cpp b/sp/src/game/server/doors.cpp new file mode 100644 index 00000000..e76ba199 --- /dev/null +++ b/sp/src/game/server/doors.cpp @@ -0,0 +1,1433 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements two types of doors: linear and rotating. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "doors.h" +#include "entitylist.h" +#include "physics.h" +#include "ndebugoverlay.h" +#include "engine/IEngineSound.h" +#include "physics_npc_solver.h" + +#ifdef HL1_DLL +#include "filters.h" +#endif + +#ifdef CSTRIKE_DLL +#include "KeyValues.h" +#endif + +#ifdef TF_DLL +#include "tf_gamerules.h" +#endif // TF_DLL + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define CLOSE_AREAPORTAL_THINK_CONTEXT "CloseAreaportalThink" + +BEGIN_DATADESC( CBaseDoor ) + + DEFINE_KEYFIELD( m_vecMoveDir, FIELD_VECTOR, "movedir" ), + + DEFINE_FIELD( m_bLockedSentence, FIELD_CHARACTER ), + DEFINE_FIELD( m_bUnlockedSentence, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_NoiseMoving, FIELD_SOUNDNAME, "noise1" ), + DEFINE_KEYFIELD( m_NoiseArrived, FIELD_SOUNDNAME, "noise2" ), + DEFINE_KEYFIELD( m_NoiseMovingClosed, FIELD_SOUNDNAME, "startclosesound" ), + DEFINE_KEYFIELD( m_NoiseArrivedClosed, FIELD_SOUNDNAME, "closesound" ), + DEFINE_KEYFIELD( m_ChainTarget, FIELD_STRING, "chainstodoor" ), + // DEFINE_FIELD( m_isChaining, FIELD_BOOLEAN ), + // DEFINE_FIELD( m_ls, locksound_t ), +// DEFINE_FIELD( m_isChaining, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_ls.sLockedSound, FIELD_SOUNDNAME, "locked_sound" ), + DEFINE_KEYFIELD( m_ls.sUnlockedSound, FIELD_SOUNDNAME, "unlocked_sound" ), + DEFINE_FIELD( m_bLocked, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_flWaveHeight, FIELD_FLOAT, "WaveHeight" ), + DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), + DEFINE_KEYFIELD( m_eSpawnPosition, FIELD_INTEGER, "spawnpos" ), + + DEFINE_KEYFIELD( m_bForceClosed, FIELD_BOOLEAN, "forceclosed" ), + DEFINE_FIELD( m_bDoorGroup, FIELD_BOOLEAN ), + +#ifdef HL1_DLL + DEFINE_KEYFIELD( m_iBlockFilterName, FIELD_STRING, "filtername" ), + DEFINE_FIELD( m_hBlockFilter, FIELD_EHANDLE ), +#endif + + DEFINE_KEYFIELD( m_bLoopMoveSound, FIELD_BOOLEAN, "loopmovesound" ), + DEFINE_KEYFIELD( m_bIgnoreDebris, FIELD_BOOLEAN, "ignoredebris" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Open", InputOpen ), + DEFINE_INPUTFUNC( FIELD_VOID, "Close", InputClose ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Lock", InputLock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Unlock", InputUnlock ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetToggleState", InputSetToggleState ), + + DEFINE_OUTPUT( m_OnBlockedOpening, "OnBlockedOpening" ), + DEFINE_OUTPUT( m_OnBlockedClosing, "OnBlockedClosing" ), + DEFINE_OUTPUT( m_OnUnblockedOpening, "OnUnblockedOpening" ), + DEFINE_OUTPUT( m_OnUnblockedClosing, "OnUnblockedClosing" ), + DEFINE_OUTPUT( m_OnFullyClosed, "OnFullyClosed" ), + DEFINE_OUTPUT( m_OnFullyOpen, "OnFullyOpen" ), + DEFINE_OUTPUT( m_OnClose, "OnClose" ), + DEFINE_OUTPUT( m_OnOpen, "OnOpen" ), + DEFINE_OUTPUT( m_OnLockedUse, "OnLockedUse" ), + + // Function Pointers + DEFINE_FUNCTION( DoorTouch ), + DEFINE_FUNCTION( DoorGoUp ), + DEFINE_FUNCTION( DoorGoDown ), + DEFINE_FUNCTION( DoorHitTop ), + DEFINE_FUNCTION( DoorHitBottom ), + DEFINE_THINKFUNC( MovingSoundThink ), + DEFINE_THINKFUNC( CloseAreaPortalsThink ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( func_door, CBaseDoor ); + +// +// func_water is implemented as a linear door so we can raise/lower the water level. +// +LINK_ENTITY_TO_CLASS( func_water, CBaseDoor ); + + +// SendTable stuff. +IMPLEMENT_SERVERCLASS_ST(CBaseDoor, DT_BaseDoor) + SendPropFloat (SENDINFO(m_flWaveHeight), 8, SPROP_ROUNDUP, 0.0f, 8.0f), +END_SEND_TABLE() + +#define DOOR_SENTENCEWAIT 6 +#define DOOR_SOUNDWAIT 1 +#define BUTTON_SOUNDWAIT 0.5 + + +//----------------------------------------------------------------------------- +// Purpose: play door or button locked or unlocked sounds. +// NOTE: this routine is shared by doors and buttons +// Input : pEdict - +// pls - +// flocked - if true, play 'door is locked' sound, otherwise play 'door +// is unlocked' sound. +// fbutton - +//----------------------------------------------------------------------------- +void PlayLockSounds(CBaseEntity *pEdict, locksound_t *pls, int flocked, int fbutton) +{ + if ( pEdict->HasSpawnFlags( SF_DOOR_SILENT ) ) + { + return; + } + float flsoundwait = ( fbutton ) ? BUTTON_SOUNDWAIT : DOOR_SOUNDWAIT; + + if ( flocked ) + { + int fplaysound = (pls->sLockedSound != NULL_STRING && gpGlobals->curtime > pls->flwaitSound); + int fplaysentence = (pls->sLockedSentence != NULL_STRING && !pls->bEOFLocked && gpGlobals->curtime > pls->flwaitSentence); + float fvol = ( fplaysound && fplaysentence ) ? 0.25f : 1.0f; + + // if there is a locked sound, and we've debounced, play sound + if (fplaysound) + { + // play 'door locked' sound + CPASAttenuationFilter filter( pEdict ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = (char*)STRING(pls->sLockedSound); + ep.m_flVolume = fvol; + ep.m_SoundLevel = SNDLVL_NORM; + + CBaseEntity::EmitSound( filter, pEdict->entindex(), ep ); + pls->flwaitSound = gpGlobals->curtime + flsoundwait; + } + + // if there is a sentence, we've not played all in list, and we've debounced, play sound + if (fplaysentence) + { + // play next 'door locked' sentence in group + int iprev = pls->iLockedSentence; + + pls->iLockedSentence = SENTENCEG_PlaySequentialSz( pEdict->edict(), + STRING(pls->sLockedSentence), + 0.85f, + SNDLVL_NORM, + 0, + 100, + pls->iLockedSentence, + FALSE); + pls->iUnlockedSentence = 0; + + // make sure we don't keep calling last sentence in list + pls->bEOFLocked = (iprev == pls->iLockedSentence); + + pls->flwaitSentence = gpGlobals->curtime + DOOR_SENTENCEWAIT; + } + } + else + { + // UNLOCKED SOUND + + int fplaysound = (pls->sUnlockedSound != NULL_STRING && gpGlobals->curtime > pls->flwaitSound); + int fplaysentence = (pls->sUnlockedSentence != NULL_STRING && !pls->bEOFUnlocked && gpGlobals->curtime > pls->flwaitSentence); + float fvol; + + // if playing both sentence and sound, lower sound volume so we hear sentence + fvol = ( fplaysound && fplaysentence ) ? 0.25f : 1.0f; + + // play 'door unlocked' sound if set + if (fplaysound) + { + CPASAttenuationFilter filter( pEdict ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = (char*)STRING(pls->sUnlockedSound); + ep.m_flVolume = fvol; + ep.m_SoundLevel = SNDLVL_NORM; + + CBaseEntity::EmitSound( filter, pEdict->entindex(), ep ); + pls->flwaitSound = gpGlobals->curtime + flsoundwait; + } + + // play next 'door unlocked' sentence in group + if (fplaysentence) + { + int iprev = pls->iUnlockedSentence; + + pls->iUnlockedSentence = SENTENCEG_PlaySequentialSz(pEdict->edict(), STRING(pls->sUnlockedSentence), + 0.85, SNDLVL_NORM, 0, 100, pls->iUnlockedSentence, FALSE); + pls->iLockedSentence = 0; + + // make sure we don't keep calling last sentence in list + pls->bEOFUnlocked = (iprev == pls->iUnlockedSentence); + pls->flwaitSentence = gpGlobals->curtime + DOOR_SENTENCEWAIT; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Cache user-entity-field values until spawn is called. +// Input : szKeyName - +// szValue - +// Output : Returns true. +//----------------------------------------------------------------------------- +bool CBaseDoor::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "locked_sentence")) + { + m_bLockedSentence = atof(szValue); + } + else if (FStrEq(szKeyName, "unlocked_sentence")) + { + m_bUnlockedSentence = atof(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::Spawn() +{ + Precache(); + +#ifdef HL1_DLL + SetSolid( SOLID_BSP ); +#else + if ( GetMoveParent() && GetRootMoveParent()->GetSolid() == SOLID_BSP ) + { + SetSolid( SOLID_BSP ); + } + else + { + SetSolid( SOLID_VPHYSICS ); + } +#endif + + // Convert movedir from angles to a vector + QAngle angMoveDir = QAngle( m_vecMoveDir.x, m_vecMoveDir.y, m_vecMoveDir.z ); + AngleVectors( angMoveDir, &m_vecMoveDir ); + + SetModel( STRING( GetModelName() ) ); + m_vecPosition1 = GetLocalOrigin(); + + // Subtract 2 from size because the engine expands bboxes by 1 in all directions making the size too big + Vector vecOBB = CollisionProp()->OBBSize(); + vecOBB -= Vector( 2, 2, 2 ); + m_vecPosition2 = m_vecPosition1 + (m_vecMoveDir * (DotProductAbs( m_vecMoveDir, vecOBB ) - m_flLip)); + + if ( !IsRotatingDoor() ) + { + if ( ( m_eSpawnPosition == FUNC_DOOR_SPAWN_OPEN ) || HasSpawnFlags( SF_DOOR_START_OPEN_OBSOLETE ) ) + { // swap pos1 and pos2, put door at pos2 + UTIL_SetOrigin( this, m_vecPosition2); + m_toggle_state = TS_AT_TOP; + } + else + { + m_toggle_state = TS_AT_BOTTOM; + } + } + + if (HasSpawnFlags(SF_DOOR_LOCKED)) + { + m_bLocked = true; + } + + SetMoveType( MOVETYPE_PUSH ); + + if (m_flSpeed == 0) + { + m_flSpeed = 100; + } + + SetTouch( &CBaseDoor::DoorTouch ); + + if ( !FClassnameIs( this, "func_water" ) ) + { + if ( HasSpawnFlags(SF_DOOR_PASSABLE) ) + { + //normal door + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + if ( HasSpawnFlags( SF_DOOR_NONSOLID_TO_PLAYER ) ) + { + SetCollisionGroup( COLLISION_GROUP_PASSABLE_DOOR ); + // HACKHACK: Set this hoping that any children of the door that get blocked by the player + // will get fixed up by vphysics + // NOTE: We could decouple this as a separate behavior, but managing player collisions is already complex enough. + // NOTE: This is necessary to prevent the player from blocking the wrecked train car in ep2_outland_01 + AddFlag( FL_UNBLOCKABLE_BY_PLAYER ); + } + if ( m_bIgnoreDebris ) + { + // both of these flags want to set the collision group and + // there isn't a combo group + Assert( !HasSpawnFlags( SF_DOOR_NONSOLID_TO_PLAYER ) ); + if ( HasSpawnFlags( SF_DOOR_NONSOLID_TO_PLAYER ) ) + { + Warning("Door %s with conflicting collision settings, removing ignoredebris\n", GetDebugName() ); + } + else + { + SetCollisionGroup( COLLISION_GROUP_INTERACTIVE ); + } + } + } + + if ( ( m_eSpawnPosition == FUNC_DOOR_SPAWN_OPEN ) && HasSpawnFlags( SF_DOOR_START_OPEN_OBSOLETE ) ) + { + Warning("Door %s using obsolete 'Start Open' spawnflag with 'Spawn Position' set to 'Open'. Reverting to old behavior.\n", GetDebugName() ); + } + + CreateVPhysics(); + +#ifdef TF_DLL + if ( TFGameRules() && TFGameRules()->IsMultiplayer() ) + { + // Never block doors in TF2 - to prevent various exploits. + m_bIgnoreNonPlayerEntsOnBlock = true; + } +#else + m_bIgnoreNonPlayerEntsOnBlock = false; +#endif // TF_DLL +} + +void CBaseDoor::MovingSoundThink( void ) +{ + CPASAttenuationFilter filter( this ); + filter.MakeReliable(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + if ( m_NoiseMovingClosed == NULL_STRING || m_toggle_state == TS_GOING_DOWN || m_toggle_state == TS_AT_BOTTOM ) + { + ep.m_pSoundName = (char*)STRING(m_NoiseMoving); + } + else + { + ep.m_pSoundName = (char*)STRING(m_NoiseMovingClosed); + } + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + + //Only loop sounds in HL1 to maintain HL2 behavior + if( ShouldLoopMoveSound() ) + { + float duration = enginesound->GetSoundDuration( ep.m_pSoundName ); + SetContextThink( &CBaseDoor::MovingSoundThink, gpGlobals->curtime + duration, "MovingSound" ); + } +} + +void CBaseDoor::StartMovingSound( void ) +{ + MovingSoundThink(); + +#ifdef CSTRIKE_DLL // this event is only used by CS:S bots + + CBasePlayer *player = ToBasePlayer(m_hActivator); + IGameEvent * event = gameeventmanager->CreateEvent( "door_moving" ); + if( event ) + { + event->SetInt( "entindex", entindex() ); + event->SetInt( "userid", (player)?player->GetUserID():0 ); + gameeventmanager->FireEvent( event ); + } + +#endif +} + +void CBaseDoor::StopMovingSound(void) +{ + SetContextThink( NULL, gpGlobals->curtime, "MovingSound" ); + char *pSoundName; + if ( m_NoiseMovingClosed == NULL_STRING || m_toggle_state == TS_GOING_UP || m_toggle_state == TS_AT_TOP ) + { + pSoundName = (char*)STRING(m_NoiseMoving); + } + else + { + pSoundName = (char*)STRING(m_NoiseMovingClosed); + } + StopSound( entindex(), CHAN_STATIC, pSoundName ); +} + + +bool CBaseDoor::ShouldSavePhysics() +{ + // don't save physics if you're func_water + return !FClassnameIs( this, "func_water" ); +} + +//----------------------------------------------------------------------------- +bool CBaseDoor::CreateVPhysics( ) +{ + if ( !FClassnameIs( this, "func_water" ) ) + { + //normal door + // NOTE: Create this even when the door is not solid to support constraints. + VPhysicsInitShadow( false, false ); + } + else + { + // special contents + AddSolidFlags( FSOLID_VOLUME_CONTENTS ); + SETBITS( m_spawnflags, SF_DOOR_SILENT ); // water is silent for now + + IPhysicsObject *pPhysics = VPhysicsInitShadow( false, false ); + fluidparams_t fluid; + + Assert( CollisionProp()->GetCollisionAngles() == vec3_angle ); + fluid.damping = 0.01f; + fluid.surfacePlane[0] = 0; + fluid.surfacePlane[1] = 0; + fluid.surfacePlane[2] = 1; + fluid.surfacePlane[3] = CollisionProp()->GetCollisionOrigin().z + CollisionProp()->OBBMaxs().z - 1; + fluid.currentVelocity.Init(0,0,0); + fluid.torqueFactor = 0.1f; + fluid.viscosityFactor = 0.01f; + fluid.pGameData = static_cast(this); + + //FIXME: Currently there's no way to specify that you want slime + fluid.contents = CONTENTS_WATER; + + physenv->CreateFluidController( pPhysics, &fluid ); + } + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::Activate( void ) +{ + BaseClass::Activate(); + + CBaseDoor *pDoorList[64]; + m_bDoorGroup = true; + + // force movement groups to sync!!! + int doorCount = GetDoorMovementGroup( pDoorList, ARRAYSIZE(pDoorList) ); + for ( int i = 0; i < doorCount; i++ ) + { + if ( pDoorList[i]->m_vecMoveDir == m_vecMoveDir ) + { + bool error = false; + if ( pDoorList[i]->IsRotatingDoor() ) + { + error = ( pDoorList[i]->GetLocalAngles() != GetLocalAngles() ) ? true : false; + } + else + { + error = ( pDoorList[i]->GetLocalOrigin() != GetLocalOrigin() ) ? true : false; + } + if ( error ) + { + // don't do group blocking + m_bDoorGroup = false; +#ifdef HL1_DLL + // UNDONE: This should probably fixup m_vecPosition1 & m_vecPosition2 + Warning("Door group %s has misaligned origin!\n", STRING(GetEntityName()) ); +#endif + } + } + } + + switch ( m_toggle_state ) + { + case TS_AT_TOP: + UpdateAreaPortals( true ); + break; + case TS_AT_BOTTOM: + UpdateAreaPortals( false ); + break; + } + +#ifdef HL1_DLL + // Get a handle to my filter entity if there is one + if (m_iBlockFilterName != NULL_STRING) + { + m_hBlockFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iBlockFilterName, NULL )); + } +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +// This is ONLY used by the node graph to test movement through a door +void CBaseDoor::InputSetToggleState( inputdata_t &inputdata ) +{ + SetToggleState( inputdata.value.Int() ); +} + +void CBaseDoor::SetToggleState( int state ) +{ + if ( state == TS_AT_TOP ) + UTIL_SetOrigin( this, m_vecPosition2 ); + else + UTIL_SetOrigin( this, m_vecPosition1 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::Precache( void ) +{ + //Fill in a default value if necessary + if ( IsRotatingDoor() ) + { + UTIL_ValidateSoundName( m_NoiseMoving, "RotDoorSound.DefaultMove" ); + UTIL_ValidateSoundName( m_NoiseArrived, "RotDoorSound.DefaultArrive" ); + UTIL_ValidateSoundName( m_ls.sLockedSound, "RotDoorSound.DefaultLocked" ); + UTIL_ValidateSoundName( m_ls.sUnlockedSound,"DoorSound.Null" ); + } + else + { + UTIL_ValidateSoundName( m_NoiseMoving, "DoorSound.DefaultMove" ); + UTIL_ValidateSoundName( m_NoiseArrived, "DoorSound.DefaultArrive" ); +#ifndef HL1_DLL + UTIL_ValidateSoundName( m_ls.sLockedSound, "DoorSound.DefaultLocked" ); +#endif + UTIL_ValidateSoundName( m_ls.sUnlockedSound,"DoorSound.Null" ); + } + +#ifdef HL1_DLL + if( m_ls.sLockedSound != NULL_STRING && strlen((char*)STRING(m_ls.sLockedSound)) < 4 ) + { + // Too short to be ANYTHING ".wav", so it must be an old index into a long-lost + // array of sound choices. slam it to a known "deny" sound. We lose the designer's + // original selection, but we don't get unresponsive doors. + m_ls.sLockedSound = AllocPooledString("buttons/button2.wav"); + } +#endif//HL1_DLL + + //Precache them all + PrecacheScriptSound( (char *) STRING(m_NoiseMoving) ); + PrecacheScriptSound( (char *) STRING(m_NoiseArrived) ); + PrecacheScriptSound( (char *) STRING(m_NoiseMovingClosed) ); + PrecacheScriptSound( (char *) STRING(m_NoiseArrivedClosed) ); + PrecacheScriptSound( (char *) STRING(m_ls.sLockedSound) ); + PrecacheScriptSound( (char *) STRING(m_ls.sUnlockedSound) ); + + //Get sentence group names, for doors which are directly 'touched' to open + switch (m_bLockedSentence) + { + case 1: m_ls.sLockedSentence = AllocPooledString("NA"); break; // access denied + case 2: m_ls.sLockedSentence = AllocPooledString("ND"); break; // security lockout + case 3: m_ls.sLockedSentence = AllocPooledString("NF"); break; // blast door + case 4: m_ls.sLockedSentence = AllocPooledString("NFIRE"); break; // fire door + case 5: m_ls.sLockedSentence = AllocPooledString("NCHEM"); break; // chemical door + case 6: m_ls.sLockedSentence = AllocPooledString("NRAD"); break; // radiation door + case 7: m_ls.sLockedSentence = AllocPooledString("NCON"); break; // gen containment + case 8: m_ls.sLockedSentence = AllocPooledString("NH"); break; // maintenance door + case 9: m_ls.sLockedSentence = AllocPooledString("NG"); break; // broken door + + default: m_ls.sLockedSentence = NULL_STRING; break; + } + + switch (m_bUnlockedSentence) + { + case 1: m_ls.sUnlockedSentence = AllocPooledString("EA"); break; // access granted + case 2: m_ls.sUnlockedSentence = AllocPooledString("ED"); break; // security door + case 3: m_ls.sUnlockedSentence = AllocPooledString("EF"); break; // blast door + case 4: m_ls.sUnlockedSentence = AllocPooledString("EFIRE"); break; // fire door + case 5: m_ls.sUnlockedSentence = AllocPooledString("ECHEM"); break; // chemical door + case 6: m_ls.sUnlockedSentence = AllocPooledString("ERAD"); break; // radiation door + case 7: m_ls.sUnlockedSentence = AllocPooledString("ECON"); break; // gen containment + case 8: m_ls.sUnlockedSentence = AllocPooledString("EH"); break; // maintenance door + + default: m_ls.sUnlockedSentence = NULL_STRING; break; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Doors not tied to anything (e.g. button, another door) can be touched, +// to make them activate. +// Input : *pOther - +//----------------------------------------------------------------------------- +void CBaseDoor::DoorTouch( CBaseEntity *pOther ) +{ + if( m_ChainTarget != NULL_STRING ) + ChainTouch( pOther ); + + // Ignore touches by anything but players. + if ( !pOther->IsPlayer() ) + { +#ifdef HL1_DLL + if( PassesBlockTouchFilter( pOther ) && m_toggle_state == TS_GOING_DOWN ) + { + DoorGoUp(); + } +#endif + return; + } + + // If door is not opened by touch, do nothing. + if ( !HasSpawnFlags(SF_DOOR_PTOUCH) ) + { +#ifdef HL1_DLL + if( m_toggle_state == TS_AT_BOTTOM ) + { + PlayLockSounds(this, &m_ls, TRUE, FALSE); + } +#endif//HL1_DLL + + return; + } + + // If door has master, and it's not ready to trigger, + // play 'locked' sound. + if (m_sMaster != NULL_STRING && !UTIL_IsMasterTriggered(m_sMaster, pOther)) + { + PlayLockSounds(this, &m_ls, TRUE, FALSE); + } + + if (m_bLocked) + { + m_OnLockedUse.FireOutput( pOther, pOther ); + PlayLockSounds(this, &m_ls, TRUE, FALSE); + return; + } + + // Remember who activated the door. + m_hActivator = pOther; + + if (DoorActivate( )) + { + // Temporarily disable the touch function, until movement is finished. + SetTouch( NULL ); + } +} + +#ifdef HL1_DLL +bool CBaseDoor::PassesBlockTouchFilter(CBaseEntity *pOther) +{ + CBaseFilter* pFilter = (CBaseFilter*)(m_hBlockFilter.Get()); + return ( pFilter && pFilter->PassesFilter( this, pOther ) ); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Delays turning off area portals when closing doors to prevent visual artifacts +//----------------------------------------------------------------------------- +void CBaseDoor::CloseAreaPortalsThink( void ) +{ + UpdateAreaPortals( false ); + SetContextThink( NULL, gpGlobals->curtime, CLOSE_AREAPORTAL_THINK_CONTEXT ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : isOpen - +//----------------------------------------------------------------------------- +void CBaseDoor::UpdateAreaPortals( bool isOpen ) +{ + // cancel pending close + SetContextThink( NULL, gpGlobals->curtime, CLOSE_AREAPORTAL_THINK_CONTEXT ); + + if ( IsRotatingDoor() && HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE) ) // logic inverted when using rot doors that start open + isOpen = !isOpen; + + string_t name = GetEntityName(); + if ( !name ) + return; + + CBaseEntity *pPortal = NULL; + while ( ( pPortal = gEntList.FindEntityByClassname( pPortal, "func_areaportal" ) ) != NULL ) + { + if ( pPortal->HasTarget( name ) ) + { + // USE_ON means open the portal, off means close it + pPortal->Use( this, this, isOpen?USE_ON:USE_OFF, 0 ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when the player uses the door. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CBaseDoor::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + m_hActivator = pActivator; + + if( m_ChainTarget != NULL_STRING ) + ChainUse(); + + // We can't +use this if it can't be +used + if ( m_hActivator != NULL && m_hActivator->IsPlayer() && HasSpawnFlags( SF_DOOR_PUSE ) == false ) + { + PlayLockSounds( this, &m_ls, TRUE, FALSE ); + return; + } + + bool bAllowUse = false; + + // if not ready to be used, ignore "use" command. + if( HasSpawnFlags(SF_DOOR_NEW_USE_RULES) ) + { + //New behavior: + // If not ready to be used, ignore "use" command. + // Allow use in these cases: + // - when the door is closed/closing + // - when the door is open/opening and can be manually closed + if ( ( m_toggle_state == TS_AT_BOTTOM || m_toggle_state == TS_GOING_DOWN ) || ( HasSpawnFlags(SF_DOOR_NO_AUTO_RETURN) && ( m_toggle_state == TS_AT_TOP || m_toggle_state == TS_GOING_UP ) ) ) + bAllowUse = true; + } + else + { + // Legacy behavior: + if (m_toggle_state == TS_AT_BOTTOM || (HasSpawnFlags(SF_DOOR_NO_AUTO_RETURN) && m_toggle_state == TS_AT_TOP) ) + bAllowUse = true; + } + + if( bAllowUse ) + { + if (m_bLocked) + { + m_OnLockedUse.FireOutput( pActivator, pCaller ); + PlayLockSounds(this, &m_ls, TRUE, FALSE); + } + else + { + DoorActivate(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Passes Use along to certain named doors. +//----------------------------------------------------------------------------- +void CBaseDoor::ChainUse( void ) +{ + if ( m_isChaining ) + return; + + CBaseEntity *ent = NULL; + while ( ( ent = gEntList.FindEntityByName( ent, m_ChainTarget, NULL ) ) != NULL ) + { + if ( ent == this ) + continue; + + CBaseDoor *door = dynamic_cast< CBaseDoor * >( ent ); + if ( door ) + { + door->SetChaining( true ); + door->Use( m_hActivator, NULL, USE_TOGGLE, 0.0f ); // only the first param is used + door->SetChaining( false ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Passes Touch along to certain named doors. +//----------------------------------------------------------------------------- +void CBaseDoor::ChainTouch( CBaseEntity *pOther ) +{ + if ( m_isChaining ) + return; + + CBaseEntity *ent = NULL; + while ( ( ent = gEntList.FindEntityByName( ent, m_ChainTarget, NULL ) ) != NULL ) + { + if ( ent == this ) + continue; + + CBaseDoor *door = dynamic_cast< CBaseDoor * >( ent ); + if ( door ) + { + door->SetChaining( true ); + door->Touch( pOther ); + door->SetChaining( false ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Closes the door if it is not already closed. +//----------------------------------------------------------------------------- +void CBaseDoor::InputClose( inputdata_t &inputdata ) +{ + if ( m_toggle_state != TS_AT_BOTTOM ) + { + DoorGoDown(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that locks the door. +//----------------------------------------------------------------------------- +void CBaseDoor::InputLock( inputdata_t &inputdata ) +{ + Lock(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Opens the door if it is not already open. +//----------------------------------------------------------------------------- +void CBaseDoor::InputOpen( inputdata_t &inputdata ) +{ + if (m_toggle_state != TS_AT_TOP && m_toggle_state != TS_GOING_UP ) + { + // I'm locked, can't open + if (m_bLocked) + return; + + // Play door unlock sounds. + PlayLockSounds(this, &m_ls, false, false); + DoorGoUp(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Opens the door if it is not already open. +//----------------------------------------------------------------------------- +void CBaseDoor::InputToggle( inputdata_t &inputdata ) +{ + // I'm locked, can't open + if (m_bLocked) + return; + + if (m_toggle_state == TS_AT_BOTTOM) + { + DoorGoUp(); + } + else if (m_toggle_state == TS_AT_TOP) + { + DoorGoDown(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that unlocks the door. +//----------------------------------------------------------------------------- +void CBaseDoor::InputUnlock( inputdata_t &inputdata ) +{ + Unlock(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseDoor::InputSetSpeed( inputdata_t &inputdata ) +{ + m_flSpeed = inputdata.value.Float(); +} + +//----------------------------------------------------------------------------- +// Purpose: Locks the door so that it cannot be opened. +//----------------------------------------------------------------------------- +void CBaseDoor::Lock( void ) +{ + m_bLocked = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Unlocks the door so that it can be opened. +//----------------------------------------------------------------------------- +void CBaseDoor::Unlock( void ) +{ + m_bLocked = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Causes the door to "do its thing", i.e. start moving, and cascade activation. +// Output : int +//----------------------------------------------------------------------------- +int CBaseDoor::DoorActivate( ) +{ + if (!UTIL_IsMasterTriggered(m_sMaster, m_hActivator)) + return 0; + + if (HasSpawnFlags(SF_DOOR_NO_AUTO_RETURN) && m_toggle_state == TS_AT_TOP) + { + // door should close + DoorGoDown(); + } + else + { + // door should open + // play door unlock sounds + PlayLockSounds(this, &m_ls, FALSE, FALSE); + + if ( m_toggle_state != TS_AT_TOP && m_toggle_state != TS_GOING_UP ) + { + DoorGoUp(); + } + } + + return 1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the door going to its "up" position (simply ToggleData->vecPosition2). +//----------------------------------------------------------------------------- +void CBaseDoor::DoorGoUp( void ) +{ + edict_t *pevActivator; + + UpdateAreaPortals( true ); + // It could be going-down, if blocked. + ASSERT(m_toggle_state == TS_AT_BOTTOM || m_toggle_state == TS_GOING_DOWN); + + // emit door moving and stop sounds on CHAN_STATIC so that the multicast doesn't + // filter them out and leave a client stuck with looping door sounds! + if ( !HasSpawnFlags(SF_DOOR_SILENT ) ) + { + // If we're not moving already, start the moving noise + if ( m_toggle_state != TS_GOING_UP && m_toggle_state != TS_GOING_DOWN ) + { + StartMovingSound(); + } + } + + m_toggle_state = TS_GOING_UP; + + SetMoveDone( &CBaseDoor::DoorHitTop ); + if ( IsRotatingDoor() ) // !!! BUGBUG Triggered doors don't work with this yet + { + float sign = 1.0; + + if ( m_hActivator != NULL ) + { + pevActivator = m_hActivator->edict(); + + if ( !HasSpawnFlags( SF_DOOR_ONEWAY ) && m_vecMoveAng.y ) // Y axis rotation, move away from the player + { + // Positive is CCW, negative is CW, so make 'sign' 1 or -1 based on which way we want to open. + // Important note: All doors face East at all times, and twist their local angle to open. + // So you can't look at the door's facing to determine which way to open. + + Vector nearestPoint; + CollisionProp()->CalcNearestPoint( m_hActivator->GetAbsOrigin(), &nearestPoint ); + Vector activatorToNearestPoint = nearestPoint - m_hActivator->GetAbsOrigin(); + activatorToNearestPoint.z = 0; + + Vector activatorToOrigin = GetAbsOrigin() - m_hActivator->GetAbsOrigin(); + activatorToOrigin.z = 0; + + // Point right hand at door hinge, curl hand towards closest spot on door, if thumb + // is up, open door CW. -- Department of Basic Cross Product Understanding for Noobs + Vector cross = activatorToOrigin.Cross( activatorToNearestPoint ); + + if( cross.z > 0.0f ) + { + sign = -1.0f; + } + } + } + AngularMove(m_vecAngle2*sign, m_flSpeed); + } + else + { + LinearMove(m_vecPosition2, m_flSpeed); + } + + //Fire our open ouput + m_OnOpen.FireOutput( this, this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: The door has reached the "up" position. Either go back down, or +// wait for another activation. +//----------------------------------------------------------------------------- +void CBaseDoor::DoorHitTop( void ) +{ + if ( !HasSpawnFlags( SF_DOOR_SILENT ) ) + { + CPASAttenuationFilter filter( this ); + filter.MakeReliable(); + StopMovingSound(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = (char*)STRING(m_NoiseArrived); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + ASSERT(m_toggle_state == TS_GOING_UP); + m_toggle_state = TS_AT_TOP; + + // toggle-doors don't come down automatically, they wait for refire. + if (HasSpawnFlags( SF_DOOR_NO_AUTO_RETURN)) + { + // Re-instate touch method, movement is complete + SetTouch( &CBaseDoor::DoorTouch ); + } + else + { + // In flWait seconds, DoorGoDown will fire, unless wait is -1, then door stays open + SetMoveDoneTime( m_flWait ); + SetMoveDone( &CBaseDoor::DoorGoDown ); + + if ( m_flWait == -1 ) + { + SetNextThink( TICK_NEVER_THINK ); + } + } + + if (HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE) ) + { + m_OnFullyClosed.FireOutput(this, this); + } + else + { + m_OnFullyOpen.FireOutput(this, this); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the door going to its "down" position (simply ToggleData->vecPosition1). +//----------------------------------------------------------------------------- +void CBaseDoor::DoorGoDown( void ) +{ + if ( !HasSpawnFlags( SF_DOOR_SILENT ) ) + { + // If we're not moving already, start the moving noise + if ( m_toggle_state != TS_GOING_UP && m_toggle_state != TS_GOING_DOWN ) + { + StartMovingSound(); + } + } + +#ifdef DOOR_ASSERT + ASSERT(m_toggle_state == TS_AT_TOP); +#endif // DOOR_ASSERT + m_toggle_state = TS_GOING_DOWN; + + SetMoveDone( &CBaseDoor::DoorHitBottom ); + if ( IsRotatingDoor() )//rotating door + AngularMove( m_vecAngle1, m_flSpeed); + else + LinearMove( m_vecPosition1, m_flSpeed); + + //Fire our closed output + m_OnClose.FireOutput( this, this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: The door has reached the "down" position. Back to quiescence. +//----------------------------------------------------------------------------- +void CBaseDoor::DoorHitBottom( void ) +{ + if ( !HasSpawnFlags( SF_DOOR_SILENT ) ) + { + CPASAttenuationFilter filter( this ); + filter.MakeReliable(); + + StopMovingSound(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + if ( m_NoiseArrivedClosed == NULL_STRING ) + ep.m_pSoundName = (char*)STRING(m_NoiseArrived); + else + ep.m_pSoundName = (char*)STRING(m_NoiseArrivedClosed); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + ASSERT(m_toggle_state == TS_GOING_DOWN); + m_toggle_state = TS_AT_BOTTOM; + + // Re-instate touch method, cycle is complete + SetTouch( &CBaseDoor::DoorTouch ); + + if (HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE)) + { + m_OnFullyOpen.FireOutput(m_hActivator, this); + } + else + { + m_OnFullyClosed.FireOutput(m_hActivator, this); + } + + // Close the area portals just after the door closes, to prevent visual artifacts in multiplayer games + SetContextThink( &CBaseDoor::CloseAreaPortalsThink, gpGlobals->curtime + 0.5f, CLOSE_AREAPORTAL_THINK_CONTEXT ); +} + + +// Lists all doors in the same movement group as this one +int CBaseDoor::GetDoorMovementGroup( CBaseDoor *pDoorList[], int listMax ) +{ + int count = 0; + CBaseEntity *pTarget = NULL; + + // Block all door pieces with the same targetname here. + if ( GetEntityName() != NULL_STRING ) + { + for (;;) + { + pTarget = gEntList.FindEntityByName( pTarget, GetEntityName(), NULL ); + + if ( pTarget != this ) + { + if ( !pTarget ) + break; + + CBaseDoor *pDoor = dynamic_cast(pTarget); + + if ( pDoor && count < listMax ) + { + pDoorList[count] = pDoor; + count++; + } + } + } + } + + return count; +} + +//----------------------------------------------------------------------------- +// Purpose: Called the first frame that the door is blocked while opening or closing. +// Input : pOther - The blocking entity. +//----------------------------------------------------------------------------- +void CBaseDoor::StartBlocked( CBaseEntity *pOther ) +{ + // + // Fire whatever events we need to due to our blocked state. + // + if (m_toggle_state == TS_GOING_DOWN) + { + m_OnBlockedClosing.FireOutput(pOther, this); + } + else + { + m_OnBlockedOpening.FireOutput(pOther, this); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called every frame when the door is blocked while opening or closing. +// Input : pOther - The blocking entity. +//----------------------------------------------------------------------------- +void CBaseDoor::Blocked( CBaseEntity *pOther ) +{ + // Hurt the blocker a little. + if ( m_flBlockDamage ) + { + // if the door is marked "force closed" or it has a negative wait, then there's nothing to do but + // push/damage the object. + // If block damage is set, but this object is a physics prop that can't be damaged, just + // give up and disable collisions + if ( (m_bForceClosed || m_flWait < 0) && pOther->GetMoveType() == MOVETYPE_VPHYSICS && + (pOther->m_takedamage == DAMAGE_NO || pOther->m_takedamage == DAMAGE_EVENTS_ONLY) ) + { + EntityPhysics_CreateSolver( this, pOther, true, 4.0f ); + } + else + { + pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); + } + } + // If set, ignore non-player ents that block us. Mainly of use in multiplayer to prevent exploits. + else if ( pOther && !pOther->IsPlayer() && m_bIgnoreNonPlayerEntsOnBlock ) + { + return; + } + + // If we're set to force ourselves closed, keep going + if ( m_bForceClosed ) + return; + + // if a door has a negative wait, it would never come back if blocked, + // so let it just squash the object to death real fast + if (m_flWait >= 0) + { + if (m_toggle_state == TS_GOING_DOWN) + { + DoorGoUp(); + } + else + { + DoorGoDown(); + } + } + + // Block all door pieces with the same targetname here. + if ( GetEntityName() != NULL_STRING ) + { + CBaseDoor *pDoorList[64]; + int doorCount = GetDoorMovementGroup( pDoorList, ARRAYSIZE(pDoorList) ); + + for ( int i = 0; i < doorCount; i++ ) + { + CBaseDoor *pDoor = pDoorList[i]; + + if ( pDoor->m_flWait >= 0) + { + if (m_bDoorGroup && pDoor->m_vecMoveDir == m_vecMoveDir && pDoor->GetAbsVelocity() == GetAbsVelocity() && pDoor->GetLocalAngularVelocity() == GetLocalAngularVelocity()) + { + pDoor->m_nSimulationTick = m_nSimulationTick; // don't run simulation this frame if you haven't run yet + + // this is the most hacked, evil, bastardized thing I've ever seen. kjb + if ( !pDoor->IsRotatingDoor() ) + {// set origin to realign normal doors + pDoor->SetLocalOrigin( GetLocalOrigin() ); + pDoor->SetAbsVelocity( vec3_origin );// stop! + + } + else + {// set angles to realign rotating doors + pDoor->SetLocalAngles( GetLocalAngles() ); + pDoor->SetLocalAngularVelocity( vec3_angle ); + } + } + + if ( pDoor->m_toggle_state == TS_GOING_DOWN) + pDoor->DoorGoUp(); + else + pDoor->DoorGoDown(); + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called the first frame that the door is unblocked while opening or closing. +//----------------------------------------------------------------------------- +void CBaseDoor::EndBlocked( void ) +{ + // + // Fire whatever events we need to due to our unblocked state. + // + if (m_toggle_state == TS_GOING_DOWN) + { + m_OnUnblockedClosing.FireOutput(this, this); + } + else + { + m_OnUnblockedOpening.FireOutput(this, this); + } +} + + +/*func_door_rotating + +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 usefull for touch or +takedamage doors). + +You need to have an origin brush as part of this entity. The +center of that brush will be +the point around which it is rotated. It will rotate around the Z +axis by default. You can +check either the X_AXIS or Y_AXIS box to change that. + +"distance" is how many degrees the door will be rotated. +"speed" determines how fast the door moves; default value is 100. + +REVERSE will cause the door to rotate in the opposite direction. + +"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) +"dmg" damage to inflict when blocked (2 default) +*/ + +//================================================== +// CRotDoor +//================================================== + +class CRotDoor : public CBaseDoor +{ +public: + DECLARE_CLASS( CRotDoor, CBaseDoor ); + + void Spawn( void ); + bool CreateVPhysics(); + // This is ONLY used by the node graph to test movement through a door + virtual void SetToggleState( int state ); + virtual bool IsRotatingDoor() { return true; } + + bool m_bSolidBsp; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( func_door_rotating, CRotDoor ); + +BEGIN_DATADESC( CRotDoor ) + DEFINE_KEYFIELD( m_bSolidBsp, FIELD_BOOLEAN, "solidbsp" ), +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRotDoor::Spawn( void ) +{ + BaseClass::Spawn(); + + // set the axis of rotation + CBaseToggle::AxisDir(); + + // check for clockwise rotation + if ( HasSpawnFlags(SF_DOOR_ROTATE_BACKWARDS) ) + m_vecMoveAng = m_vecMoveAng * -1; + + //m_flWait = 2; who the hell did this? (sjb) + m_vecAngle1 = GetLocalAngles(); + m_vecAngle2 = GetLocalAngles() + m_vecMoveAng * m_flMoveDistance; + + ASSERTSZ(m_vecAngle1 != m_vecAngle2, "rotating door start/end positions are equal\n"); + + // Starting open allows a func_door to be lighted in the closed position but + // spawn in the open position + // + // SF_DOOR_START_OPEN_OBSOLETE is an old broken way of spawning open that has + // been deprecated. + if ( HasSpawnFlags(SF_DOOR_START_OPEN_OBSOLETE) ) + { + // swap pos1 and pos2, put door at pos2, invert movement direction + QAngle vecNewAngles = m_vecAngle2; + m_vecAngle2 = m_vecAngle1; + m_vecAngle1 = vecNewAngles; + m_vecMoveAng = -m_vecMoveAng; + + // We've already had our physics setup in BaseClass::Spawn, so teleport to our + // current position. If we don't do this, our vphysics shadow will not update. + Teleport( NULL, &m_vecAngle1, NULL ); + + m_toggle_state = TS_AT_BOTTOM; + } + else if ( m_eSpawnPosition == FUNC_DOOR_SPAWN_OPEN ) + { + // We've already had our physics setup in BaseClass::Spawn, so teleport to our + // current position. If we don't do this, our vphysics shadow will not update. + Teleport( NULL, &m_vecAngle2, NULL ); + m_toggle_state = TS_AT_TOP; + } + else + { + m_toggle_state = TS_AT_BOTTOM; + } + +#ifdef HL1_DLL + SetSolid( SOLID_VPHYSICS ); +#endif + + // Slam the object back to solid - if we really want it to be solid. + if ( m_bSolidBsp ) + { + SetSolid( SOLID_BSP ); + } +} + +//----------------------------------------------------------------------------- + +bool CRotDoor::CreateVPhysics() +{ + if ( !IsSolidFlagSet( FSOLID_NOT_SOLID ) ) + { + VPhysicsInitShadow( false, false ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +// This is ONLY used by the node graph to test movement through a door +void CRotDoor::SetToggleState( int state ) +{ + if ( state == TS_AT_TOP ) + SetLocalAngles( m_vecAngle2 ); + else + SetLocalAngles( m_vecAngle1 ); +} diff --git a/sp/src/game/server/doors.h b/sp/src/game/server/doors.h new file mode 100644 index 00000000..7658482c --- /dev/null +++ b/sp/src/game/server/doors.h @@ -0,0 +1,162 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef DOORS_H +#define DOORS_H +#pragma once + + +#include "locksounds.h" +#include "entityoutput.h" + +//Since I'm here, might as well explain how these work. Base.fgd is the file that connects +//flags to entities. It is full of lines with this number, a label, and a default value. +//Voila, dynamicly generated checkboxes on the Flags tab of Entity Properties. + +// doors +#define SF_DOOR_ROTATE_YAW 0 // yaw by default +#define SF_DOOR_START_OPEN_OBSOLETE 1 +#define SF_DOOR_ROTATE_BACKWARDS 2 +#define SF_DOOR_NONSOLID_TO_PLAYER 4 +#define SF_DOOR_PASSABLE 8 +#define SF_DOOR_ONEWAY 16 +#define SF_DOOR_NO_AUTO_RETURN 32 +#define SF_DOOR_ROTATE_ROLL 64 +#define SF_DOOR_ROTATE_PITCH 128 +#define SF_DOOR_PUSE 256 // door can be opened by player's use button. +#define SF_DOOR_NONPCS 512 // NPC can't open +#define SF_DOOR_PTOUCH 1024 // player touch opens +#define SF_DOOR_LOCKED 2048 // Door is initially locked +#define SF_DOOR_SILENT 4096 // Door plays no audible sound, and does not alert NPCs when opened +#define SF_DOOR_USE_CLOSES 8192 // Door can be +used to close before its autoreturn delay has expired. +#define SF_DOOR_SILENT_TO_NPCS 16384 // Does not alert NPC's when opened. +#define SF_DOOR_IGNORE_USE 32768 // Completely ignores player +use commands. +#define SF_DOOR_NEW_USE_RULES 65536 // For func_door entities, behave more like prop_door_rotating with respect to +USE (changelist 242482) + + +enum FuncDoorSpawnPos_t +{ + FUNC_DOOR_SPAWN_CLOSED = 0, + FUNC_DOOR_SPAWN_OPEN, +}; + + +class CBaseDoor : public CBaseToggle +{ +public: + DECLARE_CLASS( CBaseDoor, CBaseToggle ); + + DECLARE_SERVERCLASS(); + + void Spawn( void ); + void Precache( void ); + bool CreateVPhysics(); + bool KeyValue( const char *szKeyName, const char *szValue ); + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + virtual void StartBlocked( CBaseEntity *pOther ); + virtual void Blocked( CBaseEntity *pOther ); + virtual void EndBlocked( void ); + + void Activate( void ); + + virtual int ObjectCaps( void ) + { + int flags = BaseClass::ObjectCaps(); + if ( HasSpawnFlags( SF_DOOR_PUSE ) ) + return flags | FCAP_IMPULSE_USE | FCAP_USE_IN_RADIUS; + + return flags; + }; + + DECLARE_DATADESC(); + + // This is ONLY used by the node graph to test movement through a door + void InputSetToggleState( inputdata_t &inputdata ); + virtual void SetToggleState( int state ); + + virtual bool IsRotatingDoor() { return false; } + virtual bool ShouldSavePhysics(); + // used to selectivly override defaults + void DoorTouch( CBaseEntity *pOther ); + + // local functions + int DoorActivate( ); + void DoorGoUp( void ); + void DoorGoDown( void ); + void DoorHitTop( void ); + void DoorHitBottom( void ); + void UpdateAreaPortals( bool isOpen ); + void Unlock( void ); + void Lock( void ); + int GetDoorMovementGroup( CBaseDoor *pDoorList[], int listMax ); + + // Input handlers + void InputClose( inputdata_t &inputdata ); + void InputLock( inputdata_t &inputdata ); + void InputOpen( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputUnlock( inputdata_t &inputdata ); + void InputSetSpeed( inputdata_t &inputdata ); + + Vector m_vecMoveDir; // The direction of motion for linear moving doors. + + locksound_t m_ls; // door lock sounds + + byte m_bLockedSentence; + byte m_bUnlockedSentence; + + bool m_bForceClosed; // If set, always close, even if we're blocked. + bool m_bDoorGroup; + bool m_bLocked; // Whether the door is locked + bool m_bIgnoreDebris; + bool m_bIgnoreNonPlayerEntsOnBlock; // Non-player entities should never block. This variable needs more letters. + + FuncDoorSpawnPos_t m_eSpawnPosition; + + float m_flBlockDamage; // Damage inflicted when blocked. + string_t m_NoiseMoving; //Start/Looping sound + string_t m_NoiseArrived; //End sound + string_t m_NoiseMovingClosed; //Start/Looping sound + string_t m_NoiseArrivedClosed; //End sound + string_t m_ChainTarget; ///< Entity name to pass Touch and Use events to + + CNetworkVar( float, m_flWaveHeight ); + + // Outputs + COutputEvent m_OnBlockedClosing; // Triggered when the door becomes blocked while closing. + COutputEvent m_OnBlockedOpening; // Triggered when the door becomes blocked while opening. + COutputEvent m_OnUnblockedClosing; // Triggered when the door becomes unblocked while closing. + COutputEvent m_OnUnblockedOpening; // Triggered when the door becomes unblocked while opening. + COutputEvent m_OnFullyClosed; // Triggered when the door reaches the fully closed position. + COutputEvent m_OnFullyOpen; // Triggered when the door reaches the fully open position. + COutputEvent m_OnClose; // Triggered when the door is told to close. + COutputEvent m_OnOpen; // Triggered when the door is told to open. + COutputEvent m_OnLockedUse; // Triggered when the user tries to open a locked door. + + void StartMovingSound( void ); + virtual void StopMovingSound( void ); + void MovingSoundThink( void ); +#ifdef HL1_DLL + bool PassesBlockTouchFilter(CBaseEntity *pOther); + string_t m_iBlockFilterName; + EHANDLE m_hBlockFilter; +#endif + + bool ShouldLoopMoveSound( void ) { return m_bLoopMoveSound; } + bool m_bLoopMoveSound; // Move sound loops until stopped + +private: + void ChainUse( void ); ///< Chains +use on through to m_ChainTarget + void ChainTouch( CBaseEntity *pOther ); ///< Chains touch on through to m_ChainTarget + void SetChaining( bool chaining ) { m_isChaining = chaining; } ///< Latch to prevent recursion + bool m_isChaining; + + void CloseAreaPortalsThink( void ); ///< Delays turning off area portals when closing doors to prevent visual artifacts +}; + +#endif // DOORS_H diff --git a/sp/src/game/server/dynamiclight.cpp b/sp/src/game/server/dynamiclight.cpp new file mode 100644 index 00000000..9c14c466 --- /dev/null +++ b/sp/src/game/server/dynamiclight.cpp @@ -0,0 +1,203 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Dynamic light. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "dlight.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +#define NUM_DL_EXPONENT_BITS 8 +#define MIN_DL_EXPONENT_VALUE -((1 << (NUM_DL_EXPONENT_BITS-1)) - 1) +#define MAX_DL_EXPONENT_VALUE ((1 << (NUM_DL_EXPONENT_BITS-1)) - 1) + + +class CDynamicLight : public CBaseEntity +{ +public: + DECLARE_CLASS( CDynamicLight, CBaseEntity ); + + void Spawn( void ); + void DynamicLightThink( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + // Turn on and off the light + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + +public: + unsigned char m_ActualFlags; + CNetworkVar( unsigned char, m_Flags ); + CNetworkVar( unsigned char, m_LightStyle ); + bool m_On; + CNetworkVar( float, m_Radius ); + CNetworkVar( int, m_Exponent ); + CNetworkVar( float, m_InnerAngle ); + CNetworkVar( float, m_OuterAngle ); + CNetworkVar( float, m_SpotRadius ); +}; + +LINK_ENTITY_TO_CLASS(light_dynamic, CDynamicLight); + +BEGIN_DATADESC( CDynamicLight ) + + DEFINE_FIELD( m_ActualFlags, FIELD_CHARACTER ), + DEFINE_FIELD( m_Flags, FIELD_CHARACTER ), + DEFINE_FIELD( m_On, FIELD_BOOLEAN ), + + DEFINE_THINKFUNC( DynamicLightThink ), + + // Inputs + DEFINE_INPUT( m_Radius, FIELD_FLOAT, "distance" ), + DEFINE_INPUT( m_Exponent, FIELD_INTEGER, "brightness" ), + DEFINE_INPUT( m_InnerAngle, FIELD_FLOAT, "_inner_cone" ), + DEFINE_INPUT( m_OuterAngle, FIELD_FLOAT, "_cone" ), + DEFINE_INPUT( m_SpotRadius, FIELD_FLOAT, "spotlight_radius" ), + DEFINE_INPUT( m_LightStyle, FIELD_CHARACTER,"style" ), + + // Input functions + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST(CDynamicLight, DT_DynamicLight) + SendPropInt( SENDINFO(m_Flags), 4, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_LightStyle), 4, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_Radius), 0, SPROP_NOSCALE), + SendPropInt( SENDINFO(m_Exponent), NUM_DL_EXPONENT_BITS), + SendPropFloat( SENDINFO(m_InnerAngle), 8, 0, 0.0, 360.0f ), + SendPropFloat( SENDINFO(m_OuterAngle), 8, 0, 0.0, 360.0f ), + SendPropFloat( SENDINFO(m_SpotRadius), 0, SPROP_NOSCALE), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CDynamicLight::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "_light" ) ) + { + color32 tmp; + UTIL_StringToColor32( &tmp, szValue ); + SetRenderColor( tmp.r, tmp.g, tmp.b ); + } + else if ( FStrEq( szKeyName, "pitch" ) ) + { + float angle = atof(szValue); + if ( angle ) + { + QAngle angles = GetAbsAngles(); + angles[PITCH] = -angle; + SetAbsAngles( angles ); + } + } + else if ( FStrEq( szKeyName, "spawnflags" ) ) + { + m_ActualFlags = m_Flags = atoi(szValue); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +//------------------------------------------------------------------------------ +// Turn on and off the light +//------------------------------------------------------------------------------ +void CDynamicLight::InputTurnOn( inputdata_t &inputdata ) +{ + m_Flags = m_ActualFlags; + m_On = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CDynamicLight::InputTurnOff( inputdata_t &inputdata ) +{ + // This basically shuts it off + m_Flags = DLIGHT_NO_MODEL_ILLUMINATION | DLIGHT_NO_WORLD_ILLUMINATION; + m_On = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CDynamicLight::InputToggle( inputdata_t &inputdata ) +{ + if (m_On) + { + InputTurnOff( inputdata ); + } + else + { + InputTurnOn( inputdata ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CDynamicLight::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); + m_On = true; + UTIL_SetSize( this, vec3_origin, vec3_origin ); + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + + // If we have a target, think so we can orient towards it + if ( m_target != NULL_STRING ) + { + SetThink( &CDynamicLight::DynamicLightThink ); + SetNextThink( gpGlobals->curtime + 0.1 ); + } + + int clampedExponent = clamp( (int) m_Exponent, MIN_DL_EXPONENT_VALUE, MAX_DL_EXPONENT_VALUE ); + if ( m_Exponent != clampedExponent ) + { + Warning( "light_dynamic at [%d %d %d] has invalid exponent value (%d must be between %d and %d).\n", + (int)GetAbsOrigin().x, (int)GetAbsOrigin().x, (int)GetAbsOrigin().x, + m_Exponent.Get(), + MIN_DL_EXPONENT_VALUE, + MAX_DL_EXPONENT_VALUE ); + + m_Exponent = clampedExponent; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDynamicLight::DynamicLightThink( void ) +{ + if ( m_target == NULL_STRING ) + return; + + CBaseEntity *pEntity = GetNextTarget(); + if ( pEntity ) + { + Vector vecToTarget = (pEntity->GetAbsOrigin() - GetAbsOrigin()); + QAngle vecAngles; + VectorAngles( vecToTarget, vecAngles ); + SetAbsAngles( vecAngles ); + } + + SetNextThink( gpGlobals->curtime + 0.1 ); +} diff --git a/sp/src/game/server/effects.cpp b/sp/src/game/server/effects.cpp new file mode 100644 index 00000000..7b07e157 --- /dev/null +++ b/sp/src/game/server/effects.cpp @@ -0,0 +1,2628 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements a grab bag of visual effects entities. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "effects.h" +#include "gib.h" +#include "beam_shared.h" +#include "decals.h" +#include "func_break.h" +#include "EntityFlame.h" +#include "entitylist.h" +#include "basecombatweapon.h" +#include "model_types.h" +#include "player.h" +#include "physics.h" +#include "baseparticleentity.h" +#include "ndebugoverlay.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "env_wind_shared.h" +#include "filesystem.h" +#include "engine/IEngineSound.h" +#include "fire.h" +#include "te_effect_dispatch.h" +#include "Sprite.h" +#include "precipitation_shared.h" +#include "shot_manipulator.h" +#ifdef MAPBASE +#include "point_template.h" +#include "TemplateEntities.h" +#include "mapentities_shared.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_FUNNEL_REVERSE 1 // funnel effect repels particles instead of attracting them. +#ifdef MAPBASE +#define SF_FUNNEL_DONT_REMOVE 2 +#endif + +#define SF_GIBSHOOTER_REPEATABLE (1<<0) // allows a gibshooter to be refired +#define SF_SHOOTER_FLAMING (1<<1) // gib is on fire +#define SF_SHOOTER_STRICT_REMOVE (1<<2) // remove this gib even if it is in the player's view + +// UNDONE: This should be client-side and not use TempEnts +class CBubbling : public CBaseEntity +{ +public: + DECLARE_CLASS( CBubbling, CBaseEntity ); + + virtual void Spawn( void ); + virtual void Precache( void ); + + void FizzThink( void ); + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + void InputDeactivate( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + void InputSetCurrent( inputdata_t &inputdata ); + void InputSetDensity( inputdata_t &inputdata ); + void InputSetFrequency( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + + void TurnOn(); + void TurnOff(); + void Toggle(); + + int m_density; + int m_frequency; + int m_bubbleModel; + int m_state; +}; + +LINK_ENTITY_TO_CLASS( env_bubbles, CBubbling ); + +BEGIN_DATADESC( CBubbling ) + + DEFINE_KEYFIELD( m_flSpeed, FIELD_FLOAT, "current" ), + DEFINE_KEYFIELD( m_density, FIELD_INTEGER, "density" ), + DEFINE_KEYFIELD( m_frequency, FIELD_INTEGER, "frequency" ), + + DEFINE_FIELD( m_state, FIELD_INTEGER ), + // Let spawn restore this! + // DEFINE_FIELD( m_bubbleModel, FIELD_INTEGER ), + + // Function Pointers + DEFINE_FUNCTION( FizzThink ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetCurrent", InputSetCurrent ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetDensity", InputSetDensity ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetFrequency", InputSetFrequency ), + +END_DATADESC() + + + +#define SF_BUBBLES_STARTOFF 0x0001 + +void CBubbling::Spawn( void ) +{ + Precache( ); + SetModel( STRING( GetModelName() ) ); // Set size + + // Make it invisible to client + SetRenderColorA( 0 ); + + SetSolid( SOLID_NONE ); // Remove model & collisions + + if ( !HasSpawnFlags(SF_BUBBLES_STARTOFF) ) + { + SetThink( &CBubbling::FizzThink ); + SetNextThink( gpGlobals->curtime + 2.0 ); + m_state = 1; + } + else + { + m_state = 0; + } +} + +void CBubbling::Precache( void ) +{ + m_bubbleModel = PrecacheModel("sprites/bubble.vmt"); // Precache bubble sprite +} + + +void CBubbling::Toggle() +{ + if (!m_state) + { + TurnOn(); + } + else + { + TurnOff(); + } +} + + +void CBubbling::TurnOn() +{ + m_state = 1; + SetThink( &CBubbling::FizzThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CBubbling::TurnOff() +{ + m_state = 0; + SetThink( NULL ); + SetNextThink( TICK_NEVER_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBubbling::InputActivate( inputdata_t &inputdata ) +{ + TurnOn(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBubbling::InputDeactivate( inputdata_t &inputdata ) +{ + TurnOff(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBubbling::InputToggle( inputdata_t &inputdata ) +{ + Toggle(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBubbling::InputSetCurrent( inputdata_t &inputdata ) +{ + m_flSpeed = (float)inputdata.value.Int(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBubbling::InputSetDensity( inputdata_t &inputdata ) +{ + m_density = inputdata.value.Int(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBubbling::InputSetFrequency( inputdata_t &inputdata ) +{ + m_frequency = inputdata.value.Int(); + + // Reset think time + if ( m_state ) + { + if ( m_frequency > 19 ) + { + SetNextThink( gpGlobals->curtime + 0.5f ); + } + else + { + SetNextThink( gpGlobals->curtime + 2.5 - (0.1 * m_frequency) ); + } + } +} + +void CBubbling::FizzThink( void ) +{ + Vector center = WorldSpaceCenter(); + CPASFilter filter( center ); + te->Fizz( filter, 0.0, this, m_bubbleModel, m_density, (int)m_flSpeed ); + + if ( m_frequency > 19 ) + { + SetNextThink( gpGlobals->curtime + 0.5f ); + } + else + { + SetNextThink( gpGlobals->curtime + 2.5 - (0.1 * m_frequency) ); + } +} + + +// ENV_TRACER +// Fakes a tracer +class CEnvTracer : public CPointEntity +{ +public: + DECLARE_CLASS( CEnvTracer, CPointEntity ); + + void Spawn( void ); + void TracerThink( void ); + void Activate( void ); + + DECLARE_DATADESC(); + + Vector m_vecEnd; + float m_flDelay; +}; + +LINK_ENTITY_TO_CLASS( env_tracer, CEnvTracer ); + +BEGIN_DATADESC( CEnvTracer ) + + DEFINE_KEYFIELD( m_flDelay, FIELD_FLOAT, "delay" ), + + DEFINE_FIELD( m_vecEnd, FIELD_POSITION_VECTOR ), + + // Function Pointers + DEFINE_FUNCTION( TracerThink ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Called after keyvalues are parsed. +//----------------------------------------------------------------------------- +void CEnvTracer::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + if (!m_flDelay) + m_flDelay = 1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Called after all the entities have been loaded. +//----------------------------------------------------------------------------- +void CEnvTracer::Activate( void ) +{ + BaseClass::Activate(); + + CBaseEntity *pEnd = gEntList.FindEntityByName( NULL, m_target ); + if (pEnd != NULL) + { + m_vecEnd = pEnd->GetLocalOrigin(); + SetThink( &CEnvTracer::TracerThink ); + SetNextThink( gpGlobals->curtime + m_flDelay ); + } + else + { + Msg( "env_tracer: unknown entity \"%s\"\n", STRING(m_target) ); + } +} + +// Think +void CEnvTracer::TracerThink( void ) +{ + UTIL_Tracer( GetAbsOrigin(), m_vecEnd ); + + SetNextThink( gpGlobals->curtime + m_flDelay ); +} + + +//################################################################################# +// >> CGibShooter +//################################################################################# +enum GibSimulation_t +{ + GIB_SIMULATE_POINT, + GIB_SIMULATE_PHYSICS, + GIB_SIMULATE_RAGDOLL, +}; + +class CGibShooter : public CBaseEntity +{ +public: + DECLARE_CLASS( CGibShooter, CBaseEntity ); + + void Spawn( void ); + void Precache( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + virtual CGib *CreateGib( void ); + +protected: + // Purpose: + CBaseEntity *SpawnGib( const Vector &vecShootDir, float flSpeed ); + + DECLARE_DATADESC(); +private: + void InitPointGib( CGib *pGib, const Vector &vecShootDir, float flSpeed ); + void ShootThink( void ); + +protected: + int m_iGibs; + int m_iGibCapacity; + int m_iGibMaterial; + int m_iGibModelIndex; + float m_flGibVelocity; + QAngle m_angGibRotation; + float m_flGibAngVelocity; + float m_flVariance; + float m_flGibLife; + int m_nSimulationType; + int m_nMaxGibModelFrame; + float m_flDelay; + + bool m_bNoGibShadows; + + bool m_bIsSprite; + string_t m_iszLightingOrigin; + + // ---------------- + // Inputs + // ---------------- + void InputShoot( inputdata_t &inputdata ); +}; + +BEGIN_DATADESC( CGibShooter ) + + DEFINE_KEYFIELD( m_iGibs, FIELD_INTEGER, "m_iGibs" ), + DEFINE_KEYFIELD( m_flGibVelocity, FIELD_FLOAT, "m_flVelocity" ), + DEFINE_KEYFIELD( m_flVariance, FIELD_FLOAT, "m_flVariance" ), + DEFINE_KEYFIELD( m_flGibLife, FIELD_FLOAT, "m_flGibLife" ), + DEFINE_KEYFIELD( m_nSimulationType, FIELD_INTEGER, "Simulation" ), + DEFINE_KEYFIELD( m_flDelay, FIELD_FLOAT, "delay" ), + DEFINE_KEYFIELD( m_angGibRotation, FIELD_VECTOR, "gibangles" ), + DEFINE_KEYFIELD( m_flGibAngVelocity, FIELD_FLOAT, "gibanglevelocity"), + DEFINE_FIELD( m_bIsSprite, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_iGibCapacity, FIELD_INTEGER ), + DEFINE_FIELD( m_iGibMaterial, FIELD_INTEGER ), + DEFINE_FIELD( m_iGibModelIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_nMaxGibModelFrame, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_iszLightingOrigin, FIELD_STRING, "LightingOrigin" ), + DEFINE_KEYFIELD( m_bNoGibShadows, FIELD_BOOLEAN, "nogibshadows" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Shoot", InputShoot ), + + // Function Pointers + DEFINE_FUNCTION( ShootThink ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( gibshooter, CGibShooter ); + + +void CGibShooter::Precache ( void ) +{ + if ( g_Language.GetInt() == LANGUAGE_GERMAN ) + { + m_iGibModelIndex = PrecacheModel ("models/germanygibs.mdl"); + } + else + { + m_iGibModelIndex = PrecacheModel ("models/gibs/hgibs.mdl"); + } +} + + +void CGibShooter::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + SetThink( &CGibShooter::ShootThink ); + SetNextThink( gpGlobals->curtime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for shooting gibs. +//----------------------------------------------------------------------------- +void CGibShooter::InputShoot( inputdata_t &inputdata ) +{ + SetThink( &CGibShooter::ShootThink ); + SetNextThink( gpGlobals->curtime ); +} + + +void CGibShooter::Spawn( void ) +{ + Precache(); + + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); + + if ( m_flDelay < 0 ) + { + m_flDelay = 0.0; + } + + if ( m_flGibLife == 0 ) + { + m_flGibLife = 25; + } + + m_iGibCapacity = m_iGibs; + + m_nMaxGibModelFrame = modelinfo->GetModelFrameCount( modelinfo->GetModel( m_iGibModelIndex ) ); +} + +CGib *CGibShooter::CreateGib ( void ) +{ + ConVarRef violence_hgibs( "violence_hgibs" ); + if ( violence_hgibs.IsValid() && !violence_hgibs.GetInt() ) + return NULL; + + CGib *pGib = CREATE_ENTITY( CGib, "gib" ); + pGib->Spawn( "models/gibs/hgibs.mdl" ); + pGib->SetBloodColor( BLOOD_COLOR_RED ); + + if ( m_nMaxGibModelFrame <= 1 ) + { + DevWarning( 2, "GibShooter Body is <= 1!\n" ); + } + + pGib->m_nBody = random->RandomInt ( 1, m_nMaxGibModelFrame - 1 );// avoid throwing random amounts of the 0th gib. (skull). + + if ( m_iszLightingOrigin != NULL_STRING ) + { + // Make the gibs use the lighting origin + pGib->SetLightingOrigin( m_iszLightingOrigin ); + } + + return pGib; +} + + +void CGibShooter::InitPointGib( CGib *pGib, const Vector &vecShootDir, float flSpeed ) +{ + if ( pGib ) + { + pGib->SetLocalOrigin( GetAbsOrigin() ); + pGib->SetAbsVelocity( vecShootDir * flSpeed ); + + QAngle angVel( random->RandomFloat ( 100, 200 ), random->RandomFloat ( 100, 300 ), 0 ); + pGib->SetLocalAngularVelocity( angVel ); + + float thinkTime = ( pGib->GetNextThink() - gpGlobals->curtime ); + + pGib->m_lifeTime = (m_flGibLife * random->RandomFloat( 0.95, 1.05 )); // +/- 5% + + // HL1 gibs always die after a certain time, other games have to opt-in +#ifndef HL1_DLL + if( HasSpawnFlags( SF_SHOOTER_STRICT_REMOVE ) ) +#endif + { + pGib->SetNextThink( gpGlobals->curtime + pGib->m_lifeTime ); + pGib->SetThink ( &CGib::DieThink ); + } + + if ( pGib->m_lifeTime < thinkTime ) + { + pGib->SetNextThink( gpGlobals->curtime + pGib->m_lifeTime ); + pGib->m_lifeTime = 0; + } + + if ( m_bIsSprite == true ) + { + pGib->SetSprite( CSprite::SpriteCreate( STRING( GetModelName() ), pGib->GetAbsOrigin(), false ) ); + + CSprite *pSprite = (CSprite*)pGib->GetSprite(); + + if ( pSprite ) + { + pSprite->SetAttachment( pGib, 0 ); + pSprite->SetOwnerEntity( pGib ); + + pSprite->SetScale( 1 ); + pSprite->SetTransparency( m_nRenderMode, m_clrRender->r, m_clrRender->g, m_clrRender->b, m_clrRender->a, m_nRenderFX ); + pSprite->AnimateForTime( 5, m_flGibLife + 1 ); //This framerate is totally wrong + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CGibShooter::SpawnGib( const Vector &vecShootDir, float flSpeed ) +{ + switch (m_nSimulationType) + { + case GIB_SIMULATE_RAGDOLL: + { + // UNDONE: Assume a mass of 200 for now + Vector force = vecShootDir * flSpeed * 200; +#ifdef MAPBASE + return CreateRagGib( STRING( GetModelName() ), GetAbsOrigin(), GetAbsAngles(), force, m_flGibLife, HasSpawnFlags(SF_SHOOTER_FLAMING) ); +#else + return CreateRagGib( STRING( GetModelName() ), GetAbsOrigin(), GetAbsAngles(), force, m_flGibLife ); +#endif + } + + case GIB_SIMULATE_PHYSICS: + { + CGib *pGib = CreateGib(); + + if ( pGib ) + { + pGib->SetAbsOrigin( GetAbsOrigin() ); + pGib->SetAbsAngles( m_angGibRotation ); + + pGib->m_lifeTime = (m_flGibLife * random->RandomFloat( 0.95, 1.05 )); // +/- 5% + + pGib->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + IPhysicsObject *pPhysicsObject = pGib->VPhysicsInitNormal( SOLID_VPHYSICS, pGib->GetSolidFlags(), false ); + pGib->SetMoveType( MOVETYPE_VPHYSICS ); + + if ( pPhysicsObject ) + { + // Set gib velocity + Vector vVel = vecShootDir * flSpeed; + pPhysicsObject->AddVelocity(&vVel, NULL); + + AngularImpulse torque; + torque.x = m_flGibAngVelocity * random->RandomFloat( 0.1f, 1.0f ); + torque.y = m_flGibAngVelocity * random->RandomFloat( 0.1f, 1.0f ); + torque.z = 0.0f; + torque *= pPhysicsObject->GetMass(); + + pPhysicsObject->ApplyTorqueCenter( torque ); + +#ifndef HL1_DLL + if( HasSpawnFlags( SF_SHOOTER_STRICT_REMOVE ) ) +#endif + { + pGib->m_bForceRemove = true; + pGib->SetNextThink( gpGlobals->curtime + pGib->m_lifeTime ); + pGib->SetThink ( &CGib::DieThink ); + } + + } + else + { + InitPointGib( pGib, vecShootDir, flSpeed ); + } + } + return pGib; + } + + case GIB_SIMULATE_POINT: + { + CGib *pGib = CreateGib(); + + if ( pGib ) + { + pGib->SetAbsAngles( m_angGibRotation ); + + InitPointGib( pGib, vecShootDir, flSpeed ); + return pGib; + } + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGibShooter::ShootThink ( void ) +{ + SetNextThink( gpGlobals->curtime + m_flDelay ); + + Vector vecShootDir, vForward,vRight,vUp; + AngleVectors( GetAbsAngles(), &vForward, &vRight, &vUp ); + vecShootDir = vForward; + vecShootDir = vecShootDir + vRight * random->RandomFloat( -1, 1) * m_flVariance; + vecShootDir = vecShootDir + vForward * random->RandomFloat( -1, 1) * m_flVariance; + vecShootDir = vecShootDir + vUp * random->RandomFloat( -1, 1) * m_flVariance; + + VectorNormalize( vecShootDir ); + + SpawnGib( vecShootDir, m_flGibVelocity ); + + if ( --m_iGibs <= 0 ) + { + if ( HasSpawnFlags(SF_GIBSHOOTER_REPEATABLE) ) + { + m_iGibs = m_iGibCapacity; + SetThink ( NULL ); + SetNextThink( gpGlobals->curtime ); + } + else + { + SetThink ( &CGibShooter::SUB_Remove ); + SetNextThink( gpGlobals->curtime ); + } + } +} + +class CEnvShooter : public CGibShooter +{ +public: + DECLARE_CLASS( CEnvShooter, CGibShooter ); + + CEnvShooter() { m_flGibGravityScale = 1.0f; } + void Precache( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + CGib *CreateGib( void ); + + DECLARE_DATADESC(); + +public: + + int m_nSkin; + float m_flGibScale; + float m_flGibGravityScale; + +#if HL2_EPISODIC + float m_flMassOverride; // allow designer to force a mass for gibs in some cases +#endif +}; + +BEGIN_DATADESC( CEnvShooter ) + + DEFINE_KEYFIELD( m_nSkin, FIELD_INTEGER, "skin" ), + DEFINE_KEYFIELD( m_flGibScale, FIELD_FLOAT ,"scale" ), + DEFINE_KEYFIELD( m_flGibGravityScale, FIELD_FLOAT, "gibgravityscale" ), + +#if HL2_EPISODIC + DEFINE_KEYFIELD( m_flMassOverride, FIELD_FLOAT, "massoverride" ), +#endif + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( env_shooter, CEnvShooter ); + +bool CEnvShooter::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "shootmodel")) + { + m_bIsSprite = false; + SetModelName( AllocPooledString(szValue) ); + + //Adrian - not pretty... + if ( Q_stristr( szValue, ".vmt" ) ) + m_bIsSprite = true; + } + + else if (FStrEq(szKeyName, "shootsounds")) + { + int iNoise = atoi(szValue); + switch( iNoise ) + { + case 0: + m_iGibMaterial = matGlass; + break; + case 1: + m_iGibMaterial = matWood; + break; + case 2: + m_iGibMaterial = matMetal; + break; + case 3: + m_iGibMaterial = matFlesh; + break; + case 4: + m_iGibMaterial = matRocks; + break; + + default: + case -1: + m_iGibMaterial = matNone; + break; + } + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +void CEnvShooter::Precache ( void ) +{ + m_iGibModelIndex = PrecacheModel( STRING( GetModelName() ) ); +} + + +CGib *CEnvShooter::CreateGib ( void ) +{ + CGib *pGib = CREATE_ENTITY( CGib, "gib" ); + + if ( m_bIsSprite == true ) + { + //HACK HACK + pGib->Spawn( "" ); + } + else + { + pGib->Spawn( STRING( GetModelName() ) ); + } + + int bodyPart = 0; + + if ( m_nMaxGibModelFrame > 1 ) + { + bodyPart = random->RandomInt( 0, m_nMaxGibModelFrame-1 ); + } + + pGib->m_nBody = bodyPart; + pGib->SetBloodColor( DONT_BLEED ); + pGib->m_material = m_iGibMaterial; + + pGib->m_nRenderMode = m_nRenderMode; + pGib->m_clrRender = m_clrRender; + pGib->m_nRenderFX = m_nRenderFX; + pGib->m_nSkin = m_nSkin; + pGib->m_lifeTime = gpGlobals->curtime + m_flGibLife; + + pGib->SetGravity( m_flGibGravityScale ); + + // Spawn a flaming gib + if ( HasSpawnFlags( SF_SHOOTER_FLAMING ) ) + { + // Tag an entity flame along with us + CEntityFlame *pFlame = CEntityFlame::Create( pGib, false ); + if ( pFlame != NULL ) + { + pFlame->SetLifetime( pGib->m_lifeTime ); + pGib->SetFlame( pFlame ); + } + } + + if ( m_iszLightingOrigin != NULL_STRING ) + { + // Make the gibs use the lighting origin + pGib->SetLightingOrigin( m_iszLightingOrigin ); + } + + if( m_bNoGibShadows ) + { + pGib->AddEffects( EF_NOSHADOW ); + } + +#if HL2_EPISODIC + // if a mass override is set, apply it to the gib + if (m_flMassOverride != 0) + { + IPhysicsObject *pPhys = pGib->VPhysicsGetObject(); + if (pPhys) + { + pPhys->SetMass( m_flMassOverride ); + } + } +#endif + + return pGib; +} + + +//----------------------------------------------------------------------------- +// An entity that shoots out junk when hit by a rotor wash +//----------------------------------------------------------------------------- +class CRotorWashShooter : public CEnvShooter, public IRotorWashShooter +{ +public: + DECLARE_CLASS( CRotorWashShooter, CEnvShooter ); + DECLARE_DATADESC(); + + virtual void Spawn(); + +public: + // Inherited from IRotorWashShooter + virtual CBaseEntity *DoWashPush( float flTimeSincePushStarted, const Vector &vecForce ); + +private: + // Amount of time we need to spend under the rotor before we shoot + float m_flTimeUnderRotor; + float m_flTimeUnderRotorVariance; + + // Last time we were hit with a wash... + float m_flLastWashStartTime; + float m_flNextGibTime; +}; + + +LINK_ENTITY_TO_CLASS( env_rotorshooter, CRotorWashShooter ); + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CRotorWashShooter ) + + DEFINE_KEYFIELD( m_flTimeUnderRotor, FIELD_FLOAT ,"rotortime" ), + DEFINE_KEYFIELD( m_flTimeUnderRotorVariance, FIELD_FLOAT ,"rotortimevariance" ), + DEFINE_FIELD( m_flLastWashStartTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextGibTime, FIELD_TIME ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Gets at the interface if the entity supports it +//----------------------------------------------------------------------------- +IRotorWashShooter *GetRotorWashShooter( CBaseEntity *pEntity ) +{ + CRotorWashShooter *pShooter = dynamic_cast(pEntity); + return pShooter; +} + + +//----------------------------------------------------------------------------- +// Inherited from IRotorWashShooter +//----------------------------------------------------------------------------- +void CRotorWashShooter::Spawn() +{ + BaseClass::Spawn(); + m_flLastWashStartTime = -1; +} + + + +//----------------------------------------------------------------------------- +// Inherited from IRotorWashShooter +//----------------------------------------------------------------------------- +CBaseEntity *CRotorWashShooter::DoWashPush( float flWashStartTime, const Vector &vecForce ) +{ + if ( flWashStartTime == m_flLastWashStartTime ) + { + if ( m_flNextGibTime > gpGlobals->curtime ) + return NULL; + } + + m_flLastWashStartTime = flWashStartTime; + m_flNextGibTime = gpGlobals->curtime + m_flTimeUnderRotor + random->RandomFloat( -1, 1) * m_flTimeUnderRotorVariance; + if ( m_flNextGibTime <= gpGlobals->curtime ) + { + m_flNextGibTime = gpGlobals->curtime + 0.01f; + } + + // Set the velocity to be what the force would cause it to accelerate to + // after one tick + Vector vecShootDir = vecForce; + VectorNormalize( vecShootDir ); + + vecShootDir.x += random->RandomFloat( -1, 1 ) * m_flVariance; + vecShootDir.y += random->RandomFloat( -1, 1 ) * m_flVariance; + vecShootDir.z += random->RandomFloat( -1, 1 ) * m_flVariance; + + VectorNormalize( vecShootDir ); + + CBaseEntity *pGib = SpawnGib( vecShootDir, m_flGibVelocity /*flLength*/ ); + + if ( --m_iGibs <= 0 ) + { + if ( HasSpawnFlags(SF_GIBSHOOTER_REPEATABLE) ) + { + m_iGibs = m_iGibCapacity; + } + else + { + SetThink ( &CGibShooter::SUB_Remove ); + SetNextThink( gpGlobals->curtime ); + } + } + + return pGib; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CTestEffect : public CBaseEntity +{ +public: + DECLARE_CLASS( CTestEffect, CBaseEntity ); + + void Spawn( void ); + void Precache( void ); + // bool KeyValue( const char *szKeyName, const char *szValue ); + void Think( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + int m_iLoop; + int m_iBeam; + CBeam *m_pBeam[24]; + float m_flBeamTime[24]; + float m_flStartTime; +}; + + +LINK_ENTITY_TO_CLASS( test_effect, CTestEffect ); + +void CTestEffect::Spawn( void ) +{ + Precache( ); +} + +void CTestEffect::Precache( void ) +{ + PrecacheModel( "sprites/lgtning.vmt" ); +} + +void CTestEffect::Think( void ) +{ + int i; + float t = (gpGlobals->curtime - m_flStartTime); + + if (m_iBeam < 24) + { + CBeam *pbeam = CBeam::BeamCreate( "sprites/lgtning.vmt", 10 ); + + trace_t tr; + + Vector vecSrc = GetAbsOrigin(); + Vector vecDir = Vector( random->RandomFloat( -1.0, 1.0 ), random->RandomFloat( -1.0, 1.0 ),random->RandomFloat( -1.0, 1.0 ) ); + VectorNormalize( vecDir ); + UTIL_TraceLine( vecSrc, vecSrc + vecDir * 128, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + pbeam->PointsInit( vecSrc, tr.endpos ); + // pbeam->SetColor( 80, 100, 255 ); + pbeam->SetColor( 255, 180, 100 ); + pbeam->SetWidth( 10.0 ); + pbeam->SetScrollRate( 12 ); + + m_flBeamTime[m_iBeam] = gpGlobals->curtime; + m_pBeam[m_iBeam] = pbeam; + m_iBeam++; + +#if 0 + Vector vecMid = (vecSrc + tr.endpos) * 0.5; + CBroadcastRecipientFilter filter; + TE_DynamicLight( filter, 0.0, + vecMid, 255, 180, 100, 3, 2.0, 0.0 ); +#endif + } + + if (t < 3.0) + { + for (i = 0; i < m_iBeam; i++) + { + t = (gpGlobals->curtime - m_flBeamTime[i]) / ( 3 + m_flStartTime - m_flBeamTime[i]); + m_pBeam[i]->SetBrightness( 255 * t ); + // m_pBeam[i]->SetScrollRate( 20 * t ); + } + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else + { + for (i = 0; i < m_iBeam; i++) + { + UTIL_Remove( m_pBeam[i] ); + } + m_flStartTime = gpGlobals->curtime; + m_iBeam = 0; + SetNextThink( TICK_NEVER_THINK ); + } +} + + +void CTestEffect::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + m_flStartTime = gpGlobals->curtime; +} + + + +// Blood effects +class CBlood : public CPointEntity +{ +public: + DECLARE_CLASS( CBlood, CPointEntity ); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + inline int Color( void ) { return m_Color; } + inline float BloodAmount( void ) { return m_flAmount; } + + inline void SetColor( int color ) { m_Color = color; } + + // Input handlers + void InputEmitBlood( inputdata_t &inputdata ); + + Vector Direction( void ); + Vector BloodPosition( CBaseEntity *pActivator ); + + DECLARE_DATADESC(); + + Vector m_vecSprayDir; + float m_flAmount; + int m_Color; + +private: +}; + +LINK_ENTITY_TO_CLASS( env_blood, CBlood ); + +BEGIN_DATADESC( CBlood ) + + DEFINE_KEYFIELD( m_vecSprayDir, FIELD_VECTOR, "spraydir" ), + DEFINE_KEYFIELD( m_flAmount, FIELD_FLOAT, "amount" ), + DEFINE_FIELD( m_Color, FIELD_INTEGER ), + + DEFINE_INPUTFUNC( FIELD_VOID, "EmitBlood", InputEmitBlood ), + +END_DATADESC() + + +#define SF_BLOOD_RANDOM 0x0001 +#define SF_BLOOD_STREAM 0x0002 +#define SF_BLOOD_PLAYER 0x0004 +#define SF_BLOOD_DECAL 0x0008 +#define SF_BLOOD_CLOUD 0x0010 +#define SF_BLOOD_DROPS 0x0020 +#define SF_BLOOD_GORE 0x0040 + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBlood::Spawn( void ) +{ + // Convert spraydir from angles to a vector + QAngle angSprayDir = QAngle( m_vecSprayDir.x, m_vecSprayDir.y, m_vecSprayDir.z ); + AngleVectors( angSprayDir, &m_vecSprayDir ); + + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + SetColor( BLOOD_COLOR_RED ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : szKeyName - +// szValue - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBlood::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "color")) + { + int color = atoi(szValue); + switch ( color ) + { + case 1: + { + SetColor( BLOOD_COLOR_YELLOW ); + break; + } + } + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +Vector CBlood::Direction( void ) +{ + if ( HasSpawnFlags( SF_BLOOD_RANDOM ) ) + return UTIL_RandomBloodVector(); + + return m_vecSprayDir; +} + + +Vector CBlood::BloodPosition( CBaseEntity *pActivator ) +{ + if ( HasSpawnFlags( SF_BLOOD_PLAYER ) ) + { + CBasePlayer *player; + + if ( pActivator && pActivator->IsPlayer() ) + { + player = ToBasePlayer( pActivator ); + } + else + { + player = UTIL_GetLocalPlayer(); + } + + if ( player ) + { + return (player->EyePosition()) + Vector( random->RandomFloat(-10,10), random->RandomFloat(-10,10), random->RandomFloat(-10,10) ); + } + } + + return GetLocalOrigin(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void UTIL_BloodSpray( const Vector &pos, const Vector &dir, int color, int amount, int flags ) +{ + if( color == DONT_BLEED ) + return; + + CEffectData data; + + data.m_vOrigin = pos; + data.m_vNormal = dir; + data.m_flScale = (float)amount; + data.m_fFlags = flags; + data.m_nColor = color; + + DispatchEffect( "bloodspray", data ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for triggering the blood effect. +//----------------------------------------------------------------------------- +void CBlood::InputEmitBlood( inputdata_t &inputdata ) +{ + if ( HasSpawnFlags( SF_BLOOD_STREAM ) ) + { + UTIL_BloodStream( BloodPosition(inputdata.pActivator), Direction(), Color(), BloodAmount() ); + } + else + { + UTIL_BloodDrips( BloodPosition(inputdata.pActivator), Direction(), Color(), BloodAmount() ); + } + + if ( HasSpawnFlags( SF_BLOOD_DECAL ) ) + { + Vector forward = Direction(); + Vector start = BloodPosition( inputdata.pActivator ); + trace_t tr; + + UTIL_TraceLine( start, start + forward * BloodAmount() * 2, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 ) + { + UTIL_BloodDecalTrace( &tr, Color() ); + } + } + + // + // New-fangled blood effects. + // + if ( HasSpawnFlags( SF_BLOOD_CLOUD | SF_BLOOD_DROPS | SF_BLOOD_GORE ) ) + { + int nFlags = 0; + if (HasSpawnFlags(SF_BLOOD_CLOUD)) + { + nFlags |= FX_BLOODSPRAY_CLOUD; + } + + if (HasSpawnFlags(SF_BLOOD_DROPS)) + { + nFlags |= FX_BLOODSPRAY_DROPS; + } + + if (HasSpawnFlags(SF_BLOOD_GORE)) + { + nFlags |= FX_BLOODSPRAY_GORE; + } + + UTIL_BloodSpray(GetAbsOrigin(), Direction(), Color(), BloodAmount(), nFlags); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Console command for emitting the blood spray effect from an NPC. +//----------------------------------------------------------------------------- +void CC_BloodSpray( const CCommand &args ) +{ + CBaseEntity *pEnt = NULL; + while ( ( pEnt = gEntList.FindEntityGeneric( pEnt, args[1] ) ) != NULL ) + { + Vector forward; + pEnt->GetVectors(&forward, NULL, NULL); + UTIL_BloodSpray( (forward * 4 ) + ( pEnt->EyePosition() + pEnt->WorldSpaceCenter() ) * 0.5f, forward, BLOOD_COLOR_RED, 4, FX_BLOODSPRAY_ALL ); + } +} + +static ConCommand bloodspray( "bloodspray", CC_BloodSpray, "blood", FCVAR_CHEAT ); + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +class CEnvFunnel : public CBaseEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( CEnvFunnel, CBaseEntity ); + + void Spawn( void ); + void Precache( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + int m_iSprite; // Don't save, precache +#ifdef MAPBASE + // This unfortunately doesn't work with the way the effect is set up + //string_t m_iszSprite; +#endif +}; + +LINK_ENTITY_TO_CLASS( env_funnel, CEnvFunnel ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CEnvFunnel ) + +#ifdef MAPBASE + //DEFINE_KEYFIELD( m_iszSprite, FIELD_STRING, "Sprite" ), + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputUse ), +#endif +// DEFINE_FIELD( m_iSprite, FIELD_INTEGER ), + +END_DATADESC() + + + +void CEnvFunnel::Precache ( void ) +{ +#ifdef MAPBASE + //if (m_iszSprite == NULL_STRING) + // m_iszSprite = AllocPooledString("sprites/flare6.vmt"); + + //m_iSprite = PrecacheModel(STRING(m_iszSprite)); + m_iSprite = PrecacheModel ( "sprites/flare6.vmt" ); +#else + m_iSprite = PrecacheModel ( "sprites/flare6.vmt" ); +#endif +} + +void CEnvFunnel::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBroadcastRecipientFilter filter; + te->LargeFunnel( filter, 0.0, + &GetAbsOrigin(), m_iSprite, HasSpawnFlags( SF_FUNNEL_REVERSE ) ? 1 : 0 ); + +#ifdef MAPBASE + if (HasSpawnFlags(SF_FUNNEL_DONT_REMOVE)) + return; +#endif + SetThink( &CEnvFunnel::SUB_Remove ); + SetNextThink( gpGlobals->curtime ); +} + +void CEnvFunnel::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); +} + +//========================================================= +// Beverage Dispenser +// overloaded m_iHealth, is now how many cans remain in the machine. +//========================================================= +class CEnvBeverage : public CBaseEntity +{ +public: + DECLARE_CLASS( CEnvBeverage, CBaseEntity ); + + void Spawn( void ); + void Precache( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +public: + bool m_CanInDispenser; + int m_nBeverageType; +}; + +void CEnvBeverage::Precache ( void ) +{ + PrecacheModel( "models/can.mdl" ); +} + +BEGIN_DATADESC( CEnvBeverage ) + DEFINE_FIELD( m_CanInDispenser, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nBeverageType, FIELD_INTEGER ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_beverage, CEnvBeverage ); + + +bool CEnvBeverage::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "beveragetype")) + { + m_nBeverageType = atoi(szValue); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +void CEnvBeverage::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( m_CanInDispenser || m_iHealth <= 0 ) + { + // no more cans while one is waiting in the dispenser, or if I'm out of cans. + return; + } + + CBaseAnimating *pCan = (CBaseAnimating *)CBaseEntity::Create( "item_sodacan", GetLocalOrigin(), GetLocalAngles(), this ); + + if ( m_nBeverageType == 6 ) + { + // random + pCan->m_nSkin = random->RandomInt( 0, 5 ); + } + else + { + pCan->m_nSkin = m_nBeverageType; + } + + m_CanInDispenser = true; + m_iHealth -= 1; + + //SetThink (SUB_Remove); + //SetNextThink( gpGlobals->curtime ); +} + +void CEnvBeverage::InputActivate( inputdata_t &inputdata ) +{ + Use( inputdata.pActivator, inputdata.pCaller, USE_ON, 0 ); +} + +void CEnvBeverage::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); + m_CanInDispenser = false; + + if ( m_iHealth == 0 ) + { + m_iHealth = 10; + } +} + +//========================================================= +// Soda can +//========================================================= +class CItemSoda : public CBaseAnimating +{ +public: + DECLARE_CLASS( CItemSoda, CBaseAnimating ); + + void Spawn( void ); + void Precache( void ); + void CanThink ( void ); + void CanTouch ( CBaseEntity *pOther ); + + DECLARE_DATADESC(); +}; + + +BEGIN_DATADESC( CItemSoda ) + + // Function Pointers + DEFINE_FUNCTION( CanThink ), + DEFINE_FUNCTION( CanTouch ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( item_sodacan, CItemSoda ); + +void CItemSoda::Precache ( void ) +{ + PrecacheModel( "models/can.mdl" ); + + PrecacheScriptSound( "ItemSoda.Bounce" ); +} + +void CItemSoda::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_FLYGRAVITY ); + + SetModel ( "models/can.mdl" ); + UTIL_SetSize ( this, Vector ( 0, 0, 0 ), Vector ( 0, 0, 0 ) ); + + SetThink (&CItemSoda::CanThink); + SetNextThink( gpGlobals->curtime + 0.5f ); +} + +void CItemSoda::CanThink ( void ) +{ + EmitSound( "ItemSoda.Bounce" ); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_TRIGGER ); + UTIL_SetSize ( this, Vector ( -8, -8, 0 ), Vector ( 8, 8, 8 ) ); + + SetThink ( NULL ); + SetTouch ( &CItemSoda::CanTouch ); +} + +void CItemSoda::CanTouch ( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() ) + { + return; + } + + // spoit sound here + + pOther->TakeHealth( 1, DMG_GENERIC );// a bit of health. + + if ( GetOwnerEntity() ) + { + // tell the machine the can was taken + CEnvBeverage *bev = (CEnvBeverage *)GetOwnerEntity(); + bev->m_CanInDispenser = false; + } + + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); + AddEffects( EF_NODRAW ); + SetTouch ( NULL ); + SetThink ( &CItemSoda::SUB_Remove ); + SetNextThink( gpGlobals->curtime ); +} + +#ifndef _XBOX +//========================================================= +// func_precipitation - temporary snow solution for first HL2 +// technology demo +//========================================================= + +class CPrecipitation : public CBaseEntity +{ +public: + DECLARE_CLASS( CPrecipitation, CBaseEntity ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CPrecipitation(); + int UpdateTransmitState(); + void Spawn( void ); + + CNetworkVar( PrecipitationType_t, m_nPrecipType ); +}; + +LINK_ENTITY_TO_CLASS( func_precipitation, CPrecipitation ); + +BEGIN_DATADESC( CPrecipitation ) + DEFINE_KEYFIELD( m_nPrecipType, FIELD_INTEGER, "preciptype" ), +END_DATADESC() + +// Just send the normal entity crap +IMPLEMENT_SERVERCLASS_ST( CPrecipitation, DT_Precipitation) + SendPropInt( SENDINFO( m_nPrecipType ), Q_log2( NUM_PRECIPITATION_TYPES ) + 1, SPROP_UNSIGNED ), +#ifdef MAPBASE + SendPropInt( SENDINFO( m_spawnflags ), 2, SPROP_UNSIGNED ), +#endif +END_SEND_TABLE() + + +CPrecipitation::CPrecipitation() +{ + m_nPrecipType = PRECIPITATION_TYPE_RAIN; // default to rain. +} + +int CPrecipitation::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +void CPrecipitation::Spawn( void ) +{ + //SetTransmitState( FL_EDICT_ALWAYS ); + SetTransmitState( FL_EDICT_PVSCHECK ); + + PrecacheMaterial( "effects/fleck_ash1" ); + PrecacheMaterial( "effects/fleck_ash2" ); + PrecacheMaterial( "effects/fleck_ash3" ); + PrecacheMaterial( "effects/ember_swirling001" ); + + Precache(); + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); // Set size + if ( IsParticleRainType( m_nPrecipType ) ) + { + SetSolid( SOLID_VPHYSICS ); + AddSolidFlags( FSOLID_NOT_SOLID ); + AddSolidFlags( FSOLID_FORCE_WORLD_ALIGNED ); + VPhysicsInitStatic(); + } + else + { + SetSolid( SOLID_NONE ); // Remove model & collisions + } + + // Default to rain. + if ( m_nPrecipType < 0 || m_nPrecipType > NUM_PRECIPITATION_TYPES ) + m_nPrecipType = PRECIPITATION_TYPE_RAIN; + + m_nRenderMode = kRenderEnvironmental; +} +#endif + +//----------------------------------------------------------------------------- +// EnvWind - global wind info +//----------------------------------------------------------------------------- +class CEnvWind : public CBaseEntity +{ +public: + DECLARE_CLASS( CEnvWind, CBaseEntity ); + + void Spawn( void ); + void Precache( void ); + void WindThink( void ); + int UpdateTransmitState( void ); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + +private: +#ifdef POSIX + CEnvWindShared m_EnvWindShared; // FIXME - fails to compile as networked var due to operator= problem +#else + CNetworkVarEmbedded( CEnvWindShared, m_EnvWindShared ); +#endif +}; + +LINK_ENTITY_TO_CLASS( env_wind, CEnvWind ); + +BEGIN_DATADESC( CEnvWind ) + + DEFINE_KEYFIELD( m_EnvWindShared.m_iMinWind, FIELD_INTEGER, "minwind" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_iMaxWind, FIELD_INTEGER, "maxwind" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_iMinGust, FIELD_INTEGER, "mingust" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_iMaxGust, FIELD_INTEGER, "maxgust" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_flMinGustDelay, FIELD_FLOAT, "mingustdelay" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_flMaxGustDelay, FIELD_FLOAT, "maxgustdelay" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_iGustDirChange, FIELD_INTEGER, "gustdirchange" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_flGustDuration, FIELD_FLOAT, "gustduration" ), +// DEFINE_KEYFIELD( m_EnvWindShared.m_iszGustSound, FIELD_STRING, "gustsound" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_EnvWindShared.m_windRadius, FIELD_FLOAT, "windradius" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_windRadiusInner, FIELD_FLOAT, "windradiusinner" ), + DEFINE_KEYFIELD( m_EnvWindShared.m_flTreeSwayScale, FIELD_FLOAT, "treeswayscale" ), +#endif + +// Just here to quiet down classcheck + // DEFINE_FIELD( m_EnvWindShared, CEnvWindShared ), + + DEFINE_FIELD( m_EnvWindShared.m_iWindDir, FIELD_INTEGER ), + DEFINE_FIELD( m_EnvWindShared.m_flWindSpeed, FIELD_FLOAT ), + +#ifdef MAPBASE + DEFINE_INPUT( m_EnvWindShared.m_iMinWind, FIELD_INTEGER, "SetMinWind" ), + DEFINE_INPUT( m_EnvWindShared.m_iMaxWind, FIELD_INTEGER, "SetMaxWind" ), + DEFINE_INPUT( m_EnvWindShared.m_iMinGust, FIELD_INTEGER, "SetMinGust" ), + DEFINE_INPUT( m_EnvWindShared.m_iMaxGust, FIELD_INTEGER, "SetMaxGust" ), + DEFINE_INPUT( m_EnvWindShared.m_flMinGustDelay, FIELD_FLOAT, "SetMinGustDelay" ), + DEFINE_INPUT( m_EnvWindShared.m_flMaxGustDelay, FIELD_FLOAT, "SetMaxGustDelay" ), + DEFINE_INPUT( m_EnvWindShared.m_iGustDirChange, FIELD_INTEGER, "SetGustDirChange" ), + DEFINE_INPUT( m_EnvWindShared.m_flGustDuration, FIELD_FLOAT, "SetGustDuration" ), + DEFINE_INPUT( m_EnvWindShared.m_windRadius, FIELD_FLOAT, "SetWindRadius" ), + DEFINE_INPUT( m_EnvWindShared.m_windRadiusInner, FIELD_FLOAT, "SetWindRadiusInner" ), + DEFINE_INPUT( m_EnvWindShared.m_flTreeSwayScale, FIELD_FLOAT, "SetTreeSwayScale" ), +#endif + + DEFINE_OUTPUT( m_EnvWindShared.m_OnGustStart, "OnGustStart" ), + DEFINE_OUTPUT( m_EnvWindShared.m_OnGustEnd, "OnGustEnd" ), + + // Function Pointers + DEFINE_FUNCTION( WindThink ), + +END_DATADESC() + + +BEGIN_SEND_TABLE_NOBASE(CEnvWindShared, DT_EnvWindShared) + // These are parameters that are used to generate the entire motion + SendPropInt (SENDINFO(m_iMinWind), 10, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_iMaxWind), 10, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_iMinGust), 10, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_iMaxGust), 10, SPROP_UNSIGNED ), + SendPropFloat (SENDINFO(m_flMinGustDelay), 0, SPROP_NOSCALE), // NOTE: Have to do this, so it's *exactly* the same on client + SendPropFloat (SENDINFO(m_flMaxGustDelay), 0, SPROP_NOSCALE), + SendPropInt (SENDINFO(m_iGustDirChange), 9, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_iWindSeed), 32, SPROP_UNSIGNED ), + + // These are related to initial state + SendPropInt (SENDINFO(m_iInitialWindDir),9, SPROP_UNSIGNED ), + SendPropFloat (SENDINFO(m_flInitialWindSpeed),0, SPROP_NOSCALE ), + SendPropFloat (SENDINFO(m_flStartTime), 0, SPROP_NOSCALE ), + + SendPropFloat (SENDINFO(m_flGustDuration), 0, SPROP_NOSCALE), + // Sound related +// SendPropInt (SENDINFO(m_iszGustSound), 10, SPROP_UNSIGNED ), +#ifdef MAPBASE + SendPropFloat (SENDINFO(m_windRadius), 0, SPROP_NOSCALE), + SendPropFloat (SENDINFO(m_windRadiusInner), 0, SPROP_NOSCALE), + SendPropVector (SENDINFO(m_location), -1, SPROP_COORD), + SendPropFloat (SENDINFO(m_flTreeSwayScale), 0, SPROP_NOSCALE), +#endif +END_SEND_TABLE() + +// This table encodes the CBaseEntity data. +IMPLEMENT_SERVERCLASS_ST_NOBASE(CEnvWind, DT_EnvWind) + SendPropDataTable(SENDINFO_DT(m_EnvWindShared), &REFERENCE_SEND_TABLE(DT_EnvWindShared)), +END_SEND_TABLE() + +void CEnvWind::Precache ( void ) +{ +// if (m_iszGustSound) +// { +// PrecacheScriptSound( STRING( m_iszGustSound ) ); +// } +} + +void CEnvWind::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); + +#ifdef MAPBASE + m_EnvWindShared.m_iInitialWindDir = (int)(anglemod( m_EnvWindShared.m_iInitialWindDir )); + m_EnvWindShared.Init( entindex(), 0, gpGlobals->curtime, GetLocalAngles().y, 0 ); + m_EnvWindShared.m_location = GetAbsOrigin(); +#else + m_EnvWindShared.Init( entindex(), 0, gpGlobals->frametime, GetLocalAngles().y, 0 ); +#endif + + SetThink( &CEnvWind::WindThink ); + SetNextThink( gpGlobals->curtime ); +} + +int CEnvWind::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +void CEnvWind::WindThink( void ) +{ + SetNextThink( m_EnvWindShared.WindThink( gpGlobals->curtime ) ); +} + + + +//================================================== +// CEmbers +//================================================== + +#define bitsSF_EMBERS_START_ON 0x00000001 +#define bitsSF_EMBERS_TOGGLE 0x00000002 + +// UNDONE: This is a brush effect-in-volume entity, move client side. +class CEmbers : public CBaseEntity +{ +public: + DECLARE_CLASS( CEmbers, CBaseEntity ); + + void Spawn( void ); + void Precache( void ); + + void EmberUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + CNetworkVar( int, m_nDensity ); + CNetworkVar( int, m_nLifetime ); + CNetworkVar( int, m_nSpeed ); + + CNetworkVar( bool, m_bEmit ); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); +}; + +LINK_ENTITY_TO_CLASS( env_embers, CEmbers ); + +//Data description +BEGIN_DATADESC( CEmbers ) + + DEFINE_KEYFIELD( m_nDensity, FIELD_INTEGER, "density" ), + DEFINE_KEYFIELD( m_nLifetime, FIELD_INTEGER, "lifetime" ), + DEFINE_KEYFIELD( m_nSpeed, FIELD_INTEGER, "speed" ), + + DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), + + //Function pointers + DEFINE_FUNCTION( EmberUse ), + +END_DATADESC() + + +//Data table +IMPLEMENT_SERVERCLASS_ST( CEmbers, DT_Embers ) + SendPropInt( SENDINFO( m_nDensity ), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_nLifetime ), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_nSpeed ), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_bEmit ), 2, SPROP_UNSIGNED ), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEmbers::Spawn( void ) +{ + Precache(); + SetModel( STRING( GetModelName() ) ); + + SetSolid( SOLID_NONE ); + SetRenderColorA( 0 ); + m_nRenderMode = kRenderTransTexture; + + SetUse( &CEmbers::EmberUse ); + + //Start off if we're targetted (unless flagged) + m_bEmit = ( HasSpawnFlags( bitsSF_EMBERS_START_ON ) || ( !GetEntityName() ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEmbers::Precache( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActivator - +// *pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CEmbers::EmberUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + //If we're not toggable, only allow one use + if ( !HasSpawnFlags( bitsSF_EMBERS_TOGGLE ) ) + { + SetUse( NULL ); + } + + //Handle it + switch ( useType ) + { + case USE_OFF: + m_bEmit = false; + break; + + case USE_ON: + m_bEmit = true; + break; + + case USE_SET: + m_bEmit = !!(int)value; + break; + + default: + case USE_TOGGLE: + m_bEmit = !m_bEmit; + break; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPhysicsWire : public CBaseEntity +{ +public: + DECLARE_CLASS( CPhysicsWire, CBaseEntity ); + + void Spawn( void ); + void Precache( void ); + + DECLARE_DATADESC(); + +protected: + + bool SetupPhysics( void ); + + int m_nDensity; +}; + +LINK_ENTITY_TO_CLASS( env_physwire, CPhysicsWire ); + +BEGIN_DATADESC( CPhysicsWire ) + + DEFINE_KEYFIELD( m_nDensity, FIELD_INTEGER, "Density" ), +// DEFINE_KEYFIELD( m_frequency, FIELD_INTEGER, "frequency" ), + +// DEFINE_FIELD( m_flFoo, FIELD_FLOAT ), + + // Function Pointers +// DEFINE_FUNCTION( WireThink ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsWire::Spawn( void ) +{ + BaseClass::Spawn(); + + Precache(); + +// if ( SetupPhysics() == false ) +// return; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsWire::Precache( void ) +{ + BaseClass::Precache(); +} + +class CPhysBallSocket; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPhysicsWire::SetupPhysics( void ) +{ +/* + CPointEntity *anchorEnt, *freeEnt; + CPhysBallSocket *socket; + + char anchorName[256]; + char freeName[256]; + + int iAnchorName, iFreeName; + + anchorEnt = (CPointEntity *) CreateEntityByName( "info_target" ); + + if ( anchorEnt == NULL ) + return false; + + //Create and connect all segments + for ( int i = 0; i < m_nDensity; i++ ) + { + // Create other end of our link + freeEnt = (CPointEntity *) CreateEntityByName( "info_target" ); + + // Create a ballsocket and attach the two + //socket = (CPhysBallSocket *) CreateEntityByName( "phys_ballsocket" ); + + Q_snprintf( anchorName,sizeof(anchorName), "__PWIREANCHOR%d", i ); + Q_snprintf( freeName,sizeof(freeName), "__PWIREFREE%d", i+1 ); + + iAnchorName = MAKE_STRING( anchorName ); + iFreeName = MAKE_STRING( freeName ); + + //Fake the names + //socket->m_nameAttach1 = anchorEnt->m_iGlobalname = iAnchorName; + //socket->m_nameAttach2 = freeEnt->m_iGlobalname = iFreeName + + //socket->Activate(); + + //The free ent is now the anchor for the next link + anchorEnt = freeEnt; + } +*/ + + return true; +} + +// +// Muzzle flash +// + +class CEnvMuzzleFlash : public CPointEntity +{ + DECLARE_CLASS( CEnvMuzzleFlash, CPointEntity ); + +public: + virtual void Spawn(); + + // Input handlers + void InputFire( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + float m_flScale; + string_t m_iszParentAttachment; +}; + +BEGIN_DATADESC( CEnvMuzzleFlash ) + + DEFINE_KEYFIELD( m_flScale, FIELD_FLOAT, "scale" ), + DEFINE_KEYFIELD( m_iszParentAttachment, FIELD_STRING, "parentattachment" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Fire", InputFire ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( env_muzzleflash, CEnvMuzzleFlash ); + + +//----------------------------------------------------------------------------- +// Spawn! +//----------------------------------------------------------------------------- +void CEnvMuzzleFlash::Spawn() +{ + if ( (m_iszParentAttachment != NULL_STRING) && GetParent() && GetParent()->GetBaseAnimating() ) + { + CBaseAnimating *pAnim = GetParent()->GetBaseAnimating(); + int nParentAttachment = pAnim->LookupAttachment( STRING(m_iszParentAttachment) ); + if ( nParentAttachment > 0 ) + { + SetParent( GetParent(), nParentAttachment ); + SetLocalOrigin( vec3_origin ); + SetLocalAngles( vec3_angle ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvMuzzleFlash::InputFire( inputdata_t &inputdata ) +{ + g_pEffects->MuzzleFlash( GetAbsOrigin(), GetAbsAngles(), m_flScale, MUZZLEFLASH_TYPE_DEFAULT ); +} + + +//========================================================= +// Splash! +//========================================================= +#define SF_ENVSPLASH_FINDWATERSURFACE 0x00000001 +#define SF_ENVSPLASH_DIMINISH 0x00000002 +class CEnvSplash : public CPointEntity +{ + DECLARE_CLASS( CEnvSplash, CPointEntity ); + +public: + // Input handlers + void InputSplash( inputdata_t &inputdata ); + +protected: + + float m_flScale; + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CEnvSplash ) + DEFINE_KEYFIELD( m_flScale, FIELD_FLOAT, "scale" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Splash", InputSplash ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_splash, CEnvSplash ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +#define SPLASH_MAX_DEPTH 120.0f +void CEnvSplash::InputSplash( inputdata_t &inputdata ) +{ + CEffectData data; + + data.m_fFlags = 0; + + float scale = m_flScale; + + if( HasSpawnFlags( SF_ENVSPLASH_FINDWATERSURFACE ) ) + { + if( UTIL_PointContents(GetAbsOrigin()) & MASK_WATER ) + { + // No splash if I'm supposed to find the surface of the water, but I'm underwater. + return; + } + + // Trace down and find the water's surface. This is designed for making + // splashes on the surface of water that can change water level. + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector( 0, 0, 4096 ), (MASK_WATER|MASK_SOLID_BRUSHONLY), this, COLLISION_GROUP_NONE, &tr ); + data.m_vOrigin = tr.endpos; + + if ( tr.contents & CONTENTS_SLIME ) + { + data.m_fFlags |= FX_WATER_IN_SLIME; + } + } + else + { + data.m_vOrigin = GetAbsOrigin(); + } + + if( HasSpawnFlags( SF_ENVSPLASH_DIMINISH ) ) + { + // Get smaller if I'm in deeper water. + float depth = 0.0f; + + trace_t tr; + UTIL_TraceLine( data.m_vOrigin, data.m_vOrigin - Vector( 0, 0, 4096 ), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + depth = fabs( tr.startpos.z - tr.endpos.z ); + + float factor = 1.0f - (depth / SPLASH_MAX_DEPTH); + + if( factor < 0.1 ) + { + // Don't bother making one this small. + return; + } + + scale *= factor; + } + + data.m_vNormal = Vector( 0, 0, 1 ); + data.m_flScale = scale; + + DispatchEffect( "watersplash", data ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +class CEnvGunfire : public CPointEntity +{ +public: + DECLARE_CLASS( CEnvGunfire, CPointEntity ); + + CEnvGunfire() + { + // !!!HACKHACK + // These fields came along kind of late, so they get + // initialized in the constructor for now. (sjb) + m_flBias = 1.0f; + m_bCollide = false; + } + + void Precache(); + void Spawn(); + void Activate(); + void StartShooting(); + void StopShooting(); + void ShootThink(); + void UpdateTarget(); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +#ifdef MAPBASE + void InputFireBurst( inputdata_t &inputdata ); +#endif + + int m_iMinBurstSize; + int m_iMaxBurstSize; + + float m_flMinBurstDelay; + float m_flMaxBurstDelay; + + float m_flRateOfFire; + + string_t m_iszShootSound; + string_t m_iszTracerType; + + bool m_bDisabled; + + int m_iShotsRemaining; + + int m_iSpread; + Vector m_vecSpread; + Vector m_vecTargetPosition; + float m_flTargetDist; + + float m_flBias; + bool m_bCollide; + + EHANDLE m_hTarget; + +#ifdef MAPBASE + COutputEvent m_OnFire; +#endif + + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CEnvGunfire ) + DEFINE_KEYFIELD( m_iMinBurstSize, FIELD_INTEGER, "minburstsize" ), + DEFINE_KEYFIELD( m_iMaxBurstSize, FIELD_INTEGER, "maxburstsize" ), + DEFINE_KEYFIELD( m_flMinBurstDelay, FIELD_TIME, "minburstdelay" ), + DEFINE_KEYFIELD( m_flMaxBurstDelay, FIELD_TIME, "maxburstdelay" ), + DEFINE_KEYFIELD( m_flRateOfFire, FIELD_FLOAT, "rateoffire" ), + DEFINE_KEYFIELD( m_iszShootSound, FIELD_STRING, "shootsound" ), + DEFINE_KEYFIELD( m_iszTracerType, FIELD_STRING, "tracertype" ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "startdisabled" ), + DEFINE_KEYFIELD( m_iSpread, FIELD_INTEGER, "spread" ), + DEFINE_KEYFIELD( m_flBias, FIELD_FLOAT, "bias" ), + DEFINE_KEYFIELD( m_bCollide, FIELD_BOOLEAN, "collisions" ), + + DEFINE_FIELD( m_iShotsRemaining, FIELD_INTEGER ), + DEFINE_FIELD( m_vecSpread, FIELD_VECTOR ), + DEFINE_FIELD( m_vecTargetPosition, FIELD_VECTOR ), + DEFINE_FIELD( m_flTargetDist, FIELD_FLOAT ), + + DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + + DEFINE_THINKFUNC( ShootThink ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_INTEGER, "FireBurst", InputFireBurst ), + + DEFINE_OUTPUT( m_OnFire, "OnFire" ), +#endif +END_DATADESC() +LINK_ENTITY_TO_CLASS( env_gunfire, CEnvGunfire ); + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::Precache() +{ + PrecacheScriptSound( STRING( m_iszShootSound ) ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::Spawn() +{ + Precache(); + + m_iShotsRemaining = 0; + m_flRateOfFire = 1.0f / m_flRateOfFire; + + switch( m_iSpread ) + { + case 1: + m_vecSpread = VECTOR_CONE_1DEGREES; + break; + case 5: + m_vecSpread = VECTOR_CONE_5DEGREES; + break; + case 10: + m_vecSpread = VECTOR_CONE_10DEGREES; + break; + case 15: + m_vecSpread = VECTOR_CONE_15DEGREES; + break; + + default: + m_vecSpread = vec3_origin; + break; + } + + if( !m_bDisabled ) + { + StartShooting(); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::Activate( void ) +{ + // Find my target + if (m_target != NULL_STRING) + { + m_hTarget = gEntList.FindEntityByName( NULL, m_target ); + } + + BaseClass::Activate(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::StartShooting() +{ + m_iShotsRemaining = random->RandomInt( m_iMinBurstSize, m_iMaxBurstSize ); + + SetThink( &CEnvGunfire::ShootThink ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::UpdateTarget() +{ + if( m_hTarget ) + { + if( m_hTarget->WorldSpaceCenter() != m_vecTargetPosition ) + { + // Target has moved. + // Locate my target and cache the position and distance. + m_vecTargetPosition = m_hTarget->WorldSpaceCenter(); + m_flTargetDist = (GetAbsOrigin() - m_vecTargetPosition).Length(); + } + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::StopShooting() +{ + SetThink( NULL ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::ShootThink() +{ + if( !m_hTarget ) + { + StopShooting(); + } + + SetNextThink( gpGlobals->curtime + m_flRateOfFire ); + + UpdateTarget(); + + Vector vecDir = m_vecTargetPosition - GetAbsOrigin(); + VectorNormalize( vecDir ); + + CShotManipulator manipulator( vecDir ); + + vecDir = manipulator.ApplySpread( m_vecSpread, m_flBias ); + + Vector vecEnd; + + if( m_bCollide ) + { + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + vecDir * 8192, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if( tr.fraction != 1.0 ) + { + DoImpactEffect( tr, DMG_BULLET ); + } + + vecEnd = tr.endpos; + } + else + { + vecEnd = GetAbsOrigin() + vecDir * m_flTargetDist; + } + + if( m_iszTracerType != NULL_STRING ) + { + UTIL_Tracer( GetAbsOrigin(), vecEnd, 0, TRACER_DONT_USE_ATTACHMENT, 5000, true, STRING(m_iszTracerType) ); + } + else + { + UTIL_Tracer( GetAbsOrigin(), vecEnd, 0, TRACER_DONT_USE_ATTACHMENT, 5000, true ); + } + + EmitSound( STRING(m_iszShootSound) ); + + m_iShotsRemaining--; + +#ifdef MAPBASE + m_OnFire.FireOutput(m_hTarget, this); +#endif + + if( m_iShotsRemaining == 0 ) + { +#ifdef MAPBASE + if (m_bDisabled) + { + StopShooting(); + } + else + { + StartShooting(); + SetNextThink( gpGlobals->curtime + random->RandomFloat( m_flMinBurstDelay, m_flMaxBurstDelay ) ); + } +#else + StartShooting(); + SetNextThink( gpGlobals->curtime + random->RandomFloat( m_flMinBurstDelay, m_flMaxBurstDelay ) ); +#endif + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; + StartShooting(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; + SetThink( NULL ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvGunfire::InputFireBurst( inputdata_t &inputdata ) +{ + m_iShotsRemaining = inputdata.value.Int(); + + SetThink( &CEnvGunfire::ShootThink ); + SetNextThink( gpGlobals->curtime ); +} +#endif + +//----------------------------------------------------------------------------- +// Quadratic spline beam effect +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CEnvQuadraticBeam ) + DEFINE_FIELD( m_targetPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_controlPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_scrollRate, FIELD_FLOAT ), + DEFINE_FIELD( m_flWidth, FIELD_FLOAT ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_quadraticbeam, CEnvQuadraticBeam ); + +IMPLEMENT_SERVERCLASS_ST( CEnvQuadraticBeam, DT_QuadraticBeam ) + SendPropVector(SENDINFO(m_targetPosition), -1, SPROP_COORD), + SendPropVector(SENDINFO(m_controlPosition), -1, SPROP_COORD), + SendPropFloat(SENDINFO(m_scrollRate), 8, 0, -4, 4), + SendPropFloat(SENDINFO(m_flWidth), -1, SPROP_NOSCALE), +END_SEND_TABLE() + +void CEnvQuadraticBeam::Spawn() +{ + BaseClass::Spawn(); + m_nRenderMode = kRenderTransAdd; + SetRenderColor( 255, 255, 255 ); +} + +CEnvQuadraticBeam *CreateQuadraticBeam( const char *pSpriteName, const Vector &start, const Vector &control, const Vector &end, float width, CBaseEntity *pOwner ) +{ + CEnvQuadraticBeam *pBeam = (CEnvQuadraticBeam *)CBaseEntity::Create( "env_quadraticbeam", start, vec3_angle, pOwner ); + UTIL_SetModel( pBeam, pSpriteName ); + pBeam->SetSpline( control, end ); + pBeam->SetScrollRate( 0.0 ); + pBeam->SetWidth(width); + return pBeam; +} + +void EffectsPrecache( void *pUser ) +{ + CBaseEntity::PrecacheScriptSound( "Underwater.BulletImpact" ); + + CBaseEntity::PrecacheScriptSound( "FX_RicochetSound.Ricochet" ); + + CBaseEntity::PrecacheScriptSound( "Physics.WaterSplash" ); + CBaseEntity::PrecacheScriptSound( "BaseExplosionEffect.Sound" ); + CBaseEntity::PrecacheScriptSound( "Splash.SplashSound" ); + + if ( gpGlobals->maxClients > 1 ) + { + CBaseEntity::PrecacheScriptSound( "HudChat.Message" ); + } +} + +PRECACHE_REGISTER_FN( EffectsPrecache ); + + +class CEnvViewPunch : public CPointEntity +{ +public: + + DECLARE_CLASS( CEnvViewPunch, CPointEntity ); + + virtual void Spawn(); + + // Input handlers + void InputViewPunch( inputdata_t &inputdata ); + +private: + + float m_flRadius; + QAngle m_angViewPunch; + + void DoViewPunch(); + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( env_viewpunch, CEnvViewPunch ); + +BEGIN_DATADESC( CEnvViewPunch ) + + DEFINE_KEYFIELD( m_angViewPunch, FIELD_VECTOR, "punchangle" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "ViewPunch", InputViewPunch ), + +END_DATADESC() + +#define SF_PUNCH_EVERYONE 0x0001 // Don't check radius +#define SF_PUNCH_IN_AIR 0x0002 // Punch players in air + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvViewPunch::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + if ( GetSpawnFlags() & SF_PUNCH_EVERYONE ) + { + m_flRadius = 0; + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvViewPunch::DoViewPunch() +{ + bool bAir = (GetSpawnFlags() & SF_PUNCH_IN_AIR) ? true : false; + UTIL_ViewPunch( GetAbsOrigin(), m_angViewPunch, m_flRadius, bAir ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvViewPunch::InputViewPunch( inputdata_t &inputdata ) +{ + DoViewPunch(); +} + +#ifdef MAPBASE +class CBreakableGibShooter : public CBaseEntity +{ + DECLARE_CLASS( CBreakableGibShooter, CBaseEntity ); + DECLARE_DATADESC(); +public: + + const char *GetRandomTemplateModel( CPointTemplate *pTemplate ); + + void Precache( void ); + + void Shoot( void ); + + // ---------------- + // Inputs + // ---------------- + void InputShoot( inputdata_t &inputdata ); + +public: + + int m_iModelType; + enum + { + MODELTYPE_BREAKABLECHUNKS, + MODELTYPE_MODEL, + MODELTYPE_TEMPLATE, + }; + + int m_iCount; + float m_flDelay; + Vector m_vecGibSize; + float m_flGibSpeed; + int m_iRandomization; + float m_flLifetime; + int m_iGibFlags; +}; + +BEGIN_DATADESC( CBreakableGibShooter ) + + DEFINE_KEYFIELD( m_iModelType, FIELD_INTEGER, "modeltype" ), + DEFINE_INPUT( m_iCount, FIELD_INTEGER, "SetCount" ), + DEFINE_INPUT( m_flDelay, FIELD_FLOAT, "SetDelay" ), + DEFINE_INPUT( m_vecGibSize, FIELD_VECTOR, "SetGibSize" ), + DEFINE_INPUT( m_flGibSpeed, FIELD_FLOAT, "SetGibSpeed" ), + DEFINE_INPUT( m_iRandomization, FIELD_INTEGER, "SetRandomization" ), + DEFINE_INPUT( m_flLifetime, FIELD_FLOAT, "SetLifetime" ), + DEFINE_INPUT( m_iGibFlags, FIELD_INTEGER, "SetGibFlags" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Shoot", InputShoot ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( env_break_shooter, CBreakableGibShooter ); + + +const char *CBreakableGibShooter::GetRandomTemplateModel( CPointTemplate *pTemplate ) +{ + int iIndex = RandomInt( 0, pTemplate->GetNumTemplates() ); + char *iszTemplate = (char*)(STRING(Templates_FindByIndex(pTemplate->GetTemplateIndexForTemplate(iIndex)))); + + CEntityMapData entData( iszTemplate ); + + // This might seem a little messy, but I think it's cheaper than creating the entity. + char szModel[MAPKEY_MAXLENGTH]; + if (!entData.ExtractValue("model", szModel)) + return NULL; + + return strdup(szModel); +} + +void CBreakableGibShooter::Precache( void ) +{ + if (m_iModelType == MODELTYPE_MODEL) + PrecacheModel( STRING(GetModelName()) ); +} + +void CBreakableGibShooter::Shoot( void ) +{ + int iModelIndex = 0; + if (m_iModelType == MODELTYPE_MODEL) + iModelIndex = modelinfo->GetModelIndex( STRING(GetModelName()) ); + + CPointTemplate *pTemplate = NULL; + if (m_iModelType == MODELTYPE_TEMPLATE) + { + pTemplate = dynamic_cast(gEntList.FindEntityByName(NULL, STRING(GetModelName()), this)); + if (!pTemplate) + { + Warning("%s cannot find point_template %s!\n", GetDebugName(), STRING(GetModelName())); + return; + } + } + + CPVSFilter filter( GetAbsOrigin() ); + for ( int i = 0; i < m_iCount; i++ ) + { + if (m_iModelType == MODELTYPE_BREAKABLECHUNKS) + iModelIndex = modelinfo->GetModelIndex( g_PropDataSystem.GetRandomChunkModel( STRING( GetModelName() ) ) ); + else if (m_iModelType == MODELTYPE_TEMPLATE) + iModelIndex = modelinfo->GetModelIndex( GetRandomTemplateModel(pTemplate) ); + + // All objects except the first one in this run are marked as slaves... + int slaveFlag = 0; + if ( i != 0 ) + { + slaveFlag = BREAK_SLAVE; + } + + Vector vecShootDir; + AngleVectors( GetAbsAngles(), &vecShootDir ); + VectorNormalize( vecShootDir ); + + te->BreakModel( filter, m_flDelay, GetAbsOrigin(), GetAbsAngles(), m_vecGibSize, vecShootDir * m_flGibSpeed, iModelIndex, m_iRandomization, 1, m_flLifetime, m_iGibFlags | slaveFlag ); + } +} + +void CBreakableGibShooter::InputShoot( inputdata_t &inputdata ) +{ + Shoot(); +} +#endif diff --git a/sp/src/game/server/effects.h b/sp/src/game/server/effects.h new file mode 100644 index 00000000..364657fb --- /dev/null +++ b/sp/src/game/server/effects.h @@ -0,0 +1,68 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef EFFECTS_H +#define EFFECTS_H + +#ifdef _WIN32 +#pragma once +#endif + + +class CBaseEntity; +class Vector; + + +//----------------------------------------------------------------------------- +// The rotor wash shooter. It emits gibs when pushed by a rotor wash +//----------------------------------------------------------------------------- +abstract_class IRotorWashShooter +{ +public: + virtual CBaseEntity *DoWashPush( float flWashStartTime, const Vector &vecForce ) = 0; +}; + + +//----------------------------------------------------------------------------- +// Gets at the interface if the entity supports it +//----------------------------------------------------------------------------- +IRotorWashShooter *GetRotorWashShooter( CBaseEntity *pEntity ); + +class CEnvQuadraticBeam : public CPointEntity +{ + DECLARE_CLASS( CEnvQuadraticBeam, CPointEntity ); + +public: + void Spawn(); + void SetSpline( const Vector &control, const Vector &target ) + { + m_targetPosition = target; + m_controlPosition = control; + } + void SetScrollRate( float rate ) + { + m_scrollRate = rate; + } + + void SetWidth( float width ) + { + m_flWidth = width; + } + +private: + CNetworkVector( m_targetPosition ); + CNetworkVector( m_controlPosition ); + CNetworkVar( float, m_scrollRate ); + CNetworkVar( float, m_flWidth ); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); +}; +CEnvQuadraticBeam *CreateQuadraticBeam( const char *pSpriteName, const Vector &start, const Vector &control, const Vector &end, float width, CBaseEntity *pOwner ); + + +#endif // EFFECTS_H diff --git a/sp/src/game/server/enginecallback.h b/sp/src/game/server/enginecallback.h new file mode 100644 index 00000000..f9b022e0 --- /dev/null +++ b/sp/src/game/server/enginecallback.h @@ -0,0 +1,147 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//===========================================================================// +#ifndef ENGINECALLBACK_H +#define ENGINECALLBACK_H + +#define EIFACEV21 + +#ifndef EIFACE_H +#include "eiface.h" +#endif +#ifdef EIFACEV21 +#include "eifacev21.h" +#endif + +#ifdef EIFACEV21 +class IVEngineServerV21; // this class from eifacev21.h +#endif +class IFileSystem; // include filesystem.h +class IEngineSound; // include engine/IEngineSound.h +class IVEngineServer; +class IVoiceServer; +class IStaticPropMgrServer; +class ISpatialPartition; +class IVModelInfo; +class IEngineTrace; +class IGameEventManager2; +class IVDebugOverlay; +class IDataCache; +class IMDLCache; +class IServerEngineTools; +class IXboxSystem; +class IScriptManager; +class CSteamAPIContext; +class CSteamGameServerAPIContext; + +#ifdef EIFACEV21 +extern IVEngineServerV21 *engineV21; // i hate this +#endif +extern IVEngineServer *engine; +extern IVoiceServer *g_pVoiceServer; +extern IFileSystem *filesystem; +extern IStaticPropMgrServer *staticpropmgr; +extern ISpatialPartition *partition; +extern IEngineSound *enginesound; +extern IVModelInfo *modelinfo; +extern IEngineTrace *enginetrace; +extern IGameEventManager2 *gameeventmanager; +extern IVDebugOverlay *debugoverlay; +extern IDataCache *datacache; +extern IMDLCache *mdlcache; +extern IServerEngineTools *serverenginetools; +extern IXboxSystem *xboxsystem; // 360 only +extern IScriptManager *scriptmanager; +extern CSteamAPIContext *steamapicontext; // available on game clients +extern CSteamGameServerAPIContext *steamgameserverapicontext; //available on game servers + + + +//----------------------------------------------------------------------------- +// Precaches a material +//----------------------------------------------------------------------------- +void PrecacheMaterial( const char *pMaterialName ); + +//----------------------------------------------------------------------------- +// Converts a previously precached material into an index +//----------------------------------------------------------------------------- +int GetMaterialIndex( const char *pMaterialName ); + +//----------------------------------------------------------------------------- +// Converts a previously precached material index into a string +//----------------------------------------------------------------------------- +const char *GetMaterialNameFromIndex( int nMaterialIndex ); + + +//----------------------------------------------------------------------------- +// Precache-related methods for particle systems +//----------------------------------------------------------------------------- +void PrecacheParticleSystem( const char *pParticleSystemName ); +int GetParticleSystemIndex( const char *pParticleSystemName ); +const char *GetParticleSystemNameFromIndex( int nIndex ); + + +class IRecipientFilter; +void EntityMessageBegin( CBaseEntity * entity, bool reliable = false ); +void UserMessageBegin( IRecipientFilter& filter, const char *messagename ); +void MessageEnd( void ); + +// bytewise +void MessageWriteByte( int iValue); +void MessageWriteChar( int iValue); +void MessageWriteShort( int iValue); +void MessageWriteWord( int iValue ); +void MessageWriteLong( int iValue); +void MessageWriteFloat( float flValue); +void MessageWriteAngle( float flValue); +void MessageWriteCoord( float flValue); +void MessageWriteVec3Coord( const Vector& rgflValue); +void MessageWriteVec3Normal( const Vector& rgflValue); +void MessageWriteAngles( const QAngle& rgflValue); +void MessageWriteString( const char *sz ); +void MessageWriteEntity( int iValue); +void MessageWriteEHandle( CBaseEntity *pEntity ); //encoded as a long + + +// bitwise +void MessageWriteBool( bool bValue ); +void MessageWriteUBitLong( unsigned int data, int numbits ); +void MessageWriteSBitLong( int data, int numbits ); +void MessageWriteBits( const void *pIn, int nBits ); + +#ifndef NO_STEAM + +/// Returns Steam ID, given player index. Returns an invalid SteamID upon +/// failure +extern CSteamID GetSteamIDForPlayerIndex( int iPlayerIndex ); + +#endif + + +// Bytewise +#define WRITE_BYTE (MessageWriteByte) +#define WRITE_CHAR (MessageWriteChar) +#define WRITE_SHORT (MessageWriteShort) +#define WRITE_WORD (MessageWriteWord) +#define WRITE_LONG (MessageWriteLong) +#define WRITE_FLOAT (MessageWriteFloat) +#define WRITE_ANGLE (MessageWriteAngle) +#define WRITE_COORD (MessageWriteCoord) +#define WRITE_VEC3COORD (MessageWriteVec3Coord) +#define WRITE_VEC3NORMAL (MessageWriteVec3Normal) +#define WRITE_ANGLES (MessageWriteAngles) +#define WRITE_STRING (MessageWriteString) +#define WRITE_ENTITY (MessageWriteEntity) +#define WRITE_EHANDLE (MessageWriteEHandle) + +// Bitwise +#define WRITE_BOOL (MessageWriteBool) +#define WRITE_UBITLONG (MessageWriteUBitLong) +#define WRITE_SBITLONG (MessageWriteSBitLong) +#define WRITE_BITS (MessageWriteBits) + +#endif //ENGINECALLBACK_H diff --git a/sp/src/game/server/entity_tools_server.cpp b/sp/src/game/server/entity_tools_server.cpp new file mode 100644 index 00000000..5032ca04 --- /dev/null +++ b/sp/src/game/server/entity_tools_server.cpp @@ -0,0 +1,458 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= +#include "cbase.h" +#include "const.h" +#include "toolframework/itoolentity.h" +#include "entitylist.h" +#include "toolframework/itoolsystem.h" +#include "KeyValues.h" +#include "icliententity.h" +#include "iserverentity.h" +#include "sceneentity.h" +#include "particles/particles.h" + + +//----------------------------------------------------------------------------- +// Interface from engine to tools for manipulating entities +//----------------------------------------------------------------------------- +class CServerTools : public IServerTools +{ +public: + // Inherited from IServerTools + virtual IServerEntity *GetIServerEntity( IClientEntity *pClientEntity ); + virtual bool GetPlayerPosition( Vector &org, QAngle &ang, IClientEntity *pClientPlayer = NULL ); + virtual bool SnapPlayerToPosition( const Vector &org, const QAngle &ang, IClientEntity *pClientPlayer = NULL ); + virtual int GetPlayerFOV( IClientEntity *pClientPlayer = NULL ); + virtual bool SetPlayerFOV( int fov, IClientEntity *pClientPlayer = NULL ); + virtual bool IsInNoClipMode( IClientEntity *pClientPlayer = NULL ); + virtual CBaseEntity *FirstEntity( void ); + virtual CBaseEntity *NextEntity( CBaseEntity *pEntity ); + virtual CBaseEntity *FindEntityByHammerID( int iHammerID ); + virtual bool GetKeyValue( CBaseEntity *pEntity, const char *szField, char *szValue, int iMaxLen ); + virtual bool SetKeyValue( CBaseEntity *pEntity, const char *szField, const char *szValue ); + virtual bool SetKeyValue( CBaseEntity *pEntity, const char *szField, float flValue ); + virtual bool SetKeyValue( CBaseEntity *pEntity, const char *szField, const Vector &vecValue ); + virtual CBaseEntity *CreateEntityByName( const char *szClassName ); + virtual void DispatchSpawn( CBaseEntity *pEntity ); + virtual void ReloadParticleDefintions( const char *pFileName, const void *pBufData, int nLen ); + virtual void AddOriginToPVS( const Vector &org ); + virtual void MoveEngineViewTo( const Vector &vPos, const QAngle &vAngles ); + virtual bool DestroyEntityByHammerId( int iHammerID ); + virtual CBaseEntity *GetBaseEntityByEntIndex( int iEntIndex ); + virtual void RemoveEntity( CBaseEntity *pEntity ); + virtual void RemoveEntityImmediate( CBaseEntity *pEntity ); + virtual IEntityFactoryDictionary *GetEntityFactoryDictionary( void ); + virtual void SetMoveType( CBaseEntity *pEntity, int val ); + virtual void SetMoveType( CBaseEntity *pEntity, int val, int moveCollide ); + virtual void ResetSequence( CBaseAnimating *pEntity, int nSequence ); + virtual void ResetSequenceInfo( CBaseAnimating *pEntity ); + virtual void ClearMultiDamage( void ); + virtual void ApplyMultiDamage( void ); + virtual void AddMultiDamage( const CTakeDamageInfo &pTakeDamageInfo, CBaseEntity *pEntity ); + virtual void RadiusDamage( const CTakeDamageInfo &info, const Vector &vecSrc, float flRadius, int iClassIgnore, CBaseEntity *pEntityIgnore ); + virtual ITempEntsSystem *GetTempEntsSystem( void ); +}; + + +//----------------------------------------------------------------------------- +// Singleton +//----------------------------------------------------------------------------- +static CServerTools g_ServerTools; + +// VSERVERTOOLS_INTERFACE_VERSION_1 is compatible with the latest since we're only adding things to the end, so expose that as well. +EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CServerTools, IServerTools001, VSERVERTOOLS_INTERFACE_VERSION_1, g_ServerTools ); +EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CServerTools, IServerTools, VSERVERTOOLS_INTERFACE_VERSION, g_ServerTools ); + +// When bumping the version to this interface, check that our assumption is still valid and expose the older version in the same way +COMPILE_TIME_ASSERT( VSERVERTOOLS_INTERFACE_VERSION_INT == 2 ); + + +IServerEntity *CServerTools::GetIServerEntity( IClientEntity *pClientEntity ) +{ + if ( pClientEntity == NULL ) + return NULL; + + CBaseHandle ehandle = pClientEntity->GetRefEHandle(); + if ( ehandle.GetEntryIndex() >= MAX_EDICTS ) + return NULL; // the first MAX_EDICTS entities are networked, the rest are client or server only + +#if 0 + // this fails, since the server entities have extra bits in their serial numbers, + // since 20 bits are reserved for serial numbers, except for networked entities, which are restricted to 10 + + // Brian believes that everything should just restrict itself to 10 to make things simpler, + // so if/when he changes NUM_SERIAL_NUM_BITS to 10, we can switch back to this simpler code + + IServerNetworkable *pNet = gEntList.GetServerNetworkable( ehandle ); + if ( pNet == NULL ) + return NULL; + + CBaseEntity *pServerEnt = pNet->GetBaseEntity(); + return pServerEnt; +#else + IHandleEntity *pEnt = gEntList.LookupEntityByNetworkIndex( ehandle.GetEntryIndex() ); + if ( pEnt == NULL ) + return NULL; + + CBaseHandle h = gEntList.GetNetworkableHandle( ehandle.GetEntryIndex() ); + const int mask = ( 1 << NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS ) - 1; + if ( !h.IsValid() || ( ( h.GetSerialNumber() & mask ) != ( ehandle.GetSerialNumber() & mask ) ) ) + return NULL; + + IServerUnknown *pUnk = static_cast< IServerUnknown* >( pEnt ); + return pUnk->GetBaseEntity(); +#endif +} + +bool CServerTools::GetPlayerPosition( Vector &org, QAngle &ang, IClientEntity *pClientPlayer ) +{ + IServerEntity *pServerPlayer = GetIServerEntity( pClientPlayer ); + CBasePlayer *pPlayer = pServerPlayer ? ( CBasePlayer* )pServerPlayer : UTIL_GetLocalPlayer(); + if ( pPlayer == NULL ) + return false; + + org = pPlayer->EyePosition(); + ang = pPlayer->EyeAngles(); + return true; +} + +bool CServerTools::SnapPlayerToPosition( const Vector &org, const QAngle &ang, IClientEntity *pClientPlayer ) +{ + IServerEntity *pServerPlayer = GetIServerEntity( pClientPlayer ); + CBasePlayer *pPlayer = pServerPlayer ? ( CBasePlayer* )pServerPlayer : UTIL_GetLocalPlayer(); + if ( pPlayer == NULL ) + return false; + + pPlayer->SetAbsOrigin( org - pPlayer->GetViewOffset() ); + pPlayer->SnapEyeAngles( ang ); + + // Disengage from hierarchy + pPlayer->SetParent( NULL ); + + return true; +} + +int CServerTools::GetPlayerFOV( IClientEntity *pClientPlayer ) +{ + IServerEntity *pServerPlayer = GetIServerEntity( pClientPlayer ); + CBasePlayer *pPlayer = pServerPlayer ? ( CBasePlayer* )pServerPlayer : UTIL_GetLocalPlayer(); + if ( pPlayer == NULL ) + return 0; + + return pPlayer->GetFOV(); +} + +bool CServerTools::SetPlayerFOV( int fov, IClientEntity *pClientPlayer ) +{ + IServerEntity *pServerPlayer = GetIServerEntity( pClientPlayer ); + CBasePlayer *pPlayer = pServerPlayer ? ( CBasePlayer* )pServerPlayer : UTIL_GetLocalPlayer(); + if ( pPlayer == NULL ) + return false; + + pPlayer->SetDefaultFOV( fov ); + CBaseEntity *pFOVOwner = pPlayer->GetFOVOwner(); + return pPlayer->SetFOV( pFOVOwner ? pFOVOwner : pPlayer, fov ); +} + +bool CServerTools::IsInNoClipMode( IClientEntity *pClientPlayer ) +{ + IServerEntity *pServerPlayer = GetIServerEntity( pClientPlayer ); + CBasePlayer *pPlayer = pServerPlayer ? ( CBasePlayer* )pServerPlayer : UTIL_GetLocalPlayer(); + if ( pPlayer == NULL ) + return true; + + return pPlayer->GetMoveType() == MOVETYPE_NOCLIP; +} + +CBaseEntity *CServerTools::FirstEntity( void ) +{ + return gEntList.FirstEnt(); +} + +CBaseEntity *CServerTools::NextEntity( CBaseEntity *pEntity ) +{ + CBaseEntity *pEnt; + + if ( pEntity == NULL ) + { + pEnt = gEntList.FirstEnt(); + } + else + { + pEnt = gEntList.NextEnt( (CBaseEntity *)pEntity ); + } + return pEnt; +} + +CBaseEntity *CServerTools::FindEntityByHammerID( int iHammerID ) +{ + CBaseEntity *pEntity = gEntList.FirstEnt(); + + while (pEntity) + { + if (pEntity->m_iHammerID == iHammerID) + return pEntity; + pEntity = gEntList.NextEnt( pEntity ); + } + return NULL; +} + +bool CServerTools::GetKeyValue( CBaseEntity *pEntity, const char *szField, char *szValue, int iMaxLen ) +{ + return pEntity->GetKeyValue( szField, szValue, iMaxLen ); +} + +bool CServerTools::SetKeyValue( CBaseEntity *pEntity, const char *szField, const char *szValue ) +{ + return pEntity->KeyValue( szField, szValue ); +} + +bool CServerTools::SetKeyValue( CBaseEntity *pEntity, const char *szField, float flValue ) +{ + return pEntity->KeyValue( szField, flValue ); +} + +bool CServerTools::SetKeyValue( CBaseEntity *pEntity, const char *szField, const Vector &vecValue ) +{ + return pEntity->KeyValue( szField, vecValue ); +} + + +//----------------------------------------------------------------------------- +// entity spawning +//----------------------------------------------------------------------------- +CBaseEntity *CServerTools::CreateEntityByName( const char *szClassName ) +{ + return ::CreateEntityByName( szClassName ); +} + +void CServerTools::DispatchSpawn( CBaseEntity *pEntity ) +{ + ::DispatchSpawn( pEntity ); +} + + +//----------------------------------------------------------------------------- +// Reload particle definitions +//----------------------------------------------------------------------------- +void CServerTools::ReloadParticleDefintions( const char *pFileName, const void *pBufData, int nLen ) +{ + // FIXME: Use file name to determine if we care about this data + CUtlBuffer buf( pBufData, nLen, CUtlBuffer::READ_ONLY ); + g_pParticleSystemMgr->ReadParticleConfigFile( buf, true ); +} + +void CServerTools::AddOriginToPVS( const Vector &org ) +{ + engine->AddOriginToPVS( org ); +} + +void CServerTools::MoveEngineViewTo( const Vector &vPos, const QAngle &vAngles ) +{ + CBasePlayer *pPlayer = UTIL_GetListenServerHost(); + if ( !pPlayer ) + return; + + extern void EnableNoClip( CBasePlayer *pPlayer ); + EnableNoClip( pPlayer ); + + Vector zOffset = pPlayer->EyePosition() - pPlayer->GetAbsOrigin(); + + pPlayer->SetAbsOrigin( vPos - zOffset ); + pPlayer->SnapEyeAngles( vAngles ); +} + +bool CServerTools::DestroyEntityByHammerId( int iHammerID ) +{ + CBaseEntity *pEntity = (CBaseEntity*)FindEntityByHammerID( iHammerID ); + if ( !pEntity ) + return false; + + UTIL_Remove( pEntity ); + return true; +} + +void CServerTools::RemoveEntity( CBaseEntity *pEntity ) +{ + UTIL_Remove( pEntity ); +} + +void CServerTools::RemoveEntityImmediate( CBaseEntity *pEntity ) +{ + UTIL_RemoveImmediate( pEntity ); +} + +CBaseEntity *CServerTools::GetBaseEntityByEntIndex( int iEntIndex ) +{ + edict_t *pEdict = INDEXENT( iEntIndex ); + if ( pEdict ) + return CBaseEntity::Instance( pEdict ); + else + return NULL; +} + +IEntityFactoryDictionary *CServerTools::GetEntityFactoryDictionary( void ) +{ + return ::EntityFactoryDictionary(); +} + + +void CServerTools::SetMoveType( CBaseEntity *pEntity, int val ) +{ + pEntity->SetMoveType( (MoveType_t)val ); +} + +void CServerTools::SetMoveType( CBaseEntity *pEntity, int val, int moveCollide ) +{ + pEntity->SetMoveType( (MoveType_t)val, (MoveCollide_t)moveCollide ); +} + +void CServerTools::ResetSequence( CBaseAnimating *pEntity, int nSequence ) +{ + pEntity->ResetSequence( nSequence ); +} + +void CServerTools::ResetSequenceInfo( CBaseAnimating *pEntity ) +{ + pEntity->ResetSequenceInfo(); +} + + +void CServerTools::ClearMultiDamage( void ) +{ + ::ClearMultiDamage(); +} + +void CServerTools::ApplyMultiDamage( void ) +{ + ::ApplyMultiDamage(); +} + +void CServerTools::AddMultiDamage( const CTakeDamageInfo &pTakeDamageInfo, CBaseEntity *pEntity ) +{ + ::AddMultiDamage( pTakeDamageInfo, pEntity ); +} + +void CServerTools::RadiusDamage( const CTakeDamageInfo &info, const Vector &vecSrc, float flRadius, int iClassIgnore, CBaseEntity *pEntityIgnore ) +{ + ::RadiusDamage( info, vecSrc, flRadius, iClassIgnore, pEntityIgnore ); +} + + +ITempEntsSystem *CServerTools::GetTempEntsSystem( void ) +{ + return (ITempEntsSystem *)te; +} + + +// Interface from engine to tools for manipulating entities +class CServerChoreoTools : public IServerChoreoTools +{ +public: + // Iterates through ALL entities (separate list for client vs. server) + virtual EntitySearchResult NextChoreoEntity( EntitySearchResult currentEnt ) + { + CBaseEntity *ent = reinterpret_cast< CBaseEntity* >( currentEnt ); + ent = gEntList.FindEntityByClassname( ent, "logic_choreographed_scene" ); + return reinterpret_cast< EntitySearchResult >( ent ); + } + + virtual const char *GetSceneFile( EntitySearchResult sr ) + { + CBaseEntity *ent = reinterpret_cast< CBaseEntity* >( sr ); + if ( !sr ) + return ""; + + if ( Q_stricmp( ent->GetClassname(), "logic_choreographed_scene" ) ) + return ""; + + return GetSceneFilename( ent ); + } + + // For interactive editing + virtual int GetEntIndex( EntitySearchResult sr ) + { + CBaseEntity *ent = reinterpret_cast< CBaseEntity* >( sr ); + if ( !ent ) + return -1; + + return ent->entindex(); + } + + virtual void ReloadSceneFromDisk( int entindex ) + { + CBaseEntity *ent = CBaseEntity::Instance( entindex ); + if ( !ent ) + return; + + ::ReloadSceneFromDisk( ent ); + } +}; + + +static CServerChoreoTools g_ServerChoreoTools; +EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CServerChoreoTools, IServerChoreoTools, VSERVERCHOREOTOOLS_INTERFACE_VERSION, g_ServerChoreoTools ); + + +//------------------------------------------------------------------------------ +// Applies keyvalues to the entity by hammer ID. +//------------------------------------------------------------------------------ +void CC_Ent_Keyvalue( const CCommand &args ) +{ + // Must have an odd number of arguments. + if ( ( args.ArgC() < 4 ) || ( args.ArgC() & 1 ) ) + { + Msg( "Format: ent_keyvalue \"key1\" \"value1\" \"key2\" \"value2\" ... \"keyN\" \"valueN\"\n" ); + return; + } + + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + CBaseEntity *pEnt; + if ( FStrEq( args[1], "" ) || FStrEq( args[1], "!picker" ) ) + { + if (!pPlayer) + return; + + extern CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ); + pEnt = FindPickerEntity( pPlayer ); + + if ( !pEnt ) + { + ClientPrint( pPlayer, HUD_PRINTCONSOLE, "No entity in front of player.\n" ); + return; + } + } + else if ( FStrEq( args[1], "!self" ) || FStrEq( args[1], "!caller" ) || FStrEq( args[1], "!activator" ) ) + { + if (!pPlayer) + return; + + pEnt = pPlayer; + } + else + { + int nID = atoi( args[1] ); + + pEnt = g_ServerTools.FindEntityByHammerID( nID ); + if ( !pEnt ) + { + Msg( "Entity ID %d not found.\n", nID ); + return; + } + } + + int nArg = 2; + while ( nArg < args.ArgC() ) + { + const char *pszKey = args[ nArg ]; + const char *pszValue = args[ nArg + 1 ]; + nArg += 2; + + g_ServerTools.SetKeyValue( pEnt, pszKey, pszValue ); + } +} + +static ConCommand ent_keyvalue("ent_keyvalue", CC_Ent_Keyvalue, "Applies the comma delimited key=value pairs to the entity with the given Hammer ID.\n\tFormat: ent_keyvalue ... \n", FCVAR_CHEAT); diff --git a/sp/src/game/server/entityapi.h b/sp/src/game/server/entityapi.h new file mode 100644 index 00000000..234001dd --- /dev/null +++ b/sp/src/game/server/entityapi.h @@ -0,0 +1,33 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef ENTITYAPI_H +#define ENTITYAPI_H + + +class SendTable; + +extern void LoadMapEntities( const char *pMapEntities ); +extern void DispatchObjectCollisionBox( edict_t *pent ); +extern float DispatchObjectPhysicsVelocity( edict_t *pent, float moveTime ); +extern ServerClass* DispatchGetObjectServerClass(edict_t *pent); +extern ServerClass* GetAllServerClasses(); +extern void SaveWriteFields( CSaveRestoreData *pSaveData, const char *pname, void *pBaseData, datamap_t *pMap, typedescription_t *pFields, int fieldCount ); +extern void SaveReadFields( CSaveRestoreData *pSaveData, const char *pname, void *pBaseData, datamap_t *pMap, typedescription_t *pFields, int fieldCount ); +extern void SaveGlobalState( CSaveRestoreData *pSaveData ); +extern void RestoreGlobalState( CSaveRestoreData *pSaveData ); +extern void ResetGlobalState( void ); +extern CSaveRestoreData *SaveInit( int size ); +extern int CreateEntityTransitionList( CSaveRestoreData *pSaveData, int levelMask ); +extern void ClearEntities( void ); +extern void FreeContainingEntity( edict_t *ed ); + +class ISaveRestoreBlockHandler; +ISaveRestoreBlockHandler *GetEntitySaveRestoreBlockHandler(); + + +#endif // ENTITYAPI_H diff --git a/sp/src/game/server/entityblocker.cpp b/sp/src/game/server/entityblocker.cpp new file mode 100644 index 00000000..cc6edbf5 --- /dev/null +++ b/sp/src/game/server/entityblocker.cpp @@ -0,0 +1,76 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "entityblocker.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( entity_blocker, CEntityBlocker ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &origin - +// &mins - +// &maxs - +// NULL - +// Output : CEntityBlocker +//----------------------------------------------------------------------------- +CEntityBlocker *CEntityBlocker::Create( const Vector &origin, const Vector &mins, const Vector &maxs, CBaseEntity *pOwner, bool bBlockPhysics ) +{ + CEntityBlocker *pBlocker = (CEntityBlocker *) CBaseEntity::Create( "entity_blocker", origin, vec3_angle, pOwner ); + + if ( pBlocker != NULL ) + { + pBlocker->SetSize( mins, maxs ); + if ( bBlockPhysics ) + { + pBlocker->VPhysicsInitStatic(); + } + } + + return pBlocker; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEntityBlocker::Spawn( void ) +{ + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_CUSTOMRAYTEST ); +} + +//----------------------------------------------------------------------------- +// Purpose: Entity blockers don't block tracelines so they don't screw up weapon fire, etc +//----------------------------------------------------------------------------- +bool CEntityBlocker::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ + return false; +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_Test_Entity_Blocker( void ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + Vector vecForward; + pPlayer->GetVectors( &vecForward, NULL, NULL ); + + trace_t tr; + Vector vecOrigin = pPlayer->GetAbsOrigin() + (vecForward * 256); + UTIL_TraceHull( vecOrigin + Vector(0,0,256), vecOrigin - Vector(0,0,256), VEC_HULL_MIN_SCALED( pPlayer ), VEC_HULL_MAX_SCALED( pPlayer ), MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); + if ( !tr.allsolid && !tr.startsolid ) + { + CEntityBlocker::Create( tr.endpos, VEC_HULL_MIN_SCALED( pPlayer ), VEC_HULL_MAX_SCALED( pPlayer ), NULL, true ); + NDebugOverlay::Box( tr.endpos, VEC_HULL_MIN_SCALED( pPlayer ), VEC_HULL_MAX_SCALED( pPlayer ), 0, 255, 0, 64, 1000.0 ); + } +} +static ConCommand test_entity_blocker("test_entity_blocker", CC_Test_Entity_Blocker, "Test command that drops an entity blocker out in front of the player.", FCVAR_CHEAT ); \ No newline at end of file diff --git a/sp/src/game/server/entityblocker.h b/sp/src/game/server/entityblocker.h new file mode 100644 index 00000000..cc6c3fda --- /dev/null +++ b/sp/src/game/server/entityblocker.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef ENTITYBLOCKER_H +#define ENTITYBLOCKER_H +#ifdef _WIN32 +#pragma once +#endif + +//================================================================================================================== +// Entity Blocker +//================================================================================================================== +class CEntityBlocker : public CBaseEntity +{ + DECLARE_CLASS( CEntityBlocker, CBaseEntity ); + +public: + + static CEntityBlocker *Create( const Vector &origin, const Vector &mins, const Vector &maxs, CBaseEntity *pOwner = NULL, bool bBlockPhysics = false ); + + void Spawn( void ); + bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); +}; + +#endif // ENTITYBLOCKER_H diff --git a/sp/src/game/server/entityinput.h b/sp/src/game/server/entityinput.h new file mode 100644 index 00000000..df6b69be --- /dev/null +++ b/sp/src/game/server/entityinput.h @@ -0,0 +1,50 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef INPUTVAR_H +#define INPUTVAR_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "baseentity.h" +#include "entitylist.h" + +//----------------------------------------------------------------------------- +// Purpose: Used to request a value, or a set of values, from a set of entities. +// used when a multi-input variable needs to refresh it's inputs +//----------------------------------------------------------------------------- +class CMultiInputVar +{ +public: + CMultiInputVar() : m_InputList(NULL) {} + ~CMultiInputVar(); + + struct inputitem_t + { + variant_t value; // local copy of variable (maybe make this a variant?) + int outputID; // the ID number of the output that sent this + inputitem_t *next; + + // allocate and free from MPool memory + static void *operator new( size_t stAllocBlock ); + static void *operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ); + static void operator delete( void *pMem ); + static void operator delete( void *pMem, int nBlockUse, const char *pFileName, int nLine ) { operator delete(pMem); } + }; + + inputitem_t *m_InputList; // list of data + int m_bUpdatedThisFrame; + + void AddValue( variant_t newVal, int outputID ); + + DECLARE_SIMPLE_DATADESC(); +}; + +#endif // INPUTVAR_H diff --git a/sp/src/game/server/entitylist.cpp b/sp/src/game/server/entitylist.cpp new file mode 100644 index 00000000..bb555a1f --- /dev/null +++ b/sp/src/game/server/entitylist.cpp @@ -0,0 +1,1716 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "entitylist.h" +#include "utlvector.h" +#include "igamesystem.h" +#include "collisionutils.h" +#include "UtlSortVector.h" +#include "tier0/vprof.h" +#include "mapentities.h" +#include "client.h" +#include "ai_initutils.h" +#include "globalstate.h" +#include "datacache/imdlcache.h" + +#ifdef HL2_DLL +#include "npc_playercompanion.h" +#ifdef MAPBASE +#include "hl2_player.h" +#endif +#endif // HL2_DLL + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ); +void SceneManager_ClientActive( CBasePlayer *player ); + +static CUtlVector g_DeleteList; + +CGlobalEntityList gEntList; +CBaseEntityList *g_pEntityList = &gEntList; + +class CAimTargetManager : public IEntityListener +{ +public: + // Called by CEntityListSystem + void LevelInitPreEntity() + { + gEntList.AddListenerEntity( this ); + Clear(); + } + void LevelShutdownPostEntity() + { + gEntList.RemoveListenerEntity( this ); + Clear(); + } + + void Clear() + { + m_targetList.Purge(); + } + + void ForceRepopulateList() + { + Clear(); + + CBaseEntity *pEnt = gEntList.FirstEnt(); + + while( pEnt ) + { + if( ShouldAddEntity(pEnt) ) + AddEntity(pEnt); + + pEnt = gEntList.NextEnt( pEnt ); + } + } + + bool ShouldAddEntity( CBaseEntity *pEntity ) + { + return ((pEntity->GetFlags() & FL_AIMTARGET) != 0); + } + + // IEntityListener + virtual void OnEntityCreated( CBaseEntity *pEntity ) {} + virtual void OnEntityDeleted( CBaseEntity *pEntity ) + { + if ( !(pEntity->GetFlags() & FL_AIMTARGET) ) + return; + RemoveEntity(pEntity); + } + void AddEntity( CBaseEntity *pEntity ) + { + if ( pEntity->IsMarkedForDeletion() ) + return; + m_targetList.AddToTail( pEntity ); + } + void RemoveEntity( CBaseEntity *pEntity ) + { + int index = m_targetList.Find( pEntity ); + if ( m_targetList.IsValidIndex(index) ) + { + m_targetList.FastRemove( index ); + } + } + int ListCount() { return m_targetList.Count(); } + int ListCopy( CBaseEntity *pList[], int listMax ) + { + int count = MIN(listMax, ListCount() ); + memcpy( pList, m_targetList.Base(), sizeof(CBaseEntity *) * count ); + return count; + } + +private: + CUtlVector m_targetList; +}; + +static CAimTargetManager g_AimManager; + +int AimTarget_ListCount() +{ + return g_AimManager.ListCount(); +} +int AimTarget_ListCopy( CBaseEntity *pList[], int listMax ) +{ + return g_AimManager.ListCopy( pList, listMax ); +} +void AimTarget_ForceRepopulateList() +{ + g_AimManager.ForceRepopulateList(); +} + + +// Manages a list of all entities currently doing game simulation or thinking +// NOTE: This is usually a small subset of the global entity list, so it's +// an optimization to maintain this list incrementally rather than polling each +// frame. +struct simthinkentry_t +{ + unsigned short entEntry; + unsigned short unused0; + int nextThinkTick; +}; +class CSimThinkManager : public IEntityListener +{ +public: + CSimThinkManager() + { + Clear(); + } + void Clear() + { + m_simThinkList.Purge(); + for ( int i = 0; i < ARRAYSIZE(m_entinfoIndex); i++ ) + { + m_entinfoIndex[i] = 0xFFFF; + } + } + void LevelInitPreEntity() + { + gEntList.AddListenerEntity( this ); + } + + void LevelShutdownPostEntity() + { + gEntList.RemoveListenerEntity( this ); + Clear(); + } + + void OnEntityCreated( CBaseEntity *pEntity ) + { + Assert( m_entinfoIndex[pEntity->GetRefEHandle().GetEntryIndex()] == 0xFFFF ); + } + void OnEntityDeleted( CBaseEntity *pEntity ) + { + RemoveEntinfoIndex( pEntity->GetRefEHandle().GetEntryIndex() ); + } + + void RemoveEntinfoIndex( int index ) + { + int listHandle = m_entinfoIndex[index]; + // If this guy is in the active list, remove him + if ( listHandle != 0xFFFF ) + { + Assert(m_simThinkList[listHandle].entEntry == index); + m_simThinkList.FastRemove( listHandle ); + m_entinfoIndex[index] = 0xFFFF; + + // fast remove shifted someone, update that someone + if ( listHandle < m_simThinkList.Count() ) + { + m_entinfoIndex[m_simThinkList[listHandle].entEntry] = listHandle; + } + } + } + int ListCount() + { + return m_simThinkList.Count(); + } + + int ListCopy( CBaseEntity *pList[], int listMax ) + { + int count = MIN(listMax, ListCount()); + int out = 0; + for ( int i = 0; i < count; i++ ) + { + // only copy out entities that will simulate or think this frame + if ( m_simThinkList[i].nextThinkTick <= gpGlobals->tickcount ) + { + Assert(m_simThinkList[i].nextThinkTick>=0); + int entinfoIndex = m_simThinkList[i].entEntry; + const CEntInfo *pInfo = gEntList.GetEntInfoPtrByIndex( entinfoIndex ); + pList[out] = (CBaseEntity *)pInfo->m_pEntity; + Assert(m_simThinkList[i].nextThinkTick==0 || pList[out]->GetFirstThinkTick()==m_simThinkList[i].nextThinkTick); + Assert( gEntList.IsEntityPtr( pList[out] ) ); + out++; + } + } + + return out; + } + + void EntityChanged( CBaseEntity *pEntity ) + { + // might change after deletion, don't put back into the list + if ( pEntity->IsMarkedForDeletion() ) + return; + + const CBaseHandle &eh = pEntity->GetRefEHandle(); + if ( !eh.IsValid() ) + return; + + int index = eh.GetEntryIndex(); + if ( pEntity->IsEFlagSet( EFL_NO_THINK_FUNCTION ) && pEntity->IsEFlagSet( EFL_NO_GAME_PHYSICS_SIMULATION ) ) + { + RemoveEntinfoIndex( index ); + } + else + { + // already in the list? (had think or sim last time, now has both - or had both last time, now just one) + if ( m_entinfoIndex[index] == 0xFFFF ) + { + MEM_ALLOC_CREDIT(); + m_entinfoIndex[index] = m_simThinkList.AddToTail(); + m_simThinkList[m_entinfoIndex[index]].entEntry = (unsigned short)index; + m_simThinkList[m_entinfoIndex[index]].nextThinkTick = 0; + if ( pEntity->IsEFlagSet(EFL_NO_GAME_PHYSICS_SIMULATION) ) + { + m_simThinkList[m_entinfoIndex[index]].nextThinkTick = pEntity->GetFirstThinkTick(); + Assert(m_simThinkList[m_entinfoIndex[index]].nextThinkTick>=0); + } + } + else + { + // updating existing entry - if no sim, reset think time + if ( pEntity->IsEFlagSet(EFL_NO_GAME_PHYSICS_SIMULATION) ) + { + m_simThinkList[m_entinfoIndex[index]].nextThinkTick = pEntity->GetFirstThinkTick(); + Assert(m_simThinkList[m_entinfoIndex[index]].nextThinkTick>=0); + } + else + { + m_simThinkList[m_entinfoIndex[index]].nextThinkTick = 0; + } + } + } + } + +private: + unsigned short m_entinfoIndex[NUM_ENT_ENTRIES]; + CUtlVector m_simThinkList; +}; + +CSimThinkManager g_SimThinkManager; + +int SimThink_ListCount() +{ + return g_SimThinkManager.ListCount(); +} + +int SimThink_ListCopy( CBaseEntity *pList[], int listMax ) +{ + return g_SimThinkManager.ListCopy( pList, listMax ); +} + +void SimThink_EntityChanged( CBaseEntity *pEntity ) +{ + g_SimThinkManager.EntityChanged( pEntity ); +} + +static CBaseEntityClassList *s_pClassLists = NULL; +CBaseEntityClassList::CBaseEntityClassList() +{ + m_pNextClassList = s_pClassLists; + s_pClassLists = this; +} +CBaseEntityClassList::~CBaseEntityClassList() +{ +} + +CGlobalEntityList::CGlobalEntityList() +{ + m_iHighestEnt = m_iNumEnts = m_iNumEdicts = 0; + m_bClearingEntities = false; +} + + +// removes the entity from the global list +// only called from with the CBaseEntity destructor +static bool g_fInCleanupDelete; + + +// mark an entity as deleted +void CGlobalEntityList::AddToDeleteList( IServerNetworkable *ent ) +{ + if ( ent && ent->GetEntityHandle()->GetRefEHandle() != INVALID_EHANDLE_INDEX ) + { + g_DeleteList.AddToTail( ent ); + } +} + +extern bool g_bDisableEhandleAccess; +// call this before and after each frame to delete all of the marked entities. +void CGlobalEntityList::CleanupDeleteList( void ) +{ + VPROF( "CGlobalEntityList::CleanupDeleteList" ); + g_fInCleanupDelete = true; + // clean up the vphysics delete list as well + PhysOnCleanupDeleteList(); + + g_bDisableEhandleAccess = true; + for ( int i = 0; i < g_DeleteList.Count(); i++ ) + { + g_DeleteList[i]->Release(); + } + g_bDisableEhandleAccess = false; + g_DeleteList.RemoveAll(); + + g_fInCleanupDelete = false; +} + +int CGlobalEntityList::ResetDeleteList( void ) +{ + int result = g_DeleteList.Count(); + g_DeleteList.RemoveAll(); + return result; +} + + + // add a class that gets notified of entity events +void CGlobalEntityList::AddListenerEntity( IEntityListener *pListener ) +{ + if ( m_entityListeners.Find( pListener ) >= 0 ) + { + AssertMsg( 0, "Can't add listeners multiple times\n" ); + return; + } + m_entityListeners.AddToTail( pListener ); +} + +void CGlobalEntityList::RemoveListenerEntity( IEntityListener *pListener ) +{ + m_entityListeners.FindAndRemove( pListener ); +} + +void CGlobalEntityList::Clear( void ) +{ + m_bClearingEntities = true; + + // Add all remaining entities in the game to the delete list and call appropriate UpdateOnRemove + CBaseHandle hCur = FirstHandle(); + while ( hCur != InvalidHandle() ) + { + IServerNetworkable *ent = GetServerNetworkable( hCur ); + if ( ent ) + { + MDLCACHE_CRITICAL_SECTION(); + // Force UpdateOnRemove to be called + UTIL_Remove( ent ); + } + hCur = NextHandle( hCur ); + } + + CleanupDeleteList(); + // free the memory + g_DeleteList.Purge(); + + CBaseEntity::m_nDebugPlayer = -1; + CBaseEntity::m_bInDebugSelect = false; + m_iHighestEnt = 0; + m_iNumEnts = 0; + + m_bClearingEntities = false; +} + + +int CGlobalEntityList::NumberOfEntities( void ) +{ + return m_iNumEnts; +} + +int CGlobalEntityList::NumberOfEdicts( void ) +{ + return m_iNumEdicts; +} + +CBaseEntity *CGlobalEntityList::NextEnt( CBaseEntity *pCurrentEnt ) +{ + if ( !pCurrentEnt ) + { + const CEntInfo *pInfo = FirstEntInfo(); + if ( !pInfo ) + return NULL; + + return (CBaseEntity *)pInfo->m_pEntity; + } + + // Run through the list until we get a CBaseEntity. + const CEntInfo *pList = GetEntInfoPtr( pCurrentEnt->GetRefEHandle() ); + if ( pList ) + pList = NextEntInfo(pList); + + while ( pList ) + { +#if 0 + if ( pList->m_pEntity ) + { + IServerUnknown *pUnk = static_cast(const_cast(pList->m_pEntity)); + CBaseEntity *pRet = pUnk->GetBaseEntity(); + if ( pRet ) + return pRet; + } +#else + return (CBaseEntity *)pList->m_pEntity; +#endif + pList = pList->m_pNext; + } + + return NULL; + +} + + +void CGlobalEntityList::ReportEntityFlagsChanged( CBaseEntity *pEntity, unsigned int flagsOld, unsigned int flagsNow ) +{ + if ( pEntity->IsMarkedForDeletion() ) + return; + // UNDONE: Move this into IEntityListener instead? + unsigned int flagsChanged = flagsOld ^ flagsNow; + if ( flagsChanged & FL_AIMTARGET ) + { + unsigned int flagsAdded = flagsNow & flagsChanged; + unsigned int flagsRemoved = flagsOld & flagsChanged; + + if ( flagsAdded & FL_AIMTARGET ) + { + g_AimManager.AddEntity( pEntity ); + } + if ( flagsRemoved & FL_AIMTARGET ) + { + g_AimManager.RemoveEntity( pEntity ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Used to confirm a pointer is a pointer to an entity, useful for +// asserts. +//----------------------------------------------------------------------------- +bool CGlobalEntityList::IsEntityPtr( void *pTest ) +{ + if ( pTest ) + { + const CEntInfo *pInfo = FirstEntInfo(); + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + if ( pTest == (void *)pInfo->m_pEntity ) + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Iterates the entities with a given classname. +// Input : pStartEntity - Last entity found, NULL to start a new iteration. +// szName - Classname to search for. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +CBaseEntity *CGlobalEntityList::FindEntityByClassname( CBaseEntity *pStartEntity, const char *szName, IEntityFindFilter *pFilter ) +#else +CBaseEntity *CGlobalEntityList::FindEntityByClassname( CBaseEntity *pStartEntity, const char *szName ) +#endif +{ + const CEntInfo *pInfo = pStartEntity ? GetEntInfoPtr( pStartEntity->GetRefEHandle() )->m_pNext : FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *pEntity = (CBaseEntity *)pInfo->m_pEntity; + if ( !pEntity ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + +#ifdef MAPBASE + if ( pEntity->ClassMatches(szName) ) + { + if ( pFilter && !pFilter->ShouldFindEntity(pEntity) ) + continue; + + return pEntity; + } +#else + if ( pEntity->ClassMatches(szName) ) + return pEntity; +#endif + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds an entity given a procedural name. +// Input : szName - The procedural name to search for, should start with '!'. +// pSearchingEntity - +// pActivator - The activator entity if this was called from an input +// or Use handler. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityProcedural( const char *szName, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + // + // Check for the name escape character. + // + if ( szName[0] == '!' ) + { + const char *pName = szName + 1; + + // + // It is a procedural name, look for the ones we understand. + // + if ( FStrEq( pName, "player" ) ) + { + return (CBaseEntity *)UTIL_PlayerByIndex( 1 ); + } + else if ( FStrEq( pName, "pvsplayer" ) ) + { + if ( pSearchingEntity ) + { + return CBaseEntity::Instance( UTIL_FindClientInPVS( pSearchingEntity->edict() ) ); + } + else if ( pActivator ) + { + // FIXME: error condition? + return CBaseEntity::Instance( UTIL_FindClientInPVS( pActivator->edict() ) ); + } + else + { + // FIXME: error condition? + return (CBaseEntity *)UTIL_PlayerByIndex( 1 ); + } + + } + else if ( FStrEq( pName, "activator" ) ) + { + return pActivator; + } + else if ( FStrEq( pName, "caller" ) ) + { + return pCaller; + } + else if ( FStrEq( pName, "picker" ) ) + { +#ifdef MAPBASE_MP + // TODO: Player could be activator instead + CBasePlayer *pPlayer = ToBasePlayer(pSearchingEntity); + return FindPickerEntity( pPlayer ? pPlayer : UTIL_PlayerByIndex(1) ); +#else + return FindPickerEntity( UTIL_PlayerByIndex(1) ); +#endif + } + else if ( FStrEq( pName, "self" ) ) + { + return pSearchingEntity; + } +#ifdef MAPBASE + else if ( FStrEq( pName, "parent" ) ) + { + return pSearchingEntity ? pSearchingEntity->GetParent() : NULL; + } + else if ( FStrEq( pName, "owner" ) ) + { + return pSearchingEntity ? pSearchingEntity->GetOwnerEntity() : NULL; + } + else if ( FStrEq( pName, "plrsquadrep" ) ) + { +#ifdef HL2_DLL + CHL2_Player *pPlayer = static_cast(UTIL_PlayerByIndex(1)); + if (pPlayer) + { + return pPlayer->GetSquadCommandRepresentative(); + } +#endif + } + else if (strchr(pName, ':')) + { + char name[128]; + Q_strncpy(name, pName, strchr(pName, ':')-pName+1); + + CBaseEntity *pEntity = FindEntityProcedural(UTIL_VarArgs("!%s", name), pSearchingEntity, pActivator, pCaller); + if (pEntity && pEntity->IsNPC()) + { + const char *target = (Q_strstr(pName, ":") + 1); + if (target[0] != '!') + target = UTIL_VarArgs("!%s", target); + + return pEntity->MyNPCPointer()->FindNamedEntity(target); + } + } + else if (pSearchingEntity && pSearchingEntity->IsCombatCharacter()) + { + // Perhaps the entity itself has the answer? + // This opens up new possibilities. The weird filter is there so it doesn't go through this twice. + CNullEntityFilter pFilter; + return pSearchingEntity->MyCombatCharacterPointer()->FindNamedEntity(szName, &pFilter); + } +#endif + else + { + Warning( "Invalid entity search name %s\n", szName ); + Assert(0); + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Iterates the entities with a given name. +// Input : pStartEntity - Last entity found, NULL to start a new iteration. +// szName - Name to search for. +// pActivator - Activator entity if this was called from an input +// handler or Use handler. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityByName( CBaseEntity *pStartEntity, const char *szName, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller, IEntityFindFilter *pFilter ) +{ + if ( !szName || szName[0] == 0 ) + return NULL; + + if ( szName[0] == '!' ) + { + // + // Avoid an infinite loop, only find one match per procedural search! + // + if (pStartEntity == NULL) + return FindEntityProcedural( szName, pSearchingEntity, pActivator, pCaller ); + + return NULL; + } + + const CEntInfo *pInfo = pStartEntity ? GetEntInfoPtr( pStartEntity->GetRefEHandle() )->m_pNext : FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if ( !ent ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + + if ( !ent->m_iName.Get() ) + continue; + + if ( ent->NameMatches( szName ) ) + { + if ( pFilter && !pFilter->ShouldFindEntity(ent) ) + continue; + + return ent; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pStartEntity - +// szModelName - +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityByModel( CBaseEntity *pStartEntity, const char *szModelName ) +{ + const CEntInfo *pInfo = pStartEntity ? GetEntInfoPtr( pStartEntity->GetRefEHandle() )->m_pNext : FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if ( !ent ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + + if ( !ent->edict() || !ent->GetModelName() ) + continue; + + if ( FStrEq( STRING(ent->GetModelName()), szModelName ) ) + return ent; + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Iterates the entities with a given target. +// Input : pStartEntity - +// szName - +//----------------------------------------------------------------------------- +// FIXME: obsolete, remove +CBaseEntity *CGlobalEntityList::FindEntityByTarget( CBaseEntity *pStartEntity, const char *szName ) +{ + const CEntInfo *pInfo = pStartEntity ? GetEntInfoPtr( pStartEntity->GetRefEHandle() )->m_pNext : FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if ( !ent ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + + if ( !ent->m_target ) + continue; + + if ( FStrEq( STRING(ent->m_target), szName ) ) + return ent; + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Used to iterate all the entities within a sphere. +// Input : pStartEntity - +// vecCenter - +// flRadius - +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityInSphere( CBaseEntity *pStartEntity, const Vector &vecCenter, float flRadius ) +{ + const CEntInfo *pInfo = pStartEntity ? GetEntInfoPtr( pStartEntity->GetRefEHandle() )->m_pNext : FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if ( !ent ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + + if ( !ent->edict() ) + continue; + + Vector vecRelativeCenter; + ent->CollisionProp()->WorldToCollisionSpace( vecCenter, &vecRelativeCenter ); + if ( !IsBoxIntersectingSphere( ent->CollisionProp()->OBBMins(), ent->CollisionProp()->OBBMaxs(), vecRelativeCenter, flRadius ) ) + continue; + + return ent; + } + + // nothing found + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the nearest entity by name within a radius +// Input : szName - Entity name to search for. +// vecSrc - Center of search radius. +// flRadius - Search radius for classname search, 0 to search everywhere. +// pSearchingEntity - The entity that is doing the search. +// pActivator - The activator entity if this was called from an input +// or Use handler, NULL otherwise. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityByNameNearest( const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + CBaseEntity *pEntity = NULL; + + // + // Check for matching class names within the search radius. + // + float flMaxDist2 = flRadius * flRadius; + if (flMaxDist2 == 0) + { + flMaxDist2 = MAX_TRACE_LENGTH * MAX_TRACE_LENGTH; + } + + CBaseEntity *pSearch = NULL; + while ((pSearch = gEntList.FindEntityByName( pSearch, szName, pSearchingEntity, pActivator, pCaller )) != NULL) + { + if ( !pSearch->edict() ) + continue; + + float flDist2 = (pSearch->GetAbsOrigin() - vecSrc).LengthSqr(); + + if (flMaxDist2 > flDist2) + { + pEntity = pSearch; + flMaxDist2 = flDist2; + } + } + + return pEntity; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Finds the first entity by name within a radius +// Input : pStartEntity - The entity to start from when doing the search. +// szName - Entity name to search for. +// vecSrc - Center of search radius. +// flRadius - Search radius for classname search, 0 to search everywhere. +// pSearchingEntity - The entity that is doing the search. +// pActivator - The activator entity if this was called from an input +// or Use handler, NULL otherwise. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityByNameWithin( CBaseEntity *pStartEntity, const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + // + // Check for matching class names within the search radius. + // + CBaseEntity *pEntity = pStartEntity; + float flMaxDist2 = flRadius * flRadius; + if (flMaxDist2 == 0) + { + return gEntList.FindEntityByName( pEntity, szName, pSearchingEntity, pActivator, pCaller ); + } + + while ((pEntity = gEntList.FindEntityByName( pEntity, szName, pSearchingEntity, pActivator, pCaller )) != NULL) + { + if ( !pEntity->edict() ) + continue; + + float flDist2 = (pEntity->GetAbsOrigin() - vecSrc).LengthSqr(); + + if (flMaxDist2 > flDist2) + { + return pEntity; + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the nearest entity by class name withing given search radius. +// Input : szName - Entity name to search for. Treated as a target name first, +// then as an entity class name, ie "info_target". +// vecSrc - Center of search radius. +// flRadius - Search radius for classname search, 0 to search everywhere. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityByClassnameNearest( const char *szName, const Vector &vecSrc, float flRadius ) +{ + CBaseEntity *pEntity = NULL; + + // + // Check for matching class names within the search radius. + // + float flMaxDist2 = flRadius * flRadius; + if (flMaxDist2 == 0) + { + flMaxDist2 = MAX_TRACE_LENGTH * MAX_TRACE_LENGTH; + } + + CBaseEntity *pSearch = NULL; + while ((pSearch = gEntList.FindEntityByClassname( pSearch, szName )) != NULL) + { + if ( !pSearch->edict() ) + continue; + + float flDist2 = (pSearch->GetAbsOrigin() - vecSrc).LengthSqr(); + + if (flMaxDist2 > flDist2) + { + pEntity = pSearch; + flMaxDist2 = flDist2; + } + } + + return pEntity; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Finds the first entity within radius distance by class name. +// Input : pStartEntity - The entity to start from when doing the search. +// szName - Entity class name, ie "info_target". +// vecSrc - Center of search radius. +// flRadius - Search radius for classname search, 0 to search everywhere. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityByClassnameWithin( CBaseEntity *pStartEntity, const char *szName, const Vector &vecSrc, float flRadius ) +{ + // + // Check for matching class names within the search radius. + // + CBaseEntity *pEntity = pStartEntity; + float flMaxDist2 = flRadius * flRadius; + if (flMaxDist2 == 0) + { + return gEntList.FindEntityByClassname( pEntity, szName ); + } + + while ((pEntity = gEntList.FindEntityByClassname( pEntity, szName )) != NULL) + { + if ( !pEntity->edict() ) + continue; + + float flDist2 = (pEntity->GetAbsOrigin() - vecSrc).LengthSqr(); + + if (flMaxDist2 > flDist2) + { + return pEntity; + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the first entity within an extent by class name. +// Input : pStartEntity - The entity to start from when doing the search. +// szName - Entity class name, ie "info_target". +// vecMins - Search mins. +// vecMaxs - Search maxs. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityByClassnameWithin( CBaseEntity *pStartEntity, const char *szName, const Vector &vecMins, const Vector &vecMaxs ) +{ + // + // Check for matching class names within the search radius. + // + CBaseEntity *pEntity = pStartEntity; + + while ((pEntity = gEntList.FindEntityByClassname( pEntity, szName )) != NULL) + { + if ( !pEntity->edict() && !pEntity->IsEFlagSet( EFL_SERVER_ONLY ) ) + continue; + + // check if the aabb intersects the search aabb. + Vector entMins, entMaxs; + pEntity->CollisionProp()->WorldSpaceAABB( &entMins, &entMaxs ); + if ( IsBoxIntersectingBox( vecMins, vecMaxs, entMins, entMaxs ) ) + { + return pEntity; + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds an entity by target name or class name. +// Input : pStartEntity - The entity to start from when doing the search. +// szName - Entity name to search for. Treated as a target name first, +// then as an entity class name, ie "info_target". +// vecSrc - Center of search radius. +// flRadius - Search radius for classname search, 0 to search everywhere. +// pSearchingEntity - The entity that is doing the search. +// pActivator - The activator entity if this was called from an input +// or Use handler, NULL otherwise. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +CBaseEntity *CGlobalEntityList::FindEntityGeneric( CBaseEntity *pStartEntity, const char *szName, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller, IEntityFindFilter *pFilter ) +{ + CBaseEntity *pEntity = NULL; + + pEntity = gEntList.FindEntityByName( pStartEntity, szName, pSearchingEntity, pActivator, pCaller, pFilter ); + if (!pEntity) + { + pEntity = gEntList.FindEntityByClassname( pStartEntity, szName, pFilter ); + } + + return pEntity; +} +#else +CBaseEntity *CGlobalEntityList::FindEntityGeneric( CBaseEntity *pStartEntity, const char *szName, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + CBaseEntity *pEntity = NULL; + + pEntity = gEntList.FindEntityByName( pStartEntity, szName, pSearchingEntity, pActivator, pCaller ); + if (!pEntity) + { + pEntity = gEntList.FindEntityByClassname( pStartEntity, szName ); + } + + return pEntity; +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Finds the first entity by target name or class name within a radius +// Input : pStartEntity - The entity to start from when doing the search. +// szName - Entity name to search for. Treated as a target name first, +// then as an entity class name, ie "info_target". +// vecSrc - Center of search radius. +// flRadius - Search radius for classname search, 0 to search everywhere. +// pSearchingEntity - The entity that is doing the search. +// pActivator - The activator entity if this was called from an input +// or Use handler, NULL otherwise. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityGenericWithin( CBaseEntity *pStartEntity, const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + CBaseEntity *pEntity = NULL; + + pEntity = gEntList.FindEntityByNameWithin( pStartEntity, szName, vecSrc, flRadius, pSearchingEntity, pActivator, pCaller ); + if (!pEntity) + { + pEntity = gEntList.FindEntityByClassnameWithin( pStartEntity, szName, vecSrc, flRadius ); + } + + return pEntity; +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the nearest entity by target name or class name within a radius. +// Input : pStartEntity - The entity to start from when doing the search. +// szName - Entity name to search for. Treated as a target name first, +// then as an entity class name, ie "info_target". +// vecSrc - Center of search radius. +// flRadius - Search radius for classname search, 0 to search everywhere. +// pSearchingEntity - The entity that is doing the search. +// pActivator - The activator entity if this was called from an input +// or Use handler, NULL otherwise. +// Output : Returns a pointer to the found entity, NULL if none. +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityGenericNearest( const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + CBaseEntity *pEntity = NULL; + + pEntity = gEntList.FindEntityByNameNearest( szName, vecSrc, flRadius, pSearchingEntity, pActivator, pCaller ); + if (!pEntity) + { + pEntity = gEntList.FindEntityByClassnameNearest( szName, vecSrc, flRadius ); + } + + return pEntity; +} + + +//----------------------------------------------------------------------------- +// Purpose: Find the nearest entity along the facing direction from the given origin +// within the angular threshold (ignores worldspawn) with the +// given classname. +// Input : origin - +// facing - +// threshold - +// classname - +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityClassNearestFacing( const Vector &origin, const Vector &facing, float threshold, char *classname) +{ + float bestDot = threshold; + CBaseEntity *best_ent = NULL; + + const CEntInfo *pInfo = FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if ( !ent ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + + // FIXME: why is this skipping pointsize entities? + if (ent->IsPointSized() ) + continue; + + // Make vector to entity + Vector to_ent = (ent->GetAbsOrigin() - origin); + + VectorNormalize( to_ent ); + float dot = DotProduct (facing , to_ent ); + if (dot > bestDot) + { + if (FClassnameIs(ent,classname)) + { + // Ignore if worldspawn + if (!FClassnameIs( ent, "worldspawn" ) && !FClassnameIs( ent, "soundent")) + { + bestDot = dot; + best_ent = ent; + } + } + } + } + return best_ent; +} + + +//----------------------------------------------------------------------------- +// Purpose: Find the nearest entity along the facing direction from the given origin +// within the angular threshold (ignores worldspawn) +// Input : origin - +// facing - +// threshold - +//----------------------------------------------------------------------------- +CBaseEntity *CGlobalEntityList::FindEntityNearestFacing( const Vector &origin, const Vector &facing, float threshold) +{ + float bestDot = threshold; + CBaseEntity *best_ent = NULL; + + const CEntInfo *pInfo = FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if ( !ent ) + { + DevWarning( "NULL entity in global entity list!\n" ); + continue; + } + + // Ignore logical entities + if (!ent->edict()) + continue; + + // Make vector to entity + Vector to_ent = ent->WorldSpaceCenter() - origin; + VectorNormalize(to_ent); + + float dot = DotProduct( facing, to_ent ); + if (dot <= bestDot) + continue; + + // Ignore if worldspawn + if (!FStrEq( STRING(ent->m_iClassname), "worldspawn") && !FStrEq( STRING(ent->m_iClassname), "soundent")) + { + bestDot = dot; + best_ent = ent; + } + } + return best_ent; +} + + +void CGlobalEntityList::OnAddEntity( IHandleEntity *pEnt, CBaseHandle handle ) +{ + int i = handle.GetEntryIndex(); + + // record current list details + m_iNumEnts++; + if ( i > m_iHighestEnt ) + m_iHighestEnt = i; + + // If it's a CBaseEntity, notify the listeners. + CBaseEntity *pBaseEnt = static_cast(pEnt)->GetBaseEntity(); + if ( pBaseEnt->edict() ) + m_iNumEdicts++; + + // NOTE: Must be a CBaseEntity on server + Assert( pBaseEnt ); + //DevMsg(2,"Created %s\n", pBaseEnt->GetClassname() ); + for ( i = m_entityListeners.Count()-1; i >= 0; i-- ) + { + m_entityListeners[i]->OnEntityCreated( pBaseEnt ); + } +} + + +void CGlobalEntityList::OnRemoveEntity( IHandleEntity *pEnt, CBaseHandle handle ) +{ +#ifdef DEBUG + if ( !g_fInCleanupDelete ) + { + int i; + for ( i = 0; i < g_DeleteList.Count(); i++ ) + { + if ( g_DeleteList[i]->GetEntityHandle() == pEnt ) + { + g_DeleteList.FastRemove( i ); + Msg( "ERROR: Entity being destroyed but previously threaded on g_DeleteList\n" ); + break; + } + } + } +#endif + + CBaseEntity *pBaseEnt = static_cast(pEnt)->GetBaseEntity(); + if ( pBaseEnt->edict() ) + m_iNumEdicts--; + + m_iNumEnts--; +} + +void CGlobalEntityList::NotifyCreateEntity( CBaseEntity *pEnt ) +{ + if ( !pEnt ) + return; + + //DevMsg(2,"Deleted %s\n", pBaseEnt->GetClassname() ); + for ( int i = m_entityListeners.Count()-1; i >= 0; i-- ) + { + m_entityListeners[i]->OnEntityCreated( pEnt ); + } +} + +void CGlobalEntityList::NotifySpawn( CBaseEntity *pEnt ) +{ + if ( !pEnt ) + return; + + //DevMsg(2,"Deleted %s\n", pBaseEnt->GetClassname() ); + for ( int i = m_entityListeners.Count()-1; i >= 0; i-- ) + { + m_entityListeners[i]->OnEntitySpawned( pEnt ); + } +} + +// NOTE: This doesn't happen in OnRemoveEntity() specifically because +// listeners may want to reference the object as it's being deleted +// OnRemoveEntity isn't called until the destructor and all data is invalid. +void CGlobalEntityList::NotifyRemoveEntity( CBaseHandle hEnt ) +{ + CBaseEntity *pBaseEnt = GetBaseEntity( hEnt ); + if ( !pBaseEnt ) + return; + + //DevMsg(2,"Deleted %s\n", pBaseEnt->GetClassname() ); + for ( int i = m_entityListeners.Count()-1; i >= 0; i-- ) + { + m_entityListeners[i]->OnEntityDeleted( pBaseEnt ); + } +} + + +//----------------------------------------------------------------------------- +// NOTIFY LIST +// +// Allows entities to get events fired when another entity changes +//----------------------------------------------------------------------------- +struct entitynotify_t +{ + CBaseEntity *pNotify; + CBaseEntity *pWatched; +}; +class CNotifyList : public INotify, public IEntityListener +{ +public: + // INotify + void AddEntity( CBaseEntity *pNotify, CBaseEntity *pWatched ); + void RemoveEntity( CBaseEntity *pNotify, CBaseEntity *pWatched ); + void ReportNamedEvent( CBaseEntity *pEntity, const char *pEventName ); + void ClearEntity( CBaseEntity *pNotify ); + void ReportSystemEvent( CBaseEntity *pEntity, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ); + + // IEntityListener + virtual void OnEntityCreated( CBaseEntity *pEntity ); + virtual void OnEntityDeleted( CBaseEntity *pEntity ); + + // Called from CEntityListSystem + void LevelInitPreEntity(); + void LevelShutdownPreEntity(); + +private: + CUtlVector m_notifyList; +}; + +void CNotifyList::AddEntity( CBaseEntity *pNotify, CBaseEntity *pWatched ) +{ + // OPTIMIZE: Also flag pNotify for faster "RemoveAllNotify" ? + pWatched->AddEFlags( EFL_NOTIFY ); + int index = m_notifyList.AddToTail(); + entitynotify_t ¬ify = m_notifyList[index]; + notify.pNotify = pNotify; + notify.pWatched = pWatched; +} + +// Remove noitfication for an entity +void CNotifyList::RemoveEntity( CBaseEntity *pNotify, CBaseEntity *pWatched ) +{ + for ( int i = m_notifyList.Count(); --i >= 0; ) + { + if ( m_notifyList[i].pNotify == pNotify && m_notifyList[i].pWatched == pWatched) + { + m_notifyList.FastRemove(i); + } + } +} + + +void CNotifyList::ReportNamedEvent( CBaseEntity *pEntity, const char *pInputName ) +{ + variant_t emptyVariant; + + if ( !pEntity->IsEFlagSet(EFL_NOTIFY) ) + return; + + for ( int i = 0; i < m_notifyList.Count(); i++ ) + { + if ( m_notifyList[i].pWatched == pEntity ) + { + m_notifyList[i].pNotify->AcceptInput( pInputName, pEntity, pEntity, emptyVariant, 0 ); + } + } +} + +void CNotifyList::LevelInitPreEntity() +{ + gEntList.AddListenerEntity( this ); +} + +void CNotifyList::LevelShutdownPreEntity( void ) +{ + gEntList.RemoveListenerEntity( this ); + m_notifyList.Purge(); +} + +void CNotifyList::OnEntityCreated( CBaseEntity *pEntity ) +{ +} + +void CNotifyList::OnEntityDeleted( CBaseEntity *pEntity ) +{ + ReportDestroyEvent( pEntity ); + ClearEntity( pEntity ); +} + + +// UNDONE: Slow linear search? +void CNotifyList::ClearEntity( CBaseEntity *pNotify ) +{ + for ( int i = m_notifyList.Count(); --i >= 0; ) + { + if ( m_notifyList[i].pNotify == pNotify || m_notifyList[i].pWatched == pNotify) + { + m_notifyList.FastRemove(i); + } + } +} + +void CNotifyList::ReportSystemEvent( CBaseEntity *pEntity, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ) +{ + if ( !pEntity->IsEFlagSet(EFL_NOTIFY) ) + return; + + for ( int i = 0; i < m_notifyList.Count(); i++ ) + { + if ( m_notifyList[i].pWatched == pEntity ) + { + m_notifyList[i].pNotify->NotifySystemEvent( pEntity, eventType, params ); + } + } +} + +static CNotifyList g_NotifyList; +INotify *g_pNotify = &g_NotifyList; + +class CEntityTouchManager : public IEntityListener +{ +public: + // called by CEntityListSystem + void LevelInitPreEntity() + { + gEntList.AddListenerEntity( this ); + Clear(); + } + void LevelShutdownPostEntity() + { + gEntList.RemoveListenerEntity( this ); + Clear(); + } + void FrameUpdatePostEntityThink(); + + void Clear() + { + m_updateList.Purge(); + } + + // IEntityListener + virtual void OnEntityCreated( CBaseEntity *pEntity ) {} + virtual void OnEntityDeleted( CBaseEntity *pEntity ) + { + if ( !pEntity->GetCheckUntouch() ) + return; + int index = m_updateList.Find( pEntity ); + if ( m_updateList.IsValidIndex(index) ) + { + m_updateList.FastRemove( index ); + } + } + void AddEntity( CBaseEntity *pEntity ) + { + if ( pEntity->IsMarkedForDeletion() ) + return; + m_updateList.AddToTail( pEntity ); + } + +private: + CUtlVector m_updateList; +}; + +static CEntityTouchManager g_TouchManager; + +void EntityTouch_Add( CBaseEntity *pEntity ) +{ + g_TouchManager.AddEntity( pEntity ); +} + + +void CEntityTouchManager::FrameUpdatePostEntityThink() +{ + VPROF( "CEntityTouchManager::FrameUpdatePostEntityThink" ); + // Loop through all entities again, checking their untouch if flagged to do so + + int count = m_updateList.Count(); + if ( count ) + { + // copy off the list + CBaseEntity **ents = (CBaseEntity **)stackalloc( sizeof(CBaseEntity *) * count ); + memcpy( ents, m_updateList.Base(), sizeof(CBaseEntity *) * count ); + // clear it + m_updateList.RemoveAll(); + + // now update those ents + for ( int i = 0; i < count; i++ ) + { + //Assert( ents[i]->GetCheckUntouch() ); + if ( ents[i]->GetCheckUntouch() ) + { + ents[i]->PhysicsCheckForEntityUntouch(); + } + } + stackfree( ents ); + } +} + +class CRespawnEntitiesFilter : public IMapEntityFilter +{ +public: + virtual bool ShouldCreateEntity( const char *pClassname ) + { + // Create everything but the world + return Q_stricmp( pClassname, "worldspawn" ) != 0; + } + + virtual CBaseEntity* CreateNextEntity( const char *pClassname ) + { + return CreateEntityByName( pClassname ); + } +}; + +// One hook to rule them all... +// Since most of the little list managers in here only need one or two of the game +// system callbacks, this hook is a game system that passes them the appropriate callbacks +class CEntityListSystem : public CAutoGameSystemPerFrame +{ +public: + CEntityListSystem( char const *name ) : CAutoGameSystemPerFrame( name ) + { + m_bRespawnAllEntities = false; + } + void LevelInitPreEntity() + { + g_NotifyList.LevelInitPreEntity(); + g_TouchManager.LevelInitPreEntity(); + g_AimManager.LevelInitPreEntity(); + g_SimThinkManager.LevelInitPreEntity(); +#ifdef HL2_DLL + OverrideMoveCache_LevelInitPreEntity(); +#endif // HL2_DLL + } + void LevelShutdownPreEntity() + { + g_NotifyList.LevelShutdownPreEntity(); + } + void LevelShutdownPostEntity() + { + g_TouchManager.LevelShutdownPostEntity(); + g_AimManager.LevelShutdownPostEntity(); + g_SimThinkManager.LevelShutdownPostEntity(); +#ifdef HL2_DLL + OverrideMoveCache_LevelShutdownPostEntity(); +#endif // HL2_DLL + CBaseEntityClassList *pClassList = s_pClassLists; + while ( pClassList ) + { + pClassList->LevelShutdownPostEntity(); + pClassList = pClassList->m_pNextClassList; + } + } + + void FrameUpdatePostEntityThink() + { + g_TouchManager.FrameUpdatePostEntityThink(); + + if ( m_bRespawnAllEntities ) + { + m_bRespawnAllEntities = false; + + // Don't change globalstate owing to deletion here + GlobalEntity_EnableStateUpdates( false ); + + // Remove all entities + int nPlayerIndex = -1; + CBaseEntity *pEnt = gEntList.FirstEnt(); + while ( pEnt ) + { + CBaseEntity *pNextEnt = gEntList.NextEnt( pEnt ); + if ( pEnt->IsPlayer() ) + { + nPlayerIndex = pEnt->entindex(); + } + if ( !pEnt->IsEFlagSet( EFL_KEEP_ON_RECREATE_ENTITIES ) ) + { + UTIL_Remove( pEnt ); + } + pEnt = pNextEnt; + } + + gEntList.CleanupDeleteList(); + + GlobalEntity_EnableStateUpdates( true ); + + // Allows us to immediately re-use the edict indices we just freed to avoid edict overflow + engine->AllowImmediateEdictReuse(); + + // Reset node counter used during load + CNodeEnt::m_nNodeCount = 0; + + CRespawnEntitiesFilter filter; + MapEntity_ParseAllEntities( engine->GetMapEntitiesString(), &filter, true ); + + // Allocate a CBasePlayer for pev, and call spawn + if ( nPlayerIndex >= 0 ) + { + edict_t *pEdict = engine->PEntityOfEntIndex( nPlayerIndex ); + ClientPutInServer( pEdict, "unnamed" ); + ClientActive( pEdict, false ); + + CBasePlayer *pPlayer = ( CBasePlayer * )CBaseEntity::Instance( pEdict ); + SceneManager_ClientActive( pPlayer ); + } + } + } + + bool m_bRespawnAllEntities; +}; + +static CEntityListSystem g_EntityListSystem( "CEntityListSystem" ); + +//----------------------------------------------------------------------------- +// Respawns all entities in the level +//----------------------------------------------------------------------------- +void RespawnEntities() +{ + g_EntityListSystem.m_bRespawnAllEntities = true; +} + +static ConCommand restart_entities( "respawn_entities", RespawnEntities, "Respawn all the entities in the map.", FCVAR_CHEAT | FCVAR_SPONLY ); + +class CSortedEntityList +{ +public: + CSortedEntityList() : m_sortedList(), m_emptyCount(0) {} + + typedef CBaseEntity *ENTITYPTR; + class CEntityReportLess + { + public: + bool Less( const ENTITYPTR &src1, const ENTITYPTR &src2, void *pCtx ) + { + if ( stricmp( src1->GetClassname(), src2->GetClassname() ) < 0 ) + return true; + + return false; + } + }; + + void AddEntityToList( CBaseEntity *pEntity ) + { + if ( !pEntity ) + { + m_emptyCount++; + } + else + { + m_sortedList.Insert( pEntity ); + } + } + void ReportEntityList() + { + const char *pLastClass = ""; + int count = 0; + int edicts = 0; + for ( int i = 0; i < m_sortedList.Count(); i++ ) + { + CBaseEntity *pEntity = m_sortedList[i]; + if ( !pEntity ) + continue; + + if ( pEntity->edict() ) + edicts++; + + const char *pClassname = pEntity->GetClassname(); + if ( !FStrEq( pClassname, pLastClass ) ) + { + if ( count ) + { + Msg("Class: %s (%d)\n", pLastClass, count ); + } + + pLastClass = pClassname; + count = 1; + } + else + count++; + } + if ( pLastClass[0] != 0 && count ) + { + Msg("Class: %s (%d)\n", pLastClass, count ); + } + if ( m_sortedList.Count() ) + { + Msg("Total %d entities (%d empty, %d edicts)\n", m_sortedList.Count(), m_emptyCount, edicts ); + } + } +private: + CUtlSortVector< CBaseEntity *, CEntityReportLess > m_sortedList; + int m_emptyCount; +}; + + + +CON_COMMAND(report_entities, "Lists all entities") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CSortedEntityList list; + CBaseEntity *pEntity = gEntList.FirstEnt(); + while ( pEntity ) + { + list.AddEntityToList( pEntity ); + pEntity = gEntList.NextEnt( pEntity ); + } + list.ReportEntityList(); +} + + +CON_COMMAND(report_touchlinks, "Lists all touchlinks") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CSortedEntityList list; + CBaseEntity *pEntity = gEntList.FirstEnt(); + const char *pClassname = NULL; + if ( args.ArgC() > 1 ) + { + pClassname = args.Arg(1); + } + while ( pEntity ) + { + if ( !pClassname || FClassnameIs(pEntity, pClassname) ) + { + touchlink_t *root = ( touchlink_t * )pEntity->GetDataObject( TOUCHLINK ); + if ( root ) + { + touchlink_t *link = root->nextLink; + while ( link != root ) + { + list.AddEntityToList( link->entityTouched ); + link = link->nextLink; + } + } + } + pEntity = gEntList.NextEnt( pEntity ); + } + list.ReportEntityList(); +} + +CON_COMMAND(report_simthinklist, "Lists all simulating/thinking entities") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CBaseEntity *pTmp[NUM_ENT_ENTRIES]; + int count = SimThink_ListCopy( pTmp, ARRAYSIZE(pTmp) ); + + CSortedEntityList list; + for ( int i = 0; i < count; i++ ) + { + if ( !pTmp[i] ) + continue; + + list.AddEntityToList( pTmp[i] ); + } + list.ReportEntityList(); +} + diff --git a/sp/src/game/server/entitylist.h b/sp/src/game/server/entitylist.h new file mode 100644 index 00000000..505b1968 --- /dev/null +++ b/sp/src/game/server/entitylist.h @@ -0,0 +1,382 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef ENTITYLIST_H +#define ENTITYLIST_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "baseentity.h" + +class IEntityListener; + +abstract_class CBaseEntityClassList +{ +public: + CBaseEntityClassList(); + ~CBaseEntityClassList(); + virtual void LevelShutdownPostEntity() = 0; + + CBaseEntityClassList *m_pNextClassList; +}; + +template< class T > +class CEntityClassList : public CBaseEntityClassList +{ +public: + virtual void LevelShutdownPostEntity() { m_pClassList = NULL; } + + void Insert( T *pEntity ) + { + pEntity->m_pNext = m_pClassList; + m_pClassList = pEntity; + } + + void Remove( T *pEntity ) + { + T **pPrev = &m_pClassList; + T *pCur = *pPrev; + while ( pCur ) + { + if ( pCur == pEntity ) + { + *pPrev = pCur->m_pNext; + return; + } + pPrev = &pCur->m_pNext; + pCur = *pPrev; + } + } + + static T *m_pClassList; +}; + +// Derive a class from this if you want to filter entity list searches +abstract_class IEntityFindFilter +{ +public: + virtual bool ShouldFindEntity( CBaseEntity *pEntity ) = 0; + virtual CBaseEntity *GetFilterResult( void ) = 0; +}; + +#ifdef MAPBASE +// Returns false every time. Created for some sick hack involving FindEntityProcedural looking at FindNamedEntity. +class CNullEntityFilter : public IEntityFindFilter +{ +public: + virtual bool ShouldFindEntity( CBaseEntity *pEntity ) { return false; } + virtual CBaseEntity *GetFilterResult( void ) { return NULL; } +}; +#endif + +//----------------------------------------------------------------------------- +// Purpose: a global list of all the entities in the game. All iteration through +// entities is done through this object. +//----------------------------------------------------------------------------- +class CGlobalEntityList : public CBaseEntityList +{ +public: +private: + int m_iHighestEnt; // the topmost used array index + int m_iNumEnts; + int m_iNumEdicts; + + bool m_bClearingEntities; + CUtlVector m_entityListeners; + +public: + IServerNetworkable* GetServerNetworkable( CBaseHandle hEnt ) const; + CBaseNetworkable* GetBaseNetworkable( CBaseHandle hEnt ) const; + CBaseEntity* GetBaseEntity( CBaseHandle hEnt ) const; + edict_t* GetEdict( CBaseHandle hEnt ) const; + + int NumberOfEntities( void ); + int NumberOfEdicts( void ); + + // mark an entity as deleted + void AddToDeleteList( IServerNetworkable *ent ); + // call this before and after each frame to delete all of the marked entities. + void CleanupDeleteList( void ); + int ResetDeleteList( void ); + + // frees all entities in the game + void Clear( void ); + + // Returns true while in the Clear() call. + bool IsClearingEntities() {return m_bClearingEntities;} + + // add a class that gets notified of entity events + void AddListenerEntity( IEntityListener *pListener ); + void RemoveListenerEntity( IEntityListener *pListener ); + + void ReportEntityFlagsChanged( CBaseEntity *pEntity, unsigned int flagsOld, unsigned int flagsNow ); + + // entity is about to be removed, notify the listeners + void NotifyCreateEntity( CBaseEntity *pEnt ); + void NotifySpawn( CBaseEntity *pEnt ); + void NotifyRemoveEntity( CBaseHandle hEnt ); + // iteration functions + + // returns the next entity after pCurrentEnt; if pCurrentEnt is NULL, return the first entity + CBaseEntity *NextEnt( CBaseEntity *pCurrentEnt ); + CBaseEntity *FirstEnt() { return NextEnt(NULL); } + + // returns the next entity of the specified class, using RTTI + template< class T > + T *NextEntByClass( T *start ) + { + for ( CBaseEntity *x = NextEnt( start ); x; x = NextEnt( x ) ) + { + start = dynamic_cast( x ); + if ( start ) + return start; + } + return NULL; + } + + // search functions + bool IsEntityPtr( void *pTest ); +#ifdef MAPBASE + CBaseEntity *FindEntityByClassname( CBaseEntity *pStartEntity, const char *szName, IEntityFindFilter *pFilter = NULL ); +#else + CBaseEntity *FindEntityByClassname( CBaseEntity *pStartEntity, const char *szName ); +#endif + CBaseEntity *FindEntityByName( CBaseEntity *pStartEntity, const char *szName, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL, IEntityFindFilter *pFilter = NULL ); + CBaseEntity *FindEntityByName( CBaseEntity *pStartEntity, string_t iszName, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL, IEntityFindFilter *pFilter = NULL ) + { + return FindEntityByName( pStartEntity, STRING(iszName), pSearchingEntity, pActivator, pCaller, pFilter ); + } + CBaseEntity *FindEntityInSphere( CBaseEntity *pStartEntity, const Vector &vecCenter, float flRadius ); + CBaseEntity *FindEntityByTarget( CBaseEntity *pStartEntity, const char *szName ); + CBaseEntity *FindEntityByModel( CBaseEntity *pStartEntity, const char *szModelName ); + + CBaseEntity *FindEntityByNameNearest( const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ); + CBaseEntity *FindEntityByNameWithin( CBaseEntity *pStartEntity, const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ); + CBaseEntity *FindEntityByClassnameNearest( const char *szName, const Vector &vecSrc, float flRadius ); + CBaseEntity *FindEntityByClassnameWithin( CBaseEntity *pStartEntity , const char *szName, const Vector &vecSrc, float flRadius ); + CBaseEntity *FindEntityByClassnameWithin( CBaseEntity *pStartEntity , const char *szName, const Vector &vecMins, const Vector &vecMaxs ); + +#ifdef MAPBASE + CBaseEntity *FindEntityGeneric( CBaseEntity *pStartEntity, const char *szName, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL, IEntityFindFilter *pFilter = NULL ); +#else + CBaseEntity *FindEntityGeneric( CBaseEntity *pStartEntity, const char *szName, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ); +#endif + CBaseEntity *FindEntityGenericWithin( CBaseEntity *pStartEntity, const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ); + CBaseEntity *FindEntityGenericNearest( const char *szName, const Vector &vecSrc, float flRadius, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ); + + CBaseEntity *FindEntityNearestFacing( const Vector &origin, const Vector &facing, float threshold); + CBaseEntity *FindEntityClassNearestFacing( const Vector &origin, const Vector &facing, float threshold, char *classname); + CBaseEntity *FindEntityByNetname( CBaseEntity *pStartEntity, const char *szModelName ); + + CBaseEntity *FindEntityProcedural( const char *szName, CBaseEntity *pSearchingEntity = NULL, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ); + + CGlobalEntityList(); + +// CBaseEntityList overrides. +protected: + + virtual void OnAddEntity( IHandleEntity *pEnt, CBaseHandle handle ); + virtual void OnRemoveEntity( IHandleEntity *pEnt, CBaseHandle handle ); + +}; + +extern CGlobalEntityList gEntList; + + +//----------------------------------------------------------------------------- +// Inlines. +//----------------------------------------------------------------------------- +inline edict_t* CGlobalEntityList::GetEdict( CBaseHandle hEnt ) const +{ + IServerUnknown *pUnk = static_cast(LookupEntity( hEnt )); + if ( pUnk ) + return pUnk->GetNetworkable()->GetEdict(); + else + return NULL; +} + +inline CBaseNetworkable* CGlobalEntityList::GetBaseNetworkable( CBaseHandle hEnt ) const +{ + IServerUnknown *pUnk = static_cast(LookupEntity( hEnt )); + if ( pUnk ) + return pUnk->GetNetworkable()->GetBaseNetworkable(); + else + return NULL; +} + +inline IServerNetworkable* CGlobalEntityList::GetServerNetworkable( CBaseHandle hEnt ) const +{ + IServerUnknown *pUnk = static_cast(LookupEntity( hEnt )); + if ( pUnk ) + return pUnk->GetNetworkable(); + else + return NULL; +} + +inline CBaseEntity* CGlobalEntityList::GetBaseEntity( CBaseHandle hEnt ) const +{ + IServerUnknown *pUnk = static_cast(LookupEntity( hEnt )); + if ( pUnk ) + return pUnk->GetBaseEntity(); + else + return NULL; +} + + +//----------------------------------------------------------------------------- +// Common finds +#if 0 + +template +inline bool FindEntityByName( const char *pszName, ENT_TYPE **ppResult) +{ + CBaseEntity *pBaseEntity = gEntList.FindEntityByName( NULL, pszName ); + + if ( pBaseEntity ) + *ppResult = dynamic_cast( pBaseEntity ); + else + *ppResult = NULL; + + return ( *ppResult != NULL ); +} + +template <> +inline bool FindEntityByName( const char *pszName, CBaseEntity **ppResult) +{ + *ppResult = gEntList.FindEntityByName( NULL, pszName ); + return ( *ppResult != NULL ); +} + +template <> +inline bool FindEntityByName( const char *pszName, CAI_BaseNPC **ppResult) +{ + CBaseEntity *pBaseEntity = gEntList.FindEntityByName( NULL, pszName ); + + if ( pBaseEntity ) + *ppResult = pBaseEntity->MyNPCPointer(); + else + *ppResult = NULL; + + return ( *ppResult != NULL ); +} +#endif +//----------------------------------------------------------------------------- +// Purpose: Simple object for storing a list of objects +//----------------------------------------------------------------------------- +struct entitem_t +{ + EHANDLE hEnt; + struct entitem_t *pNext; + + // uses pool memory + static void* operator new( size_t stAllocateBlock ); + static void *operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ); + static void operator delete( void *pMem ); + static void operator delete( void *pMem, int nBlockUse, const char *pFileName, int nLine ) { operator delete( pMem ); } +}; + +class CEntityList +{ +public: + CEntityList(); + ~CEntityList(); + + int m_iNumItems; + entitem_t *m_pItemList; // null terminated singly-linked list + + void AddEntity( CBaseEntity * ); + void DeleteEntity( CBaseEntity * ); +}; + +enum notify_system_event_t +{ + NOTIFY_EVENT_TELEPORT = 0, + NOTIFY_EVENT_DESTROY, +}; + +struct notify_teleport_params_t +{ + Vector prevOrigin; + QAngle prevAngles; + bool physicsRotate; +}; + +struct notify_destroy_params_t +{ +}; + +struct notify_system_event_params_t +{ + union + { + const notify_teleport_params_t *pTeleport; + const notify_destroy_params_t *pDestroy; + }; + notify_system_event_params_t( const notify_teleport_params_t *pInTeleport ) { pTeleport = pInTeleport; } + notify_system_event_params_t( const notify_destroy_params_t *pInDestroy ) { pDestroy = pInDestroy; } +}; + + +abstract_class INotify +{ +public: + // Add notification for an entity + virtual void AddEntity( CBaseEntity *pNotify, CBaseEntity *pWatched ) = 0; + + // Remove notification for an entity + virtual void RemoveEntity( CBaseEntity *pNotify, CBaseEntity *pWatched ) = 0; + + // Call the named input in each entity who is watching pEvent's status + virtual void ReportNamedEvent( CBaseEntity *pEntity, const char *pEventName ) = 0; + + // System events don't make sense as inputs, so are handled through a generic notify function + virtual void ReportSystemEvent( CBaseEntity *pEntity, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ) = 0; + + inline void ReportDestroyEvent( CBaseEntity *pEntity ) + { + notify_destroy_params_t destroy; + ReportSystemEvent( pEntity, NOTIFY_EVENT_DESTROY, notify_system_event_params_t(&destroy) ); + } + + inline void ReportTeleportEvent( CBaseEntity *pEntity, const Vector &prevOrigin, const QAngle &prevAngles, bool physicsRotate ) + { + notify_teleport_params_t teleport; + teleport.prevOrigin = prevOrigin; + teleport.prevAngles = prevAngles; + teleport.physicsRotate = physicsRotate; + ReportSystemEvent( pEntity, NOTIFY_EVENT_TELEPORT, notify_system_event_params_t(&teleport) ); + } + + // Remove this entity from the notify list + virtual void ClearEntity( CBaseEntity *pNotify ) = 0; +}; + +// Implement this class and register with gEntList to receive entity create/delete notification +class IEntityListener +{ +public: + virtual void OnEntityCreated( CBaseEntity *pEntity ) {}; + virtual void OnEntitySpawned( CBaseEntity *pEntity ) {}; + virtual void OnEntityDeleted( CBaseEntity *pEntity ) {}; +}; + +// singleton +extern INotify *g_pNotify; + +void EntityTouch_Add( CBaseEntity *pEntity ); +int AimTarget_ListCount(); +int AimTarget_ListCopy( CBaseEntity *pList[], int listMax ); +void AimTarget_ForceRepopulateList(); + +void SimThink_EntityChanged( CBaseEntity *pEntity ); +int SimThink_ListCount(); +int SimThink_ListCopy( CBaseEntity *pList[], int listMax ); + +#endif // ENTITYLIST_H diff --git a/sp/src/game/server/entityoutput.h b/sp/src/game/server/entityoutput.h new file mode 100644 index 00000000..424ae761 --- /dev/null +++ b/sp/src/game/server/entityoutput.h @@ -0,0 +1,225 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Declares basic entity communications classes, for input/output of data +// between entities +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ENTITYOUTPUT_H +#define ENTITYOUTPUT_H + +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" + + +#define EVENT_FIRE_ALWAYS -1 + + +//----------------------------------------------------------------------------- +// Purpose: A COutputEvent consists of an array of these CEventActions. +// Each CEventAction holds the information to fire a single input in +// a target entity, after a specific delay. +//----------------------------------------------------------------------------- +class CEventAction +{ +public: + CEventAction( const char *ActionData = NULL ); + + string_t m_iTarget; // name of the entity(s) to cause the action in + string_t m_iTargetInput; // the name of the action to fire + string_t m_iParameter; // parameter to send, 0 if none + float m_flDelay; // the number of seconds to wait before firing the action + int m_nTimesToFire; // The number of times to fire this event, or EVENT_FIRE_ALWAYS. + + int m_iIDStamp; // unique identifier stamp + + static int s_iNextIDStamp; + + CEventAction *m_pNext; + + // allocates memory from engine.MPool/g_EntityListPool + static void *operator new( size_t stAllocateBlock ); + static void *operator new( size_t stAllocateBlock, int nBlockUse, const char *pFileName, int nLine ); + static void operator delete( void *pMem ); + static void operator delete( void *pMem , int nBlockUse, const char *pFileName, int nLine ) { operator delete(pMem); } + + DECLARE_SIMPLE_DATADESC(); +}; + + +//----------------------------------------------------------------------------- +// Purpose: Stores a list of connections to other entities, for data/commands to be +// communicated along. +//----------------------------------------------------------------------------- +class CBaseEntityOutput +{ +public: + ~CBaseEntityOutput(); + + void ParseEventAction( const char *EventData ); + void AddEventAction( CEventAction *pEventAction ); + void RemoveEventAction( CEventAction *pEventAction ); + + int Save( ISave &save ); + int Restore( IRestore &restore, int elementCount ); + + int NumberOfElements( void ); + + float GetMaxDelay( void ); + + fieldtype_t ValueFieldType() { return m_Value.FieldType(); } + + void FireOutput( variant_t Value, CBaseEntity *pActivator, CBaseEntity *pCaller, float fDelay = 0 ); + + /// Delete every single action in the action list. + void DeleteAllElements( void ) ; + +#ifdef MAPBASE + // Needed for ReplaceOutput, hopefully not bad + CEventAction *GetActionList() { return m_ActionList; } + void SetActionList(CEventAction *newlist) { m_ActionList = newlist; } +#endif + +protected: + variant_t m_Value; + CEventAction *m_ActionList; + DECLARE_SIMPLE_DATADESC(); + + CBaseEntityOutput() {} // this class cannot be created, only it's children + +private: + CBaseEntityOutput( CBaseEntityOutput& ); // protect from accidental copying +}; + + +//----------------------------------------------------------------------------- +// Purpose: wraps variant_t data handling in convenient, compiler type-checked template +//----------------------------------------------------------------------------- +template< class Type, fieldtype_t fieldType > +class CEntityOutputTemplate : public CBaseEntityOutput +{ +public: + // + // Sets an initial value without firing the output. + // + void Init( Type value ) + { + m_Value.Set( fieldType, &value ); + } + + // + // Sets a value and fires the output. + // + void Set( Type value, CBaseEntity *pActivator, CBaseEntity *pCaller ) + { + m_Value.Set( fieldType, &value ); + FireOutput( m_Value, pActivator, pCaller ); + } + + // + // Returns the current value. + // + Type Get( void ) + { + return *((Type*)&m_Value); + } +}; + + +// +// Template specializations for type Vector, so we can implement Get, Set, and Init differently. +// +template<> +class CEntityOutputTemplate : public CBaseEntityOutput +{ +public: + void Init( const Vector &value ) + { + m_Value.SetVector3D( value ); + } + + void Set( const Vector &value, CBaseEntity *pActivator, CBaseEntity *pCaller ) + { + m_Value.SetVector3D( value ); + FireOutput( m_Value, pActivator, pCaller ); + } + + void Get( Vector &vec ) + { + m_Value.Vector3D(vec); + } + +#ifdef MAPBASE + // Shortcut to using QAngles in Vector outputs, makes it look cleaner and allows easy modification + void Init( const QAngle &value ) + { + // reinterpret_cast(value) + m_Value.SetAngle3D( value ); + } + + // Shortcut to using QAngles in Vector outputs, makes it look cleaner and allows easy modification + void Set( const QAngle &value, CBaseEntity *pActivator, CBaseEntity *pCaller ) + { + // reinterpret_cast(value) + m_Value.SetAngle3D( value ); + FireOutput( m_Value, pActivator, pCaller ); + } + + // Shortcut to using QAngles in Vector outputs, makes it look cleaner and allows easy modification + void Get( QAngle &ang ) + { + m_Value.Angle3D(ang); + } +#endif +}; + + +template<> +class CEntityOutputTemplate : public CBaseEntityOutput +{ +public: + void Init( const Vector &value ) + { + m_Value.SetPositionVector3D( value ); + } + + void Set( const Vector &value, CBaseEntity *pActivator, CBaseEntity *pCaller ) + { + m_Value.SetPositionVector3D( value ); + FireOutput( m_Value, pActivator, pCaller ); + } + + void Get( Vector &vec ) + { + m_Value.Vector3D(vec); + } +}; + + +//----------------------------------------------------------------------------- +// Purpose: parameterless entity event +//----------------------------------------------------------------------------- +class COutputEvent : public CBaseEntityOutput +{ +public: + // void Firing, no parameter + void FireOutput( CBaseEntity *pActivator, CBaseEntity *pCaller, float fDelay = 0 ); +}; + + +// useful typedefs for allowed output data types +typedef CEntityOutputTemplate COutputVariant; +typedef CEntityOutputTemplate COutputInt; +typedef CEntityOutputTemplate COutputFloat; +typedef CEntityOutputTemplate COutputString; +typedef CEntityOutputTemplate COutputEHANDLE; +typedef CEntityOutputTemplate COutputVector; +typedef CEntityOutputTemplate COutputPositionVector; +typedef CEntityOutputTemplate COutputColor32; + +#endif // ENTITYOUTPUT_H diff --git a/sp/src/game/server/env_cascade_light.cpp b/sp/src/game/server/env_cascade_light.cpp new file mode 100644 index 00000000..27b41025 --- /dev/null +++ b/sp/src/game/server/env_cascade_light.cpp @@ -0,0 +1,505 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity to control screen overlays on a player +// +//============================================================================= + +#include "cbase.h" +#include "shareddefs.h" +#include "lights.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define ENV_CASCADE_STARTON (1<<0) + +static ConVar defdist("csm_default_distance", "1000", FCVAR_DEVELOPMENTONLY, "Default Z distance. Used for some fov calculations. Please dont change"); +static ConVar curdist("csm_current_distance","14000", 0, "Current Z distance. You can change it."); +static ConVar defFOV("csm_default_fov","15", FCVAR_DEVELOPMENTONLY, "Default FOV. Used for some fov calculations. Please dont change"); +static ConVar curFOV("csm_current_fov","15", 0, "Current FOV. You can change it"); +static ConVar csm_second_fov("csm_second_fov", "26", FCVAR_NONE ,"FOV of the second csm."); + +class CLightOrigin : public CPointEntity +{ + DECLARE_CLASS(CLightOrigin, CPointEntity); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + void Spawn(); + + CLightOrigin(); + + bool angFEnv = false; + + void InitialThink(void); + +private: + + CNetworkVector(LightEnvVector); +}; + +LINK_ENTITY_TO_CLASS(csmorigin, CLightOrigin); + +BEGIN_DATADESC(CLightOrigin) +DEFINE_FIELD(LightEnvVector, FIELD_VECTOR), +DEFINE_THINKFUNC(InitialThink) +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST(CLightOrigin, DT_LightOrigin) +SendPropVector(SENDINFO(LightEnvVector)) +END_SEND_TABLE() + +CLightOrigin::CLightOrigin() +{ +} + +void CLightOrigin::Spawn() +{ + if (angFEnv) + { + CBaseEntity* pEntity = NULL; + pEntity = gEntList.FindEntityByClassname(pEntity, "light_environment"); + if (pEntity) + { + CEnvLight* pEnv = dynamic_cast(pEntity); + + QAngle bb = pEnv->GetAbsAngles(); + bb.x = bb.x; + SetAbsAngles(bb); + + ConColorMsg(Color(0,230,0), "light_environment Founded!\n"); + } + else + { + //Msg("What the fuck? Map dont have light_environment with targetname!"); + ConColorMsg(Color(230, 0, 0), "What the fuck? Map dont have light_environment with targetname!\n"); + } + } +} + +void CLightOrigin::InitialThink() +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: second csm +//----------------------------------------------------------------------------- + +class CEnvCascadeLightSecond : public CPointEntity +{ + DECLARE_CLASS(CEnvCascadeLightSecond, CPointEntity); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CEnvCascadeLightSecond(); + bool KeyValue(const char* szKeyName, const char* szValue); + + // Always transmit to clients + virtual int UpdateTransmitState(); + virtual void Activate(void); + + void InitialThink(void); + + CNetworkHandle(CBaseEntity, m_hTargetEntity); + CNetworkVector(m_LinearFloatLightColor); + +private: + CNetworkVar(bool, m_bState); + CNetworkVar(float, m_flLightFOV); + CNetworkVar(bool, m_bEnableShadows); + CNetworkVar(bool, m_bLightOnlyTarget); + CNetworkVar(bool, m_bLightWorld); + CNetworkVar(bool, m_bCameraSpace); + CNetworkVar(float, m_flAmbient); + CNetworkString(m_SpotlightTextureName, MAX_PATH); + CNetworkVar(int, m_nSpotlightTextureFrame); + CNetworkVar(float, m_flNearZ); + CNetworkVar(float, m_flFarZ); + CNetworkVar(int, m_nShadowQuality); +}; + +LINK_ENTITY_TO_CLASS(second_csm, CEnvCascadeLightSecond); + +BEGIN_DATADESC(CEnvCascadeLightSecond) +DEFINE_FIELD(m_hTargetEntity, FIELD_EHANDLE), +DEFINE_FIELD(m_bState, FIELD_BOOLEAN), +DEFINE_KEYFIELD(m_flLightFOV, FIELD_FLOAT, "lightfov"), +DEFINE_KEYFIELD(m_bEnableShadows, FIELD_BOOLEAN, "enableshadows"), +DEFINE_KEYFIELD(m_bLightOnlyTarget, FIELD_BOOLEAN, "lightonlytarget"), +DEFINE_KEYFIELD(m_bLightWorld, FIELD_BOOLEAN, "lightworld"), +DEFINE_KEYFIELD(m_bCameraSpace, FIELD_BOOLEAN, "cameraspace"), +DEFINE_KEYFIELD(m_flAmbient, FIELD_FLOAT, "ambient"), +DEFINE_AUTO_ARRAY_KEYFIELD(m_SpotlightTextureName, FIELD_CHARACTER, "texturename"), +DEFINE_KEYFIELD(m_nSpotlightTextureFrame, FIELD_INTEGER, "textureframe"), +DEFINE_KEYFIELD(m_flNearZ, FIELD_FLOAT, "nearz"), +DEFINE_KEYFIELD(m_flFarZ, FIELD_FLOAT, "farz"), +DEFINE_KEYFIELD(m_nShadowQuality, FIELD_INTEGER, "shadowquality"), +DEFINE_FIELD(m_LinearFloatLightColor, FIELD_VECTOR), +DEFINE_THINKFUNC(InitialThink), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST(CEnvCascadeLightSecond, DT_EnvCascadeLightSecond) +SendPropEHandle(SENDINFO(m_hTargetEntity)), +SendPropBool(SENDINFO(m_bState)), +SendPropFloat(SENDINFO(m_flLightFOV)), +SendPropBool(SENDINFO(m_bEnableShadows)), +SendPropBool(SENDINFO(m_bLightOnlyTarget)), +SendPropBool(SENDINFO(m_bLightWorld)), +SendPropBool(SENDINFO(m_bCameraSpace)), +SendPropVector(SENDINFO(m_LinearFloatLightColor)), +SendPropFloat(SENDINFO(m_flAmbient)), +SendPropString(SENDINFO(m_SpotlightTextureName)), +SendPropInt(SENDINFO(m_nSpotlightTextureFrame)), +SendPropFloat(SENDINFO(m_flNearZ), 16, SPROP_ROUNDDOWN, 0.0f, 500.0f), +SendPropFloat(SENDINFO(m_flFarZ), 18, SPROP_ROUNDDOWN, 0.0f, 1500.0f), +SendPropInt(SENDINFO(m_nShadowQuality), 1, SPROP_UNSIGNED) // Just one bit for now +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEnvCascadeLightSecond::CEnvCascadeLightSecond(void) +{ + m_bState = true; + m_flLightFOV = 45.0f; + m_bEnableShadows = true; + m_bLightOnlyTarget = false; + m_bLightWorld = true; + m_bCameraSpace = false; + + Q_strcpy(m_SpotlightTextureName.GetForModify(), "tools\\fakecsm\\mask_ring"); + m_nSpotlightTextureFrame = 0; + m_LinearFloatLightColor.Init(1.0f, 1.0f, 1.0f); + m_flAmbient = 0.0f; + m_flNearZ = 8000.0f; + m_flFarZ = 16000.0f; + m_nShadowQuality = 0; +} + +void UTIL_ColorStringToLinearFloatColorCSMFakeSecond(Vector& color, const char* pString) +{ + float tmp[4]; + UTIL_StringToFloatArray(tmp, 4, pString); + if (tmp[3] <= 0.0f) + { + tmp[3] = 255.0f; + } + tmp[3] *= (1.0f / 255.0f); + color.x = GammaToLinear(tmp[0] * (1.0f / 255.0f)) * tmp[3]; + color.y = GammaToLinear(tmp[1] * (1.0f / 255.0f)) * tmp[3]; + color.z = GammaToLinear(tmp[2] * (1.0f / 255.0f)) * tmp[3]; +} + +bool CEnvCascadeLightSecond::KeyValue(const char* szKeyName, const char* szValue) +{ + if (FStrEq(szKeyName, "lightcolor")) + { + Vector tmp; + UTIL_ColorStringToLinearFloatColorCSMFakeSecond(tmp, szValue); + m_LinearFloatLightColor = tmp; + } + else + { + return BaseClass::KeyValue(szKeyName, szValue); + } + + return true; +} + +void CEnvCascadeLightSecond::Activate(void) +{ + if (GetSpawnFlags() & ENV_CASCADE_STARTON) + { + m_bState = true; + } + + SetThink(&CEnvCascadeLightSecond::InitialThink); + SetNextThink(gpGlobals->curtime + 0.1f); + + BaseClass::Activate(); +} + +void CEnvCascadeLightSecond::InitialThink(void) +{ + float bibigon = defdist.GetFloat() / curdist.GetFloat(); + m_flLightFOV = csm_second_fov.GetFloat() * bibigon; + m_hTargetEntity = gEntList.FindEntityByName(NULL, m_target); +} + +int CEnvCascadeLightSecond::UpdateTransmitState() +{ + return SetTransmitState(FL_EDICT_ALWAYS); +} + + + +//----------------------------------------------------------------------------- +// Purpose: main csm code +//----------------------------------------------------------------------------- +class CEnvCascadeLight : public CPointEntity +{ + DECLARE_CLASS(CEnvCascadeLight, CPointEntity); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CEnvCascadeLight(); + bool KeyValue(const char* szKeyName, const char* szValue); + + // Always transmit to clients + virtual int UpdateTransmitState(); + virtual void Activate(void); + void Spawn(); + void Preparation(); + + void InputTurnOn(inputdata_t& inputdata); + void InputTurnOff(inputdata_t& inputdata); + void InputSetEnableShadows(inputdata_t& inputdata); + void InputSetLightColor( inputdata_t &inputdata ); + void InputSetSpotlightTexture(inputdata_t& inputdata); + void InputSetAmbient(inputdata_t& inputdata); + + void InitialThink(void); + + CNetworkHandle(CBaseEntity, m_hTargetEntity); + +private: + CLightOrigin* pEnv; + CNetworkVar(bool, m_bState); + CNetworkVar(float, m_flLightFOV); + CNetworkVar(bool, EnableAngleFromEnv); + CNetworkVar(bool, m_bEnableShadows); + CNetworkVar(bool, m_bLightOnlyTarget); + CNetworkVar(bool, m_bLightWorld); + CNetworkVar(bool, m_bCameraSpace); + CNetworkVector(m_LinearFloatLightColor); + CNetworkVar(float, m_flAmbient); + CNetworkString(m_SpotlightTextureName, MAX_PATH); + CNetworkVar(int, m_nSpotlightTextureFrame); + CNetworkVar(float, m_flNearZ); + CNetworkVar(float, m_flFarZ); + CNetworkVar(int, m_nShadowQuality); +}; + +LINK_ENTITY_TO_CLASS(env_cascade_light, CEnvCascadeLight); + +BEGIN_DATADESC(CEnvCascadeLight) +DEFINE_FIELD(m_hTargetEntity, FIELD_EHANDLE), +DEFINE_FIELD(m_bState, FIELD_BOOLEAN), +DEFINE_KEYFIELD(m_bEnableShadows, FIELD_BOOLEAN, "enableshadows"), +DEFINE_KEYFIELD(m_bLightOnlyTarget, FIELD_BOOLEAN, "lightonlytarget"), +DEFINE_KEYFIELD(m_bLightWorld, FIELD_BOOLEAN, "lightworld"), +DEFINE_KEYFIELD(m_bCameraSpace, FIELD_BOOLEAN, "cameraspace"), +DEFINE_KEYFIELD(m_flAmbient, FIELD_FLOAT, "ambient"), +DEFINE_AUTO_ARRAY_KEYFIELD(m_SpotlightTextureName, FIELD_CHARACTER, "texturename"), +DEFINE_KEYFIELD(m_nSpotlightTextureFrame, FIELD_INTEGER, "textureframe"), +DEFINE_KEYFIELD(m_flNearZ, FIELD_FLOAT, "nearz"), +DEFINE_KEYFIELD(m_flFarZ, FIELD_FLOAT, "farz"), +DEFINE_KEYFIELD(m_nShadowQuality, FIELD_INTEGER, "shadowquality"), +DEFINE_FIELD(m_LinearFloatLightColor, FIELD_VECTOR), +DEFINE_KEYFIELD(EnableAngleFromEnv, FIELD_BOOLEAN, "uselightenvangles"), + +DEFINE_INPUTFUNC(FIELD_VOID, "TurnOn", InputTurnOn), +DEFINE_INPUTFUNC(FIELD_VOID, "TurnOff", InputTurnOff), +DEFINE_INPUTFUNC(FIELD_BOOLEAN, "EnableShadows", InputSetEnableShadows), +// this is broken . . need to be able to set color and intensity like light_dynamic +// DEFINE_INPUTFUNC( FIELD_COLOR32, "LightColor", InputSetLightColor ), +DEFINE_INPUTFUNC(FIELD_FLOAT, "Ambient", InputSetAmbient), +DEFINE_INPUTFUNC(FIELD_STRING, "SpotlightTexture", InputSetSpotlightTexture), +DEFINE_THINKFUNC(InitialThink), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST(CEnvCascadeLight, DT_EnvCascadeLight) +SendPropEHandle(SENDINFO(m_hTargetEntity)), +SendPropBool(SENDINFO(m_bState)), +SendPropFloat(SENDINFO(m_flLightFOV)), +SendPropBool(SENDINFO(m_bEnableShadows)), +SendPropBool(SENDINFO(m_bLightOnlyTarget)), +SendPropBool(SENDINFO(m_bLightWorld)), +SendPropBool(SENDINFO(m_bCameraSpace)), +SendPropVector(SENDINFO(m_LinearFloatLightColor)), +SendPropFloat(SENDINFO(m_flAmbient)), +SendPropString(SENDINFO(m_SpotlightTextureName)), +SendPropInt(SENDINFO(m_nSpotlightTextureFrame)), +SendPropFloat(SENDINFO(m_flNearZ), 16, SPROP_ROUNDDOWN, 0.0f, 500.0f), +SendPropFloat(SENDINFO(m_flFarZ), 18, SPROP_ROUNDDOWN, 0.0f, 1500.0f), +SendPropInt(SENDINFO(m_nShadowQuality), 1, SPROP_UNSIGNED) // Just one bit for now +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEnvCascadeLight::CEnvCascadeLight(void) +{ + m_bState = true; + m_flLightFOV = 45.0f; + m_bEnableShadows = true; + m_bLightOnlyTarget = false; + m_bLightWorld = true; + m_bCameraSpace = false; + EnableAngleFromEnv = false; + + Q_strcpy(m_SpotlightTextureName.GetForModify(), "tools\\fakecsm\\mask_center"); + m_nSpotlightTextureFrame = 0; + m_LinearFloatLightColor.Init(1.0f, 1.0f, 1.0f); + m_flAmbient = 0.0f; + m_flNearZ = 8000.0f; + m_flFarZ = 16000.0f; + m_nShadowQuality = 0; +} +ConVar csm_second_intensity("csm_second_intensity", "2"); +void CEnvCascadeLight::Preparation() +{ + CreateEntityByName("csmorigin"); + CreateEntityByName("second_csm"); + defFOV.SetValue(m_flLightFOV); + CBaseEntity* CSMOrigin = NULL; + CBaseEntity* CSMSecond = NULL; + + CSMOrigin = gEntList.FindEntityByClassname(CSMOrigin, "csmorigin"); + //if origin is exist + if (CSMOrigin) + { + + pEnv = dynamic_cast(CSMOrigin); + + + CSMSecond = gEntList.FindEntityByClassname(CSMSecond, "second_csm"); + //if second csm is exist + if (CSMSecond) + { + CEnvCascadeLightSecond* SecondCSM = dynamic_cast(CSMSecond); + SecondCSM->SetAbsAngles(GetAbsAngles()); + SecondCSM->SetAbsOrigin(GetAbsOrigin()); + SecondCSM->SetParent(GetBaseEntity()); + SecondCSM->m_LinearFloatLightColor = m_LinearFloatLightColor * csm_second_intensity.GetFloat(); + + + DispatchSpawn(SecondCSM); + } + + SetParent(pEnv, 1); + SetAbsOrigin(Vector(pEnv->GetAbsOrigin().x, pEnv->GetAbsOrigin().y, pEnv->GetAbsOrigin().z + curdist.GetInt())); + + if (EnableAngleFromEnv) + { + pEnv->angFEnv = true; + SetLocalAngles(QAngle(90, 0, 0)); + + } + else + { + pEnv->SetAbsAngles(QAngle((GetLocalAngles().x - 90), GetLocalAngles().y, -GetLocalAngles().z)); + + Msg("pEnv local angle = %f %f %f \n", pEnv->GetLocalAngles().x, pEnv->GetLocalAngles().y, pEnv->GetLocalAngles().z); + + SetLocalAngles(QAngle(90, 0, 0)); + DevMsg("CSM using light_environment \n"); + } + //DispatchSpawn(CSMSecond); + DispatchSpawn(CSMOrigin); + } + else + { + Msg("Main csm entity can't find \"csmorigin\" entity!"); + } + +} + +void CEnvCascadeLight::Spawn() +{ + Preparation(); +} + +void UTIL_ColorStringToLinearFloatColorCSMFake(Vector& color, const char* pString) +{ + float tmp[4]; + UTIL_StringToFloatArray(tmp, 4, pString); + if (tmp[3] <= 0.0f) + { + tmp[3] = 255.0f; + } + tmp[3] *= (1.0f / 255.0f); + color.x = GammaToLinear(tmp[0] * (1.0f / 255.0f)) * tmp[3]; + color.y = GammaToLinear(tmp[1] * (1.0f / 255.0f)) * tmp[3]; + color.z = GammaToLinear(tmp[2] * (1.0f / 255.0f)) * tmp[3]; +} + +bool CEnvCascadeLight::KeyValue(const char* szKeyName, const char* szValue) +{ + if (FStrEq(szKeyName, "lightcolor")) + { + Vector tmp; + UTIL_ColorStringToLinearFloatColorCSMFake(tmp, szValue); + m_LinearFloatLightColor = tmp; + } + else + { + return BaseClass::KeyValue(szKeyName, szValue); + } + + return true; +} + +void CEnvCascadeLight::InputTurnOn(inputdata_t& inputdata) +{ + m_bState = true; +} + +void CEnvCascadeLight::InputTurnOff(inputdata_t& inputdata) +{ + m_bState = false; +} + + +void CEnvCascadeLight::InputSetEnableShadows(inputdata_t& inputdata) +{ + m_bEnableShadows = inputdata.value.Bool(); +} + +//void CEnvProjectedTexture::InputSetLightColor( inputdata_t &inputdata ) +//{ + //m_cLightColor = inputdata.value.Color32(); +//} + +void CEnvCascadeLight::InputSetAmbient(inputdata_t& inputdata) +{ + m_flAmbient = inputdata.value.Float(); +} + +void CEnvCascadeLight::InputSetSpotlightTexture(inputdata_t& inputdata) +{ + Q_strcpy(m_SpotlightTextureName.GetForModify(), inputdata.value.String()); +} + +void CEnvCascadeLight::Activate(void) +{ + if (GetSpawnFlags() & ENV_CASCADE_STARTON) + { + m_bState = true; + } + + SetThink(&CEnvCascadeLight::InitialThink); + SetNextThink(gpGlobals->curtime + 0.1f); + + BaseClass::Activate(); +} + +void CEnvCascadeLight::InitialThink(void) +{ + m_hTargetEntity = gEntList.FindEntityByName(NULL, m_target); + + float bibigon = defdist.GetFloat() / curdist.GetFloat(); + curFOV.SetValue(defFOV.GetFloat() * bibigon); + m_flLightFOV = curFOV.GetFloat(); + //if(pEnv != NULL) + //SetAbsOrigin(Vector(pEnv->GetAbsOrigin().x, pEnv->GetAbsOrigin().y, pEnv->GetAbsOrigin().z + curdist.GetInt())); +} + +int CEnvCascadeLight::UpdateTransmitState() +{ + return SetTransmitState(FL_EDICT_ALWAYS); +} diff --git a/sp/src/game/server/env_debughistory.cpp b/sp/src/game/server/env_debughistory.cpp new file mode 100644 index 00000000..bf249ecf --- /dev/null +++ b/sp/src/game/server/env_debughistory.cpp @@ -0,0 +1,367 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= +#include "cbase.h" +#include "isaverestore.h" +#include "env_debughistory.h" +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Number of characters worth of debug to use per history category +#define DEBUG_HISTORY_VERSION 6 +#define DEBUG_HISTORY_FIRST_VERSIONED 5 +#define MAX_DEBUG_HISTORY_LINE_LENGTH 256 +#define MAX_DEBUG_HISTORY_LENGTH (1000 * MAX_DEBUG_HISTORY_LINE_LENGTH) + +//----------------------------------------------------------------------------- +// Purpose: Stores debug history in savegame files for debugging reference +//----------------------------------------------------------------------------- +class CDebugHistory : public CBaseEntity +{ + DECLARE_CLASS( CDebugHistory, CBaseEntity ); +public: + DECLARE_DATADESC(); + + void Spawn(); + void AddDebugHistoryLine( int iCategory, const char *szLine ); + void ClearHistories( void ); + void DumpDebugHistory( int iCategory ); + + int Save( ISave &save ); + int Restore( IRestore &restore ); + +private: + char m_DebugLines[MAX_HISTORY_CATEGORIES][MAX_DEBUG_HISTORY_LENGTH]; + char *m_DebugLineEnd[MAX_HISTORY_CATEGORIES]; +}; + +BEGIN_DATADESC( CDebugHistory ) + //DEFINE_FIELD( m_DebugLines, FIELD_CHARACTER ), // Not saved because we write it out manually + //DEFINE_FIELD( m_DebugLineEnd, FIELD_CHARACTER ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_debughistory, CDebugHistory ); + +// The handle to the debug history singleton. Created on first access via GetDebugHistory. +static CHandle< CDebugHistory > s_DebugHistory; + + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CDebugHistory::Spawn() +{ + BaseClass::Spawn(); + +#ifdef DISABLE_DEBUG_HISTORY + UTIL_Remove( this ); +#else + if ( g_pGameRules && g_pGameRules->IsMultiplayer() ) + { + UTIL_Remove( this ); + } + else + { + Warning( "DEBUG HISTORY IS ENABLED. Disable before release (in env_debughistory.h).\n" ); + } +#endif + + ClearHistories(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDebugHistory::AddDebugHistoryLine( int iCategory, const char *szLine ) +{ + if ( iCategory < 0 || iCategory >= MAX_HISTORY_CATEGORIES ) + { + Warning("Attempted to add a debughistory line to category %d. Valid categories are %d to %d.\n", iCategory, 0, (MAX_HISTORY_CATEGORIES-1) ); + return; + } + + // Don't do debug history before the singleton is properly set up. + if ( !m_DebugLineEnd[iCategory] ) + return; + + const char *pszRemaining = szLine; + int iCharsToWrite = strlen( pszRemaining ) + 1; // Add 1 so that we copy the null terminator + + // Clip the line if it's too long. Wasteful doing it this way, but keeps code below nice & simple. + char szTmpBuffer[MAX_DEBUG_HISTORY_LINE_LENGTH]; + if ( iCharsToWrite > MAX_DEBUG_HISTORY_LINE_LENGTH) + { + memcpy( szTmpBuffer, szLine, sizeof(szTmpBuffer) ); + szTmpBuffer[MAX_DEBUG_HISTORY_LINE_LENGTH-1] = '\0'; + pszRemaining = szTmpBuffer; + iCharsToWrite = MAX_DEBUG_HISTORY_LINE_LENGTH; + } + + while ( iCharsToWrite ) + { + int iCharsLeftBeforeLoop = sizeof(m_DebugLines[iCategory]) - (m_DebugLineEnd[iCategory] - m_DebugLines[iCategory]); + + // Write into the buffer + int iWrote = MIN( iCharsToWrite, iCharsLeftBeforeLoop ); + memcpy( m_DebugLineEnd[iCategory], pszRemaining, iWrote ); + m_DebugLineEnd[iCategory] += iWrote; + pszRemaining += iWrote; + + // Did we loop? + if ( iWrote == iCharsLeftBeforeLoop ) + { + m_DebugLineEnd[iCategory] = m_DebugLines[iCategory]; + } + + iCharsToWrite -= iWrote; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDebugHistory::DumpDebugHistory( int iCategory ) +{ + if ( iCategory < 0 || iCategory >= MAX_HISTORY_CATEGORIES ) + { + Warning("Attempted to dump a history for category %d. Valid categories are %d to %d.\n", iCategory, 0, (MAX_HISTORY_CATEGORIES-1) ); + return; + } + + // Find the start of the oldest whole debug line. + const char *pszLine = m_DebugLineEnd[iCategory] + 1; + if ( (pszLine - m_DebugLines[iCategory]) >= sizeof(m_DebugLines[iCategory]) ) + { + pszLine = m_DebugLines[iCategory]; + } + + // Are we at the start of a line? If there's a null terminator before us, then we're good to go. + while ( (!( pszLine == m_DebugLines[iCategory] && *(m_DebugLines[iCategory]+sizeof(m_DebugLines[iCategory])-1) == '\0' ) && + !( pszLine != m_DebugLines[iCategory] && *(pszLine-1) == '\0' )) + || *pszLine == '\0' ) + { + pszLine++; + + // Have we looped? + if ( (pszLine - m_DebugLines[iCategory]) >= sizeof(m_DebugLines[iCategory]) ) + { + pszLine = m_DebugLines[iCategory]; + } + + if ( pszLine == m_DebugLineEnd[iCategory] ) + { + // We looped through the entire history, and found nothing. + Msg( "Debug History of Category %d is EMPTY\n", iCategory ); + return; + } + } + + // Now print everything up till the end + char szMsgBuffer[MAX_DEBUG_HISTORY_LINE_LENGTH]; + char *pszMsg = szMsgBuffer; + Msg( "Starting Debug History Dump of Category %d\n", iCategory ); + while ( pszLine != m_DebugLineEnd[iCategory] ) + { + *pszMsg = *pszLine; + if ( *pszLine == '\0' ) + { + if ( szMsgBuffer[0] != '\0' ) + { + // Found a full line, so print it + Msg( "%s", szMsgBuffer ); + } + + // Clear the buffer + pszMsg = szMsgBuffer; + *pszMsg = '\0'; + } + else + { + pszMsg++; + } + + pszLine++; + + // Have we looped? + if ( (pszLine - m_DebugLines[iCategory]) >= sizeof(m_DebugLines[iCategory]) ) + { + pszLine = m_DebugLines[iCategory]; + } + } + Msg("Ended Debug History Dump of Category %d\n", iCategory ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDebugHistory::ClearHistories( void ) +{ + for ( int i = 0; i < MAX_HISTORY_CATEGORIES; i++ ) + { + memset( m_DebugLines[i], 0, sizeof(m_DebugLines[i]) ); + m_DebugLineEnd[i] = m_DebugLines[i]; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CDebugHistory::Save( ISave &save ) +{ + int iVersion = DEBUG_HISTORY_VERSION; + save.WriteInt( &iVersion ); + int iMaxCategorys = MAX_HISTORY_CATEGORIES; + save.WriteInt( &iMaxCategorys ); + for ( int iCategory = 0; iCategory < MAX_HISTORY_CATEGORIES; iCategory++ ) + { + int iEnd = m_DebugLineEnd[iCategory] - m_DebugLines[iCategory]; + save.WriteInt( &iEnd ); + save.WriteData( m_DebugLines[iCategory], MAX_DEBUG_HISTORY_LENGTH ); + } + + return BaseClass::Save(save); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CDebugHistory::Restore( IRestore &restore ) +{ + ClearHistories(); + + int iVersion = restore.ReadInt(); + + if ( iVersion >= DEBUG_HISTORY_FIRST_VERSIONED ) + { + int iMaxCategorys = restore.ReadInt(); + for ( int iCategory = 0; iCategory < MIN(iMaxCategorys,MAX_HISTORY_CATEGORIES); iCategory++ ) + { + int iEnd = restore.ReadInt(); + m_DebugLineEnd[iCategory] = m_DebugLines[iCategory] + iEnd; + restore.ReadData( m_DebugLines[iCategory], sizeof(m_DebugLines[iCategory]), 0 ); + } + } + else + { + int iMaxCategorys = iVersion; + for ( int iCategory = 0; iCategory < MIN(iMaxCategorys,MAX_HISTORY_CATEGORIES); iCategory++ ) + { + int iEnd = restore.ReadInt(); + m_DebugLineEnd[iCategory] = m_DebugLines[iCategory] + iEnd; + restore.ReadData( m_DebugLines[iCategory], sizeof(m_DebugLines[iCategory]), 0 ); + } + } + + return BaseClass::Restore(restore); +} + + +//----------------------------------------------------------------------------- +// Purpose: Singleton debug history. Created by first usage. +//----------------------------------------------------------------------------- +CDebugHistory *GetDebugHistory() +{ +#ifdef DISABLE_DEBUG_HISTORY + return NULL; +#endif + + if ( g_pGameRules && g_pGameRules->IsMultiplayer() ) + return NULL; + + if ( s_DebugHistory == NULL ) + { + CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, "env_debughistory" ); + if ( pEnt ) + { + s_DebugHistory = dynamic_cast(pEnt); + } + else + { + s_DebugHistory = ( CDebugHistory * )CreateEntityByName( "env_debughistory" ); + if ( s_DebugHistory ) + { + s_DebugHistory->Spawn(); + } + } + } + + Assert( s_DebugHistory ); + return s_DebugHistory; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void AddDebugHistoryLine( int iCategory, const char *pszLine ) +{ +#ifdef DISABLE_DEBUG_HISTORY + return; +#else + if ( g_pGameRules && g_pGameRules->IsMultiplayer() ) + return; + + if ( !GetDebugHistory() ) + { + Warning("Failed to find or create an env_debughistory.\n" ); + return; + } + + GetDebugHistory()->AddDebugHistoryLine( iCategory, pszLine ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CC_DebugHistory_AddLine( const CCommand &args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() < 3 ) + { + Warning("Incorrect parameters. Format: \n"); + return; + } + + int iCategory = atoi(args[ 1 ]); + const char *pszLine = args[ 2 ]; + AddDebugHistoryLine( iCategory, pszLine ); +} +static ConCommand dbghist_addline( "dbghist_addline", CC_DebugHistory_AddLine, "Add a line to the debug history. Format: ", FCVAR_NONE ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CC_DebugHistory_Dump( const CCommand &args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() < 2 ) + { + Warning("Incorrect parameters. Format: \n"); + return; + } + + if ( GetDebugHistory() ) + { + int iCategory = atoi(args[ 1 ]); + GetDebugHistory()->DumpDebugHistory( iCategory ); + } +} + +static ConCommand dbghist_dump("dbghist_dump", CC_DebugHistory_Dump, + "Dump the debug history to the console. Format: \n" + " Categories:\n" + " 0: Entity I/O\n" + " 1: AI Decisions\n" + " 2: Scene Print\n" + " 3: Alyx Blind\n" + " 4: Log of damage done to player", + FCVAR_NONE ); + diff --git a/sp/src/game/server/env_debughistory.h b/sp/src/game/server/env_debughistory.h new file mode 100644 index 00000000..7bce102a --- /dev/null +++ b/sp/src/game/server/env_debughistory.h @@ -0,0 +1,35 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef ENV_DEBUGHISTORY_H +#define ENV_DEBUGHISTORY_H +#ifdef _WIN32 +#pragma once +#endif + +enum debughistorycategories_t +{ + HISTORY_ENTITY_IO, + HISTORY_AI_DECISIONS, + HISTORY_SCENE_PRINT, + HISTORY_ALYX_BLIND, // TEMP: until we find and fix this bug + HISTORY_PLAYER_DAMAGE, // record all damage done to the player + + // Add new categories here + + MAX_HISTORY_CATEGORIES, +}; + +#define DISABLE_DEBUG_HISTORY + +#if defined(DISABLE_DEBUG_HISTORY) +#define ADD_DEBUG_HISTORY( category, line ) ((void)0) +#else +#define ADD_DEBUG_HISTORY( category, line ) AddDebugHistoryLine( category, line ) +void AddDebugHistoryLine( int iCategory, const char *pszLine ); +#endif + +#endif // ENV_DEBUGHISTORY_H diff --git a/sp/src/game/server/env_dof_controller.cpp b/sp/src/game/server/env_dof_controller.cpp new file mode 100644 index 00000000..d061fc73 --- /dev/null +++ b/sp/src/game/server/env_dof_controller.cpp @@ -0,0 +1,186 @@ +//====== Copyright © 1996-2004, Valve Corporation, All rights reserved. ======= +// +// Purpose: Depth of field controller entity +// +//============================================================================= + +#include "cbase.h" +#include "baseentity.h" +#include "entityoutput.h" +#include "env_dof_controller.h" +#include "ai_utils.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( env_dof_controller, CEnvDOFController ); + +BEGIN_DATADESC( CEnvDOFController ) + + DEFINE_KEYFIELD( m_bDOFEnabled, FIELD_BOOLEAN, "enabled" ), + DEFINE_KEYFIELD( m_flNearBlurDepth, FIELD_FLOAT, "near_blur" ), + DEFINE_KEYFIELD( m_flNearFocusDepth, FIELD_FLOAT, "near_focus" ), + DEFINE_KEYFIELD( m_flFarFocusDepth, FIELD_FLOAT, "far_focus" ), + DEFINE_KEYFIELD( m_flFarBlurDepth, FIELD_FLOAT, "far_blur" ), + DEFINE_KEYFIELD( m_flNearBlurRadius, FIELD_FLOAT, "near_radius" ), + DEFINE_KEYFIELD( m_flFarBlurRadius, FIELD_FLOAT, "far_radius" ), + DEFINE_KEYFIELD( m_strFocusTargetName, FIELD_STRING, "focus_target" ), + DEFINE_KEYFIELD( m_flFocusTargetRange, FIELD_FLOAT, "focus_range" ), + + DEFINE_FIELD( m_hFocusTarget, FIELD_EHANDLE ), + + DEFINE_THINKFUNC( UpdateParamBlend ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetNearBlurDepth", InputSetNearBlurDepth ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetNearFocusDepth", InputSetNearFocusDepth ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFarFocusDepth", InputSetFarFocusDepth ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFarBlurDepth", InputSetFarBlurDepth ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetNearBlurRadius", InputSetNearBlurRadius ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFarBlurRadius", InputSetFarBlurRadius ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetFocusTarget", InputSetFocusTarget ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFocusTargetRange", InputSetFocusTargetRange ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CEnvDOFController, DT_EnvDOFController ) + SendPropInt( SENDINFO(m_bDOFEnabled), 1, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_flNearBlurDepth), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flNearFocusDepth), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flFarFocusDepth), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flFarBlurDepth), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flNearBlurRadius), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flFarBlurRadius), 0, SPROP_NOSCALE), +END_SEND_TABLE() + +void CEnvDOFController::Spawn() +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + +#ifdef MAPBASE + // Find our target entity and hold on to it + m_hFocusTarget = gEntList.FindEntityByName( NULL, m_strFocusTargetName, this ); + + // Update if we have a focal target + if ( m_hFocusTarget ) + { + SetThink( &CEnvDOFController::UpdateParamBlend ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } +#endif +} + +void CEnvDOFController::Activate() +{ + BaseClass::Activate(); + +#ifndef MAPBASE // Mapbase moves this to Spawn() to avoid issues with save/restore and entities set via the SetFocusTarget input + // Find our target entity and hold on to it + m_hFocusTarget = gEntList.FindEntityByName( NULL, m_strFocusTargetName ); + + // Update if we have a focal target + if ( m_hFocusTarget ) + { + SetThink( &CEnvDOFController::UpdateParamBlend ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } +#endif +} + +int CEnvDOFController::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +void CEnvDOFController::InputSetNearBlurDepth( inputdata_t &inputdata ) +{ + m_flNearBlurDepth = inputdata.value.Float(); +} + +void CEnvDOFController::InputSetNearFocusDepth( inputdata_t &inputdata ) +{ + m_flNearFocusDepth = inputdata.value.Float(); +} + +void CEnvDOFController::InputSetFarFocusDepth( inputdata_t &inputdata ) +{ + m_flFarFocusDepth = inputdata.value.Float(); +} + +void CEnvDOFController::InputSetFarBlurDepth( inputdata_t &inputdata ) +{ + m_flFarBlurDepth = inputdata.value.Float(); +} + +void CEnvDOFController::InputSetNearBlurRadius( inputdata_t &inputdata ) +{ + m_flNearBlurRadius = inputdata.value.Float(); + m_bDOFEnabled = ( m_flNearBlurRadius > 0.0f ) || ( m_flFarBlurRadius > 0.0f ); +} + +void CEnvDOFController::InputSetFarBlurRadius( inputdata_t &inputdata ) +{ + m_flFarBlurRadius = inputdata.value.Float(); + m_bDOFEnabled = ( m_flNearBlurRadius > 0.0f ) || ( m_flFarBlurRadius > 0.0f ); +} + +void CEnvDOFController::SetControllerState( DOFControlSettings_t setting ) +{ + m_flNearBlurDepth = setting.flNearBlurDepth; + m_flNearBlurRadius = setting.flNearBlurRadius; + m_flNearFocusDepth = setting.flNearFocusDistance; + + m_flFarBlurDepth = setting.flFarBlurDepth; + m_flFarBlurRadius = setting.flFarBlurRadius; + m_flFarFocusDepth = setting.flFarFocusDistance; + + m_bDOFEnabled = ( m_flNearBlurRadius > 0.0f ) || ( m_flFarBlurRadius > 0.0f ); +} + +#define BLUR_DEPTH 500.0f + +//----------------------------------------------------------------------------- +// Purpose: Blend the parameters to the specified value +//----------------------------------------------------------------------------- +void CEnvDOFController::UpdateParamBlend() +{ + // Update our focal target if we have one + if ( m_hFocusTarget ) + { + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + float flDistToFocus = ( m_hFocusTarget->GetAbsOrigin() - pPlayer->GetAbsOrigin() ).Length(); + m_flFarFocusDepth.GetForModify() = flDistToFocus + m_flFocusTargetRange; + m_flFarBlurDepth.GetForModify() = m_flFarFocusDepth + BLUR_DEPTH; + + SetThink( &CEnvDOFController::UpdateParamBlend ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the "focus" target entity +//----------------------------------------------------------------------------- +void CEnvDOFController::InputSetFocusTarget( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + m_hFocusTarget = gEntList.FindEntityByName( NULL, inputdata.value.String(), this, inputdata.pActivator, inputdata.pCaller ); +#else + m_hFocusTarget = gEntList.FindEntityByName( NULL, inputdata.value.String() ); +#endif + + // Update if we have a focal target + if ( m_hFocusTarget ) + { + SetThink( &CEnvDOFController::UpdateParamBlend ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the range behind the focus entity that we'll blur (in units) +//----------------------------------------------------------------------------- +void CEnvDOFController::InputSetFocusTargetRange( inputdata_t &inputdata ) +{ + m_flFocusTargetRange = inputdata.value.Float(); +} diff --git a/sp/src/game/server/env_dof_controller.h b/sp/src/game/server/env_dof_controller.h new file mode 100644 index 00000000..e323ef52 --- /dev/null +++ b/sp/src/game/server/env_dof_controller.h @@ -0,0 +1,56 @@ +#pragma once + +struct DOFControlSettings_t +{ + // Near plane + float flNearBlurDepth; + float flNearBlurRadius; + float flNearFocusDistance; + // Far plane + float flFarBlurDepth; + float flFarBlurRadius; + float flFarFocusDistance; +}; + +//----------------------------------------------------------------------------- +// Purpose: Entity that controls depth of field postprocessing +//----------------------------------------------------------------------------- +class CEnvDOFController : public CPointEntity +{ + DECLARE_CLASS( CEnvDOFController, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + virtual void Spawn(); + virtual void Activate(); + virtual int UpdateTransmitState(); + void SetControllerState( DOFControlSettings_t setting ); + + void UpdateParamBlend(); + + // Inputs + void InputSetNearBlurDepth( inputdata_t &inputdata ); + void InputSetNearFocusDepth( inputdata_t &inputdata ); + void InputSetFarFocusDepth( inputdata_t &inputdata ); + void InputSetFarBlurDepth( inputdata_t &inputdata ); + void InputSetNearBlurRadius( inputdata_t &inputdata ); + void InputSetFarBlurRadius( inputdata_t &inputdata ); + + void InputSetFocusTarget( inputdata_t &inputdata ); + void InputSetFocusTargetRange( inputdata_t &inputdata ); + +private: + float m_flFocusTargetRange; + + string_t m_strFocusTargetName; // Name of the entity to focus on + EHANDLE m_hFocusTarget; + + CNetworkVar( bool, m_bDOFEnabled ); + CNetworkVar( float, m_flNearBlurDepth ); + CNetworkVar( float, m_flNearFocusDepth ); + CNetworkVar( float, m_flFarFocusDepth ); + CNetworkVar( float, m_flFarBlurDepth ); + CNetworkVar( float, m_flNearBlurRadius ); + CNetworkVar( float, m_flFarBlurRadius ); +}; diff --git a/sp/src/game/server/env_effectsscript.cpp b/sp/src/game/server/env_effectsscript.cpp new file mode 100644 index 00000000..98f1a97d --- /dev/null +++ b/sp/src/game/server/env_effectsscript.cpp @@ -0,0 +1,542 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseanimating.h" +#include "Sprite.h" +#include "SpriteTrail.h" +#include +#include "animation.h" +#include "eventlist.h" +#include "npcevent.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +enum EffectType +{ + EFFECT_TYPE_TRAIL = 1, + EFFECT_TYPE_SPRITE +}; + + +bool g_bUnget = false; +unsigned char *buffer; +char name[ 256 ]; +const char *currenttoken; +int tokencount; +char token[ 1204 ]; + +class CEffectScriptElement +{ +public: + + CEffectScriptElement(); + + char m_szEffectName[128]; + CHandle m_pTrail; + CHandle m_pSprite; + int m_iType; + int m_iRenderType; + + int m_iR; + int m_iG; + int m_iB; + int m_iA; + + char m_szAttachment[128]; + char m_szMaterial[128]; + + float m_flScale; + float m_flFadeTime; + float m_flTextureRes; + + bool m_bStopFollowOnKill; + + bool IsActive( void ) { return m_bActive; } + void Activate( void ) { m_bActive = true; } + void Deactivate( void ) { m_bActive = false; } +private: + + bool m_bActive; +}; + +CEffectScriptElement::CEffectScriptElement() +{ + m_pTrail = NULL; + m_pSprite = NULL; + m_iType = 0; + + Deactivate(); + m_iRenderType = kRenderTransAdd; + + m_iR = 255; + m_iG = 0; + m_iB = 0; + m_iA = 255; + + m_flScale = 1.0f; + m_flFadeTime = 1.0f; + m_flTextureRes = -1.0f; + m_bStopFollowOnKill = false; +} + + +//----------------------------------------------------------------------------- +// An entity which emits other entities at points +//----------------------------------------------------------------------------- +class CEnvEffectsScript : public CBaseAnimating +{ +public: + DECLARE_CLASS( CEnvEffectsScript, CBaseAnimating ); + DECLARE_DATADESC(); + + virtual void Precache(); + virtual void Spawn(); + virtual int UpdateTransmitState(); + + void InputSetSequence( inputdata_t &inputdata ); + void ParseScriptFile( void ); + void LoadFromBuffer( const char *scriptfile, const char *buffer ); + + virtual void Think( void ); + + void ParseNewEffect( void ); + + const char *GetScriptFile( void ) + { + return STRING( m_iszScriptName ); + } + + void HandleAnimEvent ( animevent_t *pEvent ); + void TrailEffectEvent( CEffectScriptElement *pEffect ); + void SpriteEffectEvent( CEffectScriptElement *pEffect ); + + CEffectScriptElement *GetScriptElementByName( const char *pName ); + +private: + + string_t m_iszScriptName; + + CUtlVector< CEffectScriptElement > m_ScriptElements; + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- + bool IsRootCommand( void ) + { + if ( !Q_stricmp( token, "effect" ) ) + return true; + + return false; + } +}; + +inline bool ParseToken( void ) +{ + if ( g_bUnget ) + { + g_bUnget = false; + return true; + } + + currenttoken = engine->ParseFile( currenttoken, token, sizeof( token ) ); + tokencount++; + return currenttoken != NULL ? true : false; +} + +inline void Unget() +{ + g_bUnget = true; +} + +inline bool TokenWaiting( void ) +{ + + const char *p = currenttoken; + while ( *p && *p!='\n') + { + // Special handler for // comment blocks + if ( *p == '/' && *(p+1) == '/' ) + return false; + + if ( !V_isspace( *p ) || V_isalnum( *p ) ) + return true; + + p++; + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CEnvEffectsScript ) + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetSequence", InputSetSequence ), + DEFINE_KEYFIELD( m_iszScriptName, FIELD_STRING, "scriptfile" ), + // DEFINE_FIELD( m_ScriptElements, CUtlVector < CEffectScriptElement > ), + + DEFINE_FUNCTION( Think ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_effectscript, CEnvEffectsScript ); + +//----------------------------------------------------------------------------- +// Should we transmit it to the client? +//----------------------------------------------------------------------------- +int CEnvEffectsScript::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Precache +//----------------------------------------------------------------------------- +void CEnvEffectsScript::Precache() +{ + BaseClass::Precache(); + PrecacheModel( STRING( GetModelName() ) ); + + if ( m_iszScriptName != NULL_STRING ) + ParseScriptFile(); + else + Warning( "CEnvEffectsScript with no script!\n" ); +} + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CEnvEffectsScript::Spawn() +{ + Precache(); + BaseClass::Spawn(); + + // We need a model for its animation sequences even though we don't render it + SetModel( STRING( GetModelName() ) ); + + AddEffects( EF_NODRAW ); + + SetThink( &CEnvEffectsScript::Think ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CEnvEffectsScript::Think( void ) +{ + StudioFrameAdvance(); + DispatchAnimEvents( this ); + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CEnvEffectsScript::TrailEffectEvent( CEffectScriptElement *pEffect ) +{ + if ( pEffect->IsActive() == false ) + { + //Only one type of this effect active at a time. + if ( pEffect->m_pTrail == NULL ) + { + pEffect->m_pTrail = CSpriteTrail::SpriteTrailCreate( pEffect->m_szMaterial, GetAbsOrigin(), true ); + pEffect->m_pTrail->FollowEntity( this ); + pEffect->m_pTrail->SetTransparency( pEffect->m_iRenderType, pEffect->m_iR, pEffect->m_iG, pEffect->m_iB, pEffect->m_iA, kRenderFxNone ); + pEffect->m_pTrail->SetStartWidth( pEffect->m_flScale ); + if ( pEffect->m_flTextureRes < 0.0f ) + { + pEffect->m_pTrail->SetTextureResolution( 1.0f / ( 16.0f * pEffect->m_flScale ) ); + } + else + { + pEffect->m_pTrail->SetTextureResolution( pEffect->m_flTextureRes ); + } + pEffect->m_pTrail->SetLifeTime( pEffect->m_flFadeTime ); + pEffect->m_pTrail->TurnOn(); + pEffect->m_pTrail->SetAttachment( this, LookupAttachment( pEffect->m_szAttachment ) ); + + pEffect->Activate(); + } + } +} + +void CEnvEffectsScript::SpriteEffectEvent( CEffectScriptElement *pEffect ) +{ + if ( pEffect->IsActive() == false ) + { + //Only one type of this effect active at a time. + if ( pEffect->m_pSprite == NULL ) + { + pEffect->m_pSprite = CSprite::SpriteCreate( pEffect->m_szMaterial, GetAbsOrigin(), true ); + pEffect->m_pSprite->FollowEntity( this ); + pEffect->m_pSprite->SetTransparency( pEffect->m_iRenderType, pEffect->m_iR, pEffect->m_iG, pEffect->m_iB, pEffect->m_iA, kRenderFxNone ); + pEffect->m_pSprite->SetScale( pEffect->m_flScale ); + pEffect->m_pSprite->TurnOn(); + pEffect->m_pSprite->SetAttachment( this, LookupAttachment( pEffect->m_szAttachment ) ); + + pEffect->Activate(); + } + } +} + +void CEnvEffectsScript::HandleAnimEvent ( animevent_t *pEvent ) +{ + if ( pEvent->event == AE_START_SCRIPTED_EFFECT ) + { + CEffectScriptElement *pCurrent = GetScriptElementByName( pEvent->options ); + + if ( pCurrent ) + { + if ( pCurrent->m_iType == EFFECT_TYPE_TRAIL ) + TrailEffectEvent( pCurrent ); + else if ( pCurrent->m_iType == EFFECT_TYPE_SPRITE ) + SpriteEffectEvent( pCurrent ); + } + + return; + } + + if ( pEvent->event == AE_STOP_SCRIPTED_EFFECT ) + { + CEffectScriptElement *pCurrent = GetScriptElementByName( pEvent->options ); + + if ( pCurrent && pCurrent->IsActive() ) + { + pCurrent->Deactivate(); + + if ( pCurrent->m_iType == EFFECT_TYPE_TRAIL ) + { + if ( pCurrent->m_bStopFollowOnKill == true ) + { + Vector vOrigin; + GetAttachment( pCurrent->m_pTrail->m_nAttachment, vOrigin ); + + pCurrent->m_pTrail->StopFollowingEntity(); + + pCurrent->m_pTrail->m_hAttachedToEntity = NULL; + pCurrent->m_pTrail->m_nAttachment = 0; + + pCurrent->m_pTrail->SetAbsOrigin( vOrigin); + } + + pCurrent->m_pTrail->FadeAndDie( pCurrent->m_flFadeTime ); + pCurrent->m_pTrail = NULL; + } + + else if ( pCurrent->m_iType == EFFECT_TYPE_SPRITE ) + { + if ( pCurrent->m_bStopFollowOnKill == true ) + { + Vector vOrigin; + GetAttachment( pCurrent->m_pSprite->m_nAttachment, vOrigin ); + + pCurrent->m_pSprite->StopFollowingEntity(); + + pCurrent->m_pSprite->m_hAttachedToEntity = NULL; + pCurrent->m_pSprite->m_nAttachment = 0; + + pCurrent->m_pSprite->SetAbsOrigin( vOrigin); + } + + pCurrent->m_pSprite->FadeAndDie( pCurrent->m_flFadeTime ); + pCurrent->m_pSprite = NULL; + } + } + return; + } + + BaseClass::HandleAnimEvent( pEvent ); +} +//----------------------------------------------------------------------------- +// Purpose: Input that sets the sequence of the entity +//----------------------------------------------------------------------------- +void CEnvEffectsScript::InputSetSequence( inputdata_t &inputdata ) +{ + if ( inputdata.value.StringID() != NULL_STRING ) + { + int nSequence = LookupSequence( STRING( inputdata.value.StringID() ) ); + if ( nSequence != ACT_INVALID ) + { + SetSequence( nSequence ); + ResetSequenceInfo(); + SetCycle( 0.0f ); + m_flPlaybackRate = 1.0f; + } + } +} + +void CEnvEffectsScript::ParseScriptFile( void ) +{ + int length = 0; + m_ScriptElements.RemoveAll(); + const char *pScriptName = GetScriptFile(); + + //Reset everything. + g_bUnget = false; + currenttoken = NULL; + tokencount = 0; + memset( token, 0, 1204 ); + memset( name, 0, 256 ); + + + unsigned char *buffer = (unsigned char *)UTIL_LoadFileForMe( pScriptName, &length ); + if ( length <= 0 || !buffer ) + { + DevMsg( 1, "CEnvEffectsScript: failed to load %s\n", pScriptName ); + return; + } + + currenttoken = (const char *)buffer; + LoadFromBuffer( pScriptName, (const char *)buffer ); + + UTIL_FreeFile( buffer ); +} + +void CEnvEffectsScript::LoadFromBuffer( const char *scriptfile, const char *buffer ) +{ + while ( 1 ) + { + ParseToken(); + + if ( !token[0] ) + { + break; + } + + if ( !Q_stricmp( token, "effect" ) ) + { + ParseNewEffect(); + } + else + { + Warning( "CEnvEffectsScript: Unknown entry type '%s'\n", token ); + break; + } + } +} + +void CEnvEffectsScript::ParseNewEffect( void ) +{ + //Add a new effect to the list. + CEffectScriptElement NewElement; + + // Effect Group Name + ParseToken(); + Q_strncpy( NewElement.m_szEffectName, token, sizeof( NewElement.m_szEffectName ) ); + + while ( 1 ) + { + ParseToken(); + + // Oops, part of next definition + if( IsRootCommand() ) + { + Unget(); + break; + } + + if ( !Q_stricmp( token, "{" ) ) + { + while ( 1 ) + { + ParseToken(); + if ( !Q_stricmp( token, "}" ) ) + break; + + if ( !Q_stricmp( token, "type" ) ) + { + ParseToken(); + + if ( !Q_stricmp( token, "trail" ) ) + NewElement.m_iType = EFFECT_TYPE_TRAIL; + else if ( !Q_stricmp( token, "sprite" ) ) + NewElement.m_iType = EFFECT_TYPE_SPRITE; + + continue; + } + + if ( !Q_stricmp( token, "material" ) ) + { + ParseToken(); + Q_strncpy( NewElement.m_szMaterial, token, sizeof( NewElement.m_szMaterial ) ); + PrecacheModel( NewElement.m_szMaterial ); + + continue; + } + + if ( !Q_stricmp( token, "attachment" ) ) + { + ParseToken(); + Q_strncpy( NewElement.m_szAttachment, token, sizeof( NewElement.m_szAttachment ) ); + + continue; + } + + if ( !Q_stricmp( token, "color" ) ) + { + ParseToken(); + sscanf( token, "%i %i %i %i", &NewElement.m_iR, &NewElement.m_iG, &NewElement.m_iB, &NewElement.m_iA ); + + continue; + } + + if ( !Q_stricmp( token, "scale" ) ) + { + ParseToken(); + + NewElement.m_flScale = atof( token ); + continue; + } + + if ( !Q_stricmp( token, "texturescale" ) ) + { + ParseToken(); + + float flTextureScale = atof( token ); + NewElement.m_flTextureRes = (flTextureScale > 0.0f) ? 1.0f / flTextureScale : 0.0f; + continue; + } + + if ( !Q_stricmp( token, "fadetime" ) ) + { + ParseToken(); + + NewElement.m_flFadeTime = atof( token ); + continue; + } + + if ( !Q_stricmp( token, "stopfollowonkill" ) ) + { + ParseToken(); + + NewElement.m_bStopFollowOnKill = !!atoi( token ); + continue; + } + + } + break; + } + } + + m_ScriptElements.AddToTail( NewElement ); +} + +CEffectScriptElement *CEnvEffectsScript::GetScriptElementByName( const char *pName ) +{ + for ( int i = 0; i < m_ScriptElements.Count(); i++ ) + { + CEffectScriptElement *pCurrent = &m_ScriptElements.Element( i ); + + if ( pCurrent && !Q_stricmp( pCurrent->m_szEffectName, pName ) ) + { + return pCurrent; + } + } + + return NULL; +} diff --git a/sp/src/game/server/env_entity_maker.cpp b/sp/src/game/server/env_entity_maker.cpp new file mode 100644 index 00000000..19d8ddf6 --- /dev/null +++ b/sp/src/game/server/env_entity_maker.cpp @@ -0,0 +1,475 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "entityoutput.h" +#include "TemplateEntities.h" +#include "point_template.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_ENTMAKER_AUTOSPAWN 0x0001 +#define SF_ENTMAKER_WAITFORDESTRUCTION 0x0002 +#define SF_ENTMAKER_IGNOREFACING 0x0004 +#define SF_ENTMAKER_CHECK_FOR_SPACE 0x0008 +#define SF_ENTMAKER_CHECK_PLAYER_LOOKING 0x0010 + + +//----------------------------------------------------------------------------- +// Purpose: An entity that mapmakers can use to ensure there's a required entity never runs out. +// i.e. physics cannisters that need to be used. +//----------------------------------------------------------------------------- +class CEnvEntityMaker : public CPointEntity +{ + DECLARE_CLASS( CEnvEntityMaker, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_ENT_SCRIPTDESC(); + + virtual void Spawn( void ); + virtual void Activate( void ); + + void SpawnEntity( Vector vecAlternateOrigin = vec3_invalid, QAngle vecAlternateAngles = vec3_angle ); + void CheckSpawnThink( void ); + void InputForceSpawn( inputdata_t &inputdata ); + void InputForceSpawnAtEntityOrigin( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputForceSpawnAtEntityCenter( inputdata_t &inputdata ); + void InputForceSpawnAtPosition( inputdata_t &inputdata ); +#endif + + void SpawnEntityFromScript(); + void SpawnEntityAtEntityOriginFromScript(HSCRIPT hEntity); + void SpawnEntityAtNamedEntityOriginFromScript(const char* pszName); + void SpawnEntityAtLocationFromScript(const Vector& vecAlternateOrigin, const Vector& vecAlternateAngles); +private: + + CPointTemplate *FindTemplate(); + + bool HasRoomToSpawn(); + bool IsPlayerLooking(); + + Vector m_vecEntityMins; + Vector m_vecEntityMaxs; + EHANDLE m_hCurrentInstance; + EHANDLE m_hCurrentBlocker; // Last entity that blocked us spawning something + Vector m_vecBlockerOrigin; + + // Movement after spawn + QAngle m_angPostSpawnDirection; + float m_flPostSpawnDirectionVariance; + float m_flPostSpawnSpeed; + bool m_bPostSpawnUseAngles; + + string_t m_iszTemplate; + + COutputEvent m_pOutputOnSpawned; + COutputEvent m_pOutputOnFailedSpawn; +#ifdef MAPBASE + COutputEHANDLE m_pOutputOutEntity; +#endif +}; + +BEGIN_DATADESC( CEnvEntityMaker ) + // DEFINE_FIELD( m_vecEntityMins, FIELD_VECTOR ), + // DEFINE_FIELD( m_vecEntityMaxs, FIELD_VECTOR ), + DEFINE_FIELD( m_hCurrentInstance, FIELD_EHANDLE ), + DEFINE_FIELD( m_hCurrentBlocker, FIELD_EHANDLE ), + DEFINE_FIELD( m_vecBlockerOrigin, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_iszTemplate, FIELD_STRING, "EntityTemplate" ), + DEFINE_KEYFIELD( m_angPostSpawnDirection, FIELD_VECTOR, "PostSpawnDirection" ), + DEFINE_KEYFIELD( m_flPostSpawnDirectionVariance, FIELD_FLOAT, "PostSpawnDirectionVariance" ), + DEFINE_KEYFIELD( m_flPostSpawnSpeed, FIELD_FLOAT, "PostSpawnSpeed" ), + DEFINE_KEYFIELD( m_bPostSpawnUseAngles, FIELD_BOOLEAN, "PostSpawnInheritAngles" ), + + // Outputs + DEFINE_OUTPUT( m_pOutputOnSpawned, "OnEntitySpawned" ), + DEFINE_OUTPUT( m_pOutputOnFailedSpawn, "OnEntityFailedSpawn" ), +#ifdef MAPBASE + DEFINE_OUTPUT( m_pOutputOutEntity, "OutSpawnedEntity" ), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "ForceSpawn", InputForceSpawn ), + DEFINE_INPUTFUNC( FIELD_STRING, "ForceSpawnAtEntityOrigin", InputForceSpawnAtEntityOrigin ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "ForceSpawnAtEntityCenter", InputForceSpawnAtEntityCenter ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "ForceSpawnAtPosition", InputForceSpawnAtPosition ), +#endif + + // Functions + DEFINE_THINKFUNC( CheckSpawnThink ), +END_DATADESC() + +BEGIN_ENT_SCRIPTDESC( CEnvEntityMaker, CBaseEntity, "env_entity_maker" ) + DEFINE_SCRIPTFUNC_NAMED( SpawnEntityFromScript, "SpawnEntity", "Create an entity at the location of the maker" ) + DEFINE_SCRIPTFUNC_NAMED( SpawnEntityAtEntityOriginFromScript, "SpawnEntityAtEntityOrigin", "Create an entity at the location of a specified entity instance" ) + DEFINE_SCRIPTFUNC_NAMED( SpawnEntityAtNamedEntityOriginFromScript, "SpawnEntityAtNamedEntityOrigin", "Create an entity at the location of a named entity" ) + DEFINE_SCRIPTFUNC_NAMED( SpawnEntityAtLocationFromScript, "SpawnEntityAtLocation", "Create an entity at a specified location and orientaton, orientation is Euler angle in degrees (pitch, yaw, roll)" ) +END_SCRIPTDESC() + +LINK_ENTITY_TO_CLASS( env_entity_maker, CEnvEntityMaker ); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvEntityMaker::Spawn( void ) +{ + m_vecEntityMins = vec3_origin; + m_vecEntityMaxs = vec3_origin; + m_hCurrentInstance = NULL; + m_hCurrentBlocker = NULL; + m_vecBlockerOrigin = vec3_origin; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvEntityMaker::Activate( void ) +{ + BaseClass::Activate(); + + // check for valid template + if ( m_iszTemplate == NULL_STRING ) + { + Warning( "env_entity_maker %s has no template entity!\n", GetEntityName().ToCStr() ); + UTIL_Remove( this ); + return; + } + + // Spawn an instance + if ( m_spawnflags & SF_ENTMAKER_AUTOSPAWN ) + { + SpawnEntity(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPointTemplate *CEnvEntityMaker::FindTemplate() +{ + // Find our point_template + CPointTemplate *pTemplate = dynamic_cast(gEntList.FindEntityByName( NULL, STRING(m_iszTemplate) )); + if ( !pTemplate ) + { + Warning( "env_entity_maker %s failed to find template %s.\n", GetEntityName().ToCStr(), STRING(m_iszTemplate) ); + } + + return pTemplate; +} + + +//----------------------------------------------------------------------------- +// Purpose: Spawn an instance of the entity +//----------------------------------------------------------------------------- +void CEnvEntityMaker::SpawnEntity( Vector vecAlternateOrigin, QAngle vecAlternateAngles ) +{ + CPointTemplate *pTemplate = FindTemplate(); + if (!pTemplate) + return; + + // Spawn our template + Vector vecSpawnOrigin = GetAbsOrigin(); + QAngle vecSpawnAngles = GetAbsAngles(); + + if( vecAlternateOrigin != vec3_invalid ) + { + // We have a valid alternate origin and angles. Use those instead + // of spawning the items at my own origin and angles. + vecSpawnOrigin = vecAlternateOrigin; + vecSpawnAngles = vecAlternateAngles; + } + + CUtlVector hNewEntities; + if ( !pTemplate->CreateInstance( vecSpawnOrigin, vecSpawnAngles, &hNewEntities ) ) + return; + + //Adrian: oops we couldn't spawn the entity (or entities) for some reason! + if ( hNewEntities.Count() == 0 ) + return; + + m_hCurrentInstance = hNewEntities[0]; + + // Assume it'll block us + m_hCurrentBlocker = m_hCurrentInstance; + m_vecBlockerOrigin = m_hCurrentBlocker->GetAbsOrigin(); + + // Store off the mins & maxs the first time we spawn + if ( m_vecEntityMins == vec3_origin ) + { + m_hCurrentInstance->CollisionProp()->WorldSpaceAABB( &m_vecEntityMins, &m_vecEntityMaxs ); + m_vecEntityMins -= m_hCurrentInstance->GetAbsOrigin(); + m_vecEntityMaxs -= m_hCurrentInstance->GetAbsOrigin(); + } + + // Fire our output + m_pOutputOnSpawned.FireOutput( this, this ); + + // Start thinking + if ( m_spawnflags & SF_ENTMAKER_AUTOSPAWN ) + { + SetThink( &CEnvEntityMaker::CheckSpawnThink ); + SetNextThink( gpGlobals->curtime + 0.5f ); + } + + // If we have a specified post spawn speed, apply it to all spawned entities + if ( m_flPostSpawnSpeed ) + { + for ( int i = 0; i < hNewEntities.Count(); i++ ) + { + CBaseEntity *pEntity = hNewEntities[i]; + +#ifdef MAPBASE + m_pOutputOutEntity.Set(pEntity, pEntity, this); +#endif + + if ( pEntity->GetMoveType() == MOVETYPE_NONE ) + continue; + + // Calculate a velocity for this entity + Vector vForward,vRight,vUp; + QAngle angSpawnDir( m_angPostSpawnDirection ); + if ( m_bPostSpawnUseAngles ) + { + if ( GetParent() ) + { + angSpawnDir += GetParent()->GetAbsAngles(); + } + else + { + angSpawnDir += GetAbsAngles(); + } + } + AngleVectors( angSpawnDir, &vForward, &vRight, &vUp ); + Vector vecShootDir = vForward; + vecShootDir += vRight * random->RandomFloat(-1, 1) * m_flPostSpawnDirectionVariance; + vecShootDir += vForward * random->RandomFloat(-1, 1) * m_flPostSpawnDirectionVariance; + vecShootDir += vUp * random->RandomFloat(-1, 1) * m_flPostSpawnDirectionVariance; + VectorNormalize( vecShootDir ); + vecShootDir *= m_flPostSpawnSpeed; + + // Apply it to the entity + IPhysicsObject *pPhysicsObject = pEntity->VPhysicsGetObject(); + if ( pPhysicsObject ) + { + pPhysicsObject->AddVelocity(&vecShootDir, NULL); + } + else + { + pEntity->SetAbsVelocity( vecShootDir ); + } + } + } +#ifdef MAPBASE + else + { + for ( int i = 0; i < hNewEntities.Count(); i++ ) + { + m_pOutputOutEntity.Set(hNewEntities[i], hNewEntities[i], this); + } + } +#endif + + pTemplate->CreationComplete( hNewEntities ); +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn an instance of the entity +//----------------------------------------------------------------------------- +void CEnvEntityMaker::SpawnEntityFromScript() +{ + SpawnEntity(); +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn an instance of the entity +//----------------------------------------------------------------------------- +void CEnvEntityMaker::SpawnEntityAtEntityOriginFromScript( HSCRIPT hEntity ) +{ + CBaseEntity *pTargetEntity = ToEnt( hEntity ); + if ( pTargetEntity ) + { + SpawnEntity( pTargetEntity->GetAbsOrigin(), pTargetEntity->GetAbsAngles() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn an instance of the entity +//----------------------------------------------------------------------------- +void CEnvEntityMaker::SpawnEntityAtNamedEntityOriginFromScript( const char *pszName ) +{ + CBaseEntity *pTargetEntity = gEntList.FindEntityByName( NULL, pszName, this, NULL, NULL ); + + if( pTargetEntity ) + { + SpawnEntity( pTargetEntity->GetAbsOrigin(), pTargetEntity->GetAbsAngles() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn an instance of the entity +//----------------------------------------------------------------------------- +void CEnvEntityMaker::SpawnEntityAtLocationFromScript( const Vector &vecAlternateOrigin, const Vector &vecAlternateAngles ) +{ + SpawnEntity( vecAlternateOrigin, *((QAngle *)&vecAlternateAngles) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not the template entities can fit if spawned. +// Input : pBlocker - Returns blocker unless NULL. +//----------------------------------------------------------------------------- +bool CEnvEntityMaker::HasRoomToSpawn() +{ + // Do we have a blocker from last time? + if ( m_hCurrentBlocker ) + { + // If it hasn't moved, abort immediately + if ( m_vecBlockerOrigin == m_hCurrentBlocker->GetAbsOrigin() ) + { + return false; + } + } + + // Check to see if there's enough room to spawn + trace_t tr; + UTIL_TraceHull( GetAbsOrigin(), GetAbsOrigin(), m_vecEntityMins, m_vecEntityMaxs, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.m_pEnt || tr.startsolid ) + { + // Store off our blocker to check later + m_hCurrentBlocker = tr.m_pEnt; + if ( m_hCurrentBlocker ) + { + m_vecBlockerOrigin = m_hCurrentBlocker->GetAbsOrigin(); + } + + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the player is looking towards us. +//----------------------------------------------------------------------------- +bool CEnvEntityMaker::IsPlayerLooking() +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer ) + { + // Only spawn if the player's looking away from me + Vector vLookDir = pPlayer->EyeDirection3D(); + Vector vTargetDir = GetAbsOrigin() - pPlayer->EyePosition(); + VectorNormalize( vTargetDir ); + + float fDotPr = DotProduct( vLookDir,vTargetDir ); + if ( fDotPr > 0 ) + return true; + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Check to see if we should spawn another instance +//----------------------------------------------------------------------------- +void CEnvEntityMaker::CheckSpawnThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.5f ); + + // Do we have an instance? + if ( m_hCurrentInstance ) + { + // If Wait-For-Destruction is set, abort immediately + if ( m_spawnflags & SF_ENTMAKER_WAITFORDESTRUCTION ) + return; + } + + // Check to see if there's enough room to spawn + if ( !HasRoomToSpawn() ) + return; + + // We're clear, now check to see if the player's looking + if ( !( HasSpawnFlags( SF_ENTMAKER_IGNOREFACING ) ) && IsPlayerLooking() ) + return; + + // Clear, no player watching, so spawn! + SpawnEntity(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Spawns the entities, checking for space if flagged to do so. +//----------------------------------------------------------------------------- +void CEnvEntityMaker::InputForceSpawn( inputdata_t &inputdata ) +{ + CPointTemplate *pTemplate = FindTemplate(); + if (!pTemplate) + return; + + if ( HasSpawnFlags( SF_ENTMAKER_CHECK_FOR_SPACE ) && !HasRoomToSpawn() ) + { + m_pOutputOnFailedSpawn.FireOutput( this, this ); + return; + } + + if ( HasSpawnFlags( SF_ENTMAKER_CHECK_PLAYER_LOOKING ) && IsPlayerLooking() ) + { + m_pOutputOnFailedSpawn.FireOutput( this, this ); + return; + } + + SpawnEntity(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvEntityMaker::InputForceSpawnAtEntityOrigin( inputdata_t &inputdata ) +{ + CBaseEntity *pTargetEntity = gEntList.FindEntityByName( NULL, inputdata.value.String(), this, inputdata.pActivator, inputdata.pCaller ); + + if( pTargetEntity ) + { + SpawnEntity( pTargetEntity->GetAbsOrigin(), pTargetEntity->GetAbsAngles() ); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvEntityMaker::InputForceSpawnAtEntityCenter( inputdata_t &inputdata ) +{ + CBaseEntity *pTargetEntity = gEntList.FindEntityByName( NULL, inputdata.value.String(), this, inputdata.pActivator, inputdata.pCaller ); + + if( pTargetEntity ) + { + SpawnEntity( pTargetEntity->WorldSpaceCenter(), pTargetEntity->GetAbsAngles() ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvEntityMaker::InputForceSpawnAtPosition(inputdata_t &inputdata) +{ + Vector vecPos; + inputdata.value.Vector3D(vecPos); + if (vecPos != vec3_origin && vecPos.IsValid()) + { + SpawnEntity(vecPos, GetLocalAngles()); + } +} +#endif // MAPBASE + diff --git a/sp/src/game/server/env_global_light.cpp b/sp/src/game/server/env_global_light.cpp new file mode 100644 index 00000000..6c1549b2 --- /dev/null +++ b/sp/src/game/server/env_global_light.cpp @@ -0,0 +1,336 @@ +//========= Copyright 1996-2010, Valve Corporation, All rights reserved. ============// +// +// Purpose: global dynamic light. Ported from Insolence's port of Alien Swarm's env_global_light. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//------------------------------------------------------------------------------ +// FIXME: This really should inherit from something more lightweight +//------------------------------------------------------------------------------ + + +//------------------------------------------------------------------------------ +// Purpose : Sunlight shadow control entity +//------------------------------------------------------------------------------ +class CGlobalLight : public CBaseEntity +{ +public: + DECLARE_CLASS( CGlobalLight, CBaseEntity ); + + CGlobalLight(); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + virtual bool GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ); + int UpdateTransmitState(); + + // Inputs + void InputSetAngles( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputSetTexture( inputdata_t &inputdata ); + void InputSetEnableShadows( inputdata_t &inputdata ); + void InputSetLightColor( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetBrightness( inputdata_t &inputdata ); + void InputSetColorTransitionTime( inputdata_t &inputdata ); + void InputSetXOffset( inputdata_t &inputdata ) { m_flEastOffset = inputdata.value.Float(); } + void InputSetYOffset( inputdata_t &inputdata ) { m_flForwardOffset = inputdata.value.Float(); } + void InputSetOrthoSize( inputdata_t &inputdata ) { m_flOrthoSize = inputdata.value.Float(); } + void InputSetDistance( inputdata_t &inputdata ) { m_flSunDistance = inputdata.value.Float(); } + void InputSetFOV( inputdata_t &inputdata ) { m_flFOV = inputdata.value.Float(); } + void InputSetNearZDistance( inputdata_t &inputdata ) { m_flNearZ = inputdata.value.Float(); } + void InputSetNorthOffset( inputdata_t &inputdata ) { m_flNorthOffset = inputdata.value.Float(); } +#endif + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + CNetworkVector( m_shadowDirection ); + + CNetworkVar( bool, m_bEnabled ); + + CNetworkString( m_TextureName, MAX_PATH ); +#ifdef MAPBASE + CNetworkVar( int, m_nSpotlightTextureFrame ); +#endif + CNetworkColor32( m_LightColor ); +#ifdef MAPBASE + CNetworkVar( float, m_flBrightnessScale ); +#endif + CNetworkVar( float, m_flColorTransitionTime ); + CNetworkVar( float, m_flSunDistance ); + CNetworkVar( float, m_flFOV ); + CNetworkVar( float, m_flNearZ ); + CNetworkVar( float, m_flNorthOffset ); +#ifdef MAPBASE + CNetworkVar( float, m_flEastOffset ); // xoffset + CNetworkVar( float, m_flForwardOffset ); // yoffset + CNetworkVar( float, m_flOrthoSize ); +#endif + CNetworkVar( bool, m_bEnableShadows ); +}; + +LINK_ENTITY_TO_CLASS(env_global_light, CGlobalLight); + +BEGIN_DATADESC( CGlobalLight ) + + DEFINE_KEYFIELD( m_bEnabled, FIELD_BOOLEAN, "enabled" ), + DEFINE_AUTO_ARRAY_KEYFIELD( m_TextureName, FIELD_CHARACTER, "texturename" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_nSpotlightTextureFrame, FIELD_INTEGER, "textureframe" ), +#endif + DEFINE_KEYFIELD( m_flSunDistance, FIELD_FLOAT, "distance" ), + DEFINE_KEYFIELD( m_flFOV, FIELD_FLOAT, "fov" ), + DEFINE_KEYFIELD( m_flNearZ, FIELD_FLOAT, "nearz" ), + DEFINE_KEYFIELD( m_flNorthOffset, FIELD_FLOAT, "northoffset" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_flEastOffset, FIELD_FLOAT, "eastoffset" ), + DEFINE_KEYFIELD( m_flForwardOffset, FIELD_FLOAT, "forwardoffset" ), + DEFINE_KEYFIELD( m_flOrthoSize, FIELD_FLOAT, "orthosize" ), +#endif + DEFINE_KEYFIELD( m_bEnableShadows, FIELD_BOOLEAN, "enableshadows" ), + DEFINE_FIELD( m_LightColor, FIELD_COLOR32 ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_flBrightnessScale, FIELD_FLOAT, "brightnessscale" ), +#endif + DEFINE_KEYFIELD( m_flColorTransitionTime, FIELD_FLOAT, "colortransitiontime" ), + + DEFINE_FIELD( m_shadowDirection, FIELD_VECTOR ), + + // Inputs +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetXOffset", InputSetXOffset ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetYOffset", InputSetYOffset ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetOrthoSize", InputSetOrthoSize ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDistance", InputSetDistance ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFOV", InputSetFOV ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetNearZDistance", InputSetNearZDistance ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetNorthOffset", InputSetNorthOffset ), +#else + DEFINE_INPUT( m_flSunDistance, FIELD_FLOAT, "SetDistance" ), + DEFINE_INPUT( m_flFOV, FIELD_FLOAT, "SetFOV" ), + DEFINE_INPUT( m_flNearZ, FIELD_FLOAT, "SetNearZDistance" ), + DEFINE_INPUT( m_flNorthOffset, FIELD_FLOAT, "SetNorthOffset" ), +#endif + + DEFINE_INPUTFUNC( FIELD_COLOR32, "LightColor", InputSetLightColor ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetAngles", InputSetAngles ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTexture", InputSetTexture ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "EnableShadows", InputSetEnableShadows ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetBrightness", InputSetBrightness ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetColorTransitionTime", InputSetColorTransitionTime ), +#endif + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CGlobalLight, DT_GlobalLight) + SendPropVector(SENDINFO(m_shadowDirection), -1, SPROP_NOSCALE ), + SendPropBool(SENDINFO(m_bEnabled) ), + SendPropString(SENDINFO(m_TextureName)), +#ifdef MAPBASE + SendPropInt(SENDINFO(m_nSpotlightTextureFrame)), +#endif + /*SendPropInt(SENDINFO (m_LightColor ), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt32 ),*/ + SendPropInt(SENDINFO (m_LightColor ), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), +#ifdef MAPBASE + SendPropFloat( SENDINFO( m_flBrightnessScale ) ), +#endif + SendPropFloat( SENDINFO( m_flColorTransitionTime ) ), + SendPropFloat(SENDINFO(m_flSunDistance), 0, SPROP_NOSCALE ), + SendPropFloat(SENDINFO(m_flFOV), 0, SPROP_NOSCALE ), + SendPropFloat(SENDINFO(m_flNearZ), 0, SPROP_NOSCALE ), + SendPropFloat(SENDINFO(m_flNorthOffset), 0, SPROP_NOSCALE ), +#ifdef MAPBASE + SendPropFloat(SENDINFO(m_flEastOffset), 0, SPROP_NOSCALE ), + SendPropFloat(SENDINFO(m_flForwardOffset), 0, SPROP_NOSCALE ), + SendPropFloat(SENDINFO(m_flOrthoSize), 0, SPROP_NOSCALE ), +#endif + SendPropBool( SENDINFO( m_bEnableShadows ) ), +END_SEND_TABLE() + + +CGlobalLight::CGlobalLight() +{ +#if defined( _X360 ) + Q_strcpy( m_TextureName.GetForModify(), "effects/flashlight_border" ); +#else + Q_strcpy( m_TextureName.GetForModify(), "effects/flashlight001" ); +#endif +#ifdef MAPBASE + m_LightColor.Init( 255, 255, 255, 255 ); +#else + m_LightColor.Init( 255, 255, 255, 1 ); +#endif + m_flColorTransitionTime = 0.5f; + m_flSunDistance = 10000.0f; + m_flFOV = 5.0f; + m_bEnableShadows = false; +#ifdef MAPBASE + m_nSpotlightTextureFrame = 0; + m_flBrightnessScale = 1.0f; + m_flOrthoSize = 1000.0f; +#endif + m_bEnabled = true; +} + + +//------------------------------------------------------------------------------ +// Purpose : Send even though we don't have a model +//------------------------------------------------------------------------------ +int CGlobalLight::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +bool CGlobalLight::KeyValue( const char *szKeyName, const char *szValue ) +{ +#ifdef MAPBASE + if ( FStrEq( szKeyName, "lightcolor" ) || FStrEq( szKeyName, "color" ) ) +#else + if ( FStrEq( szKeyName, "color" ) ) +#endif + { + float tmp[4]; + UTIL_StringToFloatArray( tmp, 4, szValue ); + + m_LightColor.SetR( tmp[0] ); + m_LightColor.SetG( tmp[1] ); + m_LightColor.SetB( tmp[2] ); + m_LightColor.SetA( tmp[3] ); + } + else if ( FStrEq( szKeyName, "angles" ) ) + { + QAngle angles; + UTIL_StringToVector( angles.Base(), szValue ); + if (angles == vec3_angle) + { + angles.Init( 80, 30, 0 ); + } + Vector vForward; + AngleVectors( angles, &vForward ); + m_shadowDirection = vForward; + return true; + } + else if ( FStrEq( szKeyName, "texturename" ) ) + { +#if defined( _X360 ) + if ( Q_strcmp( szValue, "effects/flashlight001" ) == 0 ) + { + // Use this as the default for Xbox + Q_strcpy( m_TextureName.GetForModify(), "effects/flashlight_border" ); + } + else + { + Q_strcpy( m_TextureName.GetForModify(), szValue ); + } +#else + Q_strcpy( m_TextureName.GetForModify(), szValue ); +#endif + } + else if ( FStrEq( szKeyName, "StartDisabled" ) ) + { + m_bEnabled.Set( atoi( szValue ) <= 0 ); + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +bool CGlobalLight::GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ) +{ + if ( FStrEq( szKeyName, "color" ) ) + { + Q_snprintf( szValue, iMaxLen, "%d %d %d %d", m_LightColor.GetR(), m_LightColor.GetG(), m_LightColor.GetB(), m_LightColor.GetA() ); + return true; + } + else if ( FStrEq( szKeyName, "texturename" ) ) + { + Q_snprintf( szValue, iMaxLen, "%s", m_TextureName.Get() ); + return true; + } + else if ( FStrEq( szKeyName, "StartDisabled" ) ) + { + Q_snprintf( szValue, iMaxLen, "%d", !m_bEnabled.Get() ); + return true; + } + return BaseClass::GetKeyValue( szKeyName, szValue, iMaxLen ); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CGlobalLight::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); +} + +//------------------------------------------------------------------------------ +// Input values +//------------------------------------------------------------------------------ +void CGlobalLight::InputSetAngles( inputdata_t &inputdata ) +{ + const char *pAngles = inputdata.value.String(); + + QAngle angles; + UTIL_StringToVector( angles.Base(), pAngles ); + + Vector vTemp; + AngleVectors( angles, &vTemp ); + m_shadowDirection = vTemp; +} + +//------------------------------------------------------------------------------ +// Purpose : Input handlers +//------------------------------------------------------------------------------ +void CGlobalLight::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; +} + +void CGlobalLight::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; +} + +void CGlobalLight::InputSetTexture( inputdata_t &inputdata ) +{ + Q_strcpy( m_TextureName.GetForModify(), inputdata.value.String() ); +} + +void CGlobalLight::InputSetEnableShadows( inputdata_t &inputdata ) +{ + m_bEnableShadows = inputdata.value.Bool(); +} + +void CGlobalLight::InputSetLightColor( inputdata_t &inputdata ) +{ + m_LightColor = inputdata.value.Color32(); +} + +#ifdef MAPBASE +void CGlobalLight::InputSetBrightness( inputdata_t &inputdata ) +{ + m_flBrightnessScale = inputdata.value.Float(); +} + +void CGlobalLight::InputSetColorTransitionTime( inputdata_t &inputdata ) +{ + m_flColorTransitionTime = inputdata.value.Float(); +} +#endif diff --git a/sp/src/game/server/env_instructor_hint.cpp b/sp/src/game/server/env_instructor_hint.cpp new file mode 100644 index 00000000..32b75041 --- /dev/null +++ b/sp/src/game/server/env_instructor_hint.cpp @@ -0,0 +1,229 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: An entity for creating instructor hints entirely with map logic +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" +#include "world.h" + +#ifdef INFESTED_DLL + #include "asw_marine.h" + #include "asw_player.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CEnvInstructorHint : public CPointEntity +{ +public: + DECLARE_CLASS( CEnvInstructorHint, CPointEntity ); + DECLARE_DATADESC(); + +private: + void InputShowHint( inputdata_t &inputdata ); + void InputEndHint( inputdata_t &inputdata ); + +#ifdef MAPBASE + void InputSetCaption( inputdata_t &inputdata ) { m_iszCaption = inputdata.value.StringID(); } +#endif + + string_t m_iszReplace_Key; + string_t m_iszHintTargetEntity; + int m_iTimeout; + string_t m_iszIcon_Onscreen; + string_t m_iszIcon_Offscreen; + string_t m_iszCaption; + string_t m_iszActivatorCaption; + color32 m_Color; + float m_fIconOffset; + float m_fRange; + uint8 m_iPulseOption; + uint8 m_iAlphaOption; + uint8 m_iShakeOption; + bool m_bStatic; + bool m_bNoOffscreen; + bool m_bForceCaption; + string_t m_iszBinding; + bool m_bAllowNoDrawTarget; + bool m_bLocalPlayerOnly; +#ifdef MAPBASE + string_t m_iszStartSound; + int m_iHintTargetPos; +#endif +}; + +LINK_ENTITY_TO_CLASS( env_instructor_hint, CEnvInstructorHint ); + +BEGIN_DATADESC( CEnvInstructorHint ) + + DEFINE_KEYFIELD( m_iszReplace_Key, FIELD_STRING, "hint_replace_key" ), + DEFINE_KEYFIELD( m_iszHintTargetEntity, FIELD_STRING, "hint_target" ), + DEFINE_KEYFIELD( m_iTimeout, FIELD_INTEGER, "hint_timeout" ), + DEFINE_KEYFIELD( m_iszIcon_Onscreen, FIELD_STRING, "hint_icon_onscreen" ), + DEFINE_KEYFIELD( m_iszIcon_Offscreen, FIELD_STRING, "hint_icon_offscreen" ), + DEFINE_KEYFIELD( m_iszCaption, FIELD_STRING, "hint_caption" ), + DEFINE_KEYFIELD( m_iszActivatorCaption, FIELD_STRING, "hint_activator_caption" ), + DEFINE_KEYFIELD( m_Color, FIELD_COLOR32, "hint_color" ), + DEFINE_KEYFIELD( m_fIconOffset, FIELD_FLOAT, "hint_icon_offset" ), + DEFINE_KEYFIELD( m_fRange, FIELD_FLOAT, "hint_range" ), + DEFINE_KEYFIELD( m_iPulseOption, FIELD_CHARACTER, "hint_pulseoption" ), + DEFINE_KEYFIELD( m_iAlphaOption, FIELD_CHARACTER, "hint_alphaoption" ), + DEFINE_KEYFIELD( m_iShakeOption, FIELD_CHARACTER, "hint_shakeoption" ), + DEFINE_KEYFIELD( m_bStatic, FIELD_BOOLEAN, "hint_static" ), + DEFINE_KEYFIELD( m_bNoOffscreen, FIELD_BOOLEAN, "hint_nooffscreen" ), + DEFINE_KEYFIELD( m_bForceCaption, FIELD_BOOLEAN, "hint_forcecaption" ), + DEFINE_KEYFIELD( m_iszBinding, FIELD_STRING, "hint_binding" ), + DEFINE_KEYFIELD( m_bAllowNoDrawTarget, FIELD_BOOLEAN, "hint_allow_nodraw_target" ), + DEFINE_KEYFIELD( m_bLocalPlayerOnly, FIELD_BOOLEAN, "hint_local_player_only" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iszStartSound, FIELD_STRING, "hint_start_sound" ), + DEFINE_KEYFIELD( m_iHintTargetPos, FIELD_INTEGER, "hint_target_pos" ), +#endif + + DEFINE_INPUTFUNC( FIELD_STRING, "ShowHint", InputShowHint ), + DEFINE_INPUTFUNC( FIELD_VOID, "EndHint", InputEndHint ), + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetCaption", InputSetCaption ), +#endif + +END_DATADESC() + + +#define LOCATOR_ICON_FX_PULSE_SLOW 0x00000001 +#define LOCATOR_ICON_FX_ALPHA_SLOW 0x00000008 +#define LOCATOR_ICON_FX_SHAKE_NARROW 0x00000040 +#define LOCATOR_ICON_FX_STATIC 0x00000100 // This icon draws at a fixed location on the HUD. + +//----------------------------------------------------------------------------- +// Purpose: Input handler for showing the message and/or playing the sound. +//----------------------------------------------------------------------------- +void CEnvInstructorHint::InputShowHint( inputdata_t &inputdata ) +{ + IGameEvent * event = gameeventmanager->CreateEvent( "instructor_server_hint_create", false ); + if ( event ) + { + CBaseEntity *pTargetEntity = gEntList.FindEntityByName( NULL, m_iszHintTargetEntity ); + if( pTargetEntity == NULL && !m_bStatic ) + pTargetEntity = inputdata.pActivator; + + if( pTargetEntity == NULL ) + pTargetEntity = GetWorldEntity(); + + char szColorString[128]; + Q_snprintf( szColorString, sizeof( szColorString ), "%.3d,%.3d,%.3d", m_Color.r, m_Color.g, m_Color.b ); + + int iFlags = 0; + + iFlags |= (m_iPulseOption == 0) ? 0 : (LOCATOR_ICON_FX_PULSE_SLOW << (m_iPulseOption - 1)); + iFlags |= (m_iAlphaOption == 0) ? 0 : (LOCATOR_ICON_FX_ALPHA_SLOW << (m_iAlphaOption - 1)); + iFlags |= (m_iShakeOption == 0) ? 0 : (LOCATOR_ICON_FX_SHAKE_NARROW << (m_iShakeOption - 1)); + iFlags |= m_bStatic ? LOCATOR_ICON_FX_STATIC : 0; + + CBasePlayer *pActivator = NULL; + bool bFilterByActivator = m_bLocalPlayerOnly; + +#ifdef INFESTED_DLL + CASW_Marine *pMarine = dynamic_cast( inputdata.pActivator ); + if ( pMarine ) + { + pActivator = pMarine->GetCommander(); + } +#else + if ( inputdata.value.StringID() != NULL_STRING ) + { + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, inputdata.value.String() ); + pActivator = dynamic_cast( pTarget ); + if ( pActivator ) + { + bFilterByActivator = true; + } + } + else + { + if ( GameRules()->IsMultiplayer() == false ) + { + pActivator = UTIL_GetLocalPlayer(); + } + else + { + Warning( "Failed to play server side instructor hint: no player specified for hint\n" ); + Assert( 0 ); + } + } +#endif + + const char *pActivatorCaption = m_iszActivatorCaption.ToCStr(); + if ( !pActivatorCaption || pActivatorCaption[ 0 ] == '\0' ) + { + pActivatorCaption = m_iszCaption.ToCStr(); + } + + event->SetString( "hint_name", GetEntityName().ToCStr() ); + event->SetString( "hint_replace_key", m_iszReplace_Key.ToCStr() ); + event->SetInt( "hint_target", pTargetEntity->entindex() ); + event->SetInt( "hint_activator_userid", ( pActivator ? pActivator->GetUserID() : 0 ) ); + event->SetInt( "hint_timeout", m_iTimeout ); + event->SetString( "hint_icon_onscreen", m_iszIcon_Onscreen.ToCStr() ); + event->SetString( "hint_icon_offscreen", m_iszIcon_Offscreen.ToCStr() ); + event->SetString( "hint_caption", m_iszCaption.ToCStr() ); + event->SetString( "hint_activator_caption", pActivatorCaption ); + event->SetString( "hint_color", szColorString ); + event->SetFloat( "hint_icon_offset", m_fIconOffset ); + event->SetFloat( "hint_range", m_fRange ); + event->SetInt( "hint_flags", iFlags ); + event->SetString( "hint_binding", m_iszBinding.ToCStr() ); + event->SetBool( "hint_allow_nodraw_target", m_bAllowNoDrawTarget ); + event->SetBool( "hint_nooffscreen", m_bNoOffscreen ); + event->SetBool( "hint_forcecaption", m_bForceCaption ); + event->SetBool( "hint_local_player_only", bFilterByActivator ); +#ifdef MAPBASE + event->SetString( "hint_start_sound", m_iszStartSound.ToCStr() ); + event->SetInt( "hint_target_pos", m_iHintTargetPos ); +#endif + + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CEnvInstructorHint::InputEndHint( inputdata_t &inputdata ) +{ + IGameEvent * event = gameeventmanager->CreateEvent( "instructor_server_hint_stop", false ); + if ( event ) + { + event->SetString( "hint_name", GetEntityName().ToCStr() ); + + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: A generic target entity that gets replicated to the client for instructor hint targetting +//----------------------------------------------------------------------------- +class CInfoInstructorHintTarget : public CPointEntity +{ +public: + DECLARE_CLASS( CInfoInstructorHintTarget, CPointEntity ); + + virtual int UpdateTransmitState( void ) // set transmit filter to transmit always + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( info_target_instructor_hint, CInfoInstructorHintTarget ); + +BEGIN_DATADESC( CInfoInstructorHintTarget ) + +END_DATADESC() diff --git a/sp/src/game/server/env_particlescript.cpp b/sp/src/game/server/env_particlescript.cpp new file mode 100644 index 00000000..bf69224d --- /dev/null +++ b/sp/src/game/server/env_particlescript.cpp @@ -0,0 +1,193 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseanimating.h" +#include "SkyCamera.h" +#include "studio.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// HACK HACK: Must match cl_dll/cl_animevent.h!!!! +#define CL_EVENT_SPRITEGROUP_CREATE 6002 + +//----------------------------------------------------------------------------- +// An entity which emits other entities at points +//----------------------------------------------------------------------------- +class CEnvParticleScript : public CBaseAnimating +{ +public: + DECLARE_CLASS( CEnvParticleScript, CBaseAnimating ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CEnvParticleScript(); + + virtual void Precache(); + virtual void Spawn(); + virtual void Activate(); + virtual int UpdateTransmitState(); + + void InputSetSequence( inputdata_t &inputdata ); + +private: + + void PrecacheAnimationEventMaterials(); + + CNetworkVar( float, m_flSequenceScale ); +}; + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CEnvParticleScript ) + + DEFINE_FIELD( m_flSequenceScale, FIELD_FLOAT ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetSequence", InputSetSequence ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_particlescript, CEnvParticleScript ); + + +//----------------------------------------------------------------------------- +// Datatable +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST( CEnvParticleScript, DT_EnvParticleScript ) + SendPropFloat(SENDINFO(m_flSequenceScale), 0, SPROP_NOSCALE), +END_SEND_TABLE() + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CEnvParticleScript::CEnvParticleScript() +{ + UseClientSideAnimation(); +} + + +void CEnvParticleScript::PrecacheAnimationEventMaterials() +{ + CStudioHdr *hdr = GetModelPtr(); + if ( hdr ) + { + int numseq = hdr->GetNumSeq(); + for ( int i = 0; i < numseq; ++i ) + { + mstudioseqdesc_t& seqdesc = hdr->pSeqdesc( i ); + int ecount = seqdesc.numevents; + for ( int j = 0 ; j < ecount; ++j ) + { + const mstudioevent_t* event = seqdesc.pEvent( j ); + if ( event->event == CL_EVENT_SPRITEGROUP_CREATE ) + { + char pAttachmentName[256]; + char pSpriteName[256]; + int nArgs = sscanf( event->pszOptions(), "%255s %255s", pAttachmentName, pSpriteName ); + if ( nArgs == 2 ) + { + PrecacheMaterial( pSpriteName ); + } + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Precache +//----------------------------------------------------------------------------- +void CEnvParticleScript::Precache() +{ + BaseClass::Precache(); + PrecacheModel( STRING( GetModelName() ) ); + + // We need a model for its animation sequences even though we don't render it + SetModel( STRING( GetModelName() ) ); + + PrecacheAnimationEventMaterials(); +} + + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CEnvParticleScript::Spawn() +{ + Precache(); + BaseClass::Spawn(); + AddEffects( EF_NOSHADOW ); + // We need a model for its animation sequences even though we don't render it + SetModel( STRING( GetModelName() ) ); +} + + +//----------------------------------------------------------------------------- +// Activate +//----------------------------------------------------------------------------- +void CEnvParticleScript::Activate() +{ + BaseClass::Activate(); + + DetectInSkybox(); + CSkyCamera *pCamera = GetEntitySkybox(); + if ( pCamera ) + { + float flSkyboxScale = pCamera->m_skyboxData.scale; + if ( flSkyboxScale == 0.0f ) + { + flSkyboxScale = 1.0f; + } + + m_flSequenceScale = flSkyboxScale; + } + else + { + m_flSequenceScale = 1.0f; + } + + m_flPlaybackRate = 1.0f; +} + +//----------------------------------------------------------------------------- +// Should we transmit it to the client? +//----------------------------------------------------------------------------- +int CEnvParticleScript::UpdateTransmitState() +{ + if ( IsEffectActive( EF_NODRAW ) ) + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } + + if ( IsEFlagSet( EFL_IN_SKYBOX ) ) + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + return SetTransmitState( FL_EDICT_PVSCHECK ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input that sets the sequence of the entity +//----------------------------------------------------------------------------- +void CEnvParticleScript::InputSetSequence( inputdata_t &inputdata ) +{ + if ( inputdata.value.StringID() != NULL_STRING ) + { + int nSequence = LookupSequence( STRING( inputdata.value.StringID() ) ); + if ( nSequence != ACT_INVALID ) + { + SetSequence( nSequence ); + } + } +} diff --git a/sp/src/game/server/env_player_surface_trigger.cpp b/sp/src/game/server/env_player_surface_trigger.cpp new file mode 100644 index 00000000..e25038ec --- /dev/null +++ b/sp/src/game/server/env_player_surface_trigger.cpp @@ -0,0 +1,135 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "decals.h" +#include "env_player_surface_trigger.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( env_player_surface_trigger, CEnvPlayerSurfaceTrigger ); + +BEGIN_DATADESC( CEnvPlayerSurfaceTrigger ) + DEFINE_KEYFIELD( m_iTargetGameMaterial, FIELD_INTEGER, "gamematerial" ), + DEFINE_FIELD( m_iCurrentGameMaterial, FIELD_INTEGER ), + DEFINE_FIELD( m_bDisabled, FIELD_BOOLEAN ), + + DEFINE_THINKFUNC( UpdateMaterialThink ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + + // Outputs + DEFINE_OUTPUT(m_OnSurfaceChangedToTarget, "OnSurfaceChangedToTarget"), + DEFINE_OUTPUT(m_OnSurfaceChangedFromTarget, "OnSurfaceChangedFromTarget"), +END_DATADESC() + +// Global list of surface triggers +CUtlVector< CHandle > g_PlayerSurfaceTriggers; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEnvPlayerSurfaceTrigger::~CEnvPlayerSurfaceTrigger( void ) +{ + g_PlayerSurfaceTriggers.FindAndRemove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvPlayerSurfaceTrigger::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + m_iCurrentGameMaterial = 0; + m_bDisabled = false; + + g_PlayerSurfaceTriggers.AddToTail( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvPlayerSurfaceTrigger::OnRestore( void ) +{ + BaseClass::OnRestore(); + + g_PlayerSurfaceTriggers.AddToTail( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvPlayerSurfaceTrigger::SetPlayerSurface( CBasePlayer *pPlayer, char gameMaterial ) +{ + // Ignore players in the air (stops bunny hoppers escaping triggers) + if ( gameMaterial == 0 ) + return; + + // Loop through the surface triggers and tell them all about the change + int iCount = g_PlayerSurfaceTriggers.Count(); + for ( int i = 0; i < iCount; i++ ) + { + g_PlayerSurfaceTriggers[i]->PlayerSurfaceChanged( pPlayer, gameMaterial ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvPlayerSurfaceTrigger::PlayerSurfaceChanged( CBasePlayer *pPlayer, char gameMaterial ) +{ + if ( m_bDisabled ) + return; + + // Fire the output if we've changed, but only if it involves the target material + if ( gameMaterial != (char)m_iCurrentGameMaterial && + ( gameMaterial == m_iTargetGameMaterial || m_iCurrentGameMaterial == m_iTargetGameMaterial ) ) + { + DevMsg( 2, "Player changed material to %d (was %d)\n", gameMaterial, m_iCurrentGameMaterial ); + + m_iCurrentGameMaterial = (int)gameMaterial; + + SetThink( &CEnvPlayerSurfaceTrigger::UpdateMaterialThink ); + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Think function to fire outputs. Done this way so that sv_alternate ticks +// doesn't allow multiple surface changes in the same tick to fire outputs. +//----------------------------------------------------------------------------- +void CEnvPlayerSurfaceTrigger::UpdateMaterialThink( void ) +{ + if ( m_iCurrentGameMaterial == m_iTargetGameMaterial ) + { + m_OnSurfaceChangedToTarget.FireOutput( NULL, this ); + } + else + { + m_OnSurfaceChangedFromTarget.FireOutput( NULL, this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvPlayerSurfaceTrigger::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvPlayerSurfaceTrigger::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} diff --git a/sp/src/game/server/env_player_surface_trigger.h b/sp/src/game/server/env_player_surface_trigger.h new file mode 100644 index 00000000..96eb0810 --- /dev/null +++ b/sp/src/game/server/env_player_surface_trigger.h @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef ENV_PLAYER_SURFACE_TRIGGER_H +#define ENV_PLAYER_SURFACE_TRIGGER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "baseentity.h" +#include "entityoutput.h" + +//----------------------------------------------------------------------------- +// Purpose: Entity that fires outputs whenever the player stands on a different surface +//----------------------------------------------------------------------------- +class CEnvPlayerSurfaceTrigger : public CPointEntity +{ + DECLARE_CLASS( CEnvPlayerSurfaceTrigger, CPointEntity ); +public: + DECLARE_DATADESC(); + + ~CEnvPlayerSurfaceTrigger( void ); + void Spawn( void ); + void OnRestore( void ); + + // Main interface to all surface triggers + static void SetPlayerSurface( CBasePlayer *pPlayer, char gameMaterial ); + + void UpdateMaterialThink( void ); + +private: + void PlayerSurfaceChanged( CBasePlayer *pPlayer, char gameMaterial ); + void InputDisable( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + +private: + int m_iTargetGameMaterial; + int m_iCurrentGameMaterial; + bool m_bDisabled; + + // Outputs + COutputEvent m_OnSurfaceChangedToTarget; + COutputEvent m_OnSurfaceChangedFromTarget; +}; + +#endif // ENV_PLAYER_SURFACE_TRIGGER_H diff --git a/sp/src/game/server/env_projectedtexture.cpp b/sp/src/game/server/env_projectedtexture.cpp new file mode 100644 index 00000000..0299fbd0 --- /dev/null +++ b/sp/src/game/server/env_projectedtexture.cpp @@ -0,0 +1,791 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity to control screen overlays on a player +// +//============================================================================= + +#include "cbase.h" +#include "shareddefs.h" +#ifdef ASW_PROJECTED_TEXTURES +#include "env_projectedtexture.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef ASW_PROJECTED_TEXTURES + +LINK_ENTITY_TO_CLASS( env_projectedtexture, CEnvProjectedTexture ); + +BEGIN_DATADESC( CEnvProjectedTexture ) + DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bDontFollowTarget, FIELD_BOOLEAN, "dontfollowtarget" ), + DEFINE_FIELD( m_bAlwaysUpdate, FIELD_BOOLEAN ), +#endif + DEFINE_FIELD( m_bState, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_flLightFOV, FIELD_FLOAT, "lightfov" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_flLightHorFOV, FIELD_FLOAT, "lighthorfov" ), +#endif + DEFINE_KEYFIELD( m_bEnableShadows, FIELD_BOOLEAN, "enableshadows" ), + DEFINE_KEYFIELD( m_bLightOnlyTarget, FIELD_BOOLEAN, "lightonlytarget" ), + DEFINE_KEYFIELD( m_bLightWorld, FIELD_BOOLEAN, "lightworld" ), + DEFINE_KEYFIELD( m_bCameraSpace, FIELD_BOOLEAN, "cameraspace" ), + DEFINE_KEYFIELD( m_flAmbient, FIELD_FLOAT, "ambient" ), + DEFINE_AUTO_ARRAY( m_SpotlightTextureName, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_nSpotlightTextureFrame, FIELD_INTEGER, "textureframe" ), + DEFINE_KEYFIELD( m_flNearZ, FIELD_FLOAT, "nearz" ), + DEFINE_KEYFIELD( m_flFarZ, FIELD_FLOAT, "farz" ), + DEFINE_KEYFIELD( m_nShadowQuality, FIELD_INTEGER, "shadowquality" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bAlwaysDraw, FIELD_BOOLEAN, "alwaysdraw" ), + DEFINE_KEYFIELD( m_bProjectedTextureVersion, FIELD_BOOLEAN, "ProjectedTextureVersion" ), +#endif + DEFINE_KEYFIELD( m_flBrightnessScale, FIELD_FLOAT, "brightnessscale" ), + DEFINE_FIELD( m_LightColor, FIELD_COLOR32 ), + DEFINE_KEYFIELD( m_flColorTransitionTime, FIELD_FLOAT, "colortransitiontime" ), +#ifdef MAPBASE + DEFINE_FIELD( m_flConstantAtten, FIELD_FLOAT ), + DEFINE_FIELD( m_flLinearAtten, FIELD_FLOAT ), + DEFINE_FIELD( m_flQuadraticAtten, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flShadowAtten, FIELD_FLOAT, "shadowatten" ), +#endif + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "AlwaysUpdateOn", InputAlwaysUpdateOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "AlwaysUpdateOff", InputAlwaysUpdateOff ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "FOV", InputSetFOV ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_FLOAT, "VerFOV", InputSetVerFOV ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "HorFOV", InputSetHorFOV ), +#endif + DEFINE_INPUTFUNC( FIELD_EHANDLE, "Target", InputSetTarget ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "CameraSpace", InputSetCameraSpace ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "LightOnlyTarget", InputSetLightOnlyTarget ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "LightWorld", InputSetLightWorld ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "EnableShadows", InputSetEnableShadows ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "LightColor", InputSetLightColor ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "Ambient", InputSetAmbient ), + DEFINE_INPUTFUNC( FIELD_STRING, "SpotlightTexture", InputSetSpotlightTexture ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetSpotlightFrame", InputSetSpotlightFrame ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetBrightness", InputSetBrightness ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetColorTransitionTime", InputSetColorTransitionTime ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetQuadratic", InputSetQuadratic ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetLinear", InputSetLinear ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetConstant", InputSetConstant ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetShadowAtten", InputSetShadowAtten ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetNearZ", InputSetNearZ ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFarZ", InputSetFarZ ), + DEFINE_INPUTFUNC( FIELD_VOID, "AlwaysDrawOn", InputAlwaysDrawOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "AlwaysDrawOff", InputAlwaysDrawOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopFollowingTarget", InputStopFollowingTarget ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartFollowingTarget", InputStartFollowingTarget ), +#endif + DEFINE_THINKFUNC( InitialThink ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CEnvProjectedTexture, DT_EnvProjectedTexture ) + SendPropEHandle( SENDINFO( m_hTargetEntity ) ), +#ifdef MAPBASE + SendPropBool( SENDINFO( m_bDontFollowTarget ) ), +#endif + SendPropBool( SENDINFO( m_bState ) ), + SendPropBool( SENDINFO( m_bAlwaysUpdate ) ), + SendPropFloat( SENDINFO( m_flLightFOV ) ), +#ifdef MAPBASE + SendPropFloat( SENDINFO( m_flLightHorFOV ) ), +#endif + SendPropBool( SENDINFO( m_bEnableShadows ) ), + SendPropBool( SENDINFO( m_bLightOnlyTarget ) ), + SendPropBool( SENDINFO( m_bLightWorld ) ), + SendPropBool( SENDINFO( m_bCameraSpace ) ), + SendPropFloat( SENDINFO( m_flBrightnessScale ) ), + SendPropInt( SENDINFO ( m_LightColor ), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), + SendPropFloat( SENDINFO( m_flColorTransitionTime ) ), + SendPropFloat( SENDINFO( m_flAmbient ) ), + SendPropString( SENDINFO( m_SpotlightTextureName ) ), + SendPropInt( SENDINFO( m_nSpotlightTextureFrame ) ), + SendPropFloat( SENDINFO( m_flNearZ ), 16, SPROP_ROUNDDOWN, 0.0f, 500.0f ), + SendPropFloat( SENDINFO( m_flFarZ ), 18, SPROP_ROUNDDOWN, 0.0f, 1500.0f ), + SendPropInt( SENDINFO( m_nShadowQuality ), 1, SPROP_UNSIGNED ), // Just one bit for now +#ifdef MAPBASE + SendPropFloat( SENDINFO( m_flConstantAtten ) ), + SendPropFloat( SENDINFO( m_flLinearAtten ) ), + SendPropFloat( SENDINFO( m_flQuadraticAtten ) ), + SendPropFloat( SENDINFO( m_flShadowAtten ) ), + SendPropBool( SENDINFO( m_bAlwaysDraw ) ), + + // Not needed on the client right now, change when it actually is needed + //SendPropBool( SENDINFO( m_bProjectedTextureVersion ) ), +#endif +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEnvProjectedTexture::CEnvProjectedTexture( void ) +{ + m_bState = true; + m_bAlwaysUpdate = false; + m_flLightFOV = 45.0f; + m_bEnableShadows = false; + m_bLightOnlyTarget = false; + m_bLightWorld = true; + m_bCameraSpace = false; + +#ifndef MAPBASE + Q_strcpy( m_SpotlightTextureName.GetForModify(), "effects/flashlight_border" ); +#endif + Q_strcpy( m_SpotlightTextureName.GetForModify(), "effects/flashlight001" ); + + m_nSpotlightTextureFrame = 0; + m_flBrightnessScale = 1.0f; + m_LightColor.Init( 255, 255, 255, 255 ); +#ifdef MAPBASE + m_flColorTransitionTime = 0.0f; +#else + m_flColorTransitionTime = 0.5f; +#endif + m_flAmbient = 0.0f; + m_flNearZ = 4.0f; + m_flFarZ = 750.0f; + m_nShadowQuality = 0; +#ifdef MAPBASE + m_flQuadraticAtten = 0.0f; + m_flLinearAtten = 100.0f; + m_flConstantAtten = 0.0f; + m_flShadowAtten = 0.0f; +#endif +} + +void UTIL_ColorStringToLinearFloatColor( Vector &color, const char *pString ) +{ + float tmp[4]; + UTIL_StringToFloatArray( tmp, 4, pString ); + if( tmp[3] <= 0.0f ) + { + tmp[3] = 255.0f; + } + tmp[3] *= ( 1.0f / 255.0f ); + color.x = tmp[0] * ( 1.0f / 255.0f ) * tmp[3]; + color.y = tmp[1] * ( 1.0f / 255.0f ) * tmp[3]; + color.z = tmp[2] * ( 1.0f / 255.0f ) * tmp[3]; +} + +bool CEnvProjectedTexture::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "lightcolor" ) ) + { +#ifdef MAPBASE + // + // Most existing projected textures don't have intensity implemented. + // This can give them 0% intensity, which makes them invisible. + // If intensity is not detected, assume 255. + // + // Also, get rid of the floats, for god's sake. Have you ever seen a color32 with decimals? + // + int tmp[4]; + tmp[3] = 255; + UTIL_StringToIntArray_PreserveArray( tmp, 4, szValue ); + + m_LightColor.SetR( tmp[0] ); + m_LightColor.SetG( tmp[1] ); + m_LightColor.SetB( tmp[2] ); + m_LightColor.SetA( tmp[3] ); +#else + float tmp[4]; + UTIL_StringToFloatArray( tmp, 4, szValue ); + + m_LightColor.SetR( tmp[0] ); + m_LightColor.SetG( tmp[1] ); + m_LightColor.SetB( tmp[2] ); + m_LightColor.SetA( tmp[3] ); +#endif + } + else if ( FStrEq( szKeyName, "texturename" ) ) + { +#if defined( _X360 ) + if ( Q_strcmp( szValue, "effects/flashlight001" ) == 0 ) + { + // Use this as the default for Xbox + Q_strcpy( m_SpotlightTextureName.GetForModify(), "effects/flashlight_border" ); + } + else + { + Q_strcpy( m_SpotlightTextureName.GetForModify(), szValue ); + } +#else + Q_strcpy( m_SpotlightTextureName.GetForModify(), szValue ); +#endif + } +#ifdef MAPBASE + else if ( FStrEq( szKeyName, "constant_attn" ) ) + { + m_flConstantAtten = CorrectConstantAtten( atof( szValue ) ); + } + else if ( FStrEq( szKeyName, "linear_attn" ) ) + { + m_flLinearAtten = CorrectLinearAtten( atof( szValue ) ); + } + else if ( FStrEq( szKeyName, "quadratic_attn" ) ) + { + m_flQuadraticAtten = CorrectQuadraticAtten( atof( szValue ) ); + } +#endif + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +bool CEnvProjectedTexture::GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ) +{ + if ( FStrEq( szKeyName, "lightcolor" ) ) + { + Q_snprintf( szValue, iMaxLen, "%d %d %d %d", m_LightColor.GetR(), m_LightColor.GetG(), m_LightColor.GetB(), m_LightColor.GetA() ); + return true; + } + else if ( FStrEq( szKeyName, "texturename" ) ) + { + Q_snprintf( szValue, iMaxLen, "%s", m_SpotlightTextureName.Get() ); + return true; + } +#ifdef MAPBASE + else if ( FStrEq( szKeyName, "constant_attn" ) ) + { + // Undo correction + Q_snprintf( szValue, iMaxLen, "%f", m_flConstantAtten *= 2.0f ); + return true; + } + else if ( FStrEq( szKeyName, "linear_attn" ) ) + { + // Undo correction + Q_snprintf( szValue, iMaxLen, "%f", m_flLinearAtten *= 0.01f ); + return true; + } + else if ( FStrEq( szKeyName, "quadratic_attn" ) ) + { + // Undo correction + Q_snprintf( szValue, iMaxLen, "%f", m_flQuadraticAtten *= 0.0001f ); + return true; + } +#endif + return BaseClass::GetKeyValue( szKeyName, szValue, iMaxLen ); +} + +void CEnvProjectedTexture::InputTurnOn( inputdata_t &inputdata ) +{ + m_bState = true; +} + +void CEnvProjectedTexture::InputTurnOff( inputdata_t &inputdata ) +{ + m_bState = false; +} + +void CEnvProjectedTexture::InputAlwaysUpdateOn( inputdata_t &inputdata ) +{ + m_bAlwaysUpdate = true; +} + +void CEnvProjectedTexture::InputAlwaysUpdateOff( inputdata_t &inputdata ) +{ + m_bAlwaysUpdate = false; +} + +void CEnvProjectedTexture::InputSetFOV( inputdata_t &inputdata ) +{ + m_flLightFOV = inputdata.value.Float(); +#ifdef MAPBASE + m_flLightHorFOV = inputdata.value.Float(); +#endif +} + +#ifdef MAPBASE +void CEnvProjectedTexture::InputSetVerFOV( inputdata_t &inputdata ) +{ + m_flLightFOV = inputdata.value.Float(); +} + +void CEnvProjectedTexture::InputSetHorFOV( inputdata_t &inputdata ) +{ + m_flLightHorFOV = inputdata.value.Float(); +} +#endif + +void CEnvProjectedTexture::InputSetTarget( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + // env_projectedtexture's "Target" uses FIELD_EHANDLE while Mapbase's base entity "SetTarget" uses FIELD_STRING + if (inputdata.value.FieldType() != FIELD_EHANDLE) + inputdata.value.Convert(FIELD_EHANDLE, this, inputdata.pActivator, inputdata.pCaller); +#endif + m_hTargetEntity = inputdata.value.Entity(); +} + +void CEnvProjectedTexture::InputSetCameraSpace( inputdata_t &inputdata ) +{ + m_bCameraSpace = inputdata.value.Bool(); +} + +void CEnvProjectedTexture::InputSetLightOnlyTarget( inputdata_t &inputdata ) +{ + m_bLightOnlyTarget = inputdata.value.Bool(); +} + +void CEnvProjectedTexture::InputSetLightWorld( inputdata_t &inputdata ) +{ + m_bLightWorld = inputdata.value.Bool(); +} + +void CEnvProjectedTexture::InputSetEnableShadows( inputdata_t &inputdata ) +{ + m_bEnableShadows = inputdata.value.Bool(); +} + +void CEnvProjectedTexture::InputSetLightColor( inputdata_t &inputdata ) +{ + m_LightColor = inputdata.value.Color32(); +} + +void CEnvProjectedTexture::InputSetAmbient( inputdata_t &inputdata ) +{ + m_flAmbient = inputdata.value.Float(); +} + +void CEnvProjectedTexture::InputSetSpotlightTexture( inputdata_t &inputdata ) +{ + Q_strcpy( m_SpotlightTextureName.GetForModify(), inputdata.value.String() ); +} + +#ifdef MAPBASE +void CEnvProjectedTexture::InputSetSpotlightFrame( inputdata_t &inputdata ) +{ + m_nSpotlightTextureFrame = inputdata.value.Int(); +} + +void CEnvProjectedTexture::InputSetBrightness( inputdata_t &inputdata ) +{ + m_flBrightnessScale = inputdata.value.Float(); +} + +void CEnvProjectedTexture::InputSetColorTransitionTime( inputdata_t &inputdata ) +{ + m_flColorTransitionTime = inputdata.value.Float(); +} + +void CEnvProjectedTexture::InputSetNearZ( inputdata_t &inputdata ) +{ + m_flNearZ = inputdata.value.Float(); +} + +void CEnvProjectedTexture::InputSetFarZ( inputdata_t &inputdata ) +{ + m_flFarZ = inputdata.value.Float(); +} +#endif + +#ifdef MAPBASE +void CEnvProjectedTexture::Spawn( void ) +{ + // Set m_flLightHorFOV to m_flLightFOV if it's still 0, indicating either legacy support or desire to do this + if (m_flLightHorFOV == 0.0f) + { + m_flLightHorFOV = m_flLightFOV; + } + + m_bState = ( ( GetSpawnFlags() & ENV_PROJECTEDTEXTURE_STARTON ) != 0 ); + m_bAlwaysUpdate = ( ( GetSpawnFlags() & ENV_PROJECTEDTEXTURE_ALWAYSUPDATE ) != 0 ); + + BaseClass::Spawn(); +} +#endif + +void CEnvProjectedTexture::Activate( void ) +{ +#ifndef MAPBASE // Putting this in Activate() breaks projected textures which start off or don't start always updating in savegames. Moved to Spawn() instead + m_bState = ( ( GetSpawnFlags() & ENV_PROJECTEDTEXTURE_STARTON ) != 0 ); + m_bAlwaysUpdate = ( ( GetSpawnFlags() & ENV_PROJECTEDTEXTURE_ALWAYSUPDATE ) != 0 ); +#endif + + SetThink( &CEnvProjectedTexture::InitialThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + +#ifdef MAPBASE + if (m_bProjectedTextureVersion == 0 && GetMoveParent()) + { + // Pre-Mapbase projected textures should use the VDC parenting fix. + m_bAlwaysUpdate = true; + } +#endif + + BaseClass::Activate(); +} + +#ifdef MAPBASE +void CEnvProjectedTexture::SetParent( CBaseEntity* pNewParent, int iAttachment ) +{ + BaseClass::SetParent( pNewParent, iAttachment ); + + if (m_bProjectedTextureVersion == 0) + { + // Pre-Mapbase projected textures should use the VDC parenting fix. + // Since the ASW changes structure projected textures differently, + // we have to set it here on the server when our parent changes. + // Don't run this code if we already have the spawnflag though. + if ( ( GetSpawnFlags() & ENV_PROJECTEDTEXTURE_ALWAYSUPDATE ) == 0 ) + { + m_bAlwaysUpdate = GetMoveParent() != NULL; + } + } +} +#endif + +void CEnvProjectedTexture::InitialThink( void ) +{ + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target ); +} + +int CEnvProjectedTexture::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +#else + +#define ENV_PROJECTEDTEXTURE_STARTON (1<<0) +#ifdef MAPBASE +#define ENV_PROJECTEDTEXTURE_ALWAYSUPDATE (1<<1) +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CEnvProjectedTexture : public CPointEntity +{ + DECLARE_CLASS( CEnvProjectedTexture, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CEnvProjectedTexture(); + bool KeyValue( const char *szKeyName, const char *szValue ); + + // Always transmit to clients + virtual int UpdateTransmitState(); + virtual void Activate( void ); + + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputAlwaysUpdateOn( inputdata_t &inputdata ); + void InputAlwaysUpdateOff( inputdata_t &inputdata ); +#endif + void InputSetFOV( inputdata_t &inputdata ); + void InputSetTarget( inputdata_t &inputdata ); + void InputSetCameraSpace( inputdata_t &inputdata ); + void InputSetLightOnlyTarget( inputdata_t &inputdata ); + void InputSetLightWorld( inputdata_t &inputdata ); + void InputSetEnableShadows( inputdata_t &inputdata ); +#ifndef MAPBASE +// void InputSetLightColor( inputdata_t &inputdata ); +#else + void InputSetLightColor( inputdata_t &inputdata ); +#endif + void InputSetSpotlightTexture( inputdata_t &inputdata ); + void InputSetAmbient( inputdata_t &inputdata ); + + void InitialThink( void ); + + CNetworkHandle( CBaseEntity, m_hTargetEntity ); + +private: + + CNetworkVar( bool, m_bState ); +#ifdef MAPBASE + CNetworkVar( bool, m_bAlwaysUpdate ); +#endif + CNetworkVar( float, m_flLightFOV ); + CNetworkVar( bool, m_bEnableShadows ); + CNetworkVar( bool, m_bLightOnlyTarget ); + CNetworkVar( bool, m_bLightWorld ); + CNetworkVar( bool, m_bCameraSpace ); + CNetworkVector( m_LinearFloatLightColor ); + CNetworkVar( float, m_flAmbient ); + CNetworkString( m_SpotlightTextureName, MAX_PATH ); + CNetworkVar( int, m_nSpotlightTextureFrame ); + CNetworkVar( float, m_flNearZ ); + CNetworkVar( float, m_flFarZ ); + CNetworkVar( int, m_nShadowQuality ); +}; + +LINK_ENTITY_TO_CLASS( env_projectedtexture, CEnvProjectedTexture ); + +BEGIN_DATADESC( CEnvProjectedTexture ) + DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_bState, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_flLightFOV, FIELD_FLOAT, "lightfov" ), + DEFINE_KEYFIELD( m_bEnableShadows, FIELD_BOOLEAN, "enableshadows" ), + DEFINE_KEYFIELD( m_bLightOnlyTarget, FIELD_BOOLEAN, "lightonlytarget" ), + DEFINE_KEYFIELD( m_bLightWorld, FIELD_BOOLEAN, "lightworld" ), + DEFINE_KEYFIELD( m_bCameraSpace, FIELD_BOOLEAN, "cameraspace" ), + DEFINE_KEYFIELD( m_flAmbient, FIELD_FLOAT, "ambient" ), +#ifndef MAPBASE + DEFINE_AUTO_ARRAY_KEYFIELD( m_SpotlightTextureName, FIELD_CHARACTER, "texturename" ), +#else + DEFINE_AUTO_ARRAY( m_SpotlightTextureName, FIELD_CHARACTER ), +#endif + DEFINE_KEYFIELD( m_nSpotlightTextureFrame, FIELD_INTEGER, "textureframe" ), + DEFINE_KEYFIELD( m_flNearZ, FIELD_FLOAT, "nearz" ), + DEFINE_KEYFIELD( m_flFarZ, FIELD_FLOAT, "farz" ), + DEFINE_KEYFIELD( m_nShadowQuality, FIELD_INTEGER, "shadowquality" ), + DEFINE_FIELD( m_LinearFloatLightColor, FIELD_VECTOR ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "AlwaysUpdateOn", InputAlwaysUpdateOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "AlwaysUpdateOff", InputAlwaysUpdateOff ), +#endif + DEFINE_INPUTFUNC( FIELD_FLOAT, "FOV", InputSetFOV ), + DEFINE_INPUTFUNC( FIELD_EHANDLE, "Target", InputSetTarget ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "CameraSpace", InputSetCameraSpace ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "LightOnlyTarget", InputSetLightOnlyTarget ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "LightWorld", InputSetLightWorld ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "EnableShadows", InputSetEnableShadows ), +#ifndef MAPBASE + // this is broken . . need to be able to set color and intensity like light_dynamic +// DEFINE_INPUTFUNC( FIELD_COLOR32, "LightColor", InputSetLightColor ), +#else + DEFINE_INPUTFUNC( FIELD_STRING, "LightColor", InputSetLightColor ), +#endif + DEFINE_INPUTFUNC( FIELD_FLOAT, "Ambient", InputSetAmbient ), + DEFINE_INPUTFUNC( FIELD_STRING, "SpotlightTexture", InputSetSpotlightTexture ), + DEFINE_THINKFUNC( InitialThink ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CEnvProjectedTexture, DT_EnvProjectedTexture ) + SendPropEHandle( SENDINFO( m_hTargetEntity ) ), + SendPropBool( SENDINFO( m_bState ) ), +#ifdef MAPBASE + SendPropBool( SENDINFO( m_bAlwaysUpdate ) ), +#endif + SendPropFloat( SENDINFO( m_flLightFOV ) ), + SendPropBool( SENDINFO( m_bEnableShadows ) ), + SendPropBool( SENDINFO( m_bLightOnlyTarget ) ), + SendPropBool( SENDINFO( m_bLightWorld ) ), + SendPropBool( SENDINFO( m_bCameraSpace ) ), + SendPropVector( SENDINFO( m_LinearFloatLightColor ) ), + SendPropFloat( SENDINFO( m_flAmbient ) ), + SendPropString( SENDINFO( m_SpotlightTextureName ) ), + SendPropInt( SENDINFO( m_nSpotlightTextureFrame ) ), + SendPropFloat( SENDINFO( m_flNearZ ), 16, SPROP_ROUNDDOWN, 0.0f, 500.0f ), + SendPropFloat( SENDINFO( m_flFarZ ), 18, SPROP_ROUNDDOWN, 0.0f, 1500.0f ), + SendPropInt( SENDINFO( m_nShadowQuality ), 1, SPROP_UNSIGNED ), // Just one bit for now +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEnvProjectedTexture::CEnvProjectedTexture( void ) +{ + m_bState = true; + m_flLightFOV = 45.0f; + m_bEnableShadows = false; + m_bLightOnlyTarget = false; + m_bLightWorld = true; + m_bCameraSpace = false; + +// if ( g_pHardwareConfig->SupportsBorderColor() ) +#if defined( _X360 ) + Q_strcpy( m_SpotlightTextureName.GetForModify(), "effects/flashlight_border" ); +#else + Q_strcpy( m_SpotlightTextureName.GetForModify(), "effects/flashlight001" ); +#endif + + m_nSpotlightTextureFrame = 0; + m_LinearFloatLightColor.Init( 1.0f, 1.0f, 1.0f ); + m_flAmbient = 0.0f; + m_flNearZ = 4.0f; + m_flFarZ = 750.0f; + m_nShadowQuality = 0; +} + +void UTIL_ColorStringToLinearFloatColor( Vector &color, const char *pString ) +{ + float tmp[4]; + UTIL_StringToFloatArray( tmp, 4, pString ); + if( tmp[3] <= 0.0f ) + { + tmp[3] = 255.0f; + } + tmp[3] *= ( 1.0f / 255.0f ); + color.x = GammaToLinear( tmp[0] * ( 1.0f / 255.0f ) ) * tmp[3]; + color.y = GammaToLinear( tmp[1] * ( 1.0f / 255.0f ) ) * tmp[3]; + color.z = GammaToLinear( tmp[2] * ( 1.0f / 255.0f ) ) * tmp[3]; +} + +bool CEnvProjectedTexture::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "lightcolor" ) ) + { + Vector tmp; + UTIL_ColorStringToLinearFloatColor( tmp, szValue ); + m_LinearFloatLightColor = tmp; + } +#ifdef MAPBASE + else if ( FStrEq(szKeyName, "texturename") ) + { + Q_strcpy(m_SpotlightTextureName.GetForModify(), szValue); + } +#endif + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +void CEnvProjectedTexture::InputTurnOn( inputdata_t &inputdata ) +{ + m_bState = true; +} + +void CEnvProjectedTexture::InputTurnOff( inputdata_t &inputdata ) +{ + m_bState = false; +} + +#ifdef MAPBASE +void CEnvProjectedTexture::InputAlwaysUpdateOn( inputdata_t &inputdata ) +{ + m_bAlwaysUpdate = true; +} + +void CEnvProjectedTexture::InputAlwaysUpdateOff( inputdata_t &inputdata ) +{ + m_bAlwaysUpdate = false; +} +#endif + +void CEnvProjectedTexture::InputSetFOV( inputdata_t &inputdata ) +{ + m_flLightFOV = inputdata.value.Float(); +} + +void CEnvProjectedTexture::InputSetTarget( inputdata_t &inputdata ) +{ + m_hTargetEntity = inputdata.value.Entity(); +} + +void CEnvProjectedTexture::InputSetCameraSpace( inputdata_t &inputdata ) +{ + m_bCameraSpace = inputdata.value.Bool(); +} + +void CEnvProjectedTexture::InputSetLightOnlyTarget( inputdata_t &inputdata ) +{ + m_bLightOnlyTarget = inputdata.value.Bool(); +} + +void CEnvProjectedTexture::InputSetLightWorld( inputdata_t &inputdata ) +{ + m_bLightWorld = inputdata.value.Bool(); +} + +void CEnvProjectedTexture::InputSetEnableShadows( inputdata_t &inputdata ) +{ + m_bEnableShadows = inputdata.value.Bool(); +} + +#ifndef MAPBASE +//void CEnvProjectedTexture::InputSetLightColor( inputdata_t &inputdata ) +//{ +// m_cLightColor = inputdata.value.Color32(); +//} +#else +void CEnvProjectedTexture::InputSetLightColor( inputdata_t &inputdata ) +{ + Vector tmp; + UTIL_ColorStringToLinearFloatColor( tmp, inputdata.value.String() ); + m_LinearFloatLightColor = tmp; +} +#endif + +void CEnvProjectedTexture::InputSetAmbient( inputdata_t &inputdata ) +{ + m_flAmbient = inputdata.value.Float(); +} + +void CEnvProjectedTexture::InputSetSpotlightTexture( inputdata_t &inputdata ) +{ + Q_strcpy( m_SpotlightTextureName.GetForModify(), inputdata.value.String() ); +} + +void CEnvProjectedTexture::Activate( void ) +{ + if ( GetSpawnFlags() & ENV_PROJECTEDTEXTURE_STARTON ) + { + m_bState = true; + } +#ifdef MAPBASE + m_bAlwaysUpdate = ( ( GetSpawnFlags() & ENV_PROJECTEDTEXTURE_ALWAYSUPDATE ) != 0 ); +#endif + + SetThink( &CEnvProjectedTexture::InitialThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + BaseClass::Activate(); +} + +void CEnvProjectedTexture::InitialThink( void ) +{ +#ifndef MAPBASE + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target ); +#else + if (m_hTargetEntity == NULL && m_target != NULL_STRING) + m_hTargetEntity = gEntList.FindEntityByName(NULL, m_target); + if (m_hTargetEntity == NULL) + return; + Vector vecToTarget = (m_hTargetEntity->GetAbsOrigin() - GetAbsOrigin()); + QAngle vecAngles; + VectorAngles(vecToTarget, vecAngles); + SetAbsAngles(vecAngles); + SetNextThink(gpGlobals->curtime + 0.1); +#endif +} + +int CEnvProjectedTexture::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +#endif + + +// Console command for creating env_projectedtexture entities +void CC_CreateFlashlight( const CCommand &args ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if( !pPlayer ) + return; + + QAngle angles = pPlayer->EyeAngles(); + Vector origin = pPlayer->EyePosition(); + + CEnvProjectedTexture *pFlashlight = dynamic_cast< CEnvProjectedTexture * >( CreateEntityByName("env_projectedtexture") ); + if( args.ArgC() > 1 ) + { + pFlashlight->SetName( AllocPooledString( args[1] ) ); + } + + pFlashlight->Teleport( &origin, &angles, NULL ); + +} +static ConCommand create_flashlight("create_flashlight", CC_CreateFlashlight, 0, FCVAR_CHEAT); diff --git a/sp/src/game/server/env_projectedtexture.h b/sp/src/game/server/env_projectedtexture.h new file mode 100644 index 00000000..06ff04ee --- /dev/null +++ b/sp/src/game/server/env_projectedtexture.h @@ -0,0 +1,119 @@ + +#ifndef ENV_PROJECTEDTEXTURE_H +#define ENV_PROJECTEDTEXTURE_H +#ifdef _WIN32 +#pragma once +#endif + +#define ENV_PROJECTEDTEXTURE_STARTON (1<<0) +#define ENV_PROJECTEDTEXTURE_ALWAYSUPDATE (1<<1) + +#ifdef ASW_PROJECTED_TEXTURES +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CEnvProjectedTexture : public CPointEntity +{ + DECLARE_CLASS( CEnvProjectedTexture, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CEnvProjectedTexture(); + bool KeyValue( const char *szKeyName, const char *szValue ); + virtual bool GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ); + + // Always transmit to clients + virtual int UpdateTransmitState(); +#ifdef MAPBASE + virtual void Spawn( void ); +#endif + virtual void Activate( void ); +#ifdef MAPBASE + void SetParent( CBaseEntity* pNewParent, int iAttachment = -1 ); +#endif + + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputAlwaysUpdateOn( inputdata_t &inputdata ); + void InputAlwaysUpdateOff( inputdata_t &inputdata ); + void InputSetFOV( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetVerFOV( inputdata_t &inputdata ); + void InputSetHorFOV( inputdata_t &inputdata ); +#endif + void InputSetTarget( inputdata_t &inputdata ); + void InputSetCameraSpace( inputdata_t &inputdata ); + void InputSetLightOnlyTarget( inputdata_t &inputdata ); + void InputSetLightWorld( inputdata_t &inputdata ); + void InputSetEnableShadows( inputdata_t &inputdata ); + void InputSetLightColor( inputdata_t &inputdata ); + void InputSetSpotlightTexture( inputdata_t &inputdata ); + void InputSetAmbient( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetSpotlightFrame( inputdata_t &inputdata ); + void InputSetBrightness( inputdata_t &inputdata ); + void InputSetColorTransitionTime( inputdata_t &inputdata ); + void InputSetConstant( inputdata_t &inputdata ) { m_flConstantAtten = CorrectConstantAtten(inputdata.value.Float()); } + void InputSetLinear( inputdata_t &inputdata ) { m_flLinearAtten = CorrectLinearAtten(inputdata.value.Float()); } + void InputSetQuadratic( inputdata_t &inputdata ) { m_flQuadraticAtten = CorrectQuadraticAtten(inputdata.value.Float()); } + void InputSetShadowAtten( inputdata_t &inputdata ) { m_flShadowAtten = inputdata.value.Float(); } + void InputSetNearZ( inputdata_t &inputdata ); + void InputSetFarZ( inputdata_t &inputdata ); + void InputAlwaysDrawOn( inputdata_t &inputdata ) { m_bAlwaysDraw = true; } + void InputAlwaysDrawOff( inputdata_t &inputdata ) { m_bAlwaysDraw = false; } + void InputStopFollowingTarget( inputdata_t &inputdata ) { m_bDontFollowTarget = true; } + void InputStartFollowingTarget( inputdata_t &inputdata ) { m_bDontFollowTarget = false; } + + // Corrects keyvalue/input attenuation for internal FlashlightEffect_t attenuation. + float CorrectConstantAtten( float fl ) { return fl * 0.5f; } + float CorrectLinearAtten( float fl ) { return fl * 100.0f; } + float CorrectQuadraticAtten( float fl ) { return fl * 10000.0f; } +#endif + + void InitialThink( void ); + + CNetworkHandle( CBaseEntity, m_hTargetEntity ); +#ifdef MAPBASE + CNetworkVar( bool, m_bDontFollowTarget ); +#endif + +private: + + CNetworkVar( bool, m_bState ); + CNetworkVar( bool, m_bAlwaysUpdate ); + CNetworkVar( float, m_flLightFOV ); +#ifdef MAPBASE + CNetworkVar( float, m_flLightHorFOV ); +#endif + CNetworkVar( bool, m_bEnableShadows ); + CNetworkVar( bool, m_bLightOnlyTarget ); + CNetworkVar( bool, m_bLightWorld ); + CNetworkVar( bool, m_bCameraSpace ); + CNetworkVar( float, m_flBrightnessScale ); + CNetworkColor32( m_LightColor ); + CNetworkVar( float, m_flColorTransitionTime ); + CNetworkVar( float, m_flAmbient ); + CNetworkString( m_SpotlightTextureName, MAX_PATH ); + CNetworkVar( int, m_nSpotlightTextureFrame ); + CNetworkVar( float, m_flNearZ ); + CNetworkVar( float, m_flFarZ ); + CNetworkVar( int, m_nShadowQuality ); +#ifdef MAPBASE + CNetworkVar( float, m_flConstantAtten ); + CNetworkVar( float, m_flLinearAtten ); + CNetworkVar( float, m_flQuadraticAtten ); + CNetworkVar( float, m_flShadowAtten ); + + CNetworkVar( bool, m_bAlwaysDraw ); + + // 1 = New projected texture + // 0 = Non-Mapbase projected texture, e.g. one that uses the VDC parenting fix instead of the spawnflag + // Not needed on the client right now, change to CNetworkVar when it actually is needed + bool m_bProjectedTextureVersion; +#endif +}; +#endif + + +#endif // ENV_PROJECTEDTEXTURE_H \ No newline at end of file diff --git a/sp/src/game/server/env_screenoverlay.cpp b/sp/src/game/server/env_screenoverlay.cpp new file mode 100644 index 00000000..07bbf08d --- /dev/null +++ b/sp/src/game/server/env_screenoverlay.cpp @@ -0,0 +1,269 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity to control screen overlays on a player +// +//=============================================================================// + +#include "cbase.h" +#include "shareddefs.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CEnvScreenOverlay : public CPointEntity +{ + DECLARE_CLASS( CEnvScreenOverlay, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CEnvScreenOverlay(); + + // Always transmit to clients + virtual int UpdateTransmitState(); + virtual void Spawn( void ); + virtual void Precache( void ); + + void InputStartOverlay( inputdata_t &inputdata ); + void InputStopOverlay( inputdata_t &inputdata ); + void InputSwitchOverlay( inputdata_t &inputdata ); + + void SetActive( bool bActive ) { m_bIsActive = bActive; } + +protected: + CNetworkArray( string_t, m_iszOverlayNames, MAX_SCREEN_OVERLAYS ); + CNetworkArray( float, m_flOverlayTimes, MAX_SCREEN_OVERLAYS ); + CNetworkVar( float, m_flStartTime ); + CNetworkVar( int, m_iDesiredOverlay ); + CNetworkVar( bool, m_bIsActive ); +}; + +LINK_ENTITY_TO_CLASS( env_screenoverlay, CEnvScreenOverlay ); + +BEGIN_DATADESC( CEnvScreenOverlay ) + +// Silence, Classcheck! +// DEFINE_ARRAY( m_iszOverlayNames, FIELD_STRING, MAX_SCREEN_OVERLAYS ), +// DEFINE_ARRAY( m_flOverlayTimes, FIELD_FLOAT, MAX_SCREEN_OVERLAYS ), + + DEFINE_KEYFIELD( m_iszOverlayNames[0], FIELD_STRING, "OverlayName1" ), + DEFINE_KEYFIELD( m_iszOverlayNames[1], FIELD_STRING, "OverlayName2" ), + DEFINE_KEYFIELD( m_iszOverlayNames[2], FIELD_STRING, "OverlayName3" ), + DEFINE_KEYFIELD( m_iszOverlayNames[3], FIELD_STRING, "OverlayName4" ), + DEFINE_KEYFIELD( m_iszOverlayNames[4], FIELD_STRING, "OverlayName5" ), + DEFINE_KEYFIELD( m_iszOverlayNames[5], FIELD_STRING, "OverlayName6" ), + DEFINE_KEYFIELD( m_iszOverlayNames[6], FIELD_STRING, "OverlayName7" ), + DEFINE_KEYFIELD( m_iszOverlayNames[7], FIELD_STRING, "OverlayName8" ), + DEFINE_KEYFIELD( m_iszOverlayNames[8], FIELD_STRING, "OverlayName9" ), + DEFINE_KEYFIELD( m_iszOverlayNames[9], FIELD_STRING, "OverlayName10" ), + DEFINE_KEYFIELD( m_flOverlayTimes[0], FIELD_FLOAT, "OverlayTime1" ), + DEFINE_KEYFIELD( m_flOverlayTimes[1], FIELD_FLOAT, "OverlayTime2" ), + DEFINE_KEYFIELD( m_flOverlayTimes[2], FIELD_FLOAT, "OverlayTime3" ), + DEFINE_KEYFIELD( m_flOverlayTimes[3], FIELD_FLOAT, "OverlayTime4" ), + DEFINE_KEYFIELD( m_flOverlayTimes[4], FIELD_FLOAT, "OverlayTime5" ), + DEFINE_KEYFIELD( m_flOverlayTimes[5], FIELD_FLOAT, "OverlayTime6" ), + DEFINE_KEYFIELD( m_flOverlayTimes[6], FIELD_FLOAT, "OverlayTime7" ), + DEFINE_KEYFIELD( m_flOverlayTimes[7], FIELD_FLOAT, "OverlayTime8" ), + DEFINE_KEYFIELD( m_flOverlayTimes[8], FIELD_FLOAT, "OverlayTime9" ), + DEFINE_KEYFIELD( m_flOverlayTimes[9], FIELD_FLOAT, "OverlayTime10" ), + + // Class CEnvScreenOverlay: + DEFINE_FIELD( m_iDesiredOverlay, FIELD_INTEGER ), + DEFINE_FIELD( m_flStartTime, FIELD_TIME ), + DEFINE_FIELD( m_bIsActive, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartOverlays", InputStartOverlay ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopOverlays", InputStopOverlay ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SwitchOverlay", InputSwitchOverlay ), + +END_DATADESC() + +void SendProxy_String_tToString( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + string_t *pString = (string_t*)pData; + pOut->m_pString = (char*)STRING( *pString ); +} + +IMPLEMENT_SERVERCLASS_ST( CEnvScreenOverlay, DT_EnvScreenOverlay ) + SendPropArray( SendPropString( SENDINFO_ARRAY( m_iszOverlayNames ), 0, SendProxy_String_tToString ), m_iszOverlayNames ), + SendPropArray( SendPropFloat( SENDINFO_ARRAY( m_flOverlayTimes ), 11, SPROP_ROUNDDOWN, -1.0f, 63.0f ), m_flOverlayTimes ), + SendPropFloat( SENDINFO( m_flStartTime ), 32, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_iDesiredOverlay ), 5 ), + SendPropBool( SENDINFO( m_bIsActive ) ), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEnvScreenOverlay::CEnvScreenOverlay( void ) +{ + m_flStartTime = 0; + m_iDesiredOverlay = 0; + m_bIsActive = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvScreenOverlay::Spawn( void ) +{ + Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvScreenOverlay::Precache( void ) +{ + for ( int i = 0; i < 10; i++ ) + { + if ( m_iszOverlayNames[i] == NULL_STRING ) + continue; + + PrecacheMaterial( STRING( m_iszOverlayNames[i] ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvScreenOverlay::InputStartOverlay( inputdata_t &inputdata ) +{ + if ( m_iszOverlayNames[0] == NULL_STRING ) + { + Warning("env_screenoverlay %s has no overlays to display.\n", STRING(GetEntityName()) ); + return; + } + + m_flStartTime = gpGlobals->curtime; + m_bIsActive = true; + + // Turn off any other screen overlays out there + CBaseEntity *pEnt = NULL; + while ( (pEnt = gEntList.FindEntityByClassname( pEnt, "env_screenoverlay" )) != NULL ) + { + if ( pEnt != this ) + { + CEnvScreenOverlay *pOverlay = assert_cast(pEnt); + pOverlay->SetActive( false ); + } + } +} + +int CEnvScreenOverlay::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +void CEnvScreenOverlay::InputSwitchOverlay( inputdata_t &inputdata ) +{ + int iNewOverlay = inputdata.value.Int() - 1; + iNewOverlay = abs( iNewOverlay ); + + if ( m_iszOverlayNames[iNewOverlay] == NULL_STRING ) + { + Warning("env_screenoverlay %s has no overlays to display.\n", STRING(GetEntityName()) ); + return; + } + + m_iDesiredOverlay = iNewOverlay; + m_flStartTime = gpGlobals->curtime; +} + +void CEnvScreenOverlay::InputStopOverlay( inputdata_t &inputdata ) +{ + if ( m_iszOverlayNames[0] == NULL_STRING ) + { + Warning("env_screenoverlay %s has no overlays to display.\n", STRING(GetEntityName()) ); + return; + } + + m_flStartTime = -1; + m_bIsActive = false; +} + +// ==================================================================================== +// +// Screen-space effects +// +// ==================================================================================== + +class CEnvScreenEffect : public CPointEntity +{ + DECLARE_CLASS( CEnvScreenEffect, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + // We always want to be sent to the client + CEnvScreenEffect( void ) { AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); } + virtual int UpdateTransmitState( void ) { return SetTransmitState( FL_EDICT_ALWAYS ); } + virtual void Spawn( void ); + virtual void Precache( void ); + +private: + + void InputStartEffect( inputdata_t &inputdata ); + void InputStopEffect( inputdata_t &inputdata ); + + CNetworkVar( float, m_flDuration ); + CNetworkVar( int, m_nType ); +}; + +LINK_ENTITY_TO_CLASS( env_screeneffect, CEnvScreenEffect ); + +// CEnvScreenEffect +BEGIN_DATADESC( CEnvScreenEffect ) + DEFINE_FIELD( m_flDuration, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_nType, FIELD_INTEGER, "type" ), + DEFINE_FIELD( m_flDuration, FIELD_FLOAT ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "StartEffect", InputStartEffect ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "StopEffect", InputStopEffect ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CEnvScreenEffect, DT_EnvScreenEffect ) + SendPropFloat( SENDINFO( m_flDuration ), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_nType ), 32, SPROP_UNSIGNED ), +END_SEND_TABLE() + +void CEnvScreenEffect::Spawn( void ) +{ + Precache(); +} + +void CEnvScreenEffect::Precache( void ) +{ + PrecacheMaterial( "effects/stun" ); + PrecacheMaterial( "effects/introblur" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvScreenEffect::InputStartEffect( inputdata_t &inputdata ) +{ + // Take the duration as our value + m_flDuration = inputdata.value.Float(); + + EntityMessageBegin( this ); + WRITE_BYTE( 0 ); + MessageEnd(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvScreenEffect::InputStopEffect( inputdata_t &inputdata ) +{ + m_flDuration = inputdata.value.Float(); + + // Send the stop notification + EntityMessageBegin( this ); + WRITE_BYTE( 1 ); + MessageEnd(); +} diff --git a/sp/src/game/server/env_texturetoggle.cpp b/sp/src/game/server/env_texturetoggle.cpp new file mode 100644 index 00000000..53ccbd5f --- /dev/null +++ b/sp/src/game/server/env_texturetoggle.cpp @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CTextureToggle : public CPointEntity +{ +public: + DECLARE_CLASS( CTextureToggle, CPointEntity ); + + void InputIncrementBrushTexIndex( inputdata_t &inputdata ); + void InputSetBrushTexIndex( inputdata_t &inputdata ); + +private: + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( env_texturetoggle, CTextureToggle ); + +BEGIN_DATADESC( CTextureToggle ) + + DEFINE_INPUTFUNC( FIELD_VOID, "IncrementTextureIndex", InputIncrementBrushTexIndex ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetTextureIndex", InputSetBrushTexIndex ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CTextureToggle::InputIncrementBrushTexIndex( inputdata_t& inputdata ) +{ + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_target ); + + while( pEntity ) + { + int iCurrentIndex = pEntity->GetTextureFrameIndex() + 1; + pEntity->SetTextureFrameIndex( iCurrentIndex ); + + pEntity = gEntList.FindEntityByName( pEntity, m_target ); + } +} + +void CTextureToggle::InputSetBrushTexIndex( inputdata_t& inputdata ) +{ + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_target ); + + while( pEntity ) + { + int iData = inputdata.value.Int(); + + pEntity->SetTextureFrameIndex( iData ); + pEntity = gEntList.FindEntityByName( pEntity, m_target ); + } +} + diff --git a/sp/src/game/server/env_tonemap_controller.cpp b/sp/src/game/server/env_tonemap_controller.cpp new file mode 100644 index 00000000..39998e4b --- /dev/null +++ b/sp/src/game/server/env_tonemap_controller.cpp @@ -0,0 +1,372 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "baseentity.h" +#include "entityoutput.h" +#include "convar.h" + +#include "player.h" //Tony; need player.h so we can trigger inputs on the player, from our inputs! + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar mat_hdr_tonemapscale( "mat_hdr_tonemapscale", "1.0", FCVAR_CHEAT, "The HDR tonemap scale. 1 = Use autoexposure, 0 = eyes fully closed, 16 = eyes wide open." ); + +// 0 - eyes fully closed / fully black +// 1 - nominal +// 16 - eyes wide open / fully white + +//----------------------------------------------------------------------------- +// Purpose: Entity that controls player's tonemap +//----------------------------------------------------------------------------- +class CEnvTonemapController : public CPointEntity +{ + DECLARE_CLASS( CEnvTonemapController, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + void Spawn( void ); + int UpdateTransmitState( void ); + void UpdateTonemapScaleBlend( void ); + + // Inputs + void InputSetTonemapScale( inputdata_t &inputdata ); + void InputBlendTonemapScale( inputdata_t &inputdata ); + void InputSetTonemapRate( inputdata_t &inputdata ); + void InputSetAutoExposureMin( inputdata_t &inputdata ); + void InputSetAutoExposureMax( inputdata_t &inputdata ); + void InputUseDefaultAutoExposure( inputdata_t &inputdata ); + void InputSetBloomScale( inputdata_t &inputdata ); + void InputUseDefaultBloomScale( inputdata_t &inputdata ); + void InputSetBloomScaleRange( inputdata_t &inputdata ); + +private: + float m_flBlendTonemapStart; // HDR Tonemap at the start of the blend + float m_flBlendTonemapEnd; // Target HDR Tonemap at the end of the blend + float m_flBlendEndTime; // Time at which the blend ends + float m_flBlendStartTime; // Time at which the blend started + + CNetworkVar( bool, m_bUseCustomAutoExposureMin ); + CNetworkVar( bool, m_bUseCustomAutoExposureMax ); + CNetworkVar( bool, m_bUseCustomBloomScale ); + CNetworkVar( float, m_flCustomAutoExposureMin ); + CNetworkVar( float, m_flCustomAutoExposureMax ); + CNetworkVar( float, m_flCustomBloomScale); + CNetworkVar( float, m_flCustomBloomScaleMinimum); +}; + +LINK_ENTITY_TO_CLASS( env_tonemap_controller, CEnvTonemapController ); + +BEGIN_DATADESC( CEnvTonemapController ) + DEFINE_FIELD( m_flBlendTonemapStart, FIELD_FLOAT ), + DEFINE_FIELD( m_flBlendTonemapEnd, FIELD_FLOAT ), + DEFINE_FIELD( m_flBlendEndTime, FIELD_TIME ), + DEFINE_FIELD( m_flBlendStartTime, FIELD_TIME ), + DEFINE_FIELD( m_bUseCustomAutoExposureMin, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bUseCustomAutoExposureMax, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flCustomAutoExposureMin, FIELD_FLOAT ), + DEFINE_FIELD( m_flCustomAutoExposureMax, FIELD_FLOAT ), + DEFINE_FIELD( m_flCustomBloomScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flCustomBloomScaleMinimum, FIELD_FLOAT ), + DEFINE_FIELD( m_bUseCustomBloomScale, FIELD_BOOLEAN ), + + DEFINE_THINKFUNC( UpdateTonemapScaleBlend ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetTonemapScale", InputSetTonemapScale ), + DEFINE_INPUTFUNC( FIELD_STRING, "BlendTonemapScale", InputBlendTonemapScale ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetTonemapRate", InputSetTonemapRate ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetAutoExposureMin", InputSetAutoExposureMin ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetAutoExposureMax", InputSetAutoExposureMax ), + DEFINE_INPUTFUNC( FIELD_VOID, "UseDefaultAutoExposure", InputUseDefaultAutoExposure ), + DEFINE_INPUTFUNC( FIELD_VOID, "UseDefaultBloomScale", InputUseDefaultBloomScale ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetBloomScale", InputSetBloomScale ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetBloomScaleRange", InputSetBloomScaleRange ), +#else + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetBloomScaleRange", InputSetBloomScaleRange ), +#endif +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CEnvTonemapController, DT_EnvTonemapController ) + SendPropInt( SENDINFO(m_bUseCustomAutoExposureMin), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_bUseCustomAutoExposureMax), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_bUseCustomBloomScale), 1, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_flCustomAutoExposureMin), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flCustomAutoExposureMax), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flCustomBloomScale), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO(m_flCustomBloomScaleMinimum), 0, SPROP_NOSCALE), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvTonemapController::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CEnvTonemapController::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the tonemap scale to the specified value +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputSetTonemapScale( inputdata_t &inputdata ) +{ + //Tony; in multiplayer, we check to see if the activator is a player, if they are, we trigger an input on them, and then get out. + //if there is no activator, or the activator is not a player; ie: LogicAuto, we set the 'global' values. + if ( ( gpGlobals->maxClients > 1 ) ) + { + if ( inputdata.pActivator != NULL && inputdata.pActivator->IsPlayer() ) + { +// DevMsg("activator is a player: InputSetTonemapScale\n"); + CBasePlayer *pPlayer = ToBasePlayer(inputdata.pActivator); + if (pPlayer) + { + pPlayer->InputSetTonemapScale( inputdata ); + return; + } + } + } + + float flRemapped = inputdata.value.Float(); + mat_hdr_tonemapscale.SetValue( flRemapped ); +} + +//----------------------------------------------------------------------------- +// Purpose: Blend the tonemap scale to the specified value +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputBlendTonemapScale( inputdata_t &inputdata ) +{ + //Tony; TODO!!! -- tonemap scale blending does _not_ work properly in multiplayer.. + if ( ( gpGlobals->maxClients > 1 ) ) + return; + + char parseString[255]; + Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); + + // Get the target tonemap scale + char *pszParam = strtok(parseString," "); + if ( !pszParam || !pszParam[0] ) + { + Warning("%s (%s) received BlendTonemapScale input without a target tonemap scale. Syntax: \n", GetClassname(), GetDebugName() ); + return; + } + m_flBlendTonemapEnd = atof( pszParam ); + + // Get the blend time + pszParam = strtok(NULL," "); + if ( !pszParam || !pszParam[0] ) + { + Warning("%s (%s) received BlendTonemapScale input without a blend time. Syntax: \n", GetClassname(), GetDebugName() ); + return; + } + m_flBlendEndTime = gpGlobals->curtime + atof( pszParam ); + + m_flBlendStartTime = gpGlobals->curtime; + m_flBlendTonemapStart = mat_hdr_tonemapscale.GetFloat(); + + // Start thinking + SetNextThink( gpGlobals->curtime + 0.1 ); + SetThink( &CEnvTonemapController::UpdateTonemapScaleBlend ); +} + +//----------------------------------------------------------------------------- +// Purpose: set a base and minimum bloom scale +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputSetBloomScaleRange( inputdata_t &inputdata ) +{ + float bloom_max=1, bloom_min=1; + int nargs=sscanf("%f %f",inputdata.value.String(), bloom_max, bloom_min ); + if (nargs != 2) + { + Warning("%s (%s) received SetBloomScaleRange input without 2 arguments. Syntax: \n", GetClassname(), GetDebugName() ); + return; + } + m_flCustomBloomScale=bloom_max; + m_flCustomBloomScaleMinimum=bloom_min; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputSetTonemapRate( inputdata_t &inputdata ) +{ + //Tony; in multiplayer, we check to see if the activator is a player, if they are, we trigger an input on them, and then get out. + //if there is no activator, or the activator is not a player; ie: LogicAuto, we set the 'global' values. + if ( ( gpGlobals->maxClients > 1 ) ) + { + if ( inputdata.pActivator != NULL && inputdata.pActivator->IsPlayer() ) + { +// DevMsg("activator is a player: InputSetTonemapRate\n"); + CBasePlayer *pPlayer = ToBasePlayer(inputdata.pActivator); + if (pPlayer) + { + pPlayer->InputSetTonemapRate( inputdata ); + return; + } + } + } + + // TODO: There should be a better way to do this. + ConVarRef mat_hdr_manual_tonemap_rate( "mat_hdr_manual_tonemap_rate" ); + if ( mat_hdr_manual_tonemap_rate.IsValid() ) + { + float flTonemapRate = inputdata.value.Float(); + mat_hdr_manual_tonemap_rate.SetValue( flTonemapRate ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Blend the tonemap scale to the specified value +//----------------------------------------------------------------------------- +void CEnvTonemapController::UpdateTonemapScaleBlend( void ) +{ + float flRemapped = RemapValClamped( gpGlobals->curtime, m_flBlendStartTime, m_flBlendEndTime, m_flBlendTonemapStart, m_flBlendTonemapEnd ); + mat_hdr_tonemapscale.SetValue( flRemapped ); + + //Msg("Setting tonemap scale to %f (curtime %f, %f -> %f)\n", flRemapped, gpGlobals->curtime, m_flBlendStartTime, m_flBlendEndTime ); + + // Stop when we're out of the blend range + if ( gpGlobals->curtime >= m_flBlendEndTime ) + return; + + SetNextThink( gpGlobals->curtime + 0.1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the auto exposure min to the specified value +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputSetAutoExposureMin( inputdata_t &inputdata ) +{ + //Tony; in multiplayer, we check to see if the activator is a player, if they are, we trigger an input on them, and then get out. + //if there is no activator, or the activator is not a player; ie: LogicAuto, we set the 'global' values. + if ( ( gpGlobals->maxClients > 1 ) ) + { + if ( inputdata.pActivator != NULL && inputdata.pActivator->IsPlayer() ) + { +// DevMsg("activator is a player: InputSetAutoExposureMin\n"); + CBasePlayer *pPlayer = ToBasePlayer(inputdata.pActivator); + if (pPlayer) + { + pPlayer->InputSetAutoExposureMin( inputdata ); + return; + } + } + } + + m_flCustomAutoExposureMin = inputdata.value.Float(); + m_bUseCustomAutoExposureMin = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Set the auto exposure max to the specified value +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputSetAutoExposureMax( inputdata_t &inputdata ) +{ + //Tony; in multiplayer, we check to see if the activator is a player, if they are, we trigger an input on them, and then get out. + //if there is no activator, or the activator is not a player; ie: LogicAuto, we set the 'global' values. + if ( ( gpGlobals->maxClients > 1 ) ) + { + if ( inputdata.pActivator != NULL && inputdata.pActivator->IsPlayer() ) + { +// DevMsg("activator is a player: InputSetAutoExposureMax\n"); + CBasePlayer *pPlayer = ToBasePlayer(inputdata.pActivator); + if (pPlayer) + { + pPlayer->InputSetAutoExposureMax( inputdata ); + return; + } + } + } + + m_flCustomAutoExposureMax = inputdata.value.Float(); + m_bUseCustomAutoExposureMax = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputUseDefaultAutoExposure( inputdata_t &inputdata ) +{ + //Tony; in multiplayer, we check to see if the activator is a player, if they are, we trigger an input on them, and then get out. + //if there is no activator, or the activator is not a player; ie: LogicAuto, we set the 'global' values. + if ( ( gpGlobals->maxClients > 1 ) ) + { + if ( inputdata.pActivator != NULL && inputdata.pActivator->IsPlayer() ) + { +// DevMsg("activator is a player: InputUseDefaultAutoExposure\n"); + CBasePlayer *pPlayer = ToBasePlayer(inputdata.pActivator); + if (pPlayer) + { + pPlayer->InputUseDefaultAutoExposure( inputdata ); + return; + } + } + } + + m_bUseCustomAutoExposureMin = false; + m_bUseCustomAutoExposureMax = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputSetBloomScale( inputdata_t &inputdata ) +{ + //Tony; in multiplayer, we check to see if the activator is a player, if they are, we trigger an input on them, and then get out. + //if there is no activator, or the activator is not a player; ie: LogicAuto, we set the 'global' values. + if ( ( gpGlobals->maxClients > 1 ) ) + { + if ( inputdata.pActivator != NULL && inputdata.pActivator->IsPlayer() ) + { +// DevMsg("activator is a player: InputSetBloomScale\n"); + CBasePlayer *pPlayer = ToBasePlayer(inputdata.pActivator); + if (pPlayer) + { + pPlayer->InputSetBloomScale( inputdata ); + return; + } + } + } + + m_flCustomBloomScale = inputdata.value.Float(); + m_flCustomBloomScaleMinimum = m_flCustomBloomScale; + m_bUseCustomBloomScale = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvTonemapController::InputUseDefaultBloomScale( inputdata_t &inputdata ) +{ + //Tony; in multiplayer, we check to see if the activator is a player, if they are, we trigger an input on them, and then get out. + //if there is no activator, or the activator is not a player; ie: LogicAuto, we set the 'global' values. + if ( ( gpGlobals->maxClients > 1 ) ) + { + if ( inputdata.pActivator != NULL && inputdata.pActivator->IsPlayer() ) + { +// DevMsg("activator is a player: InputUseDefaultBloomScale\n"); + CBasePlayer *pPlayer = ToBasePlayer(inputdata.pActivator); + if (pPlayer) + { + pPlayer->InputUseDefaultBloomScale( inputdata ); + return; + } + } + } + + m_bUseCustomBloomScale = false; +} diff --git a/sp/src/game/server/env_zoom.cpp b/sp/src/game/server/env_zoom.cpp new file mode 100644 index 00000000..ec679152 --- /dev/null +++ b/sp/src/game/server/env_zoom.cpp @@ -0,0 +1,150 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "env_zoom.h" + +#ifdef HL2_DLL +#include "hl2_player.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define ENV_ZOOM_OVERRIDE (1<<0) + +class CEnvZoom : public CPointEntity +{ +public: + DECLARE_CLASS( CEnvZoom, CPointEntity ); + + void InputZoom( inputdata_t &inputdata ); + void InputUnZoom( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputUnZoomWithRate( inputdata_t &inputdata ); + void InputSetZoomRate( inputdata_t &inputdata ); +#endif + + int GetFOV( void ) { return m_nFOV; } + float GetSpeed( void ) { return m_flSpeed; } +private: + + float m_flSpeed; + int m_nFOV; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( env_zoom, CEnvZoom ); + +BEGIN_DATADESC( CEnvZoom ) + + DEFINE_KEYFIELD( m_flSpeed, FIELD_FLOAT, "Rate" ), + DEFINE_KEYFIELD( m_nFOV, FIELD_INTEGER, "FOV" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Zoom", InputZoom ), + DEFINE_INPUTFUNC( FIELD_VOID, "UnZoom", InputUnZoom ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "UnZoomWithRate", InputUnZoomWithRate ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetZoomRate", InputSetZoomRate ), +#endif + +END_DATADESC() + +bool CanOverrideEnvZoomOwner( CBaseEntity *pZoomOwner ) +{ + CEnvZoom *pZoom = dynamic_cast(pZoomOwner ); + + if ( pZoom == NULL || ( pZoom && pZoom->HasSpawnFlags( ENV_ZOOM_OVERRIDE ) == false ) ) + return false; + + return true; +} + +float GetZoomOwnerDesiredFOV( CBaseEntity *pZoomOwner ) +{ + if ( CanOverrideEnvZoomOwner( pZoomOwner ) ) + { + CEnvZoom *pZoom = dynamic_cast( pZoomOwner ); + + return pZoom->GetFOV(); + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvZoom::InputZoom( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( pPlayer ) + { + +#ifdef HL2_DLL + if ( pPlayer == pPlayer->GetFOVOwner() ) + { + CHL2_Player *pHLPlayer = static_cast( pPlayer ); + + pHLPlayer->StopZooming(); + } +#endif + + // If the player's already holding a fov from another env_zoom, we're allowed to overwrite it + if ( pPlayer->GetFOVOwner() && FClassnameIs( pPlayer->GetFOVOwner(), "env_zoom" ) ) + { + pPlayer->ClearZoomOwner(); + } + + //Stuff the values + pPlayer->SetFOV( this, m_nFOV, m_flSpeed ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvZoom::InputUnZoom( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( pPlayer ) + { + // Stuff the values + pPlayer->SetFOV( this, 0 ); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvZoom::InputUnZoomWithRate( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( pPlayer ) + { + // Stuff the values + pPlayer->SetFOV( this, 0, m_flSpeed, m_nFOV ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvZoom::InputSetZoomRate( inputdata_t &inputdata ) +{ + m_flSpeed = inputdata.value.Float(); +} +#endif + diff --git a/sp/src/game/server/env_zoom.h b/sp/src/game/server/env_zoom.h new file mode 100644 index 00000000..1af1ef5b --- /dev/null +++ b/sp/src/game/server/env_zoom.h @@ -0,0 +1,13 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef ENV_ZOOM_H +#define ENV_ZOOM_H + +bool CanOverrideEnvZoomOwner( CBaseEntity *pZoomOwner ); +float GetZoomOwnerDesiredFOV( CBaseEntity *pZoomOwner ); + +#endif //ENV_ZOOM_H \ No newline at end of file diff --git a/sp/src/game/server/envmicrophone.cpp b/sp/src/game/server/envmicrophone.cpp new file mode 100644 index 00000000..29a59ead --- /dev/null +++ b/sp/src/game/server/envmicrophone.cpp @@ -0,0 +1,648 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements an entity that measures sound volume at a point in a map. +// +// This entity listens as though it is an NPC, meaning it will only +// hear sounds that were emitted using the CSound::InsertSound function. +// +// It does not hear danger sounds since they are not technically sounds. +// +//============================================================================= + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "mathlib/mathlib.h" +#include "soundent.h" +#include "envmicrophone.h" +#include "soundflags.h" +#include "engine/IEngineSound.h" +#include "filters.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//#define DEBUG_MICROPHONE + +const float MICROPHONE_SETTLE_EPSILON = 0.005; + +// List of env_microphones who want to be told whenever a sound is started +static CUtlVector< CHandle > s_Microphones; + + +LINK_ENTITY_TO_CLASS(env_microphone, CEnvMicrophone); + +BEGIN_DATADESC( CEnvMicrophone ) + + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled"), + DEFINE_FIELD(m_hMeasureTarget, FIELD_EHANDLE), + DEFINE_KEYFIELD(m_nSoundMask, FIELD_INTEGER, "SoundMask"), + DEFINE_KEYFIELD(m_flSensitivity, FIELD_FLOAT, "Sensitivity"), + DEFINE_KEYFIELD(m_flSmoothFactor, FIELD_FLOAT, "SmoothFactor"), + DEFINE_KEYFIELD(m_iszSpeakerName, FIELD_STRING, "SpeakerName"), + DEFINE_KEYFIELD(m_iszListenFilter, FIELD_STRING, "ListenFilter"), + DEFINE_FIELD(m_hListenFilter, FIELD_EHANDLE), + DEFINE_FIELD(m_hSpeaker, FIELD_EHANDLE), +#ifdef MAPBASE + DEFINE_KEYFIELD(m_iszLandmarkName, FIELD_STRING, "landmark"), + DEFINE_FIELD(m_hLandmark, FIELD_EHANDLE), + DEFINE_KEYFIELD(m_flPitchScale, FIELD_FLOAT, "PitchScale"), + DEFINE_KEYFIELD(m_flVolumeScale, FIELD_FLOAT, "VolumeScale"), + DEFINE_KEYFIELD(m_nChannel, FIELD_INTEGER, "channel"), +#endif + // DEFINE_FIELD(m_bAvoidFeedback, FIELD_BOOLEAN), // DONT SAVE + DEFINE_KEYFIELD(m_iSpeakerDSPPreset, FIELD_INTEGER, "speaker_dsp_preset" ), + DEFINE_KEYFIELD(m_flMaxRange, FIELD_FLOAT, "MaxRange"), + DEFINE_AUTO_ARRAY(m_szLastSound, FIELD_CHARACTER), + + DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable), + DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable), + DEFINE_INPUTFUNC(FIELD_STRING, "SetSpeakerName", InputSetSpeakerName), +#ifdef MAPBASE + DEFINE_INPUTFUNC(FIELD_INTEGER, "SetDSPPreset", InputSetDSPPreset), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPitchScale", InputSetPitchScale ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetVolumeScale", InputSetVolumeScale ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetChannel", InputSetChannel ), +#endif + + DEFINE_OUTPUT(m_SoundLevel, "SoundLevel"), + DEFINE_OUTPUT(m_OnRoutedSound, "OnRoutedSound" ), + DEFINE_OUTPUT(m_OnHeardSound, "OnHeardSound" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CEnvMicrophone::~CEnvMicrophone( void ) +{ + s_Microphones.FindAndRemove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after keyvalues have been handled. +//----------------------------------------------------------------------------- +void CEnvMicrophone::Spawn(void) +{ + // + // Build our sound type mask from our spawnflags. + // + static int nFlags[][2] = + { + { SF_MICROPHONE_SOUND_COMBAT, SOUND_COMBAT }, + { SF_MICROPHONE_SOUND_WORLD, SOUND_WORLD }, + { SF_MICROPHONE_SOUND_PLAYER, SOUND_PLAYER }, + { SF_MICROPHONE_SOUND_BULLET_IMPACT, SOUND_BULLET_IMPACT }, + { SF_MICROPHONE_SOUND_EXPLOSION, SOUND_CONTEXT_EXPLOSION }, + }; + + for (int i = 0; i < sizeof(nFlags) / sizeof(nFlags[0]); i++) + { + if (m_spawnflags & nFlags[i][0]) + { + m_nSoundMask |= nFlags[i][1]; + } + } + + if (m_flSensitivity == 0) + { + // + // Avoid a divide by zero in CanHearSound. + // + m_flSensitivity = 1; + } + else if (m_flSensitivity > 10) + { + m_flSensitivity = 10; + } + + m_flSmoothFactor = clamp(m_flSmoothFactor, 0.f, 0.9f); + + if (!m_bDisabled) + { + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called after all entities have spawned and after a load game. +// Finds the reference point at which to measure sound level. +//----------------------------------------------------------------------------- +void CEnvMicrophone::Activate(void) +{ + BaseClass::Activate(); + + // Get a handle to my filter entity if there is one + if (m_iszListenFilter != NULL_STRING) + { + m_hListenFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iszListenFilter )); + } + + if (m_target != NULL_STRING) + { + m_hMeasureTarget = gEntList.FindEntityByName(NULL, STRING(m_target) ); + + // + // If we were given a bad measure target, just measure sound where we are. + // + if ((m_hMeasureTarget == NULL) || (m_hMeasureTarget->edict() == NULL)) + { + // We've decided to disable this warning since this seems to be the 90% case. + //Warning( "EnvMicrophone - Measure target not found or measure target with no origin. Using Self.!\n"); + m_hMeasureTarget = this; + } + } + else + { + m_hMeasureTarget = this; + } + + ActivateSpeaker(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvMicrophone::OnRestore( void ) +{ + BaseClass::OnRestore(); + + ActivateSpeaker(); +} + +//----------------------------------------------------------------------------- +// Purpose: If we've got a speaker, add ourselves to the list of microphones that want to listen +//----------------------------------------------------------------------------- +void CEnvMicrophone::ActivateSpeaker( void ) +{ + // If we're enabled, set the dsp_speaker preset to my specified one + if ( !m_bDisabled ) + { + ConVarRef dsp_speaker( "dsp_speaker" ); + if ( dsp_speaker.IsValid() ) + { + int iDSPPreset = m_iSpeakerDSPPreset; + if ( !iDSPPreset ) + { + // Reset it to the default + iDSPPreset = atoi( dsp_speaker.GetDefault() ); + } + DevMsg( 2, "Microphone %s set dsp_speaker to %d.\n", STRING(GetEntityName()), iDSPPreset); + dsp_speaker.SetValue( m_iSpeakerDSPPreset ); + } + } + + if ( m_iszSpeakerName != NULL_STRING ) + { + // We've got a speaker to play heard sounds through. To do this, we need to add ourselves + // to the list of microphones who want to be told whenever a sound is played. + if ( s_Microphones.Find(this) == -1 ) + { + s_Microphones.AddToTail( this ); + } + } + +#ifdef MAPBASE + if (m_iszLandmarkName != NULL_STRING) + { + m_hLandmark = gEntList.FindEntityByName(NULL, m_iszLandmarkName, this, NULL, NULL); + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Stops the microphone from sampling the sound level and firing the +// SoundLevel output. +//----------------------------------------------------------------------------- +void CEnvMicrophone::InputEnable( inputdata_t &inputdata ) +{ + if (m_bDisabled) + { + m_bDisabled = false; + SetNextThink( gpGlobals->curtime + 0.1f ); + + ActivateSpeaker(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Resumes sampling the sound level and firing the SoundLevel output. +//----------------------------------------------------------------------------- +void CEnvMicrophone::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; + if ( m_hSpeaker ) + { + CBaseEntity::StopSound( m_hSpeaker->entindex(), CHAN_STATIC, m_szLastSound ); + m_szLastSound[0] = 0; + + // Remove ourselves from the list of active mics + s_Microphones.FindAndRemove( this ); + } + SetNextThink( TICK_NEVER_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvMicrophone::InputSetSpeakerName( inputdata_t &inputdata ) +{ + SetSpeakerName( inputdata.value.StringID() ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvMicrophone::InputSetDSPPreset( inputdata_t &inputdata ) +{ + m_iSpeakerDSPPreset = inputdata.value.Int(); + ActivateSpeaker(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvMicrophone::InputSetPitchScale( inputdata_t &inputdata ) +{ + m_flPitchScale = inputdata.value.Float(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvMicrophone::InputSetVolumeScale( inputdata_t &inputdata ) +{ + m_flVolumeScale = inputdata.value.Float(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvMicrophone::InputSetChannel( inputdata_t &inputdata ) +{ + m_nChannel = inputdata.value.Int(); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Checks whether this microphone can hear a given sound, and at what +// relative volume level. +// Input : pSound - Sound to test. +// flVolume - Returns with the relative sound volume from 0 - 1. +// Output : Returns true if the sound could be heard at the sample point, false if not. +//----------------------------------------------------------------------------- +bool CEnvMicrophone::CanHearSound(CSound *pSound, float &flVolume) +{ + flVolume = 0; + + if ( m_bDisabled ) + { + return false; + } + + // Cull out sounds except from specific entities + CBaseFilter *pFilter = m_hListenFilter.Get(); + if ( pFilter ) + { + CBaseEntity *pSoundOwner = pSound->m_hOwner.Get(); + if ( !pSoundOwner || !pFilter->PassesFilter( this, pSoundOwner ) ) + { + return false; + } + } + + float flDistance = (pSound->GetSoundOrigin() - m_hMeasureTarget->GetAbsOrigin()).Length(); + + if (flDistance == 0) + { + flVolume = 1.0; + return true; + } + + // Over our max range? + if ( m_flMaxRange && flDistance > m_flMaxRange ) + { + return false; + } + + if (flDistance <= pSound->Volume() * m_flSensitivity) + { + flVolume = 1 - (flDistance / (pSound->Volume() * m_flSensitivity)); + flVolume = clamp(flVolume, 0.f, 1.f); + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if the microphone can hear the specified sound +//----------------------------------------------------------------------------- +bool CEnvMicrophone::CanHearSound( int entindex, soundlevel_t soundlevel, float &flVolume, const Vector *pOrigin ) +{ + if ( m_bDisabled ) + { + flVolume = 0; + return false; + } + + if ( ( m_spawnflags & SF_MICROPHONE_IGNORE_NONATTENUATED ) && soundlevel == SNDLVL_NONE ) + { + return false; + } + + // Sound might be coming from an origin or from an entity. + CBaseEntity *pEntity = NULL; + if ( entindex ) + { + pEntity = CBaseEntity::Instance( engine->PEntityOfEntIndex(entindex) ); + } + + // Cull out sounds except from specific entities + CBaseFilter *pFilter = m_hListenFilter.Get(); + if ( pFilter ) + { + if ( !pEntity || !pFilter->PassesFilter( this, pEntity ) ) + { + flVolume = 0; + return false; + } + } + + float flDistance = 0; + if ( pOrigin ) + { + flDistance = pOrigin->DistTo( m_hMeasureTarget->GetAbsOrigin() ); + } + else if ( pEntity ) + { + flDistance = pEntity->WorldSpaceCenter().DistTo( m_hMeasureTarget->GetAbsOrigin() ); + } + + // Over our max range? + if ( m_flMaxRange && flDistance > m_flMaxRange ) + { +#ifdef DEBUG_MICROPHONE + Msg("OUT OF RANGE.\n" ); +#endif + return false; + } + +#ifdef DEBUG_MICROPHONE + Msg(" flVolume %f ", flVolume ); +#endif + + // Reduce the volume by the amount it fell to get to the microphone + float gain = enginesound->GetDistGainFromSoundLevel( soundlevel, flDistance ); + flVolume *= gain; + +#ifdef DEBUG_MICROPHONE + Msg("dist %2f, soundlevel %d: gain %f", flDistance, (int)soundlevel, gain ); + if ( !flVolume ) + { + Msg(" : REJECTED\n" ); + } + else + { + Msg(" : SENT\n" ); + } +#endif + + return ( flVolume > 0 ); +} + +void CEnvMicrophone::SetSensitivity( float flSensitivity ) +{ + m_flSensitivity = flSensitivity; +} + +void CEnvMicrophone::SetSpeakerName( string_t iszSpeakerName ) +{ + m_iszSpeakerName = iszSpeakerName; + + // Set the speaker to null. This will force it to find the speaker next time a sound is routed. + m_hSpeaker = NULL; + ActivateSpeaker(); +} + +//----------------------------------------------------------------------------- +// Purpose: Listens for sounds and updates the value of the SoundLevel output. +//----------------------------------------------------------------------------- +void CEnvMicrophone::Think(void) +{ + int nSound = CSoundEnt::ActiveList(); + bool fHearSound = false; + + float flMaxVolume = 0; + + // + // Find the loudest sound that this microphone cares about. + // + while (nSound != SOUNDLIST_EMPTY) + { + CSound *pCurrentSound = CSoundEnt::SoundPointerForIndex(nSound); + + if (pCurrentSound) + { + if (m_nSoundMask & pCurrentSound->SoundType()) + { + float flVolume = 0; + if (CanHearSound(pCurrentSound, flVolume) && (flVolume > flMaxVolume)) + { + flMaxVolume = flVolume; + fHearSound = true; + } + } + } + + nSound = pCurrentSound->NextSound(); + } + + if( fHearSound ) + { + m_OnHeardSound.FireOutput( this, this ); + } + + if (flMaxVolume != m_SoundLevel.Get()) + { + // + // Don't smooth if we are within an epsilon. This allows the output to stop firing + // much more quickly. + // + if (fabs(flMaxVolume - m_SoundLevel.Get()) < MICROPHONE_SETTLE_EPSILON) + { + m_SoundLevel.Set(flMaxVolume, this, this); + } + else + { + m_SoundLevel.Set(flMaxVolume * (1 - m_flSmoothFactor) + m_SoundLevel.Get() * m_flSmoothFactor, this, this); + } + } + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Hook for the sound system to tell us when a sound's been played +//----------------------------------------------------------------------------- +MicrophoneResult_t CEnvMicrophone::SoundPlayed( int entindex, const char *soundname, soundlevel_t soundlevel, float flVolume, int iFlags, int iPitch, const Vector *pOrigin, float soundtime, CUtlVector< Vector >& soundorigins ) +{ + if ( m_bAvoidFeedback ) + return MicrophoneResult_Ok; + + // Don't hear sounds that have already been heard by a microphone to avoid feedback! + if ( iFlags & SND_SPEAKER ) + return MicrophoneResult_Ok; + +#ifdef DEBUG_MICROPHONE + Msg("%s heard %s: ", STRING(GetEntityName()), soundname ); +#endif + + if ( !CanHearSound( entindex, soundlevel, flVolume, pOrigin ) ) + return MicrophoneResult_Ok; + + // We've heard it. Play it out our speaker. If our speaker's gone away, we're done. + if ( !m_hSpeaker ) + { + // First time, find our speaker. Done here, because finding it in Activate() wouldn't + // find players, and we need to be able to specify !player for a speaker. + if ( m_iszSpeakerName != NULL_STRING ) + { + m_hSpeaker = gEntList.FindEntityByName(NULL, STRING(m_iszSpeakerName) ); + + if ( !m_hSpeaker ) + { + Warning( "EnvMicrophone %s specifies a non-existent speaker name: %s\n", STRING(GetEntityName()), STRING(m_iszSpeakerName) ); + m_iszSpeakerName = NULL_STRING; + } + } + + if ( !m_hSpeaker ) + { + return MicrophoneResult_Remove; + } + } + +#ifdef MAPBASE + // Something similar to trigger_teleport landmarks for sounds transmitting to speaker + Vector vecOrigin = m_hSpeaker->GetAbsOrigin(); + if (m_hLandmark) + { + Vector vecSoundPos; + if (pOrigin) + vecSoundPos = *pOrigin; + else if (CBaseEntity *pEntity = CBaseEntity::Instance(engine->PEntityOfEntIndex(entindex))) + vecSoundPos = pEntity->GetAbsOrigin(); + + vecOrigin += (vecSoundPos - m_hLandmark->GetAbsOrigin()); + } +#endif + + m_bAvoidFeedback = true; + + // Add the speaker flag. Detected at playback and applies the speaker filter. + iFlags |= SND_SPEAKER; + CPASAttenuationFilter filter( m_hSpeaker ); + + EmitSound_t ep; +#ifdef MAPBASE + ep.m_nChannel = m_nChannel; + if (m_flVolumeScale != 1.0f) + ep.m_flVolume = (flVolume * m_flVolumeScale); +#else + ep.m_nChannel = CHAN_STATIC; + ep.m_flVolume = flVolume; +#endif + ep.m_pSoundName = soundname; + ep.m_SoundLevel = soundlevel; + ep.m_nFlags = iFlags; +#ifdef MAPBASE + if (m_flPitchScale != 1.0f) + ep.m_nPitch = (int)((float)iPitch * m_flPitchScale); + else + ep.m_nPitch = iPitch; + ep.m_pOrigin = &vecOrigin; +#else + ep.m_nPitch = iPitch; + ep.m_pOrigin = &m_hSpeaker->GetAbsOrigin(); +#endif + ep.m_flSoundTime = soundtime; + ep.m_nSpeakerEntity = entindex; + + CBaseEntity::EmitSound( filter, m_hSpeaker->entindex(), ep ); + + Q_strncpy( m_szLastSound, soundname, sizeof(m_szLastSound) ); + m_OnRoutedSound.FireOutput( this, this, 0 ); + + m_bAvoidFeedback = false; + + // Copy emitted origin to soundorigins array + for ( int i = 0; i < ep.m_UtlVecSoundOrigin.Count(); ++i ) + { + soundorigins.AddToTail( ep.m_UtlVecSoundOrigin[ i ] ); + } + + // Do we want to allow the original sound to play? + if ( m_spawnflags & SF_MICROPHONE_SWALLOW_ROUTED_SOUNDS ) + { + return MicrophoneResult_Swallow; + } + + return MicrophoneResult_Ok; +} + + +//----------------------------------------------------------------------------- +// Purpose: Called by the sound system whenever a sound is played so that +// active microphones can have a chance to pick up the sound. +// Output : Returns whether or not the sound was swallowed by the microphone. +// Swallowed sounds should not be played by the sound system. +//----------------------------------------------------------------------------- +bool CEnvMicrophone::OnSoundPlayed( int entindex, const char *soundname, soundlevel_t soundlevel, float flVolume, int iFlags, int iPitch, const Vector *pOrigin, float soundtime, CUtlVector< Vector >& soundorigins ) +{ + bool bSwallowed = false; + + // Loop through all registered microphones and tell them the sound was just played + int iCount = s_Microphones.Count(); + if ( iCount > 0 ) + { + // Iterate backwards because we might be deleting microphones. + for ( int i = iCount - 1; i >= 0; i-- ) + { + if ( s_Microphones[i] ) + { + MicrophoneResult_t eResult = s_Microphones[i]->SoundPlayed( + entindex, + soundname, + soundlevel, + flVolume, + iFlags, + iPitch, + pOrigin, + soundtime, + soundorigins ); + + if ( eResult == MicrophoneResult_Swallow ) + { + // Microphone told us to swallow it + bSwallowed = true; + } + else if ( eResult == MicrophoneResult_Remove ) + { + s_Microphones.FastRemove( i ); + } + } + } + } + + return bSwallowed; +} diff --git a/sp/src/game/server/envmicrophone.h b/sp/src/game/server/envmicrophone.h new file mode 100644 index 00000000..11527d58 --- /dev/null +++ b/sp/src/game/server/envmicrophone.h @@ -0,0 +1,103 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef ENVMICROPHONE_H +#define ENVMICROPHONE_H +#ifdef _WIN32 +#pragma once +#endif + +class CBaseFilter; + + +const int SF_MICROPHONE_SOUND_COMBAT = 0x01; +const int SF_MICROPHONE_SOUND_WORLD = 0x02; +const int SF_MICROPHONE_SOUND_PLAYER = 0x04; +const int SF_MICROPHONE_SOUND_BULLET_IMPACT = 0x08; +const int SF_MICROPHONE_SWALLOW_ROUTED_SOUNDS = 0x10; +const int SF_MICROPHONE_SOUND_EXPLOSION = 0x20; +const int SF_MICROPHONE_IGNORE_NONATTENUATED = 0x40; + + +// Return codes from SoundPlayed +enum MicrophoneResult_t +{ + MicrophoneResult_Ok = 0, + MicrophoneResult_Swallow, // The microphone swallowed the sound. Don't play it. + MicrophoneResult_Remove, // The microphone should be removed from the list of microphones. +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CEnvMicrophone : public CPointEntity +{ + DECLARE_CLASS( CEnvMicrophone, CPointEntity ); + +public: + ~CEnvMicrophone(); + + void Spawn(void); + void Activate(void); + void OnRestore( void ); + void ActivateSpeaker( void ); + void Think(void); + bool CanHearSound(CSound *pSound, float &flVolume); + bool CanHearSound( int entindex, soundlevel_t soundlevel, float &flVolume, const Vector *pOrigin ); + + void SetSensitivity( float flSensitivity ); + void SetSpeakerName( string_t iszSpeakerName ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputSetSpeakerName( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetDSPPreset( inputdata_t &inputdata ); + void InputSetPitchScale( inputdata_t &inputdata ); + void InputSetVolumeScale( inputdata_t &inputdata ); + void InputSetChannel( inputdata_t &inputdata ); +#endif + + DECLARE_DATADESC(); + + // Hook for the sound system to tell us when a sound's been played. Returns true if it's to swallow the passed in sound. + static bool OnSoundPlayed( int entindex, const char *soundname, soundlevel_t soundlevel, + float flVolume, int iFlags, int iPitch, const Vector *pOrigin, float soundtime, CUtlVector< Vector >& soundorigins ); + +private: + + // Per-microphone notification that a sound has played. + MicrophoneResult_t SoundPlayed( int entindex, const char *soundname, soundlevel_t soundlevel, + float flVolume, int iFlags, int iPitch, const Vector *pOrigin, float soundtime, CUtlVector< Vector >& soundorigins ); + + bool m_bDisabled; // If true, the microphone will not measure sound. + EHANDLE m_hMeasureTarget; // Point at which to measure sound level. + int m_nSoundMask; // Which sound types we are interested in. + float m_flSensitivity; // 0 = deaf, 1 = default, 10 = maximum sensitivity + float m_flSmoothFactor; // 0 = no smoothing of samples, 0.9 = maximum smoothing + float m_flMaxRange; // Maximum sound hearing range, irrelevant of attenuation + string_t m_iszSpeakerName; // Name of a speaker to output any heard sounds through + EHANDLE m_hSpeaker; // Speaker to output any heard sounds through + bool m_bAvoidFeedback; + int m_iSpeakerDSPPreset; // Speaker DSP preset to use when this microphone is enabled + string_t m_iszListenFilter; + CHandle m_hListenFilter; +#ifdef MAPBASE + string_t m_iszLandmarkName; + EHANDLE m_hLandmark; + float m_flPitchScale = 1.0f; + float m_flVolumeScale = 1.0f; + int m_nChannel = CHAN_STATIC; +#endif + + COutputFloat m_SoundLevel; // Fired when the sampled volume level changes. + COutputEvent m_OnRoutedSound; // Fired when a sound has been played through our speaker + COutputEvent m_OnHeardSound; // Heard sound. + + char m_szLastSound[256]; +}; + +#endif // ENVMICROPHONE_H diff --git a/sp/src/game/server/envspark.h b/sp/src/game/server/envspark.h new file mode 100644 index 00000000..b9d62df9 --- /dev/null +++ b/sp/src/game/server/envspark.h @@ -0,0 +1,46 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A point entity that periodically emits sparks and "bzzt" sounds. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ENVSPARK_H +#define ENVSPARK_H +#ifdef _WIN32 +#pragma once +#endif + +class CEnvSpark : public CPointEntity +{ + DECLARE_CLASS( CEnvSpark, CPointEntity ); + +public: + CEnvSpark( void ); + + void Spawn( void ); + void Precache( void ); + void SparkThink( void ); + + void StartSpark( void ); + void StopSpark( void ); + + // Input handlers + void InputStartSpark( inputdata_t &inputdata ); + void InputStopSpark( inputdata_t &inputdata ); + void InputToggleSpark( inputdata_t &inputdata ); + void InputSparkOnce( inputdata_t &inputdata ); + + bool IsSparking( void ){ return ( GetNextThink() != TICK_NEVER_THINK ); } + + DECLARE_DATADESC(); + + float m_flDelay; + int m_nGlowSpriteIndex; + int m_nMagnitude; + int m_nTrailLength; + + COutputEvent m_OnSpark; +}; + +#endif // ENVSPARK_H \ No newline at end of file diff --git a/sp/src/game/server/event_tempentity_tester.cpp b/sp/src/game/server/event_tempentity_tester.cpp new file mode 100644 index 00000000..e8a2e05d --- /dev/null +++ b/sp/src/game/server/event_tempentity_tester.cpp @@ -0,0 +1,125 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "event_tempentity_tester.h" +#include "tier1/strtools.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define TEMPENT_TEST_GAP 1.0f + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &vecOrigin - +// &vecAngles - +// *single_te - +// Output : CBaseEntity +//----------------------------------------------------------------------------- +CBaseEntity *CTempEntTester::Create( const Vector &vecOrigin, const QAngle &vecAngles, const char *lifetime, const char *single_te ) +{ + float life; + char classname[ 128 ]; + if ( lifetime && lifetime[0] ) + { + life = atoi( lifetime ); + life = MAX( 1.0, life ); + life = MIN( 1000.0, life ); + + life += gpGlobals->curtime; + } + else + { + Msg( "Usage: te \n" ); + return NULL; + } + + if ( single_te && single_te[0] ) + { + Q_strncpy( classname, single_te ,sizeof(classname)); + strlwr( classname ); + } + else + { + Msg( "Usage: te \n" ); + return NULL; + } + + CTempEntTester *p = ( CTempEntTester * )CBaseEntity::CreateNoSpawn( "te_tester", vecOrigin, vecAngles ); + if ( !p ) + { + return NULL; + } + + Q_strncpy( p->m_szClass, classname ,sizeof(p->m_szClass)); + p->m_fLifeTime = life; + + p->Spawn(); + + return p; +} + +LINK_ENTITY_TO_CLASS( te_tester, CTempEntTester ); + +//----------------------------------------------------------------------------- +// Purpose: Called when object is being created +//----------------------------------------------------------------------------- +void CTempEntTester::Spawn( void ) +{ + // Not a physical thing... + AddEffects( EF_NODRAW ); + + m_pCurrent = CBaseTempEntity::GetList(); + while ( m_pCurrent ) + { + char name[ 128 ]; + Q_strncpy( name, m_pCurrent->GetName() ,sizeof(name)); + strlwr( name ); + if ( strstr( name, m_szClass ) ) + { + break; + } + + m_pCurrent = m_pCurrent->GetNext(); + } + + if ( !m_pCurrent ) + { + DevMsg("Couldn't find temp entity '%s'\n", m_szClass ); + UTIL_Remove( this ); + return; + } + + // Think right away + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called when object should fire itself and move on +//----------------------------------------------------------------------------- +void CTempEntTester::Think( void ) +{ + // Should never happen + if ( !m_pCurrent ) + { + UTIL_Remove( this ); + return; + } + + m_pCurrent->Test( GetLocalOrigin(), GetLocalAngles() ); + SetNextThink( gpGlobals->curtime + TEMPENT_TEST_GAP ); + + // Time to destroy? + if ( gpGlobals->curtime >= m_fLifeTime ) + { + UTIL_Remove( this ); + return; + } +} \ No newline at end of file diff --git a/sp/src/game/server/event_tempentity_tester.h b/sp/src/game/server/event_tempentity_tester.h new file mode 100644 index 00000000..9ece13a4 --- /dev/null +++ b/sp/src/game/server/event_tempentity_tester.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#if !defined( EVENT_TEMPENTITY_TESTER_H ) +#define EVENT_TEMPENTITY_TESTER_H +#ifdef _WIN32 +#pragma once +#endif + +class CBaseTempEntity; + + +//----------------------------------------------------------------------------- +// Purpose: A server object that is used to test all registered temp entities +//----------------------------------------------------------------------------- +class CTempEntTester : public CPointEntity +{ +public: + DECLARE_CLASS( CTempEntTester, CPointEntity ); + + void Spawn( void ); + void Think( void ); + + static CBaseEntity *Create( const Vector &vecOrigin, const QAngle &vecAngles, const char *lifetime, const char *single_te ); +private: + // Current temp entity to test + CBaseTempEntity *m_pCurrent; + + // Lifetime + float m_fLifeTime; + + char m_szClass[ 64 ]; +}; + +#endif // EVENT_TEMPENTITY_TESTER_H \ No newline at end of file diff --git a/sp/src/game/server/eventqueue.h b/sp/src/game/server/eventqueue.h new file mode 100644 index 00000000..d5cc2d5f --- /dev/null +++ b/sp/src/game/server/eventqueue.h @@ -0,0 +1,94 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A global class that holds a prioritized queue of entity I/O events. +// Events can be posted with a nonzero delay, which determines how long +// they are held before being dispatched to their recipients. +// +// The queue is serviced once per server frame. +// +//=============================================================================// + +#ifndef EVENTQUEUE_H +#define EVENTQUEUE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "mempool.h" + +struct EventQueuePrioritizedEvent_t +{ + float m_flFireTime; + string_t m_iTarget; + string_t m_iTargetInput; + EHANDLE m_pActivator; + EHANDLE m_pCaller; + int m_iOutputID; + EHANDLE m_pEntTarget; // a pointer to the entity to target; overrides m_iTarget + + variant_t m_VariantValue; // variable-type parameter + + EventQueuePrioritizedEvent_t *m_pNext; + EventQueuePrioritizedEvent_t *m_pPrev; + + DECLARE_SIMPLE_DATADESC(); + + DECLARE_FIXEDSIZE_ALLOCATOR( PrioritizedEvent_t ); +}; + +class CEventQueue +{ +public: + // pushes an event into the queue, targeting a string name (m_iName), or directly by a pointer +#ifdef MAPBASE_VSCRIPT + int AddEvent( const char *target, const char *action, variant_t Value, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID = 0 ); + int AddEvent( CBaseEntity *target, const char *action, variant_t Value, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID = 0 ); +#else + void AddEvent( const char *target, const char *action, variant_t Value, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID = 0 ); + void AddEvent( CBaseEntity *target, const char *action, variant_t Value, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID = 0 ); +#endif + void AddEvent( CBaseEntity *target, const char *action, float fireDelay, CBaseEntity *pActivator, CBaseEntity *pCaller, int outputID = 0 ); + + void CancelEvents( CBaseEntity *pCaller ); + void CancelEventOn( CBaseEntity *pTarget, const char *sInputName ); + bool HasEventPending( CBaseEntity *pTarget, const char *sInputName ); + + // services the queue, firing off any events who's time hath come + void ServiceEvents( void ); + + // debugging + void ValidateQueue( void ); + + // serialization + int Save( ISave &save ); + int Restore( IRestore &restore ); + + CEventQueue(); + ~CEventQueue(); + + void Init( void ); + void Clear( void ); // resets the list + + void Dump( void ); + +#ifdef MAPBASE_VSCRIPT + void CancelEventsByInput( CBaseEntity *pTarget, const char *szInput ); + bool RemoveEvent( int event ); + float GetTimeLeft( int event ); +#endif // MAPBASE_VSCRIPT + +private: + + void AddEvent( EventQueuePrioritizedEvent_t *event ); + void RemoveEvent( EventQueuePrioritizedEvent_t *pe ); + + DECLARE_SIMPLE_DATADESC(); + EventQueuePrioritizedEvent_t m_Events; + int m_iListCount; +}; + +extern CEventQueue g_EventQueue; + + +#endif // EVENTQUEUE_H + diff --git a/sp/src/game/server/explode.cpp b/sp/src/game/server/explode.cpp new file mode 100644 index 00000000..d0321356 --- /dev/null +++ b/sp/src/game/server/explode.cpp @@ -0,0 +1,515 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements an explosion entity and a support spark shower entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "decals.h" +#include "explode.h" +#include "ai_basenpc.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "tier1/strtools.h" +#include "shareddefs.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef MAPBASE +ConVar explosion_sparks("explosion_sparks", "0", FCVAR_NONE); +#endif + +//----------------------------------------------------------------------------- +// Purpose: Spark shower, created by the explosion entity. +//----------------------------------------------------------------------------- +class CShower : public CPointEntity +{ +public: + DECLARE_CLASS( CShower, CPointEntity ); + + void Spawn( void ); + void Think( void ); + void Touch( CBaseEntity *pOther ); + int ObjectCaps( void ) { return FCAP_DONT_SAVE; } +}; + +LINK_ENTITY_TO_CLASS( spark_shower, CShower ); + + +void CShower::Spawn( void ) +{ + Vector vecForward; + AngleVectors( GetLocalAngles(), &vecForward ); + + Vector vecNewVelocity; + vecNewVelocity = random->RandomFloat( 200, 300 ) * vecForward; + vecNewVelocity.x += random->RandomFloat(-100.f,100.f); + vecNewVelocity.y += random->RandomFloat(-100.f,100.f); + if ( vecNewVelocity.z >= 0 ) + vecNewVelocity.z += 200; + else + vecNewVelocity.z -= 200; + SetAbsVelocity( vecNewVelocity ); + + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + SetGravity( UTIL_ScaleForGravity( 400 ) ); // fall a bit more slowly than normal + SetNextThink( gpGlobals->curtime + 0.1f ); + SetSolid( SOLID_NONE ); + UTIL_SetSize(this, vec3_origin, vec3_origin ); + AddEffects( EF_NODRAW ); + m_flSpeed = random->RandomFloat( 0.5, 1.5 ); + + SetLocalAngles( vec3_angle ); +} + + +void CShower::Think( void ) +{ + g_pEffects->Sparks( GetAbsOrigin() ); + + m_flSpeed -= 0.1; + if ( m_flSpeed > 0 ) + SetNextThink( gpGlobals->curtime + 0.1f ); + else + UTIL_Remove( this ); + SetGroundEntity( NULL ); +} + + +void CShower::Touch( CBaseEntity *pOther ) +{ + Vector vecNewVelocity = GetAbsVelocity(); + + if ( GetFlags() & FL_ONGROUND ) + vecNewVelocity *= 0.1; + else + vecNewVelocity *= 0.6; + + if ( (vecNewVelocity.x*vecNewVelocity.x+vecNewVelocity.y*vecNewVelocity.y) < 10.0 ) + m_flSpeed = 0; + + SetAbsVelocity( vecNewVelocity ); +} + + +class CEnvExplosion : public CPointEntity +{ +public: + DECLARE_CLASS( CEnvExplosion, CPointEntity ); + + CEnvExplosion( void ) + { + // Default to invalid. + m_sFireballSprite = -1; + }; + + void Precache( void ); + void Spawn( ); + void Smoke ( void ); + void SetCustomDamageType( int iType ) { m_iCustomDamageType = iType; } + bool KeyValue( const char *szKeyName, const char *szValue ); + + int DrawDebugTextOverlays(void); + + // Input handlers + void InputExplode( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetIgnoredEntity( inputdata_t &inputdata ); +#endif + + DECLARE_DATADESC(); + + int m_iMagnitude;// how large is the fireball? how much damage? + int m_iRadiusOverride;// For use when m_iMagnitude results in larger radius than designer desires. + int m_spriteScale; // what's the exact fireball sprite scale? + float m_flDamageForce; // How much damage force should we use? + string_t m_iszFireballSprite; + short m_sFireballSprite; + EHANDLE m_hInflictor; + int m_iCustomDamageType; + + // passed along to the RadiusDamage call + int m_iClassIgnore; + EHANDLE m_hEntityIgnore; + +}; + +LINK_ENTITY_TO_CLASS( env_explosion, CEnvExplosion ); + +BEGIN_DATADESC( CEnvExplosion ) + + DEFINE_KEYFIELD( m_iMagnitude, FIELD_INTEGER, "iMagnitude" ), + DEFINE_KEYFIELD( m_iRadiusOverride, FIELD_INTEGER, "iRadiusOverride" ), + DEFINE_FIELD( m_spriteScale, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_flDamageForce, FIELD_FLOAT, "DamageForce" ), + DEFINE_FIELD( m_iszFireballSprite, FIELD_STRING ), + DEFINE_FIELD( m_sFireballSprite, FIELD_SHORT ), + DEFINE_FIELD( m_hInflictor, FIELD_EHANDLE ), + DEFINE_FIELD( m_iCustomDamageType, FIELD_INTEGER ), + + DEFINE_FIELD( m_iClassIgnore, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_hEntityIgnore, FIELD_EHANDLE, "ignoredEntity" ), + + // Function Pointers + DEFINE_THINKFUNC( Smoke ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Explode", InputExplode), +#ifdef MAPBASE + DEFINE_INPUTFUNC(FIELD_EHANDLE, "SetIgnoredEntity", InputSetIgnoredEntity), +#endif + +END_DATADESC() + + +bool CEnvExplosion::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "fireballsprite")) + { + m_iszFireballSprite = AllocPooledString( szValue ); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +void CEnvExplosion::Precache( void ) +{ + if ( m_iszFireballSprite != NULL_STRING ) + { + m_sFireballSprite = PrecacheModel( STRING( m_iszFireballSprite ) ); + } +} + +void CEnvExplosion::Spawn( void ) +{ + Precache(); + + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); + + SetMoveType( MOVETYPE_NONE ); + /* + if ( m_iMagnitude > 250 ) + { + m_iMagnitude = 250; + } + */ + + float flSpriteScale; + flSpriteScale = ( m_iMagnitude - 50) * 0.6; + + // Control the clamping of the fireball sprite + if( m_spawnflags & SF_ENVEXPLOSION_NOCLAMPMIN ) + { + // Don't inhibit clamping altogether. Just relax it a bit. + if ( flSpriteScale < 1 ) + { + flSpriteScale = 1; + } + } + else + { + if ( flSpriteScale < 10 ) + { + flSpriteScale = 10; + } + } + + if( m_spawnflags & SF_ENVEXPLOSION_NOCLAMPMAX ) + { + // We may need to adjust this to suit designers' needs. + if ( flSpriteScale > 200 ) + { + flSpriteScale = 200; + } + } + else + { + if ( flSpriteScale > 50 ) + { + flSpriteScale = 50; + } + } + + m_spriteScale = (int)flSpriteScale; + m_iCustomDamageType = -1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for making the explosion explode. +//----------------------------------------------------------------------------- +void CEnvExplosion::InputExplode( inputdata_t &inputdata ) +{ + trace_t tr; + + SetModelName( NULL_STRING );//invisible + SetSolid( SOLID_NONE );// intangible + + Vector vecSpot = GetAbsOrigin() + Vector( 0 , 0 , 8 ); + UTIL_TraceLine( vecSpot, vecSpot + Vector( 0, 0, -40 ), (MASK_SOLID_BRUSHONLY | MASK_WATER), this, COLLISION_GROUP_NONE, &tr ); + + // Pull out of the wall a bit. We used to move the explosion origin itself, but that seems unnecessary, not to mention a + // little weird when you consider that it might be in hierarchy. Instead we just calculate a new virtual position at + // which to place the explosion. We don't use that new position to calculate radius damage because according to Steve's + // comment, that adversely affects the force vector imparted on explosion victims when they ragdoll. + Vector vecExplodeOrigin = GetAbsOrigin(); + if ( tr.fraction != 1.0 ) + { + vecExplodeOrigin = tr.endpos + (tr.plane.normal * 24 ); + } + + // draw decal + if (! ( m_spawnflags & SF_ENVEXPLOSION_NODECAL)) + { + UTIL_DecalTrace( &tr, "Scorch" ); + } + + // It's stupid that this entity's spawnflags and the flags for the + // explosion temp ent don't match up. But because they don't, we + // have to reinterpret some of the spawnflags to determine which + // flags to pass to the temp ent. + int nFlags = TE_EXPLFLAG_NONE; + + if( m_spawnflags & SF_ENVEXPLOSION_NOFIREBALL ) + { + nFlags |= TE_EXPLFLAG_NOFIREBALL; + } + + if( m_spawnflags & SF_ENVEXPLOSION_NOSOUND ) + { + nFlags |= TE_EXPLFLAG_NOSOUND; + } + + if ( m_spawnflags & SF_ENVEXPLOSION_RND_ORIENT ) + { + nFlags |= TE_EXPLFLAG_ROTATE; + } + + if ( m_nRenderMode == kRenderTransAlpha ) + { + nFlags |= TE_EXPLFLAG_DRAWALPHA; + } + else if ( m_nRenderMode != kRenderTransAdd ) + { + nFlags |= TE_EXPLFLAG_NOADDITIVE; + } + + if( m_spawnflags & SF_ENVEXPLOSION_NOPARTICLES ) + { + nFlags |= TE_EXPLFLAG_NOPARTICLES; + } + + if( m_spawnflags & SF_ENVEXPLOSION_NODLIGHTS ) + { + nFlags |= TE_EXPLFLAG_NODLIGHTS; + } + + if ( m_spawnflags & SF_ENVEXPLOSION_NOFIREBALLSMOKE ) + { + nFlags |= TE_EXPLFLAG_NOFIREBALLSMOKE; + } + + //Get the damage override if specified + int iRadius = ( m_iRadiusOverride > 0 ) ? m_iRadiusOverride : ( m_iMagnitude * 2.5f ); + + CPASFilter filter( vecExplodeOrigin ); + te->Explosion( filter, 0.0, + &vecExplodeOrigin, + ( m_sFireballSprite < 1 ) ? g_sModelIndexFireball : m_sFireballSprite, + !( m_spawnflags & SF_ENVEXPLOSION_NOFIREBALL ) ? ( m_spriteScale / 10.0 ) : 0.0, + 15, + nFlags, + iRadius, + m_iMagnitude ); + + // do damage + if ( !( m_spawnflags & SF_ENVEXPLOSION_NODAMAGE ) ) + { + CBaseEntity *pAttacker = GetOwnerEntity() ? GetOwnerEntity() : this; + + // Only calculate damage type if we didn't get a custom one passed in + int iDamageType = m_iCustomDamageType; + if ( iDamageType == -1 ) + { + iDamageType = HasSpawnFlags( SF_ENVEXPLOSION_GENERIC_DAMAGE ) ? DMG_GENERIC : DMG_BLAST; + } + + CTakeDamageInfo info( m_hInflictor ? m_hInflictor : this, pAttacker, m_iMagnitude, iDamageType ); + + if( HasSpawnFlags( SF_ENVEXPLOSION_SURFACEONLY ) ) + { + info.AddDamageType( DMG_BLAST_SURFACE ); + } + + if ( m_flDamageForce ) + { + // Not the right direction, but it'll be fixed up by RadiusDamage. + info.SetDamagePosition( GetAbsOrigin() ); + info.SetDamageForce( Vector( m_flDamageForce, 0, 0 ) ); + } + + RadiusDamage( info, GetAbsOrigin(), iRadius, m_iClassIgnore, m_hEntityIgnore.Get() ); + } + + SetThink( &CEnvExplosion::Smoke ); + SetNextThink( gpGlobals->curtime + 0.3 ); + + // Only do these effects if we're not submerged +#ifdef MAPBASE + if ( explosion_sparks.GetBool() && !(UTIL_PointContents( GetAbsOrigin() ) & CONTENTS_WATER) ) +#else + if ( UTIL_PointContents( GetAbsOrigin() ) & CONTENTS_WATER ) +#endif + { + // draw sparks + if ( !( m_spawnflags & SF_ENVEXPLOSION_NOSPARKS ) ) + { + int sparkCount = random->RandomInt(0,3); + + for ( int i = 0; i < sparkCount; i++ ) + { + QAngle angles; + VectorAngles( tr.plane.normal, angles ); + Create( "spark_shower", vecExplodeOrigin, angles, NULL ); + } + } + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the ignored entity. +//----------------------------------------------------------------------------- +void CEnvExplosion::InputSetIgnoredEntity( inputdata_t &inputdata ) +{ + m_hEntityIgnore = inputdata.value.Entity(); +} +#endif + + +void CEnvExplosion::Smoke( void ) +{ + if ( !(m_spawnflags & SF_ENVEXPLOSION_REPEATABLE) ) + { + UTIL_Remove( this ); + } +} + + +// HACKHACK -- create one of these and fake a keyvalue to get the right explosion setup +void ExplosionCreate( const Vector ¢er, const QAngle &angles, + CBaseEntity *pOwner, int magnitude, int radius, int nSpawnFlags, float flExplosionForce, CBaseEntity *pInflictor, int iCustomDamageType, + const EHANDLE *ignoredEntity , Class_T ignoredClass ) +{ + char buf[128]; + + CEnvExplosion *pExplosion = (CEnvExplosion*)CBaseEntity::Create( "env_explosion", center, angles, pOwner ); + Q_snprintf( buf,sizeof(buf), "%3d", magnitude ); + char *szKeyName = "iMagnitude"; + char *szValue = buf; + pExplosion->KeyValue( szKeyName, szValue ); + + pExplosion->AddSpawnFlags( nSpawnFlags ); + + if ( radius ) + { + Q_snprintf( buf,sizeof(buf), "%d", radius ); + pExplosion->KeyValue( "iRadiusOverride", buf ); + } + + if ( flExplosionForce != 0.0f ) + { + Q_snprintf( buf,sizeof(buf), "%.3f", flExplosionForce ); + pExplosion->KeyValue( "DamageForce", buf ); + } + + variant_t emptyVariant; + pExplosion->m_nRenderMode = kRenderTransAdd; + pExplosion->SetOwnerEntity( pOwner ); + pExplosion->Spawn(); + pExplosion->m_hInflictor = pInflictor; + pExplosion->SetCustomDamageType( iCustomDamageType ); + if (ignoredEntity) + { + pExplosion->m_hEntityIgnore = *ignoredEntity; + } + pExplosion->m_iClassIgnore = ignoredClass; + + pExplosion->AcceptInput( "Explode", NULL, NULL, emptyVariant, 0 ); +} + + +void ExplosionCreate( const Vector ¢er, const QAngle &angles, + CBaseEntity *pOwner, int magnitude, int radius, bool doDamage, float flExplosionForce, bool bSurfaceOnly, bool bSilent, int iCustomDamageType ) +{ + // For E3, no sparks + int nFlags = SF_ENVEXPLOSION_NOSPARKS | SF_ENVEXPLOSION_NODLIGHTS | SF_ENVEXPLOSION_NOSMOKE; + if ( !doDamage ) + { + nFlags |= SF_ENVEXPLOSION_NODAMAGE; + } + + if( bSurfaceOnly ) + { + nFlags |= SF_ENVEXPLOSION_SURFACEONLY; + } + + if( bSilent ) + { + nFlags |= SF_ENVEXPLOSION_NOSOUND; + } + + ExplosionCreate( center, angles, pOwner, magnitude, radius, nFlags, flExplosionForce, NULL, iCustomDamageType ); +} + +// this version lets you specify classes or entities to be ignored +void ExplosionCreate( const Vector ¢er, const QAngle &angles, + CBaseEntity *pOwner, int magnitude, int radius, bool doDamage, + const EHANDLE *ignoredEntity, Class_T ignoredClass, + float flExplosionForce , bool bSurfaceOnly , bool bSilent , int iCustomDamageType ) +{ + // For E3, no sparks + int nFlags = SF_ENVEXPLOSION_NOSPARKS | SF_ENVEXPLOSION_NODLIGHTS | SF_ENVEXPLOSION_NOSMOKE; + if ( !doDamage ) + { + nFlags |= SF_ENVEXPLOSION_NODAMAGE; + } + + if( bSurfaceOnly ) + { + nFlags |= SF_ENVEXPLOSION_SURFACEONLY; + } + + if( bSilent ) + { + nFlags |= SF_ENVEXPLOSION_NOSOUND; + } + + ExplosionCreate( center, angles, pOwner, magnitude, radius, nFlags, flExplosionForce, NULL, iCustomDamageType, ignoredEntity, ignoredClass ); +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CEnvExplosion::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + Q_snprintf(tempstr,sizeof(tempstr)," magnitude: %i", m_iMagnitude); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} diff --git a/sp/src/game/server/explode.h b/sp/src/game/server/explode.h new file mode 100644 index 00000000..956c1e00 --- /dev/null +++ b/sp/src/game/server/explode.h @@ -0,0 +1,43 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef EXPLODE_H +#define EXPLODE_H + +#define SF_ENVEXPLOSION_NODAMAGE 0x00000001 // when set, ENV_EXPLOSION will not actually inflict damage +#define SF_ENVEXPLOSION_REPEATABLE 0x00000002 // can this entity be refired? +#define SF_ENVEXPLOSION_NOFIREBALL 0x00000004 // don't draw the fireball +#define SF_ENVEXPLOSION_NOSMOKE 0x00000008 // don't draw the smoke +#define SF_ENVEXPLOSION_NODECAL 0x00000010 // don't make a scorch mark +#define SF_ENVEXPLOSION_NOSPARKS 0x00000020 // don't make sparks +#define SF_ENVEXPLOSION_NOSOUND 0x00000040 // don't play explosion sound. +#define SF_ENVEXPLOSION_RND_ORIENT 0x00000080 // randomly oriented sprites +#define SF_ENVEXPLOSION_NOFIREBALLSMOKE 0x0100 +#define SF_ENVEXPLOSION_NOPARTICLES 0x00000200 +#define SF_ENVEXPLOSION_NODLIGHTS 0x00000400 +#define SF_ENVEXPLOSION_NOCLAMPMIN 0x00000800 // don't clamp the minimum size of the fireball sprite +#define SF_ENVEXPLOSION_NOCLAMPMAX 0x00001000 // don't clamp the maximum size of the fireball sprite +#define SF_ENVEXPLOSION_SURFACEONLY 0x00002000 // don't damage the player if he's underwater. +#define SF_ENVEXPLOSION_GENERIC_DAMAGE 0x00004000 // don't do BLAST damage + +extern short g_sModelIndexFireball; +extern short g_sModelIndexSmoke; + +void ExplosionCreate( const Vector ¢er, const QAngle &angles, + CBaseEntity *pOwner, int magnitude, int radius, bool doDamage, float flExplosionForce = 0.0f, bool bSurfaceOnly = false, bool bSilent = false, int iCustomDamageType = -1 ); + +void ExplosionCreate( const Vector ¢er, const QAngle &angles, + CBaseEntity *pOwner, int magnitude, int radius, int nSpawnFlags, + float flExplosionForce = 0.0f, CBaseEntity *pInflictor = NULL, int iCustomDamageType = -1, const EHANDLE *ignoredEntity = NULL, Class_T ignoredClass = CLASS_NONE); + +// this version lets you specify classes or entities to be ignored +void ExplosionCreate( const Vector ¢er, const QAngle &angles, + CBaseEntity *pOwner, int magnitude, int radius, bool doDamage, + const EHANDLE *ignoredEntity, Class_T ignoredClass, + float flExplosionForce = 0.0f, bool bSurfaceOnly = false, bool bSilent = false, int iCustomDamageType = -1 ); + +#endif //EXPLODE_H diff --git a/sp/src/game/server/filters.cpp b/sp/src/game/server/filters.cpp new file mode 100644 index 00000000..8b7dcfc9 --- /dev/null +++ b/sp/src/game/server/filters.cpp @@ -0,0 +1,2313 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "filters.h" +#include "entitylist.h" +#include "ai_squad.h" +#include "ai_basenpc.h" +#ifdef MAPBASE +#include "mapbase/matchers.h" +#include "AI_Criteria.h" +#include "ai_hint.h" +#include "mapbase/GlobalStrings.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// ################################################################### +// > BaseFilter +// ################################################################### +LINK_ENTITY_TO_CLASS(filter_base, CBaseFilter); + +BEGIN_DATADESC( CBaseFilter ) + + DEFINE_KEYFIELD(m_bNegated, FIELD_BOOLEAN, "Negated"), +#ifdef MAPBASE + DEFINE_KEYFIELD(m_bPassCallerWhenTested, FIELD_BOOLEAN, "PassCallerWhenTested"), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_INPUT, "TestActivator", InputTestActivator ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_EHANDLE, "TestEntity", InputTestEntity ), + DEFINE_INPUTFUNC( FIELD_INPUT, "SetField", InputSetField ), +#endif + + // Outputs + DEFINE_OUTPUT( m_OnPass, "OnPass"), + DEFINE_OUTPUT( m_OnFail, "OnFail"), + +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +BEGIN_ENT_SCRIPTDESC( CBaseFilter, CBaseEntity, "All entities which could be used as filters." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptPassesFilter, "PassesFilter", "Check if the given caller and entity pass the filter. The caller is the one who requests the filter result; For example, the entity being damaged when using this as a damage filter." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptPassesDamageFilter, "PassesDamageFilter", "Check if the given caller and damage info pass the damage filter, with the second parameter being a CTakeDamageInfo instance. The caller is the one who requests the filter result; For example, the entity being damaged when using this as a damage filter." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptPassesFinalDamageFilter, "PassesFinalDamageFilter", "Used by filter_damage_redirect to distinguish between standalone filter calls and actually damaging an entity. Returns true if there's no unique behavior. Parameters are identical to PassesDamageFilter." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptBloodAllowed, "BloodAllowed", "Check if the given caller and damage info allow for the production of blood." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptDamageMod, "DamageMod", "Mods the damage info with the given caller." ) + +END_SCRIPTDESC(); +#endif + +//----------------------------------------------------------------------------- + +bool CBaseFilter::PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) +{ + return true; +} + + +bool CBaseFilter::PassesFilter( CBaseEntity *pCaller, CBaseEntity *pEntity ) +{ + bool baseResult = PassesFilterImpl( pCaller, pEntity ); + return (m_bNegated) ? !baseResult : baseResult; +} + + +#ifdef MAPBASE +bool CBaseFilter::PassesDamageFilter(CBaseEntity *pCaller, const CTakeDamageInfo &info) +{ + bool baseResult = PassesDamageFilterImpl(pCaller, info); + return (m_bNegated) ? !baseResult : baseResult; +} +#endif + +bool CBaseFilter::PassesDamageFilter(const CTakeDamageInfo &info) +{ +#ifdef MAPBASE + Warning("WARNING: Deprecated usage of PassesDamageFilter!\n"); + bool baseResult = PassesDamageFilterImpl(NULL, info); +#else + bool baseResult = PassesDamageFilterImpl(info); +#endif + return (m_bNegated) ? !baseResult : baseResult; +} + + +#ifdef MAPBASE +bool CBaseFilter::PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) +{ + return PassesFilterImpl( pCaller, info.GetAttacker() ); +} +#else +bool CBaseFilter::PassesDamageFilterImpl( const CTakeDamageInfo &info ) +{ + return PassesFilterImpl( NULL, info.GetAttacker() ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Input handler for testing the activator. If the activator passes the +// filter test, the OnPass output is fired. If not, the OnFail output is fired. +//----------------------------------------------------------------------------- +void CBaseFilter::InputTestActivator( inputdata_t &inputdata ) +{ + if ( PassesFilter( inputdata.pCaller, inputdata.pActivator ) ) + { +#ifdef MAPBASE + m_OnPass.FireOutput( inputdata.pActivator, m_bPassCallerWhenTested ? inputdata.pCaller : this ); +#else + m_OnPass.FireOutput( inputdata.pActivator, this ); +#endif + } + else + { +#ifdef MAPBASE + m_OnFail.FireOutput( inputdata.pActivator, m_bPassCallerWhenTested ? inputdata.pCaller : this ); +#else + m_OnFail.FireOutput( inputdata.pActivator, this ); +#endif + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler for testing the activator. If the activator passes the +// filter test, the OnPass output is fired. If not, the OnFail output is fired. +//----------------------------------------------------------------------------- +void CBaseFilter::InputTestEntity( inputdata_t &inputdata ) +{ + if ( PassesFilter( inputdata.pCaller, inputdata.value.Entity() ) ) + { + m_OnPass.FireOutput( inputdata.value.Entity(), m_bPassCallerWhenTested ? inputdata.pCaller : this ); + } + else + { + m_OnFail.FireOutput( inputdata.value.Entity(), m_bPassCallerWhenTested ? inputdata.pCaller : this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Tries to set the filter's target since most filters use "filtername" anyway +//----------------------------------------------------------------------------- +void CBaseFilter::InputSetField( inputdata_t& inputdata ) +{ + KeyValue("filtername", inputdata.value.String()); + Activate(); +} +#endif + +#ifdef MAPBASE_VSCRIPT +bool CBaseFilter::ScriptPassesFilter( HSCRIPT pCaller, HSCRIPT pEntity ) { return PassesFilter( ToEnt(pCaller), ToEnt(pEntity) ); } +bool CBaseFilter::ScriptPassesDamageFilter( HSCRIPT pCaller, HSCRIPT pInfo ) { return (pInfo) ? PassesDamageFilter( ToEnt( pCaller ), *const_cast(HScriptToClass( pInfo )) ) : NULL; } +bool CBaseFilter::ScriptPassesFinalDamageFilter( HSCRIPT pCaller, HSCRIPT pInfo ) { return (pInfo) ? PassesFinalDamageFilter( ToEnt( pCaller ), *const_cast(HScriptToClass( pInfo )) ) : NULL; } +bool CBaseFilter::ScriptBloodAllowed( HSCRIPT pCaller, HSCRIPT pInfo ) { return (pInfo) ? BloodAllowed( ToEnt( pCaller ), *const_cast(HScriptToClass( pInfo )) ) : NULL; } +bool CBaseFilter::ScriptDamageMod( HSCRIPT pCaller, HSCRIPT pInfo ) { return (pInfo) ? DamageMod( ToEnt( pCaller ), *HScriptToClass( pInfo ) ) : NULL; } +#endif + + +// ################################################################### +// > FilterMultiple +// +// Allows one to filter through mutiple filters +// ################################################################### +#define MAX_FILTERS 5 +enum filter_t +{ + FILTER_AND, + FILTER_OR, +}; + +class CFilterMultiple : public CBaseFilter +{ + DECLARE_CLASS( CFilterMultiple, CBaseFilter ); + DECLARE_DATADESC(); + + filter_t m_nFilterType; + string_t m_iFilterName[MAX_FILTERS]; + EHANDLE m_hFilter[MAX_FILTERS]; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ); +#ifdef MAPBASE + bool PassesDamageFilterImpl(CBaseEntity *pCaller, const CTakeDamageInfo &info); +#else + bool PassesDamageFilterImpl(const CTakeDamageInfo &info); +#endif + void Activate(void); + +#ifdef MAPBASE + bool BloodAllowed( CBaseEntity *pCaller, const CTakeDamageInfo &info ); + bool PassesFinalDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ); + bool DamageMod( CBaseEntity *pCaller, CTakeDamageInfo &info ); +#endif +}; + +LINK_ENTITY_TO_CLASS(filter_multi, CFilterMultiple); + +BEGIN_DATADESC( CFilterMultiple ) + + + // Keys + DEFINE_KEYFIELD(m_nFilterType, FIELD_INTEGER, "FilterType"), + + // Silence, Classcheck! +// DEFINE_ARRAY( m_iFilterName, FIELD_STRING, MAX_FILTERS ), + + DEFINE_KEYFIELD(m_iFilterName[0], FIELD_STRING, "Filter01"), + DEFINE_KEYFIELD(m_iFilterName[1], FIELD_STRING, "Filter02"), + DEFINE_KEYFIELD(m_iFilterName[2], FIELD_STRING, "Filter03"), + DEFINE_KEYFIELD(m_iFilterName[3], FIELD_STRING, "Filter04"), + DEFINE_KEYFIELD(m_iFilterName[4], FIELD_STRING, "Filter05"), + DEFINE_ARRAY( m_hFilter, FIELD_EHANDLE, MAX_FILTERS ), + +END_DATADESC() + + + +//------------------------------------------------------------------------------ +// Purpose : Called after all entities have been loaded +//------------------------------------------------------------------------------ +void CFilterMultiple::Activate( void ) +{ + BaseClass::Activate(); + + // We may reject an entity specified in the array of names, but we want the array of valid filters to be contiguous! + int nNextFilter = 0; + + // Get handles to my filter entities + for ( int i = 0; i < MAX_FILTERS; i++ ) + { + if ( m_iFilterName[i] != NULL_STRING ) + { + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_iFilterName[i] ); + CBaseFilter *pFilter = dynamic_cast(pEntity); + if ( pFilter == NULL ) + { + Warning("filter_multi: Tried to add entity (%s) which is not a filter entity!\n", STRING( m_iFilterName[i] ) ); + continue; + } +#ifdef MAPBASE + else if ( pFilter == this ) + { + Warning("filter_multi: Tried to add itself!\n"); + continue; + } +#endif + + // Take this entity and increment out array pointer + m_hFilter[nNextFilter] = pFilter; + nNextFilter++; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the entity passes our filter, false if not. +// Input : pEntity - Entity to test. +//----------------------------------------------------------------------------- +bool CFilterMultiple::PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) +{ + // Test against each filter + if (m_nFilterType == FILTER_AND) + { + for (int i=0;iPassesFilter( pCaller, pEntity ) ) + { + return false; + } + } + } + return true; + } + else // m_nFilterType == FILTER_OR + { + for (int i=0;iPassesFilter( pCaller, pEntity ) ) + { + return true; + } + } + } + return false; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the entity passes our filter, false if not. +// Input : pEntity - Entity to test. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +bool CFilterMultiple::PassesDamageFilterImpl(CBaseEntity *pCaller, const CTakeDamageInfo &info) +#else +bool CFilterMultiple::PassesDamageFilterImpl(const CTakeDamageInfo &info) +#endif +{ + // Test against each filter + if (m_nFilterType == FILTER_AND) + { + for (int i=0;iPassesDamageFilter(pCaller, info)) +#else + if (!pFilter->PassesDamageFilter(info)) +#endif + { + return false; + } + } + } + return true; + } + else // m_nFilterType == FILTER_OR + { + for (int i=0;iPassesDamageFilter(pCaller, info)) +#else + if (pFilter->PassesDamageFilter(info)) +#endif + { + return true; + } + } + } + return false; + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Returns true if blood should be allowed, false if not. +// Input : pEntity - Entity to test. +//----------------------------------------------------------------------------- +bool CFilterMultiple::BloodAllowed( CBaseEntity *pCaller, const CTakeDamageInfo &info ) +{ + // Test against each filter + if (m_nFilterType == FILTER_AND) + { + for (int i=0;iBloodAllowed(pCaller, info)) + { + return false; + } + } + } + return true; + } + else // m_nFilterType == FILTER_OR + { + for (int i=0;iBloodAllowed(pCaller, info)) + { + return true; + } + } + } + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the entity passes our filter, false if not. +// Input : pEntity - Entity to test. +//----------------------------------------------------------------------------- +bool CFilterMultiple::PassesFinalDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ) +{ + // Test against each filter + if (m_nFilterType == FILTER_AND) + { + for (int i=0;iPassesFinalDamageFilter(pCaller, info)) + { + return false; + } + } + } + return true; + } + else // m_nFilterType == FILTER_OR + { + for (int i=0;iPassesFinalDamageFilter(pCaller, info)) + { + return true; + } + } + } + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns true if damage should be modded, false if not. +// Input : pEntity - Entity to test. +//----------------------------------------------------------------------------- +bool CFilterMultiple::DamageMod( CBaseEntity *pCaller, CTakeDamageInfo &info ) +{ + // Test against each filter + if (m_nFilterType == FILTER_AND) + { + for (int i=0;iDamageMod(pCaller, info)) + { + return false; + } + } + } + return true; + } + else // m_nFilterType == FILTER_OR + { + for (int i=0;iDamageMod(pCaller, info)) + { + return true; + } + } + } + return false; + } +} +#endif + + +// ################################################################### +// > FilterName +// ################################################################### +class CFilterName : public CBaseFilter +{ + DECLARE_CLASS( CFilterName, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iFilterName; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + // special check for !player as GetEntityName for player won't return "!player" as a name + if (FStrEq(STRING(m_iFilterName), "!player")) + { + return pEntity->IsPlayer(); + } + else + { + return pEntity->NameMatches( STRING(m_iFilterName) ); + } + } + +#ifdef MAPBASE + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iFilterName = inputdata.value.StringID(); + } +#endif +}; + +LINK_ENTITY_TO_CLASS( filter_activator_name, CFilterName ); + +BEGIN_DATADESC( CFilterName ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + +END_DATADESC() + + + +// ################################################################### +// > FilterClass +// ################################################################### +class CFilterClass : public CBaseFilter +{ + DECLARE_CLASS( CFilterClass, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iFilterClass; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + return pEntity->ClassMatches( STRING(m_iFilterClass) ); + } + +#ifdef MAPBASE + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iFilterClass = inputdata.value.StringID(); + } +#endif +}; + +LINK_ENTITY_TO_CLASS( filter_activator_class, CFilterClass ); + +BEGIN_DATADESC( CFilterClass ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterClass, FIELD_STRING, "filterclass" ), + +END_DATADESC() + + +// ################################################################### +// > FilterTeam +// ################################################################### +class FilterTeam : public CBaseFilter +{ + DECLARE_CLASS( FilterTeam, CBaseFilter ); + DECLARE_DATADESC(); + +public: + int m_iFilterTeam; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + return ( pEntity->GetTeamNumber() == m_iFilterTeam ); + } + +#ifdef MAPBASE + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_INTEGER); + m_iFilterTeam = inputdata.value.Int(); + } +#endif +}; + +LINK_ENTITY_TO_CLASS( filter_activator_team, FilterTeam ); + +BEGIN_DATADESC( FilterTeam ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterTeam, FIELD_INTEGER, "filterteam" ), + +END_DATADESC() + + +// ################################################################### +// > FilterMassGreater +// ################################################################### +class CFilterMassGreater : public CBaseFilter +{ + DECLARE_CLASS( CFilterMassGreater, CBaseFilter ); + DECLARE_DATADESC(); + +public: + float m_fFilterMass; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if ( pEntity->VPhysicsGetObject() == NULL ) + return false; + + return ( pEntity->VPhysicsGetObject()->GetMass() > m_fFilterMass ); + } + +#ifdef MAPBASE + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_FLOAT); + m_fFilterMass = inputdata.value.Float(); + } +#endif +}; + +LINK_ENTITY_TO_CLASS( filter_activator_mass_greater, CFilterMassGreater ); + +BEGIN_DATADESC( CFilterMassGreater ) + +// Keyfields +DEFINE_KEYFIELD( m_fFilterMass, FIELD_FLOAT, "filtermass" ), + +END_DATADESC() + + +// ################################################################### +// > FilterDamageType +// ################################################################### +class FilterDamageType : public CBaseFilter +{ + DECLARE_CLASS( FilterDamageType, CBaseFilter ); + DECLARE_DATADESC(); + +protected: + + bool PassesFilterImpl(CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + ASSERT( false ); + return true; + } + +#ifdef MAPBASE + bool PassesDamageFilterImpl(CBaseEntity *pCaller, const CTakeDamageInfo &info) +#else + bool PassesDamageFilterImpl(const CTakeDamageInfo &info) +#endif + { +#ifdef MAPBASE + switch (m_iFilterType) + { + case 1: return (info.GetDamageType() & m_iDamageType) != 0; + case 2: + { + int iRecvDT = info.GetDamageType(); + int iOurDT = m_iDamageType; + while (iRecvDT) + { + if (iRecvDT & iOurDT) + return true; + + iRecvDT >>= 1; iOurDT >>= 1; + } + return false; + } break; + } +#endif + return info.GetDamageType() == m_iDamageType; + } + +#ifdef MAPBASE + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_INTEGER); + m_iDamageType = inputdata.value.Int(); + } + + bool KeyValue( const char *szKeyName, const char *szValue ) + { + if (FStrEq( szKeyName, "damageor" ) || FStrEq( szKeyName, "damagepresets" )) + { + m_iDamageType |= atoi( szValue ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; + } +#endif + + int m_iDamageType; +#ifdef MAPBASE + int m_iFilterType; +#endif +}; + +LINK_ENTITY_TO_CLASS( filter_damage_type, FilterDamageType ); + +BEGIN_DATADESC( FilterDamageType ) + + // Keyfields + DEFINE_KEYFIELD( m_iDamageType, FIELD_INTEGER, "damagetype" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iFilterType, FIELD_INTEGER, "FilterType" ), +#endif + +END_DATADESC() + +// ################################################################### +// > CFilterEnemy +// ################################################################### + +#define SF_FILTER_ENEMY_NO_LOSE_AQUIRED (1<<0) + +class CFilterEnemy : public CBaseFilter +{ + DECLARE_CLASS( CFilterEnemy, CBaseFilter ); + // NOT SAVED + // m_iszPlayerName + DECLARE_DATADESC(); + +public: + + virtual bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ); +#ifdef MAPBASE + virtual bool PassesDamageFilterImpl(CBaseEntity *pCaller, const CTakeDamageInfo &info); +#else + virtual bool PassesDamageFilterImpl(const CTakeDamageInfo &info); +#endif + +#ifdef MAPBASE + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iszEnemyName = inputdata.value.StringID(); + } +#endif + +private: + + bool PassesNameFilter( CBaseEntity *pCaller ); + bool PassesProximityFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy ); + bool PassesMobbedFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy ); + + string_t m_iszEnemyName; // Name or classname + float m_flRadius; // Radius (enemies are acquired at this range) + float m_flOuterRadius; // Outer radius (enemies are LOST at this range) + int m_nMaxSquadmatesPerEnemy; // Maximum number of squadmates who may share the same enemy +#ifndef MAPBASE + string_t m_iszPlayerName; // "!player" +#endif +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFilterEnemy::PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) +{ + if ( pCaller == NULL || pEntity == NULL ) + return false; + + // If asked to, we'll never fail to pass an already acquired enemy + // This allows us to use test criteria to initially pick an enemy, then disregard the test until a new enemy comes along + if ( HasSpawnFlags( SF_FILTER_ENEMY_NO_LOSE_AQUIRED ) && ( pEntity == pCaller->GetEnemy() ) ) + return true; + + // This is a little weird, but it's saying that if we're not the entity we're excluding the filter to, then just pass it throughZ + if ( PassesNameFilter( pEntity ) == false ) + return true; + + if ( PassesProximityFilter( pCaller, pEntity ) == false ) + return false; + + // NOTE: This can result in some weird NPC behavior if used improperly + if ( PassesMobbedFilter( pCaller, pEntity ) == false ) + return false; + + // The filter has been passed, meaning: + // - If we wanted all criteria to fail, they have + // - If we wanted all criteria to succeed, they have + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +#ifdef MAPBASE +bool CFilterEnemy::PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) +#else +bool CFilterEnemy::PassesDamageFilterImpl( const CTakeDamageInfo &info ) +#endif +{ + // NOTE: This function has no meaning to this implementation of the filter class! + Assert( 0 ); + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Tests the enemy's name or classname +// Input : *pEnemy - Entity being assessed +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CFilterEnemy::PassesNameFilter( CBaseEntity *pEnemy ) +{ + // If there is no name specified, we're not using it + if ( m_iszEnemyName == NULL_STRING ) + return true; + +#ifdef MAPBASE + if ( m_iszEnemyName == gm_isz_name_player ) +#else + // Cache off the special case player name + if ( m_iszPlayerName == NULL_STRING ) + { + m_iszPlayerName = FindPooledString( "!player" ); + } + + if ( m_iszEnemyName == m_iszPlayerName ) +#endif + { + if ( pEnemy->IsPlayer() ) + { + if ( m_bNegated ) + return false; + + return true; + } + } + + // May be either a targetname or classname +#ifdef MAPBASE + bool bNameOrClassnameMatches = ( pEnemy->NameMatches(STRING(m_iszEnemyName)) || pEnemy->ClassMatches(STRING(m_iszEnemyName)) ); +#else + bool bNameOrClassnameMatches = ( m_iszEnemyName == pEnemy->GetEntityName() || m_iszEnemyName == pEnemy->m_iClassname ); +#endif + + // We only leave this code block in a state meaning we've "succeeded" in any context + if ( m_bNegated ) + { + // We wanted the names to not match, but they did + if ( bNameOrClassnameMatches ) + return false; + } + else + { + // We wanted them to be the same, but they weren't + if ( bNameOrClassnameMatches == false ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Tests the enemy's proximity to the caller's position +// Input : *pCaller - Entity assessing the target +// *pEnemy - Entity being assessed +// Output : Returns true if potential enemy passes this filter stage +//----------------------------------------------------------------------------- +bool CFilterEnemy::PassesProximityFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy ) +{ + // If there is no radius specified, we're not testing it + if ( m_flRadius <= 0.0f ) + return true; + + // We test the proximity differently when we've already picked up this enemy before + bool bAlreadyEnemy = ( pCaller->GetEnemy() == pEnemy ); + + // Get our squared length to the enemy from the caller + float flDistToEnemySqr = ( pCaller->GetAbsOrigin() - pEnemy->GetAbsOrigin() ).LengthSqr(); + + // Two radii are used to control oscillation between true/false cases + // The larger radius is either specified or defaulted to be double or half the size of the inner radius + float flLargerRadius = m_flOuterRadius; + if ( flLargerRadius == 0 ) + { + flLargerRadius = ( m_bNegated ) ? (m_flRadius*0.5f) : (m_flRadius*2.0f); + } + + float flSmallerRadius = m_flRadius; + if ( flSmallerRadius > flLargerRadius ) + { + ::V_swap( flLargerRadius, flSmallerRadius ); + } + + float flDist; + if ( bAlreadyEnemy ) + { + flDist = ( m_bNegated ) ? flSmallerRadius : flLargerRadius; + } + else + { + flDist = ( m_bNegated ) ? flLargerRadius : flSmallerRadius; + } + + // Test for success + if ( flDistToEnemySqr <= (flDist*flDist) ) + { + // We wanted to fail but didn't + if ( m_bNegated ) + return false; + + return true; + } + + // We wanted to succeed but didn't + if ( m_bNegated == false ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Attempt to govern how many squad members can target any given entity +// Input : *pCaller - Entity assessing the target +// *pEnemy - Entity being assessed +// Output : Returns true if potential enemy passes this filter stage +//----------------------------------------------------------------------------- +bool CFilterEnemy::PassesMobbedFilter( CBaseEntity *pCaller, CBaseEntity *pEnemy ) +{ + // Must be a valid candidate + CAI_BaseNPC *pNPC = pCaller->MyNPCPointer(); + if ( pNPC == NULL || pNPC->GetSquad() == NULL ) + return true; + + // Make sure we're checking for this + if ( m_nMaxSquadmatesPerEnemy <= 0 ) + return true; + + AISquadIter_t iter; + int nNumMatchingSquadmates = 0; + + // Look through our squad members to see how many of them are already mobbing this entity + for ( CAI_BaseNPC *pSquadMember = pNPC->GetSquad()->GetFirstMember( &iter ); pSquadMember != NULL; pSquadMember = pNPC->GetSquad()->GetNextMember( &iter ) ) + { + // Disregard ourself + if ( pSquadMember == pNPC ) + continue; + + // If the enemies match, count it + if ( pSquadMember->GetEnemy() == pEnemy ) + { + nNumMatchingSquadmates++; + + // If we're at or passed the max we stop + if ( nNumMatchingSquadmates >= m_nMaxSquadmatesPerEnemy ) + { + // We wanted to find more than allowed and we did + if ( m_bNegated ) + return true; + + // We wanted to be less but we're not + return false; + } + } + } + + // We wanted to find more than the allowed amount but we didn't + if ( m_bNegated ) + return false; + + return true; +} + +LINK_ENTITY_TO_CLASS( filter_enemy, CFilterEnemy ); + +BEGIN_DATADESC( CFilterEnemy ) + + DEFINE_KEYFIELD( m_iszEnemyName, FIELD_STRING, "filtername" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "filter_radius" ), + DEFINE_KEYFIELD( m_flOuterRadius, FIELD_FLOAT, "filter_outer_radius" ), + DEFINE_KEYFIELD( m_nMaxSquadmatesPerEnemy, FIELD_INTEGER, "filter_max_per_enemy" ), +#ifndef MAPBASE + DEFINE_FIELD( m_iszPlayerName, FIELD_STRING ), +#endif + +END_DATADESC() + +#ifdef MAPBASE +// ################################################################### +// > CFilterModel +// ################################################################### +class CFilterModel : public CBaseFilter +{ + DECLARE_CLASS( CFilterModel, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iFilterModel; + string_t m_strFilterSkin; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (FStrEq(STRING(m_strFilterSkin), "-1") /*m_strFilterSkin == NULL_STRING|| FStrEq(STRING(m_strFilterSkin), "")*/) + return Matcher_NamesMatch(STRING(m_iFilterModel), STRING(pEntity->GetModelName())); + else if (pEntity->GetBaseAnimating()) + { + //DevMsg("Skin isn't null\n"); + return Matcher_NamesMatch(STRING(m_iFilterModel), STRING(pEntity->GetModelName())) && Matcher_Match(STRING(m_strFilterSkin), pEntity->GetBaseAnimating()->m_nSkin); + } + return false; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iFilterModel = inputdata.value.StringID(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_model, CFilterModel ); + +BEGIN_DATADESC( CFilterModel ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterModel, FIELD_STRING, "filtermodel" ), + DEFINE_KEYFIELD( m_iFilterModel, FIELD_STRING, "filtername" ), + DEFINE_KEYFIELD( m_strFilterSkin, FIELD_STRING, "skin" ), + +END_DATADESC() + +// ################################################################### +// > CFilterContext +// ################################################################### +class CFilterContext : public CBaseFilter +{ + DECLARE_CLASS( CFilterContext, CBaseFilter ); + DECLARE_DATADESC(); + +public: + bool m_bAny; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + bool passes = false; + ResponseContext_t curcontext; + const char *contextvalue; + for (int i = 0; i < GetContextCount(); i++) + { + curcontext = m_ResponseContexts[i]; + if (!pEntity->HasContext(STRING(curcontext.m_iszName), NULL)) + { + if (m_bAny) + continue; + else + return false; + } + + contextvalue = pEntity->GetContextValue(STRING(curcontext.m_iszName)); + if (Matcher_NamesMatch(STRING(m_ResponseContexts[i].m_iszValue), contextvalue)) + { + passes = true; + if (m_bAny) + break; + } + else if (!m_bAny) + { + return false; + } + } + + return passes; + } + + void InputSetField( inputdata_t& inputdata ) + { + m_ResponseContexts.RemoveAll(); + AddContext(inputdata.value.String()); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_context, CFilterContext ); + +BEGIN_DATADESC( CFilterContext ) + + // Keyfields + DEFINE_KEYFIELD( m_bAny, FIELD_BOOLEAN, "any" ), + +END_DATADESC() + +// ################################################################### +// > CFilterSquad +// ################################################################### +class CFilterSquad : public CBaseFilter +{ + DECLARE_CLASS( CFilterSquad, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iFilterName; + bool m_bAllowSilentSquadMembers; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); + if (pEntity && pNPC) + { + if (pNPC->GetSquad() && Matcher_NamesMatch(STRING(m_iFilterName), pNPC->GetSquad()->GetName())) + { + if (CAI_Squad::IsSilentMember(pNPC)) + { + return m_bAllowSilentSquadMembers; + } + else + { + return true; + } + } + } + + return false; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iFilterName = inputdata.value.StringID(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_squad, CFilterSquad ); + +BEGIN_DATADESC( CFilterSquad ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + DEFINE_KEYFIELD( m_bAllowSilentSquadMembers, FIELD_BOOLEAN, "allowsilentmembers" ), + +END_DATADESC() + +// ################################################################### +// > CFilterHintGroup +// ################################################################### +class CFilterHintGroup : public CBaseFilter +{ + DECLARE_CLASS( CFilterHintGroup, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iFilterName; + ThreeState_t m_fHintLimiting; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (!pEntity) + return false; + + if (CAI_BaseNPC *pNPC = pEntity->MyNPCPointer()) + { + if (pNPC->GetHintGroup() == NULL_STRING || Matcher_NamesMatch(STRING(m_iFilterName), STRING(pNPC->GetHintGroup()))) + { + switch (m_fHintLimiting) + { + case TRS_FALSE: return !pNPC->IsLimitingHintGroups(); break; + case TRS_TRUE: return pNPC->IsLimitingHintGroups(); break; + } + + return true; + } + } + else if (CAI_Hint *pHint = dynamic_cast(pEntity)) + { + // Just in case someone somehow puts a hint node through a filter. + // Maybe they'd use a point_advanced_finder or something, I dunno. + return Matcher_NamesMatch(STRING(m_iFilterName), STRING(pHint->GetGroup())); + } + + return false; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iFilterName = inputdata.value.StringID(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_hintgroup, CFilterHintGroup ); + +BEGIN_DATADESC( CFilterHintGroup ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + DEFINE_KEYFIELD( m_fHintLimiting, FIELD_INTEGER, "hintlimiting" ), + +END_DATADESC() + +extern bool ReadUnregisteredKeyfields(CBaseEntity *pTarget, const char *szKeyName, variant_t *variant); + +// ################################################################### +// > CFilterKeyfield +// ################################################################### +class CFilterKeyfield : public CBaseFilter +{ + DECLARE_CLASS( CFilterKeyfield, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iFilterKey; + string_t m_iFilterValue; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + variant_t var; + bool found = (pEntity->ReadKeyField(STRING(m_iFilterKey), &var) || ReadUnregisteredKeyfields(pEntity, STRING(m_iFilterKey), &var)); + return m_iFilterValue != NULL_STRING ? Matcher_Match(STRING(m_iFilterValue), var.String()) : found; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iFilterKey = inputdata.value.StringID(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_keyfield, CFilterKeyfield ); + +BEGIN_DATADESC( CFilterKeyfield ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterKey, FIELD_STRING, "keyname" ), + DEFINE_KEYFIELD( m_iFilterValue, FIELD_STRING, "value" ), + +END_DATADESC() + +// ################################################################### +// > CFilterRelationship +// ################################################################### +class CFilterRelationship : public CBaseFilter +{ + DECLARE_CLASS( CFilterKeyfield, CBaseFilter ); + DECLARE_DATADESC(); + +public: + Disposition_t m_iDisposition; + string_t m_iszPriority; // string_t to support matchers + bool m_bInvertTarget; + bool m_bReciprocal; + EHANDLE m_hTarget; + + bool RelationshipPasses(CBaseCombatCharacter *pBCC, CBaseEntity *pTarget) + { + if (!pBCC || !pTarget) + return m_iDisposition == D_NU; + + Disposition_t disposition = pBCC->IRelationType(pTarget); + int priority = pBCC->IRelationPriority(pTarget); + + bool passes = (disposition == m_iDisposition); + if (!passes) + return false; + + if (m_iszPriority != NULL_STRING) + { + passes = Matcher_Match(STRING(m_iszPriority), priority); + } + + return passes; + } + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + CBaseEntity *pSubject = NULL; + if (m_target != NULL_STRING) + { + if (!m_hTarget) + { + m_hTarget = gEntList.FindEntityGeneric(NULL, STRING(m_target), pCaller, pEntity, pCaller); + } + pSubject = m_hTarget; + } + + if (!pSubject) + pSubject = pCaller; + + // No subject or entity, cannot continue + if (!pSubject || !pEntity) + return m_iDisposition == D_NU; + + CBaseCombatCharacter *pBCC1 = !m_bInvertTarget ? pSubject->MyCombatCharacterPointer() : pEntity->MyCombatCharacterPointer(); + CBaseEntity *pTarget = m_bInvertTarget ? pSubject : pEntity; + if (!pBCC1) + { + //Warning("Error: %s subject %s is not a character that uses relationships!\n", GetDebugName(), !m_bInvertTarget ? pSubject->GetDebugName() : pEntity->GetDebugName()); + return m_iDisposition == D_NU; + } + + bool passes = RelationshipPasses(pBCC1, pTarget); + if (m_bReciprocal) + passes = RelationshipPasses(pTarget->MyCombatCharacterPointer(), pBCC1); + + return passes; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_target = inputdata.value.StringID(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_relationship, CFilterRelationship ); + +BEGIN_DATADESC( CFilterRelationship ) + + // Keyfields + DEFINE_KEYFIELD( m_iDisposition, FIELD_INTEGER, "disposition" ), + DEFINE_KEYFIELD( m_iszPriority, FIELD_STRING, "rank" ), + DEFINE_KEYFIELD( m_bInvertTarget, FIELD_BOOLEAN, "inverttarget" ), + DEFINE_KEYFIELD( m_bReciprocal, FIELD_BOOLEAN, "Reciprocal" ), + DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + +END_DATADESC() + +// ################################################################### +// > CFilterClassify +// ################################################################### +class CFilterClassify : public CBaseFilter +{ + DECLARE_CLASS( CFilterClassify, CBaseFilter ); + DECLARE_DATADESC(); + +public: + Class_T m_iFilterClassify; + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + return pEntity->Classify() == m_iFilterClassify; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_INTEGER); + m_iFilterClassify = (Class_T)inputdata.value.Int(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_classify, CFilterClassify ); + +BEGIN_DATADESC( CFilterClassify ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterClassify, FIELD_INTEGER, "filterclassify" ), + +END_DATADESC() + +// ################################################################### +// > CFilterCriteria +// ################################################################### +class CFilterCriteria : public CBaseFilter +{ + DECLARE_CLASS( CFilterCriteria, CBaseFilter ); + DECLARE_DATADESC(); + +public: + bool m_bAny; + bool m_bFull; // All criteria functions are gathered + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (!pEntity) + return false; + + AI_CriteriaSet set; + pEntity->ModifyOrAppendCriteria( set ); + if (m_bFull) + { + // Meeets the full wrath of the response criteria + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if( pPlayer ) + pPlayer->ModifyOrAppendPlayerCriteria( set ); + + pEntity->ReAppendContextCriteria( set ); + } + + bool passes = false; + const char *contextname; + const char *contextvalue; + const char *matchingvalue; + for (int i = 0; i < set.GetCount(); i++) + { + contextname = set.GetName(i); + contextvalue = set.GetValue(i); + + matchingvalue = GetContextValue(contextname); + if (matchingvalue == NULL) + { + if (m_bAny) + continue; + else + return false; + } + else + { + if (Matcher_NamesMatch(matchingvalue, contextvalue)) + { + passes = true; + if (m_bAny) + break; + } + else if (!m_bAny) + { + return false; + } + } + } + + return passes; + } + + void InputSetField( inputdata_t& inputdata ) + { + m_ResponseContexts.RemoveAll(); + AddContext(inputdata.value.String()); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_criteria, CFilterCriteria ); + +BEGIN_DATADESC( CFilterCriteria ) + + // Keyfields + DEFINE_KEYFIELD( m_bAny, FIELD_BOOLEAN, "any" ), + DEFINE_KEYFIELD( m_bFull, FIELD_BOOLEAN, "full" ), + +END_DATADESC() + +extern bool TestEntityTriggerIntersection_Accurate( CBaseEntity *pTrigger, CBaseEntity *pEntity ); + +// ################################################################### +// > CFilterInVolume +// Passes when the entity is within the specified volume. +// ################################################################### +class CFilterInVolume : public CBaseFilter +{ + DECLARE_CLASS( CFilterInVolume, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iszVolumeTester; + + void Spawn() + { + BaseClass::Spawn(); + + // Assume no string = use activator + if (m_iszVolumeTester == NULL_STRING) + m_iszVolumeTester = AllocPooledString("!activator"); + } + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + CBaseEntity *pVolume = gEntList.FindEntityByNameNearest(STRING(m_target), pEntity->GetLocalOrigin(), 0, this, pEntity, pCaller); + if (!pVolume) + { + Msg("%s cannot find volume %s\n", GetDebugName(), STRING(m_target)); + return false; + } + + CBaseEntity *pTarget = gEntList.FindEntityByName(NULL, STRING(m_iszVolumeTester), this, pEntity, pCaller); + if (pTarget) + return TestEntityTriggerIntersection_Accurate(pVolume, pTarget); + else + { + Msg("%s cannot find target entity %s, returning false\n", GetDebugName(), STRING(m_iszVolumeTester)); + return false; + } + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iszVolumeTester = inputdata.value.StringID(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_involume, CFilterInVolume ); + +BEGIN_DATADESC( CFilterInVolume ) + + // Keyfields + DEFINE_KEYFIELD( m_iszVolumeTester, FIELD_STRING, "tester" ), + +END_DATADESC() + +// ################################################################### +// > CFilterSurfaceProp +// ################################################################### +class CFilterSurfaceData : public CBaseFilter +{ + DECLARE_CLASS( CFilterSurfaceData, CBaseFilter ); + DECLARE_DATADESC(); + +public: + string_t m_iFilterSurface; + int m_iSurfaceIndex; + + enum + { + SURFACETYPE_SURFACEPROP, + SURFACETYPE_GAMEMATERIAL, + }; + + // Gets the surfaceprop's game material and filters by that. + int m_iSurfaceType; + + void ParseSurfaceIndex() + { + m_iSurfaceIndex = physprops->GetSurfaceIndex(STRING(m_iFilterSurface)); + + switch (m_iSurfaceType) + { + case SURFACETYPE_GAMEMATERIAL: + { + const surfacedata_t *pSurfaceData = physprops->GetSurfaceData(m_iSurfaceIndex); + if (pSurfaceData) + m_iSurfaceIndex = pSurfaceData->game.material; + else + Warning("Can't get surface data for %s\n", STRING(m_iFilterSurface)); + } break; + } + } + + void Activate() + { + ParseSurfaceIndex(); + } + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (pEntity->VPhysicsGetObject()) + { + int iMatIndex = pEntity->VPhysicsGetObject()->GetMaterialIndex(); + switch (m_iSurfaceType) + { + case SURFACETYPE_GAMEMATERIAL: + { + const surfacedata_t *pSurfaceData = physprops->GetSurfaceData(iMatIndex); + if (pSurfaceData) + return m_iSurfaceIndex == pSurfaceData->game.material; + } + default: + return iMatIndex == m_iSurfaceIndex; + } + } + + return false; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + m_iFilterSurface = inputdata.value.StringID(); + ParseSurfaceIndex(); + } +}; + +LINK_ENTITY_TO_CLASS( filter_activator_surfacedata, CFilterSurfaceData ); + +BEGIN_DATADESC( CFilterSurfaceData ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterSurface, FIELD_STRING, "filterstring" ), + DEFINE_KEYFIELD( m_iSurfaceType, FIELD_INTEGER, "SurfaceType" ), + +END_DATADESC() + +// =================================================================== +// Redirect filters +// +// Redirects certain data to a specific filter. +// =================================================================== +class CBaseFilterRedirect : public CBaseFilter +{ + DECLARE_CLASS( CBaseFilterRedirect, CBaseFilter ); + +public: + inline CBaseEntity *GetTargetFilter() + { + // Yes, this hijacks damage filter functionality. + // It's not like it was using it before anyway. + return m_hDamageFilter.Get(); + } + + bool RedirectToFilter( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (GetTargetFilter() && pEntity) + { + CBaseFilter *pFilter = static_cast(GetTargetFilter()); + return pFilter->PassesFilter(pCaller, pEntity); + } + + return pEntity != NULL; + } + + bool RedirectToDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (GetTargetFilter()) + { + CBaseFilter *pFilter = static_cast(GetTargetFilter()); + return pFilter->PassesDamageFilter(pCaller, info); + } + + return true; + } + + virtual bool PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + return RedirectToDamageFilter( pCaller, info ); + } + + virtual bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + return RedirectToFilter( pCaller, pEntity ); + } + + virtual bool BloodAllowed( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (GetTargetFilter()) + { + CBaseFilter *pFilter = static_cast(GetTargetFilter()); + return pFilter->BloodAllowed(pCaller, info); + } + + return true; + } + + virtual bool DamageMod( CBaseEntity *pCaller, CTakeDamageInfo &info ) + { + if (GetTargetFilter()) + { + CBaseFilter *pFilter = static_cast(GetTargetFilter()); + return pFilter->DamageMod( pCaller, info ); + } + + return true; + } + + void InputSetField( inputdata_t& inputdata ) + { + inputdata.value.Convert(FIELD_STRING); + InputSetDamageFilter(inputdata); + } + + enum + { + REDIRECT_MUST_PASS_TO_DAMAGE_CALLER, // Must pass to damage caller, if damage is allowed + REDIRECT_MUST_PASS_TO_ACT, // Must pass to do action + REDIRECT_MUST_PASS_ACTIVATORS, // Each activator must pass this filter + }; +}; + +// ################################################################### +// > CFilterRedirectInflictor +// Uses the specified filter to filter by damage inflictor. +// ################################################################### +class CFilterRedirectInflictor : public CBaseFilterRedirect +{ + DECLARE_CLASS( CFilterRedirectInflictor, CBaseFilterRedirect ); + +public: + bool PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + return RedirectToFilter(pCaller, info.GetInflictor()); + } +}; + +LINK_ENTITY_TO_CLASS( filter_redirect_inflictor, CFilterRedirectInflictor ); + +// ################################################################### +// > CFilterRedirectWeapon +// Uses the specified filter to filter by either the entity's active weapon or the weapon causing damage, +// depending on the context. +// ################################################################### +class CFilterRedirectWeapon : public CBaseFilterRedirect +{ + DECLARE_CLASS( CFilterRedirectWeapon, CBaseFilterRedirect ); + +public: + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + CBaseCombatCharacter *pBCC = pEntity->MyCombatCharacterPointer(); + if (pBCC && pBCC->GetActiveWeapon()) + { + return RedirectToFilter( pCaller, pBCC->GetActiveWeapon() ); + } + + return false; + } + + bool PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + // Pass any weapon found in the damage info + if (info.GetWeapon()) + { + return RedirectToFilter( pCaller, info.GetWeapon() ); + } + + // Check the attacker's active weapon instead + if (info.GetAttacker()) + { + return PassesFilterImpl( pCaller, info.GetAttacker() ); + } + + // No weapon to check + return false; + } +}; + +LINK_ENTITY_TO_CLASS( filter_redirect_weapon, CFilterRedirectWeapon ); + +// ################################################################### +// > CFilterRedirectOwner +// Uses the specified filter to filter by owner entity. +// ################################################################### +class CFilterRedirectOwner : public CBaseFilterRedirect +{ + DECLARE_CLASS( CFilterRedirectOwner, CBaseFilterRedirect ); + +public: + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (pEntity->GetOwnerEntity()) + { + return RedirectToFilter(pCaller, pEntity->GetOwnerEntity()); + } + + return false; + } +}; + +LINK_ENTITY_TO_CLASS( filter_redirect_owner, CFilterRedirectOwner ); + +// ################################################################### +// > CFilterDamageTransfer +// Transfers damage to another entity. +// ################################################################### +class CFilterDamageTransfer : public CBaseFilterRedirect +{ + DECLARE_CLASS( CFilterDamageTransfer, CBaseFilterRedirect ); + DECLARE_DATADESC(); + +public: + void Spawn() + { + BaseClass::Spawn(); + + // Assume no string = use activator + if (m_target == NULL_STRING) + m_target = AllocPooledString("!activator"); + + // A number less than or equal to 0 is always synonymous with no limit + if (m_iMaxEntities <= 0) + m_iMaxEntities = MAX_EDICTS; + } + + // Some secondary filter modes shouldn't be used in non-final filter passes + // Always return true on non-standard secondary filter modes + /* + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + return true; + } + */ + + // A hack because of the way final damage filtering now works. + bool BloodAllowed( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (!m_bCallerDamageAllowed) + return false; + else + return m_iSecondaryFilterMode == REDIRECT_MUST_PASS_TO_DAMAGE_CALLER && GetTargetFilter() ? RedirectToDamageFilter(pCaller, info) : true; + } + + // PassesFinalDamageFilter() was created for the express purpose of having filter_damage_transfer function without + // passing damage on filter checks that don't actually lead to us taking damage in the first place. + // PassesFinalDamageFilter() is only called in certain base entity functions where we DEFINITELY will take damage otherwise. + bool PassesFinalDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (m_iSecondaryFilterMode == REDIRECT_MUST_PASS_TO_ACT) + { + // Transfer only if the secondary filter passes + if (!RedirectToDamageFilter(pCaller, info)) + { + // Otherwise just return the other flag + return m_bCallerDamageAllowed; + } + } + + CBaseEntity *pTarget = gEntList.FindEntityGeneric(NULL, STRING(m_target), this, info.GetAttacker(), pCaller); + int iNumDamaged = 0; + while (pTarget) + { + // Avoid recursive loops! + if (pTarget->m_hDamageFilter != this) + { + CTakeDamageInfo info2 = info; + + // Adjust damage position stuff + if (m_bAdjustDamagePosition) + { + info2.SetDamagePosition(pTarget->GetAbsOrigin() + (pCaller->GetAbsOrigin() - info.GetDamagePosition())); + + if (pCaller->IsCombatCharacter() && pTarget->IsCombatCharacter()) + pTarget->MyCombatCharacterPointer()->SetLastHitGroup(pCaller->MyCombatCharacterPointer()->LastHitGroup()); + } + + if (m_iSecondaryFilterMode != REDIRECT_MUST_PASS_ACTIVATORS || RedirectToFilter(pCaller, pTarget)) + { + pTarget->TakeDamage(info2); + iNumDamaged++; + } + } + + if (iNumDamaged < m_iMaxEntities) + pTarget = gEntList.FindEntityGeneric(pTarget, STRING(m_target), this, info.GetAttacker(), pCaller); + else + break; + } + + // We've transferred the damage, now determine whether the caller should take damage. + // Boolean surpasses all. + if (!m_bCallerDamageAllowed) + return false; + else + return m_iSecondaryFilterMode == REDIRECT_MUST_PASS_TO_DAMAGE_CALLER && GetTargetFilter() ? RedirectToDamageFilter(pCaller, info) : true; + } + + /* + void InputSetTarget( inputdata_t& inputdata ) + { + m_target = inputdata.value.StringID(); + m_hTarget = NULL; + } + */ + + inline CBaseEntity *GetTarget(CBaseEntity *pCaller, CBaseEntity *pActivator) + { + return gEntList.FindEntityGeneric(NULL, STRING(m_target), this, pActivator, pCaller); + } + + //EHANDLE m_hTarget; + + bool m_bAdjustDamagePosition; + + // See CBaseRedirectFilter enum for more info + int m_iSecondaryFilterMode; + + // If enabled, the caller can be damaged after the transfer. If disabled, the caller cannot. + bool m_bCallerDamageAllowed; + + int m_iMaxEntities = MAX_EDICTS; +}; + +LINK_ENTITY_TO_CLASS( filter_damage_transfer, CFilterDamageTransfer ); + +BEGIN_DATADESC( CFilterDamageTransfer ) + + //DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_bAdjustDamagePosition, FIELD_BOOLEAN, "AdjustDamagePosition" ), + DEFINE_KEYFIELD( m_iMaxEntities, FIELD_INTEGER, "MaxEntities" ), + DEFINE_KEYFIELD( m_iSecondaryFilterMode, FIELD_INTEGER, "SecondaryFilterMode" ), + DEFINE_KEYFIELD( m_bCallerDamageAllowed, FIELD_BOOLEAN, "CallerDamageAllowed" ), + +END_DATADESC() + +// ################################################################### +// > CFilterBloodControl +// Takes advantage of hacks created for filter_damage_transfer to control blood. +// ################################################################### +class CFilterBloodControl : public CBaseFilterRedirect +{ + DECLARE_CLASS( CFilterBloodControl, CBaseFilterRedirect ); + DECLARE_DATADESC(); +public: + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (GetTargetFilter() && m_bSecondaryFilterIsDamageFilter) + return RedirectToFilter(pCaller, pEntity); + + return true; + } + + bool BloodAllowed( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (m_bBloodDisabled) + return false; + + return GetTargetFilter() ? RedirectToDamageFilter(pCaller, info) : true; + } + + void InputDisableBlood( inputdata_t &inputdata ) { m_bBloodDisabled = true; } + void InputEnableBlood( inputdata_t &inputdata ) { m_bBloodDisabled = false; } + + bool m_bBloodDisabled; + + // Uses the secondary filter as a damage filter instead of just a blood filter + bool m_bSecondaryFilterIsDamageFilter; +}; + +LINK_ENTITY_TO_CLASS( filter_blood_control, CFilterBloodControl ); + +BEGIN_DATADESC( CFilterBloodControl ) + + DEFINE_KEYFIELD( m_bBloodDisabled, FIELD_BOOLEAN, "BloodDisabled" ), + DEFINE_KEYFIELD( m_bSecondaryFilterIsDamageFilter, FIELD_BOOLEAN, "SecondaryFilterMode" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "DisableBlood", InputDisableBlood ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableBlood", InputEnableBlood ), + +END_DATADESC() + +// ################################################################### +// > CFilterDamageMod +// Modifies damage. +// ################################################################### +class CFilterDamageMod : public CBaseFilterRedirect +{ + DECLARE_CLASS( CFilterDamageMod, CBaseFilterRedirect ); + DECLARE_DATADESC(); +public: + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (GetTargetFilter() && m_iSecondaryFilterMode == REDIRECT_MUST_PASS_TO_DAMAGE_CALLER) + return RedirectToFilter(pCaller, pEntity); + + return true; + } + + bool PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (GetTargetFilter() && m_iSecondaryFilterMode == REDIRECT_MUST_PASS_TO_DAMAGE_CALLER) + return RedirectToDamageFilter( pCaller, info ); + + return true; + } + + bool DamageMod( CBaseEntity *pCaller, CTakeDamageInfo &info ) + { + if (GetTargetFilter()) + { + bool bPass = true; + + switch (m_iSecondaryFilterMode) + { + case REDIRECT_MUST_PASS_TO_DAMAGE_CALLER: + case REDIRECT_MUST_PASS_TO_ACT: bPass = (RedirectToDamageFilter( pCaller, info )); break; + + case REDIRECT_MUST_PASS_ACTIVATORS: bPass = (info.GetAttacker() && RedirectToFilter(pCaller, info.GetAttacker())); break; + } + + if (!bPass) + return false; + } + + if (m_flDamageMultiplier != 1.0f) + info.ScaleDamage(m_flDamageMultiplier); + if (m_flDamageAddend != 0.0f) + info.AddDamage(m_flDamageAddend); + + if (m_iDamageBitsAdded != 0) + info.AddDamageType(m_iDamageBitsAdded); + if (m_iDamageBitsRemoved != 0) + info.AddDamageType(~m_iDamageBitsRemoved); + + if (m_iszNewAttacker != NULL_STRING) + { + if (!m_hNewAttacker) + m_hNewAttacker = gEntList.FindEntityByName(NULL, m_iszNewAttacker, this, info.GetAttacker(), pCaller); + info.SetAttacker(m_hNewAttacker); + } + if (m_iszNewInflictor != NULL_STRING) + { + if (!m_hNewInflictor) + m_hNewInflictor = gEntList.FindEntityByName(NULL, m_iszNewInflictor, this, info.GetAttacker(), pCaller); + info.SetInflictor(m_hNewInflictor); + } + if (m_iszNewWeapon != NULL_STRING) + { + if (!m_hNewWeapon) + m_hNewWeapon = gEntList.FindEntityByName(NULL, m_iszNewWeapon, this, info.GetAttacker(), pCaller); + info.SetWeapon(m_hNewWeapon); + } + + return true; + } + + void InputSetNewAttacker( inputdata_t &inputdata ) { m_iszNewAttacker = inputdata.value.StringID(); m_hNewAttacker = NULL; } + void InputSetNewInflictor( inputdata_t &inputdata ) { m_iszNewInflictor = inputdata.value.StringID(); m_hNewInflictor = NULL; } + void InputSetNewWeapon( inputdata_t &inputdata ) { m_iszNewWeapon = inputdata.value.StringID(); m_hNewWeapon = NULL; } + + float m_flDamageMultiplier = 1.0f; + float m_flDamageAddend; + int m_iDamageBitsAdded; + int m_iDamageBitsRemoved; + + string_t m_iszNewAttacker; EHANDLE m_hNewAttacker; + string_t m_iszNewInflictor; EHANDLE m_hNewInflictor; + string_t m_iszNewWeapon; EHANDLE m_hNewWeapon; + + // See CBaseRedirectFilter enum for more info + int m_iSecondaryFilterMode; +}; + +LINK_ENTITY_TO_CLASS( filter_damage_mod, CFilterDamageMod ); + +BEGIN_DATADESC( CFilterDamageMod ) + + DEFINE_KEYFIELD( m_iszNewAttacker, FIELD_STRING, "NewAttacker" ), + DEFINE_KEYFIELD( m_iszNewInflictor, FIELD_STRING, "NewInflictor" ), + DEFINE_KEYFIELD( m_iszNewWeapon, FIELD_STRING, "NewWeapon" ), + DEFINE_FIELD( m_hNewAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_hNewInflictor, FIELD_EHANDLE ), + DEFINE_FIELD( m_hNewWeapon, FIELD_EHANDLE ), + + DEFINE_INPUT( m_flDamageMultiplier, FIELD_FLOAT, "SetDamageMultiplier" ), + DEFINE_INPUT( m_flDamageAddend, FIELD_FLOAT, "SetDamageAddend" ), + DEFINE_INPUT( m_iDamageBitsAdded, FIELD_INTEGER, "SetDamageBitsAdded" ), + DEFINE_INPUT( m_iDamageBitsRemoved, FIELD_INTEGER, "SetDamageBitsRemoved" ), + + DEFINE_KEYFIELD( m_iSecondaryFilterMode, FIELD_INTEGER, "SecondaryFilterMode" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetNewAttacker", InputSetNewAttacker ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetNewInflictor", InputSetNewInflictor ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetNewWeapon", InputSetNewWeapon ), + +END_DATADESC() + +// ################################################################### +// > CFilterDamageLogic +// Fires outputs from damage information. +// ################################################################### +class CFilterDamageLogic : public CBaseFilterRedirect +{ + DECLARE_CLASS( CFilterDamageLogic, CBaseFilterRedirect ); + DECLARE_DATADESC(); +public: + bool PassesFinalDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + bool bPassesFilter = !GetTargetFilter() || RedirectToDamageFilter( pCaller, info ); + if (!bPassesFilter) + { + if (m_iSecondaryFilterMode == 2) + return true; + else if (m_iSecondaryFilterMode != 1) + return false; + } + + CBaseEntity *pActivator = info.GetAttacker(); + + m_OutInflictor.Set( info.GetInflictor(), pActivator, pCaller ); + m_OutAttacker.Set( info.GetAttacker(), pActivator, pCaller ); + m_OutWeapon.Set( info.GetWeapon(), pActivator, pCaller ); + + m_OutDamage.Set( info.GetDamage(), pActivator, pCaller ); + m_OutMaxDamage.Set( info.GetMaxDamage(), pActivator, pCaller ); + m_OutBaseDamage.Set( info.GetBaseDamage(), pActivator, pCaller ); + + m_OutDamageType.Set( info.GetDamageType(), pActivator, pCaller ); + m_OutDamageCustom.Set( info.GetDamageCustom(), pActivator, pCaller ); + m_OutDamageStats.Set( info.GetDamageStats(), pActivator, pCaller ); + m_OutAmmoType.Set( info.GetAmmoType(), pActivator, pCaller ); + + m_OutDamageForce.Set( info.GetDamageForce(), pActivator, pCaller ); + m_OutDamagePosition.Set( info.GetDamagePosition(), pActivator, pCaller ); + + m_OutForceFriendlyFire.Set( info.IsForceFriendlyFire() ? 1 : 0, pActivator, pCaller ); + + return bPassesFilter; + } + + bool PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (GetTargetFilter() && m_iSecondaryFilterMode != 2) + return RedirectToDamageFilter( pCaller, info ); + + return true; + } + + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (GetTargetFilter() && m_iSecondaryFilterMode != 2) + return RedirectToFilter( pCaller, pEntity ); + + return true; + } + + // 0 = Use as a regular damage filter. If it doesn't pass, damage won't be outputted. + // 1 = Fire outputs even if the secondary filter doesn't pass. + // 2 = Only use the secondary filter for whether to output damage, other damage is actually dealt. + int m_iSecondaryFilterMode; + + // Outputs + COutputEHANDLE m_OutInflictor; + COutputEHANDLE m_OutAttacker; + COutputEHANDLE m_OutWeapon; + + COutputFloat m_OutDamage; + COutputFloat m_OutMaxDamage; + COutputFloat m_OutBaseDamage; + + COutputInt m_OutDamageType; + COutputInt m_OutDamageCustom; + COutputInt m_OutDamageStats; + COutputInt m_OutAmmoType; + + COutputVector m_OutDamageForce; + COutputPositionVector m_OutDamagePosition; + + COutputInt m_OutForceFriendlyFire; +}; + +LINK_ENTITY_TO_CLASS( filter_damage_logic, CFilterDamageLogic ); + +BEGIN_DATADESC( CFilterDamageLogic ) + + DEFINE_KEYFIELD( m_iSecondaryFilterMode, FIELD_INTEGER, "SecondaryFilterMode" ), + + // Outputs + DEFINE_OUTPUT( m_OutInflictor, "OutInflictor" ), + DEFINE_OUTPUT( m_OutAttacker, "OutAttacker" ), + DEFINE_OUTPUT( m_OutWeapon, "OutWeapon" ), + + DEFINE_OUTPUT( m_OutDamage, "OutDamage" ), + DEFINE_OUTPUT( m_OutMaxDamage, "OutMaxDamage" ), + DEFINE_OUTPUT( m_OutBaseDamage, "OutBaseDamage" ), + + DEFINE_OUTPUT( m_OutDamageType, "OutDamageType" ), + DEFINE_OUTPUT( m_OutDamageCustom, "OutDamageCustom" ), + DEFINE_OUTPUT( m_OutDamageStats, "OutDamageStats" ), + DEFINE_OUTPUT( m_OutAmmoType, "OutAmmoType" ), + + DEFINE_OUTPUT( m_OutDamageForce, "OutDamageForce" ), + DEFINE_OUTPUT( m_OutDamagePosition, "OutDamagePosition" ), + + DEFINE_OUTPUT( m_OutForceFriendlyFire, "OutForceFriendlyFire" ), + +END_DATADESC() +#endif + +#ifdef MAPBASE_VSCRIPT +ScriptHook_t g_Hook_PassesFilter; +ScriptHook_t g_Hook_PassesDamageFilter; +ScriptHook_t g_Hook_PassesFinalDamageFilter; +ScriptHook_t g_Hook_BloodAllowed; +ScriptHook_t g_Hook_DamageMod; + +// ################################################################### +// > CFilterScript +// ################################################################### +class CFilterScript : public CBaseFilter +{ + DECLARE_CLASS( CFilterScript, CBaseFilter ); + DECLARE_DATADESC(); + DECLARE_ENT_SCRIPTDESC(); + +public: + bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) + { + if (m_ScriptScope.IsInitialized()) + { + // caller, activator + ScriptVariant_t functionReturn; + ScriptVariant_t args[] = { ToHScript( pCaller ), ToHScript( pEntity ) }; + if ( !g_Hook_PassesFilter.Call( m_ScriptScope, &functionReturn, args ) ) + { + Warning( "%s: No PassesFilter function\n", GetDebugName() ); + } + + return functionReturn.m_bool; + } + + Warning("%s: No script scope, cannot filter\n", GetDebugName()); + return false; + } + + bool PassesDamageFilterImpl( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (m_ScriptScope.IsInitialized()) + { + HSCRIPT pInfo = g_pScriptVM->RegisterInstance( const_cast(&info) ); + + // caller, info + ScriptVariant_t functionReturn; + ScriptVariant_t args[] = { ToHScript( pCaller ), pInfo }; + if ( !g_Hook_PassesDamageFilter.Call( m_ScriptScope, &functionReturn, args ) ) + { + // Fall back to main filter function + g_pScriptVM->RemoveInstance( pInfo ); + return PassesFilterImpl( pCaller, info.GetAttacker() ); + } + + g_pScriptVM->RemoveInstance( pInfo ); + + return functionReturn.m_bool; + } + + Warning("%s: No script scope, cannot filter\n", GetDebugName()); + return false; + } + + bool PassesFinalDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (m_ScriptScope.IsInitialized()) + { + HSCRIPT pInfo = g_pScriptVM->RegisterInstance( const_cast(&info) ); + + // caller, info + ScriptVariant_t functionReturn; + ScriptVariant_t args[] = { ToHScript( pCaller ), pInfo }; + if ( !g_Hook_PassesFinalDamageFilter.Call( m_ScriptScope, &functionReturn, args ) ) + { + g_pScriptVM->RemoveInstance( pInfo ); + return BaseClass::PassesFinalDamageFilter( pCaller, info ); + } + + g_pScriptVM->RemoveInstance( pInfo ); + + return functionReturn.m_bool; + } + + Warning("%s: No script scope, cannot filter\n", GetDebugName()); + return false; + } + + bool BloodAllowed( CBaseEntity *pCaller, const CTakeDamageInfo &info ) + { + if (m_ScriptScope.IsInitialized()) + { + HSCRIPT pInfo = g_pScriptVM->RegisterInstance( const_cast(&info) ); + + // caller, info + ScriptVariant_t functionReturn; + ScriptVariant_t args[] = { ToHScript( pCaller ), pInfo }; + if ( !g_Hook_BloodAllowed.Call( m_ScriptScope, &functionReturn, args ) ) + { + g_pScriptVM->RemoveInstance( pInfo ); + return BaseClass::BloodAllowed( pCaller, info ); + } + + g_pScriptVM->RemoveInstance( pInfo ); + + return functionReturn.m_bool; + } + + Warning("%s: No script scope, cannot filter\n", GetDebugName()); + return false; + } + + bool DamageMod( CBaseEntity *pCaller, CTakeDamageInfo &info ) + { + if (m_ScriptScope.IsInitialized()) + { + HSCRIPT pInfo = g_pScriptVM->RegisterInstance( &info ); + + // caller, info + ScriptVariant_t functionReturn; + ScriptVariant_t args[] = { ToHScript( pCaller ), pInfo }; + if ( !g_Hook_DamageMod.Call( m_ScriptScope, &functionReturn, args ) ) + { + g_pScriptVM->RemoveInstance( pInfo ); + return BaseClass::DamageMod( pCaller, info ); + } + + g_pScriptVM->RemoveInstance( pInfo ); + + return functionReturn.m_bool; + } + + Warning("%s: No script scope, cannot filter\n", GetDebugName()); + return false; + } +}; + +LINK_ENTITY_TO_CLASS( filter_script, CFilterScript ); + +BEGIN_DATADESC( CFilterScript ) +END_DATADESC() + +BEGIN_ENT_SCRIPTDESC( CFilterScript, CBaseFilter, "The filter_script entity which allows VScript functions to hook onto filter methods." ) + + // + // Hooks + // + + // The CFilterScript class is visible in the help string, so "A hook used by filter_script" is redundant, but these names are also + // used for functions in CBaseFilter. In order to reduce confusion, the description emphasizes that these are hooks. + BEGIN_SCRIPTHOOK( g_Hook_PassesFilter, "PassesFilter", FIELD_BOOLEAN, "A hook used by filter_script to determine what entities should pass it. Return true if the entity should pass or false if it should not. This hook is required for regular filtering." ) + DEFINE_SCRIPTHOOK_PARAM( "caller", FIELD_HSCRIPT ) + DEFINE_SCRIPTHOOK_PARAM( "activator", FIELD_HSCRIPT ) + END_SCRIPTHOOK() + + BEGIN_SCRIPTHOOK( g_Hook_PassesDamageFilter, "PassesDamageFilter", FIELD_BOOLEAN, "A hook used by filter_script to determine what damage should pass it when it's being used as a damage filter. Return true if the info should pass or false if it should not. If this hook is not defined in a filter_script, damage filter requests will instead check PassesFilter with the attacker as the activator." ) + DEFINE_SCRIPTHOOK_PARAM( "caller", FIELD_HSCRIPT ) + DEFINE_SCRIPTHOOK_PARAM( "info", FIELD_HSCRIPT ) + END_SCRIPTHOOK() + + BEGIN_SCRIPTHOOK( g_Hook_PassesFinalDamageFilter, "PassesFinalDamageFilter", FIELD_BOOLEAN, "A completely optional hook used by filter_script which only runs when the entity will take damage. This is different from PassesDamageFilter, which is sometimes used in cases where damage is not actually about to be taken. This also runs after a regular PassesDamageFilter check. Return true if the info should pass or false if it should not. If this hook is not defined, it will always return true." ) + DEFINE_SCRIPTHOOK_PARAM( "caller", FIELD_HSCRIPT ) + DEFINE_SCRIPTHOOK_PARAM( "info", FIELD_HSCRIPT ) + END_SCRIPTHOOK() + + BEGIN_SCRIPTHOOK( g_Hook_BloodAllowed, "BloodAllowed", FIELD_BOOLEAN, "A completely optional hook used by filter_script to determine if a caller is allowed to emit blood after taking damage. Return true if blood should be allowed or false if it should not. If this hook is not defined, it will always return true." ) + DEFINE_SCRIPTHOOK_PARAM( "caller", FIELD_HSCRIPT ) + DEFINE_SCRIPTHOOK_PARAM( "info", FIELD_HSCRIPT ) + END_SCRIPTHOOK() + + BEGIN_SCRIPTHOOK( g_Hook_DamageMod, "DamageMod", FIELD_BOOLEAN, "A completely optional hook used by filter_script to modify damage being taken by an entity. You are free to use CTakeDamageInfo functions on the damage info handle and it will change how the caller is damaged. Returning true or false currently has no effect on vanilla code, but you should generally return true if the damage info has been modified by your code and false if it was not. If this hook is not defined, it will always return false." ) + DEFINE_SCRIPTHOOK_PARAM( "caller", FIELD_HSCRIPT ) + DEFINE_SCRIPTHOOK_PARAM( "info", FIELD_HSCRIPT ) + END_SCRIPTHOOK() + +END_SCRIPTDESC() +#endif diff --git a/sp/src/game/server/filters.h b/sp/src/game/server/filters.h new file mode 100644 index 00000000..faaa20fd --- /dev/null +++ b/sp/src/game/server/filters.h @@ -0,0 +1,134 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Filters are outboard entities that hold a set of rules that other +// entities can use to determine behaviors. +// +// For example, triggers can use an activator filter to determine who +// activates them. NPCs and breakables can use a damage filter to +// determine what can damage them. +// +// Current filter criteria are: +// +// Activator name +// Activator class +// Activator team +// Damage type (for damage filters only) +// +// More than one filter can be combined to create a more complex boolean +// expression by using filter_multi. +// +//=============================================================================// + +#ifndef FILTERS_H +#define FILTERS_H +#ifdef _WIN32 +#pragma once +#endif + +#include "baseentity.h" +#include "entityoutput.h" + +// ################################################################### +// > BaseFilter +// ################################################################### +class CBaseFilter : public CLogicalEntity +{ + DECLARE_CLASS( CBaseFilter, CLogicalEntity ); + +public: + + DECLARE_DATADESC(); +#ifdef MAPBASE_VSCRIPT + DECLARE_ENT_SCRIPTDESC(); +#endif + + bool PassesFilter( CBaseEntity *pCaller, CBaseEntity *pEntity ); +#ifdef MAPBASE + bool PassesDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ); + + // This was made for filter_damage_transfer. Should return true on all other filters. + virtual bool PassesFinalDamageFilter( CBaseEntity *pCaller, const CTakeDamageInfo &info ) { return true; } + + virtual bool BloodAllowed( CBaseEntity *pCaller, const CTakeDamageInfo &info ) { return true; } + + virtual bool DamageMod( CBaseEntity *pCaller, CTakeDamageInfo &info ) { return false; } + + // Deprecated. Pass the caller in front. + bool PassesDamageFilter( const CTakeDamageInfo &info ); +#else + bool PassesDamageFilter( const CTakeDamageInfo &info ); +#endif + +#ifdef MAPBASE_VSCRIPT + bool ScriptPassesFilter( HSCRIPT pCaller, HSCRIPT pEntity ); + bool ScriptPassesDamageFilter( HSCRIPT pCaller, HSCRIPT pInfo ); + bool ScriptPassesFinalDamageFilter( HSCRIPT pCaller, HSCRIPT pInfo ); + bool ScriptBloodAllowed( HSCRIPT pCaller, HSCRIPT pInfo ); + bool ScriptDamageMod( HSCRIPT pCaller, HSCRIPT pInfo ); +#endif + + bool m_bNegated; + + // Inputs + void InputTestActivator( inputdata_t &inputdata ); + +#ifdef MAPBASE + void InputTestEntity( inputdata_t &inputdata ); + virtual void InputSetField( inputdata_t &inputdata ); + + bool m_bPassCallerWhenTested; +#endif + + // Outputs + COutputEvent m_OnPass; // Fired when filter is passed + COutputEvent m_OnFail; // Fired when filter is failed + +protected: + + virtual bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ); +#ifdef MAPBASE + virtual bool PassesDamageFilterImpl(CBaseEntity *pCaller, const CTakeDamageInfo &info); +#else + virtual bool PassesDamageFilterImpl(const CTakeDamageInfo &info); +#endif +}; + +#ifdef MAPBASE +//========================================================= +// Trace filter that uses a filter entity. +// If the regular trace filter stuff tells this trace to hit an entity, it will go through a filter entity. +// If the entity passes the filter, the trace will go through. +// This can be negated with m_bHitIfPassed, meaning entities that pass will be hit. +// Use m_bFilterExclusive to make the filter the sole factor in hitting an entity. +//========================================================= +class CTraceFilterEntityFilter : public CTraceFilterSimple +{ +public: + CTraceFilterEntityFilter( const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup ) {} + CTraceFilterEntityFilter( int collisionGroup ) : CTraceFilterSimple( NULL, collisionGroup ) {} + + bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + bool base = CTraceFilterSimple::ShouldHitEntity( pHandleEntity, contentsMask ); + CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity ); + + if (m_bFilterExclusive && m_pFilter) + return m_pFilter->PassesFilter(m_pCaller, pEntity) ? m_bHitIfPassed : !m_bHitIfPassed; + else if (m_pFilter && (base ? !m_bHitIfPassed : m_bHitIfPassed)) + { + return m_bHitIfPassed ? m_pFilter->PassesFilter(m_pCaller, pEntity) : !m_pFilter->PassesFilter(m_pCaller, pEntity); + } + + return base; + } + + CBaseFilter *m_pFilter; + CBaseEntity *m_pCaller; + + bool m_bHitIfPassed; + bool m_bFilterExclusive; + +}; +#endif + +#endif // FILTERS_H diff --git a/sp/src/game/server/fire.cpp b/sp/src/game/server/fire.cpp new file mode 100644 index 00000000..a4fb9720 --- /dev/null +++ b/sp/src/game/server/fire.cpp @@ -0,0 +1,1536 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +//--------------------------------------------------------- +//--------------------------------------------------------- +#include "cbase.h" +#include "decals.h" +#include "fire.h" +#include "entitylist.h" +#include "basecombatcharacter.h" +#include "ndebugoverlay.h" +#include "engine/IEngineSound.h" +#include "ispatialpartition.h" +#include "collisionutils.h" +#include "tier0/vprof.h" +#ifdef MAPBASE +#include "weapon_flaregun.h" +#include "mapbase/GlobalStrings.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +#ifndef MAPBASE +/******************************************************************** + NOTE: if you are looking at this file becase you would like flares + to be considered as fires (and thereby trigger gas traps), be aware + that the env_flare class is actually found in weapon_flaregun.cpp + and is really a repurposed piece of ammunition. (env_flare isn't the + rod-like safety flare prop, but rather the bit of flame on the end.) + + You will have some difficulty making it work here, because CFlare + does not inherit from CFire and will thus not be enumerated by + CFireSphere::EnumElement(). In order to have flares be detected and + used by this system, you will need to promote certain member functions + of CFire into an interface class from which both CFire and CFlare + inherit. You will also need to modify CFireSphere::EnumElement so that + it properly disambiguates between fires and flares. + + For some partial work towards this end, see changelist 192474. + + ********************************************************************/ +#else +// ================================================================ // +// env_firesensors now have the ability to sense flares, toggled via spawnflag. +// This is different from integrating it with the greater fire system and easily preserves original behavior. +// You could try treating flares as real fires yourself if you think it's an issue. +// ================================================================ // +#endif + + + +#define FIRE_HEIGHT 256.0f +#define FIRE_SCALE_FROM_SIZE(firesize) (firesize * (1/FIRE_HEIGHT)) + +#define FIRE_MAX_GROUND_OFFSET 24.0f //(2 feet) + +#define DEFAULT_ATTACK_TIME 4.0f +#define DEFAULT_DECAY_TIME 8.0f + +// UNDONE: This shouldn't be constant but depend on specific fire +#define FIRE_WIDTH 128 +#define FIRE_MINS Vector(-20,-20,0 ) // Sould be FIRE_WIDTH in size +#define FIRE_MAXS Vector( 20, 20,20) // Sould be FIRE_WIDTH in size +#define FIRE_SPREAD_DAMAGE_MULTIPLIER 2.0 + +#define FIRE_MAX_HEAT_LEVEL 64.0f +#define FIRE_NORMAL_ATTACK_TIME 20.0f +#define FIRE_THINK_INTERVAL 0.1 + +ConVar fire_maxabsorb( "fire_maxabsorb", "50" ); +ConVar fire_absorbrate( "fire_absorbrate", "3" ); +ConVar fire_extscale("fire_extscale", "12"); +ConVar fire_extabsorb("fire_extabsorb", "5"); +ConVar fire_heatscale( "fire_heatscale", "1.0" ); +ConVar fire_incomingheatscale( "fire_incomingheatscale", "0.1" ); +ConVar fire_dmgscale( "fire_dmgscale", "0.1" ); +ConVar fire_dmgbase( "fire_dmgbase", "1" ); +ConVar fire_growthrate( "fire_growthrate", "1.0" ); +ConVar fire_dmginterval( "fire_dmginterval", "1.0" ); + +#define VPROF_FIRE(s) VPROF( s ) + +class CFire : public CBaseEntity +{ +public: + DECLARE_CLASS( CFire, CBaseEntity ); + + int DrawDebugTextOverlays(void); + + CFire( void ); + + virtual void UpdateOnRemove( void ); + + void Precache( void ); + void Init( const Vector &position, float scale, float attackTime, float fuel, int flags, int fireType ); + bool GoOut(); + + void BurnThink(); + void GoOutThink(); + void GoOutInSeconds( float seconds ); + + void SetOwner( CBaseEntity *hOwner ) { m_hOwner = hOwner; } + + void Scale( float end, float time ); + void AddHeat( float heat, bool selfHeat = false ); + int OnTakeDamage( const CTakeDamageInfo &info ); + + bool IsBurning( void ) const; + + bool GetFireDimensions( Vector *pFireMins, Vector *pFireMaxs ); + + void Extinguish( float heat ); + void DestroyEffect(); + + virtual void Update( float simTime ); + + void Spawn( void ); + void Activate( void ); + void StartFire( void ); + void Start(); + void SetToOutSize() + { + UTIL_SetSize( this, Vector(-8,-8,0), Vector(8,8,8) ); + } + + float GetHeatLevel() { return m_flHeatLevel; } + + virtual int UpdateTransmitState(); + + void DrawDebugGeometryOverlays(void) + { + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + if ( m_lastDamage > gpGlobals->curtime && m_flHeatAbsorb > 0 ) + { + NDebugOverlay::EntityBounds(this, 88, 255, 128, 0 ,0); + char tempstr[512]; + Q_snprintf( tempstr, sizeof(tempstr), "Heat: %.1f", m_flHeatAbsorb ); + EntityText(1,tempstr, 0); + } + else if ( !IsBurning() ) + { + NDebugOverlay::EntityBounds(this, 88, 88, 128, 0 ,0); + } + + if ( IsBurning() ) + { + Vector mins, maxs; + if ( GetFireDimensions( &mins, &maxs ) ) + { + NDebugOverlay::Box(GetAbsOrigin(), mins, maxs, 128, 0, 0, 10, 0); + } + } + + + } + BaseClass::DrawDebugGeometryOverlays(); + } + + void Disable(); + + //Inputs + void InputStartFire( inputdata_t &inputdata ); + void InputExtinguish( inputdata_t &inputdata ); + void InputExtinguishTemporary( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +protected: + + void Spread( void ); + void SpawnEffect( fireType_e type, float scale ); + + CHandle m_hEffect; + EHANDLE m_hOwner; + + int m_nFireType; + + float m_flFuel; + float m_flDamageTime; + float m_lastDamage; + float m_flFireSize; // size of the fire in world units + + float m_flHeatLevel; // Used as a "health" for the fire. > 0 means the fire is burning + float m_flHeatAbsorb; // This much heat must be "absorbed" before it gets transferred to the flame size + float m_flDamageScale; + + float m_flMaxHeat; + float m_flLastHeatLevel; + + //NOTENOTE: Lifetime is an expression of the sum total of these amounts plus the global time when started + float m_flAttackTime; //Amount of time to scale up + + bool m_bEnabled; + bool m_bStartDisabled; + bool m_bDidActivate; + + + COutputEvent m_OnIgnited; + COutputEvent m_OnExtinguished; + + DECLARE_DATADESC(); +}; + +class CFireSphere : public IPartitionEnumerator +{ +public: + CFireSphere( CFire **pList, int listMax, bool onlyActiveFires, const Vector &origin, float radius ); + // This gets called by the enumeration methods with each element + // that passes the test. + virtual IterationRetval_t EnumElement( IHandleEntity *pHandleEntity ); + + int GetCount() { return m_count; } + bool AddToList( CFire *pEntity ); + +private: + Vector m_origin; + float m_radiusSqr; + CFire **m_pList; + int m_listMax; + int m_count; + bool m_onlyActiveFires; +}; + +CFireSphere::CFireSphere( CFire **pList, int listMax, bool onlyActiveFires, const Vector &origin, float radius ) +{ + m_pList = pList; + m_listMax = listMax; + m_count = 0; + m_onlyActiveFires = onlyActiveFires; + m_origin = origin; + m_radiusSqr = radius * radius; +} + +bool CFireSphere::AddToList( CFire *pFire ) +{ + if ( m_count >= m_listMax ) + return false; + m_pList[m_count] = pFire; + m_count++; + return true; +} + +IterationRetval_t CFireSphere::EnumElement( IHandleEntity *pHandleEntity ) +{ + CBaseEntity *pEntity = gEntList.GetBaseEntity( pHandleEntity->GetRefEHandle() ); + if ( pEntity ) + { + // UNDONE: Measure which of these is faster +// CFire *pFire = dynamic_cast(pEntity); +#ifdef MAPBASE + if ( !EntIsClass( pEntity, gm_isz_class_EnvFire ) ) +#else + if ( !FClassnameIs( pEntity, "env_fire" ) ) +#endif + return ITERATION_CONTINUE; + + CFire *pFire = static_cast(pEntity); + if ( pFire ) + { + if ( !m_onlyActiveFires || pFire->IsBurning() ) + { + if ( (m_origin - pFire->GetAbsOrigin()).LengthSqr() < m_radiusSqr ) + { + if ( !AddToList( pFire ) ) + return ITERATION_STOP; + } + } + } + } + + return ITERATION_CONTINUE; +} + + +int FireSystem_GetFiresInSphere( CFire **pList, int listMax, bool onlyActiveFires, const Vector &origin, float radius ) +{ + CFireSphere sphereEnum( pList, listMax, onlyActiveFires, origin, radius ); + partition->EnumerateElementsInSphere( PARTITION_ENGINE_NON_STATIC_EDICTS, origin, radius, false, &sphereEnum ); + + return sphereEnum.GetCount(); +} + + +bool FireSystem_IsValidFirePosition( const Vector &position, float testRadius ) +{ + CFire *pList[1]; + int count = FireSystem_GetFiresInSphere( pList, ARRAYSIZE(pList), true, position, testRadius ); + if ( count > 0 ) + return false; + return true; +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +bool FireSystem_IsFireInWall( Vector &position, fireType_e type ) +{ + // Don't check natural fire against walls + if (type == FIRE_NATURAL) + return false; + + trace_t tr; + UTIL_TraceHull( position, position+Vector(0,0,0.1), FIRE_MINS,FIRE_MAXS,MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr ); + if (tr.fraction != 1.0 || tr.startsolid) + { + //NDebugOverlay::Box(position,FIRE_MINS,FIRE_MAXS,255,0,0,50,10); + return true; + } + //NDebugOverlay::Box(position,FIRE_MINS,FIRE_MAXS,0,255,0,50,10); + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Determines whether or not a new fire may be placed at a given location +// Input : &position - where we are trying to put the new fire +// separationRadius - the maximum distance fires must be apart from one another +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool FireSystem_CanAddFire( Vector *position, float separationRadius, fireType_e type, int flags ) +{ + //See if we found a fire inside the sphere + if ( !FireSystem_IsValidFirePosition( *position, separationRadius ) ) + return false; + + // Unless our fire is floating, make sure were not too high + if (!(flags & SF_FIRE_DONT_DROP)) + { + trace_t tr; + Vector startpos = *position; + Vector endpos = *position; + + startpos[2] += 1; + endpos[2] -= FIRE_MAX_GROUND_OFFSET; + + UTIL_TraceLine( startpos, endpos, MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr ); + + //See if we're floating too high + if ( ( tr.allsolid ) || ( tr.startsolid) || ( tr.fraction == 1.0f ) ) + { + return false; + } + + //TODO: If we've hit an entity here, start it on fire + CBaseEntity *pEntity = tr.m_pEnt; + + if ( ENTINDEX( pEntity->edict() ) != 0 ) + { + return false; + } + } + + + + // Check if fire is in a wall, if so try shifting around a bit + if (FireSystem_IsFireInWall( *position, type )) + { + Vector vTestPos = *position; + vTestPos.x += 10; + if (FireSystem_IsValidFirePosition( vTestPos, separationRadius ) && !FireSystem_IsFireInWall( vTestPos, type )) + { + *position = vTestPos; + return true; + } + vTestPos.y += 10; + if (FireSystem_IsValidFirePosition( vTestPos, separationRadius ) && !FireSystem_IsFireInWall( vTestPos, type )) + { + *position = vTestPos; + return true; + } + vTestPos.y -= 20; + if (FireSystem_IsValidFirePosition( vTestPos, separationRadius ) && !FireSystem_IsFireInWall( vTestPos, type )) + { + *position = vTestPos; + return true; + } + vTestPos.x -= 20; + if (FireSystem_IsValidFirePosition( vTestPos, separationRadius ) && !FireSystem_IsFireInWall( vTestPos, type )) + { + *position = vTestPos; + return true; + } + return false; + } + + //Able to add here + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Starts a fire at a specified location +// Input : &position - position to start the fire at +// flags - any special modifiers +//----------------------------------------------------------------------------- +bool FireSystem_StartFire( const Vector &position, float fireHeight, float attack, float fuel, int flags, CBaseEntity *owner, fireType_e type ) +{ + VPROF_FIRE( "FireSystem_StartFire1" ); + + Vector testPos = position; + //Must be okay to add fire here + if ( FireSystem_CanAddFire( &testPos, 16.0f, type, flags ) == false ) + { + CFire *pFires[16]; + int fireCount = FireSystem_GetFiresInSphere( pFires, ARRAYSIZE(pFires), true, position, 16.0f ); + for ( int i = 0; i < fireCount; i++ ) + { + // add to this fire + pFires[i]->AddHeat( fireHeight, false ); + } + + return false; + } + + //Create a new fire entity + CFire *fire = (CFire *) CreateEntityByName( "env_fire" ); + + if ( fire == NULL ) + return false; + + //Spawn the fire + // Fires not placed by a designer should be cleaned up automatically (not catch fire again) + fire->AddSpawnFlags( SF_FIRE_DIE_PERMANENT ); + fire->Spawn(); + fire->Init( testPos, fireHeight, attack, fuel, flags, type ); + fire->Start(); + fire->SetOwner( owner ); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts a fire on a specified model. +// Input : pEntity - The model entity to catch on fire. +// fireHeight - +// attack - +// fuel - +// flags - +// owner - +// type - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool FireSystem_StartFire( CBaseAnimating *pEntity, float fireHeight, float attack, float fuel, int flags, CBaseEntity *owner, fireType_e type ) +{ + VPROF_FIRE( "FireSystem_StartFire2" ); + + Vector position = pEntity->GetAbsOrigin(); + Vector testPos = position; + + // Make sure its a valid position for fire (not in a wall, etc) + if ( FireSystem_CanAddFire( &testPos, 16.0f, type, flags ) == false ) + { + // Contribute heat to all fires within 16 units of this fire. + CFire *pFires[16]; + int fireCount = FireSystem_GetFiresInSphere( pFires, ARRAYSIZE(pFires), true, position, 16.0f ); + for ( int i = 0; i < fireCount; i++ ) + { + pFires[i]->AddHeat( fireHeight, false ); + } + + return false; + } + + // Create a new fire entity + CFire *fire = (CFire *) CreateEntityByName( "env_fire" ); + if ( fire == NULL ) + { + return false; + } + + // Spawn the fire. + // Fires not placed by a designer should be cleaned up automatically (not catch fire again). + fire->AddSpawnFlags( SF_FIRE_DIE_PERMANENT ); + fire->Spawn(); + fire->Init( testPos, fireHeight, attack, fuel, flags, type ); + fire->Start(); + fire->SetOwner( owner ); + + return true; +} + + +void FireSystem_ExtinguishInRadius( const Vector &origin, float radius, float rate ) +{ + // UNDONE: pass this instead of percent + float heat = (1-rate) * fire_extscale.GetFloat(); + + CFire *pFires[32]; + int fireCount = FireSystem_GetFiresInSphere( pFires, ARRAYSIZE(pFires), false, origin, radius ); + for ( int i = 0; i < fireCount; i++ ) + { + pFires[i]->Extinguish( heat ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &origin - +// radius - +// heat - +//----------------------------------------------------------------------------- +void FireSystem_AddHeatInRadius( const Vector &origin, float radius, float heat ) +{ + VPROF_FIRE( "FireSystem_AddHeatInRadius" ); + + CFire *pFires[32]; + + int fireCount = FireSystem_GetFiresInSphere( pFires, ARRAYSIZE(pFires), false, origin, radius ); + for ( int i = 0; i < fireCount; i++ ) + { + pFires[i]->AddHeat( heat ); + } +} + +//----------------------------------------------------------------------------- + +bool FireSystem_GetFireDamageDimensions( CBaseEntity *pEntity, Vector *pFireMins, Vector *pFireMaxs ) +{ + CFire *pFire = dynamic_cast(pEntity); + + if ( pFire && pFire->GetFireDimensions( pFireMins, pFireMaxs ) ) + { + *pFireMins /= FIRE_SPREAD_DAMAGE_MULTIPLIER; + *pFireMaxs /= FIRE_SPREAD_DAMAGE_MULTIPLIER; + return true; + } + pFireMins->Init(); + pFireMaxs->Init(); + return false; +} + + +//================================================== +// CFire +//================================================== +BEGIN_DATADESC( CFire ) + + DEFINE_FIELD( m_hEffect, FIELD_EHANDLE ), + DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_nFireType, FIELD_INTEGER, "firetype" ), + + DEFINE_FIELD( m_flFuel, FIELD_FLOAT ), + DEFINE_FIELD( m_flDamageTime, FIELD_TIME ), + DEFINE_FIELD( m_lastDamage, FIELD_TIME ), + DEFINE_KEYFIELD( m_flFireSize, FIELD_FLOAT, "firesize" ), + + DEFINE_KEYFIELD( m_flHeatLevel, FIELD_FLOAT, "ignitionpoint" ), + DEFINE_FIELD( m_flHeatAbsorb, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flDamageScale,FIELD_FLOAT, "damagescale" ), + + DEFINE_FIELD( m_flMaxHeat, FIELD_FLOAT ), + //DEFINE_FIELD( m_flLastHeatLevel, FIELD_FLOAT ), + + DEFINE_KEYFIELD( m_flAttackTime, FIELD_FLOAT, "fireattack" ), + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_bStartDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_FIELD( m_bDidActivate, FIELD_BOOLEAN ), + + DEFINE_FUNCTION( BurnThink ), + DEFINE_FUNCTION( GoOutThink ), + + + + DEFINE_INPUTFUNC( FIELD_VOID, "StartFire", InputStartFire ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "Extinguish", InputExtinguish ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "ExtinguishTemporary", InputExtinguishTemporary ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_OUTPUT( m_OnIgnited, "OnIgnited" ), + DEFINE_OUTPUT( m_OnExtinguished, "OnExtinguished" ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_fire, CFire ); + +//================================================== +// CFire +//================================================== + +CFire::CFire( void ) +{ + m_flFuel = 0.0f; + m_flAttackTime = 0.0f; + m_flDamageTime = 0.0f; + m_lastDamage = 0; + m_nFireType = FIRE_NATURAL; + + //Spreading + m_flHeatAbsorb = 8.0f; + m_flHeatLevel = 0; + + // Must be in the constructor! + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); +} + +//----------------------------------------------------------------------------- +// UpdateOnRemove +//----------------------------------------------------------------------------- +void CFire::UpdateOnRemove( void ) +{ + //Stop any looping sounds that might be playing + StopSound( "Fire.Plasma" ); + + DestroyEffect(); + + // Chain at end to mimic destructor unwind order + BaseClass::UpdateOnRemove(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFire::Precache( void ) +{ + if ( m_nFireType == FIRE_NATURAL ) + { + UTIL_PrecacheOther("_firesmoke"); + + if ( m_spawnflags & SF_FIRE_SMOKELESS ) + { + PrecacheParticleSystem( "env_fire_tiny" ); + PrecacheParticleSystem( "env_fire_small" ); + PrecacheParticleSystem( "env_fire_medium" ); + PrecacheParticleSystem( "env_fire_large" ); + } + else + { + PrecacheParticleSystem( "env_fire_tiny_smoke" ); + PrecacheParticleSystem( "env_fire_small_smoke" ); + PrecacheParticleSystem( "env_fire_medium_smoke" ); + PrecacheParticleSystem( "env_fire_large_smoke" ); + } + } + + if ( m_nFireType == FIRE_PLASMA ) + { + UTIL_PrecacheOther("_plasma"); + } + + PrecacheScriptSound( "Fire.Plasma" ); +} + +//------------------------------------------------------------------------------ +// Purpose : Input handler for starting the fire. +//------------------------------------------------------------------------------ +void CFire::InputStartFire( inputdata_t &inputdata ) +{ + if ( !m_bEnabled ) + return; + + StartFire(); +} + +void CFire::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; +} + +void CFire::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +void CFire::Disable() +{ + m_bEnabled = false; + if ( IsBurning() ) + { + GoOut(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CFire::InputExtinguish( inputdata_t &inputdata ) +{ + m_spawnflags &= ~SF_FIRE_INFINITE; + GoOutInSeconds( inputdata.value.Float() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CFire::InputExtinguishTemporary( inputdata_t &inputdata ) +{ + GoOutInSeconds( inputdata.value.Float() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Starts burning. +//----------------------------------------------------------------------------- +void CFire::StartFire( void ) +{ + if ( m_hEffect != NULL ) + return; + + // Trace down and start a fire there. Nothing fancy yet. + Vector vFirePos; + trace_t tr; + if ( m_spawnflags & SF_FIRE_DONT_DROP ) + { + vFirePos = GetAbsOrigin(); + } + else + { + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector( 0, 0, 1024 ), MASK_FIRE_SOLID, this, COLLISION_GROUP_NONE, &tr ); + vFirePos = tr.endpos; + } + + int spawnflags = m_spawnflags; + m_spawnflags |= SF_FIRE_START_ON; + Init( vFirePos, m_flFireSize, m_flAttackTime, GetHealth(), m_spawnflags, (fireType_e) m_nFireType ); + Start(); + m_spawnflags = spawnflags; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFire::Spawn( void ) +{ + BaseClass::Spawn(); + + Precache(); + + m_takedamage = DAMAGE_NO; + + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); + SetToOutSize(); + + // set up the ignition point + m_flHeatAbsorb = m_flHeatLevel * 0.05; + m_flHeatLevel = 0; + Init( GetAbsOrigin(), m_flFireSize, m_flAttackTime, m_flFuel, m_spawnflags, m_nFireType ); + + if( m_bStartDisabled ) + { + Disable(); + } + else + { + m_bEnabled = true; + } +} + +int CFire::UpdateTransmitState() +{ + // Don't want to be FL_EDICT_DONTSEND because our fire entity may make us transmit. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFire::Activate( void ) +{ + BaseClass::Activate(); + + //See if we should start active + if ( !m_bDidActivate && ( m_spawnflags & SF_FIRE_START_ON ) ) + { + m_flHeatLevel = m_flMaxHeat; + + StartFire(); + } + + m_bDidActivate = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFire::SpawnEffect( fireType_e type, float scale ) +{ + CBaseFire *pEffect = NULL; + switch ( type ) + { + default: + case FIRE_NATURAL: + { + CFireSmoke *fireSmoke = (CFireSmoke *) CreateEntityByName( "_firesmoke" ); + fireSmoke->EnableSmoke( ( m_spawnflags & SF_FIRE_SMOKELESS )==false ); + fireSmoke->EnableGlow( ( m_spawnflags & SF_FIRE_NO_GLOW )==false ); + fireSmoke->EnableVisibleFromAbove( ( m_spawnflags & SF_FIRE_VISIBLE_FROM_ABOVE )!=false ); + + pEffect = fireSmoke; + m_nFireType = FIRE_NATURAL; + m_takedamage = DAMAGE_YES; + } + break; + + case FIRE_PLASMA: + { + CPlasma *plasma = (CPlasma *) CreateEntityByName( "_plasma" ); + plasma->EnableSmoke( true ); + + pEffect = plasma; + m_nFireType = FIRE_PLASMA; + m_takedamage = DAMAGE_YES; + + // Start burn sound + EmitSound( "Fire.Plasma" ); + } + break; + } + + UTIL_SetOrigin( pEffect, GetAbsOrigin() ); + pEffect->Spawn(); + pEffect->SetParent( this ); + pEffect->Scale( m_flFireSize, m_flFireSize, 0 ); + //Start it going + pEffect->Enable( ( m_spawnflags & SF_FIRE_START_ON ) ); + m_hEffect = pEffect; +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn and initialize the fire +// Input : &position - where the fire resides +// lifetime - +//----------------------------------------------------------------------------- +void CFire::Init( const Vector &position, float scale, float attackTime, float fuel, int flags, int fireType ) +{ + m_flAttackTime = attackTime; + + m_spawnflags = flags; + m_nFireType = fireType; + + if ( flags & SF_FIRE_INFINITE ) + { + fuel = 0; + } + m_flFuel = fuel; + if ( m_flFuel ) + { + m_spawnflags |= SF_FIRE_DIE_PERMANENT; + } + + Vector localOrigin = position; + if ( GetMoveParent() ) + { + EntityMatrix parentMatrix; + parentMatrix.InitFromEntity( GetMoveParent() ); + localOrigin = parentMatrix.WorldToLocal( position ); + } + UTIL_SetOrigin( this, localOrigin ); + + SetSolid( SOLID_NONE ); + m_flFireSize = scale; + m_flMaxHeat = FIRE_MAX_HEAT_LEVEL * FIRE_SCALE_FROM_SIZE(scale); + //See if we should start on + if ( m_spawnflags & SF_FIRE_START_FULL ) + { + m_flHeatLevel = m_flMaxHeat; + } + m_flLastHeatLevel = 0; + +} + +void CFire::Start() +{ + float boxWidth = (m_flFireSize * (FIRE_WIDTH/FIRE_HEIGHT))*0.5f; + UTIL_SetSize(this, Vector(-boxWidth,-boxWidth,0),Vector(boxWidth,boxWidth,m_flFireSize)); + + //Spawn the client-side effect + SpawnEffect( (fireType_e)m_nFireType, FIRE_SCALE_FROM_SIZE(m_flFireSize) ); + m_OnIgnited.FireOutput( this, this ); + SetThink( &CFire::BurnThink ); + m_flDamageTime = 0; + // think right now + BurnThink(); +} + +//----------------------------------------------------------------------------- +// Purpose: Determines whether or not the fire is still active +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CFire::IsBurning( void ) const +{ + if ( m_flHeatLevel > 0 ) + return true; + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get the damage box of the fire +//----------------------------------------------------------------------------- +bool CFire::GetFireDimensions( Vector *pFireMins, Vector *pFireMaxs ) +{ + if ( m_flHeatLevel <= 0 ) + { + pFireMins->Init(); + pFireMaxs->Init(); + return false; + } + + float scale = m_flHeatLevel / m_flMaxHeat; + float damageRadius = scale * m_flFireSize * FIRE_WIDTH / FIRE_HEIGHT * 0.5; + + damageRadius *= FIRE_SPREAD_DAMAGE_MULTIPLIER; //FIXME: Trying slightly larger radius for burning + + if ( damageRadius < 16 ) + { + damageRadius = 16; + } + + pFireMins->Init(-damageRadius,-damageRadius,0); + pFireMaxs->Init(damageRadius,damageRadius,m_flFireSize*scale); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Update the fire and its children +//----------------------------------------------------------------------------- +void CFire::Update( float simTime ) +{ + VPROF_FIRE( "CFire::Update" ); + + if ( m_flFuel != 0 ) + { + m_flFuel -= simTime; + if ( m_flFuel <= 0 ) + { + GoOutInSeconds( 1 ); + return; + } + } + + float strength = m_flHeatLevel / FIRE_MAX_HEAT_LEVEL; + if ( m_flHeatLevel != m_flLastHeatLevel ) + { + m_flLastHeatLevel = m_flHeatLevel; + // Make the effect the appropriate size given the heat level + m_hEffect->Scale( strength, 0.5f ); + } + // add heat to myself (grow) + float addedHeat = (m_flAttackTime > 0) ? m_flMaxHeat / m_flAttackTime : m_flMaxHeat; + addedHeat *= simTime * fire_growthrate.GetFloat(); + AddHeat( addedHeat, true ); + + // add heat to nearby fires + float outputHeat = strength * m_flHeatLevel; + + Vector fireMins; + Vector fireMaxs; + Vector fireEntityDamageMins; + Vector fireEntityDamageMaxs; + + GetFireDimensions( &fireMins, &fireMaxs ); + + if ( FIRE_SPREAD_DAMAGE_MULTIPLIER != 1.0 ) // if set to 1.0, optimizer will remove this code + { + fireEntityDamageMins = fireMins / FIRE_SPREAD_DAMAGE_MULTIPLIER; + fireEntityDamageMaxs = fireMaxs / FIRE_SPREAD_DAMAGE_MULTIPLIER; + } + + //NDebugOverlay::Box( GetAbsOrigin(), fireMins, fireMaxs, 255, 255, 255, 0, fire_dmginterval.GetFloat() ); + fireMins += GetAbsOrigin(); + fireMaxs += GetAbsOrigin(); + + if ( FIRE_SPREAD_DAMAGE_MULTIPLIER != 1.0 ) + { + fireEntityDamageMins += GetAbsOrigin(); + fireEntityDamageMaxs += GetAbsOrigin(); + } + + CBaseEntity *pNearby[256]; + CFire *pFires[16]; + int nearbyCount = UTIL_EntitiesInBox( pNearby, ARRAYSIZE(pNearby), fireMins, fireMaxs, 0 ); + int fireCount = 0; + int i; + + // is it time to do damage? + bool damage = false; + int outputDamage = 0; + if ( m_flDamageTime <= gpGlobals->curtime ) + { + m_flDamageTime = gpGlobals->curtime + fire_dmginterval.GetFloat(); + outputDamage = (fire_dmgbase.GetFloat() + outputHeat * fire_dmgscale.GetFloat() * m_flDamageScale) * fire_dmginterval.GetFloat(); + if ( outputDamage ) + { + damage = true; + } + } + int damageFlags = (m_nFireType == FIRE_NATURAL) ? DMG_BURN : DMG_PLASMA; + for ( i = 0; i < nearbyCount; i++ ) + { + CBaseEntity *pOther = pNearby[i]; + + if ( pOther == this ) + { + continue; + } +#ifdef MAPBASE + else if ( EntIsClass( pOther, gm_isz_class_EnvFire ) ) +#else + else if ( FClassnameIs( pOther, "env_fire" ) ) +#endif + { + if ( fireCount < ARRAYSIZE(pFires) ) + { + pFires[fireCount] = (CFire *)pOther; + fireCount++; + } + continue; + } + else if ( pOther->m_takedamage == DAMAGE_NO ) + { + pNearby[i] = NULL; + } + else if ( damage ) + { + bool bDoDamage; + + if ( FIRE_SPREAD_DAMAGE_MULTIPLIER != 1.0 && !pOther->IsPlayer() ) // if set to 1.0, optimizer will remove this code + { + Vector otherMins, otherMaxs; + pOther->CollisionProp()->WorldSpaceAABB( &otherMins, &otherMaxs ); + bDoDamage = IsBoxIntersectingBox( otherMins, otherMaxs, + fireEntityDamageMins, fireEntityDamageMaxs ); + + } + else + bDoDamage = true; + + if ( bDoDamage ) + { + // Make sure can actually see entity (don't damage through walls) + trace_t tr; + UTIL_TraceLine( this->WorldSpaceCenter(), pOther->WorldSpaceCenter(), MASK_FIRE_SOLID, pOther, COLLISION_GROUP_NONE, &tr ); + + if (tr.fraction == 1.0 && !tr.startsolid) + { + pOther->TakeDamage( CTakeDamageInfo( this, this, outputDamage, damageFlags ) ); + } + } + } + } + + outputHeat *= fire_heatscale.GetFloat() * simTime; + + if ( fireCount > 0 ) + { + outputHeat /= fireCount; + for ( i = 0; i < fireCount; i++ ) + { + pFires[i]->AddHeat( outputHeat, false ); + } + } +} + +// Destroy any effect I have +void CFire::DestroyEffect() +{ + CBaseFire *pEffect = m_hEffect; + if ( pEffect != NULL ) + { + //disable the graphics and remove the entity + pEffect->Enable( false ); + UTIL_Remove( pEffect ); + } +} +//----------------------------------------------------------------------------- +// Purpose: Think +//----------------------------------------------------------------------------- +void CFire::BurnThink( void ) +{ + SetNextThink( gpGlobals->curtime + FIRE_THINK_INTERVAL ); + + Update( FIRE_THINK_INTERVAL ); +} + +void CFire::GoOutThink() +{ + GoOut(); +} + +void CFire::GoOutInSeconds( float seconds ) +{ + Scale( 0.0f, seconds ); + + SetThink( &CFire::GoOutThink ); + SetNextThink( gpGlobals->curtime + seconds ); +} + +//------------------------------------------------------------------------------ +// Purpose : Blasts of significant size blow out fires that take damage +// Input : +// Output : +//------------------------------------------------------------------------------ +int CFire::OnTakeDamage( const CTakeDamageInfo &info ) +{ + return 0; +} + +void CFire::AddHeat( float heat, bool selfHeat ) +{ + if ( m_bEnabled ) + { + if ( !selfHeat ) + { + if ( IsBurning() ) + { + // scale back the incoming heat from surrounding fires + // if I've already ignited + heat *= fire_incomingheatscale.GetFloat(); + } + } + m_lastDamage = gpGlobals->curtime + 0.5; + bool start = m_flHeatLevel <= 0 ? true : false; + if ( m_flHeatAbsorb > 0 ) + { + float absorbDamage = heat * fire_absorbrate.GetFloat(); + if ( absorbDamage > m_flHeatAbsorb ) + { + heat -= m_flHeatAbsorb / fire_absorbrate.GetFloat(); + m_flHeatAbsorb = 0; + } + else + { + m_flHeatAbsorb -= absorbDamage; + heat = 0; + } + } + + m_flHeatLevel += heat; + if ( start && m_flHeatLevel > 0 && m_hEffect == NULL ) + { + StartFire(); + } + if ( m_flHeatLevel > m_flMaxHeat ) + m_flHeatLevel = m_flMaxHeat; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : end - +// time - +//----------------------------------------------------------------------------- +void CFire::Scale( float end, float time ) +{ + CBaseFire *pEffect = m_hEffect; + if ( pEffect ) + { + pEffect->Scale( end, time ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : time - +//----------------------------------------------------------------------------- +void CFire::Extinguish( float heat ) +{ + if ( !m_bEnabled ) + return; + + m_lastDamage = gpGlobals->curtime + 0.5; + bool out = m_flHeatLevel > 0 ? true : false; + + m_flHeatLevel -= heat; + m_flHeatAbsorb += fire_extabsorb.GetFloat() * heat; + if ( m_flHeatAbsorb > fire_maxabsorb.GetFloat() ) + { + m_flHeatAbsorb = fire_maxabsorb.GetFloat(); + } + + // drift toward the average attack time after being sprayed + // some fires are heavily scripted so their attack looks weird + // once interacted with. Basically, this blends out the scripting + // as the fire is sprayed with the extinguisher. + float averageAttackTime = m_flMaxHeat * (FIRE_NORMAL_ATTACK_TIME/FIRE_MAX_HEAT_LEVEL); + m_flAttackTime = Approach( averageAttackTime, m_flAttackTime, 2 * gpGlobals->frametime ); + + if ( m_flHeatLevel <= 0 ) + { + m_flHeatLevel = 0; + if ( out ) + { + GoOut(); + } + } +} + +bool CFire::GoOut() +{ + //Signal death + m_OnExtinguished.FireOutput( this, this ); + + DestroyEffect(); + m_flHeatLevel -= 20; + if ( m_flHeatLevel > 0 ) + m_flHeatLevel = 0; + + m_flLastHeatLevel = m_flHeatLevel; + SetThink(NULL); + SetNextThink( TICK_NEVER_THINK ); + if ( m_spawnflags & SF_FIRE_DIE_PERMANENT ) + { + UTIL_Remove( this ); + return true; + } + SetToOutSize(); + + return false; +} + +//================================================== +// CEnvFireSource is a source of heat that the player +// cannot put out +//================================================== + +#define FIRESOURCE_THINK_TIME 0.25 // seconds to + +#define SF_FIRESOURCE_START_ON 0x0001 + +class CEnvFireSource : public CBaseEntity +{ + DECLARE_CLASS( CEnvFireSource, CBaseEntity ); +public: + void Spawn(); + void Think(); + void TurnOn(); + void TurnOff(); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + bool m_bEnabled; + float m_radius; + float m_damage; +}; + +BEGIN_DATADESC( CEnvFireSource ) + + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_radius, FIELD_FLOAT, "fireradius" ), + DEFINE_KEYFIELD( m_damage,FIELD_FLOAT, "firedamage" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_firesource, CEnvFireSource ); + +void CEnvFireSource::Spawn() +{ + if ( m_spawnflags & SF_FIRESOURCE_START_ON ) + { + TurnOn(); + } + else + { + TurnOff(); + } +} + +void CEnvFireSource::Think() +{ + if ( !m_bEnabled ) + return; + SetNextThink( gpGlobals->curtime + FIRESOURCE_THINK_TIME ); + + CFire *pFires[128]; + int fireCount = FireSystem_GetFiresInSphere( pFires, ARRAYSIZE(pFires), false, GetAbsOrigin(), m_radius ); + + for ( int i = 0; i < fireCount; i++ ) + { + pFires[i]->AddHeat( m_damage * FIRESOURCE_THINK_TIME ); + } +} + +void CEnvFireSource::TurnOn() +{ + if ( m_bEnabled ) + return; + + m_bEnabled = true; + SetNextThink( gpGlobals->curtime ); +} + +void CEnvFireSource::TurnOff() +{ + if ( !m_bEnabled ) + return; + + m_bEnabled = false; + SetNextThink( TICK_NEVER_THINK ); +} +void CEnvFireSource::InputEnable( inputdata_t &inputdata ) +{ + TurnOn(); +} +void CEnvFireSource::InputDisable( inputdata_t &inputdata ) +{ + TurnOff(); +} + +//================================================== +// CEnvFireSensor detects changes in heat +//================================================== +#define SF_FIRESENSOR_START_ON 1 +#ifdef MAPBASE +#define SF_FIRESENSOR_ACCEPT_FLARES 2 +#endif + +class CEnvFireSensor : public CBaseEntity +{ + DECLARE_CLASS( CEnvFireSensor, CBaseEntity ); +public: + void Spawn(); + void Think(); + void TurnOn(); + void TurnOff(); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); +#ifdef MAPBASE + int DrawDebugTextOverlays(void); +#endif + + DECLARE_DATADESC(); + +private: + bool m_bEnabled; + bool m_bHeatAtLevel; + float m_radius; + float m_targetLevel; + float m_targetTime; + float m_levelTime; +#ifdef MAPBASE + // Only stored for access in debug overlays, don't save + float m_curheat; +#endif + + COutputEvent m_OnHeatLevelStart; + COutputEvent m_OnHeatLevelEnd; +}; + +BEGIN_DATADESC( CEnvFireSensor ) + + DEFINE_KEYFIELD( m_radius, FIELD_FLOAT, "fireradius" ), + DEFINE_KEYFIELD( m_targetLevel, FIELD_FLOAT, "heatlevel" ), + DEFINE_KEYFIELD( m_targetTime, FIELD_FLOAT, "heattime" ), + + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bHeatAtLevel, FIELD_BOOLEAN ), + DEFINE_FIELD( m_levelTime, FIELD_FLOAT ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_OUTPUT( m_OnHeatLevelStart, "OnHeatLevelStart"), + DEFINE_OUTPUT( m_OnHeatLevelEnd, "OnHeatLevelEnd"), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( env_firesensor, CEnvFireSensor ); + +void CEnvFireSensor::Spawn() +{ + if ( m_spawnflags & SF_FIRESENSOR_START_ON ) + { + TurnOn(); + } + else + { + TurnOff(); + } +} + +void CEnvFireSensor::Think() +{ + if ( !m_bEnabled ) + return; + + float time = m_targetTime * 0.25; + if ( time < 0.1 ) + { + time = 0.1; + } + SetNextThink( gpGlobals->curtime + time ); + + float heat = 0; + CFire *pFires[128]; + int fireCount = FireSystem_GetFiresInSphere( pFires, ARRAYSIZE(pFires), true, GetAbsOrigin(), m_radius ); + for ( int i = 0; i < fireCount; i++ ) + { + heat += pFires[i]->GetHeatLevel(); + } + +#ifdef MAPBASE + if (HasSpawnFlags(SF_FIRESENSOR_ACCEPT_FLARES)) + { + // Also look for nearby flares + CBaseEntity *pEntity = gEntList.FindEntityByClassnameWithin( NULL, "env_flare", GetAbsOrigin(), m_radius ); + while (pEntity) + { + CFlare *pFlare = static_cast(pEntity); + if (pFlare) + { + heat += (pFlare->m_flTimeBurnOut > -1.0 ? (pFlare->m_flTimeBurnOut - gpGlobals->curtime) : 32); + } + + pEntity = gEntList.FindEntityByClassnameWithin( pEntity, "env_flare", GetAbsOrigin(), m_radius ); + } + } +#endif + +#ifdef MAPBASE + m_curheat = heat; +#endif + + if ( heat >= m_targetLevel ) + { + m_levelTime += time; + if ( m_levelTime >= m_targetTime ) + { + if ( !m_bHeatAtLevel ) + { + m_bHeatAtLevel = true; + m_OnHeatLevelStart.FireOutput( this, this ); + } + } + } + else + { + m_levelTime = 0; + if ( m_bHeatAtLevel ) + { + m_bHeatAtLevel = false; + m_OnHeatLevelEnd.FireOutput( this, this ); + } + } +} + +void CEnvFireSensor::TurnOn() +{ + if ( m_bEnabled ) + return; + + m_bEnabled = true; + SetNextThink( gpGlobals->curtime ); + m_bHeatAtLevel = false; + m_levelTime = 0; +} + +void CEnvFireSensor::TurnOff() +{ + if ( !m_bEnabled ) + return; + + m_bEnabled = false; + SetNextThink( TICK_NEVER_THINK ); + if ( m_bHeatAtLevel ) + { + m_bHeatAtLevel = false; + m_OnHeatLevelEnd.FireOutput( this, this ); + } + +} +void CEnvFireSensor::InputEnable( inputdata_t &inputdata ) +{ + TurnOn(); +} +void CEnvFireSensor::InputDisable( inputdata_t &inputdata ) +{ + TurnOff(); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CEnvFireSensor::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print flame size + Q_snprintf(tempstr, sizeof(tempstr), " Current Heat: %f", m_curheat); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CFire::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print flame size + Q_snprintf(tempstr,sizeof(tempstr)," size: %f", m_flFireSize); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} diff --git a/sp/src/game/server/fire.h b/sp/src/game/server/fire.h new file mode 100644 index 00000000..c04641bc --- /dev/null +++ b/sp/src/game/server/fire.h @@ -0,0 +1,52 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef FIRE_H +#define FIRE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "entityoutput.h" +#include "fire_smoke.h" +#include "plasma.h" + +//Spawnflags +#define SF_FIRE_INFINITE 0x00000001 +#define SF_FIRE_SMOKELESS 0x00000002 +#define SF_FIRE_START_ON 0x00000004 +#define SF_FIRE_START_FULL 0x00000008 +#define SF_FIRE_DONT_DROP 0x00000010 +#define SF_FIRE_NO_GLOW 0x00000020 +#define SF_FIRE_DIE_PERMANENT 0x00000080 +#define SF_FIRE_VISIBLE_FROM_ABOVE 0x00000100 + +//================================================== +// CFire +//================================================== + +enum fireType_e +{ + FIRE_NATURAL = 0, + FIRE_PLASMA, +}; + +//================================================== + +// NPCs and grates do not prevent fire from travelling +#define MASK_FIRE_SOLID ( MASK_SOLID & (~(CONTENTS_MONSTER|CONTENTS_GRATE)) ) + +//================================================== +// FireSystem +//================================================== +bool FireSystem_StartFire( const Vector &position, float fireHeight, float attack, float fuel, int flags, CBaseEntity *owner, fireType_e type = FIRE_NATURAL); +void FireSystem_ExtinguishInRadius( const Vector &origin, float radius, float rate ); +void FireSystem_AddHeatInRadius( const Vector &origin, float radius, float heat ); + +bool FireSystem_GetFireDamageDimensions( CBaseEntity *pFire, Vector *pFireMins, Vector *pFireMaxs ); + +#endif // FIRE_H diff --git a/sp/src/game/server/fire_smoke.cpp b/sp/src/game/server/fire_smoke.cpp new file mode 100644 index 00000000..edb4c04b --- /dev/null +++ b/sp/src/game/server/fire_smoke.cpp @@ -0,0 +1,191 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "fire_smoke.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CBaseFire ) + + DEFINE_FIELD( m_flStartScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flScaleTime, FIELD_TIME ), + DEFINE_FIELD( m_nFlags, FIELD_INTEGER ), + +END_DATADESC() + + +//================================================== +// CBaseFire +//================================================== + +CBaseFire::CBaseFire( void ) +{ + m_flStartScale = 0.0f; + m_flScale = 0.0f; + m_flScaleTime = 0.0f; + m_nFlags = bitsFIRE_NONE; +} + +CBaseFire::~CBaseFire( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Take the current scale of the flame and move it towards a destination +// Input : size - destination size +// time - time to scale across +//----------------------------------------------------------------------------- +void CBaseFire::Scale( float size, float time ) +{ + //Send to the client + m_flScale = size; + m_flScaleTime = time; +} + +//----------------------------------------------------------------------------- +// Purpose: Overloaded Scale() function to set size +// Input : start - beginning sizek +// size - destination size +// time - time to scale across +//----------------------------------------------------------------------------- +void CBaseFire::Scale( float start, float size, float time ) +{ + //Send to the client + m_flStartScale = start; + m_flScale = size; + m_flScaleTime = time; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +void CBaseFire::Enable( int state ) +{ + if ( state ) + { + m_nFlags |= bitsFIRE_ACTIVE; + } + else + { + m_nFlags &= ~bitsFIRE_ACTIVE; + } +} + +//================================================== +// CFireSmoke +//================================================== + +//Link the entity +LINK_ENTITY_TO_CLASS( _firesmoke, CFireSmoke ); + +//Send datatable +IMPLEMENT_SERVERCLASS_ST( CFireSmoke, DT_FireSmoke ) + SendPropFloat( SENDINFO( m_flStartScale ), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO( m_flScale ), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO( m_flScaleTime ), 0, SPROP_NOSCALE), + SendPropInt( SENDINFO( m_nFlags ), 8, SPROP_UNSIGNED ), + SendPropModelIndex( SENDINFO( m_nFlameModelIndex ) ), + SendPropModelIndex( SENDINFO( m_nFlameFromAboveModelIndex ) ), +END_SEND_TABLE() + +//Data description +BEGIN_DATADESC( CFireSmoke ) + + DEFINE_FIELD( m_flStartScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flScaleTime, FIELD_FLOAT ), + DEFINE_FIELD( m_nFlags, FIELD_INTEGER ), + DEFINE_FIELD( m_nFlameModelIndex, FIELD_MODELINDEX ), + DEFINE_FIELD( m_nFlameFromAboveModelIndex, FIELD_MODELINDEX ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CFireSmoke::CFireSmoke( void ) +{ + //Client-side + m_flScale = 0.0f; + m_flScaleTime = 0.0f; + m_nFlags = bitsFIRE_NONE; + + //Server-side + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CFireSmoke::~CFireSmoke( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFireSmoke::Precache() +{ + BaseClass::Precache(); +} + +void CFireSmoke::Spawn() +{ + Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +void CFireSmoke::EnableSmoke( int state ) +{ + if ( state ) + { + m_nFlags |= bitsFIRESMOKE_SMOKE; + } + else + { + m_nFlags &= ~bitsFIRESMOKE_SMOKE; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +void CFireSmoke::EnableGlow( int state ) +{ + if ( state ) + { + m_nFlags |= bitsFIRESMOKE_GLOW; + } + else + { + m_nFlags &= ~bitsFIRESMOKE_GLOW; + } +} + +void CFireSmoke::EnableVisibleFromAbove( int state ) +{ + if ( state ) + { + m_nFlags |= bitsFIRESMOKE_VISIBLE_FROM_ABOVE; + } + else + { + m_nFlags &= ~bitsFIRESMOKE_VISIBLE_FROM_ABOVE; + } +} \ No newline at end of file diff --git a/sp/src/game/server/fire_smoke.h b/sp/src/game/server/fire_smoke.h new file mode 100644 index 00000000..a41aeb45 --- /dev/null +++ b/sp/src/game/server/fire_smoke.h @@ -0,0 +1,78 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef FIRE_SMOKE_H +#define FIRE_SMOKE_H +#pragma once + +#include "baseparticleentity.h" + +//================================================== +// CBaseFire +//================================================== + +//NOTENOTE: Reserved for all descendants +#define bitsFIRE_NONE 0x00000000 +#define bitsFIRE_ACTIVE 0x00000001 + +class CBaseFire : public CBaseEntity +{ +public: + DECLARE_DATADESC(); + DECLARE_CLASS( CBaseFire, CBaseEntity ); + + CBaseFire( void ); + virtual ~CBaseFire( void ); + + virtual void Scale( float size, float time ); + virtual void Scale( float start, float size, float time ); + virtual void Enable( int state = true ); + + //Client-side + CNetworkVar( float, m_flStartScale ); + CNetworkVar( float, m_flScale ); + CNetworkVar( float, m_flScaleTime ); + CNetworkVar( int, m_nFlags ); +}; + +//================================================== +// CFireSmoke +//================================================== + +//NOTENOTE: Mirrored in cl_dll/c_fire_smoke.cpp +#define bitsFIRESMOKE_SMOKE 0x00000002 +#define bitsFIRESMOKE_SMOKE_COLLISION 0x00000004 +#define bitsFIRESMOKE_GLOW 0x00000008 +#define bitsFIRESMOKE_VISIBLE_FROM_ABOVE 0x00000010 + +class CFireSmoke : public CBaseFire +{ +public: + DECLARE_CLASS( CFireSmoke, CBaseFire ); + + CFireSmoke( void ); + virtual ~CFireSmoke( void ); + + void Spawn(); + void Precache(); + void EnableSmoke( int state = true ); + void EnableGlow( int state = true ); + void EnableVisibleFromAbove( int state = true ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +public: + + //Client-side + CNetworkVar( int, m_nFlameModelIndex ); + CNetworkVar( int, m_nFlameFromAboveModelIndex ); + + //Server-side +}; + +#endif //FIRE_SMOKE_H diff --git a/sp/src/game/server/fish.cpp b/sp/src/game/server/fish.cpp new file mode 100644 index 00000000..553c7659 --- /dev/null +++ b/sp/src/game/server/fish.cpp @@ -0,0 +1,831 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//===========================================================================// +// fish.cpp +// Simple fish behavior +// Author: Michael S. Booth, April 2005 + +#include "cbase.h" +#include "fish.h" +#include "saverestore_utlvector.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar fish_dormant( "fish_dormant", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "Turns off interactive fish behavior. Fish become immobile and unresponsive." ); + + +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( fish, CFish ); + + +//----------------------------------------------------------------------------------------------------- +BEGIN_DATADESC( CFish ) + DEFINE_FIELD( m_pool, FIELD_EHANDLE ), + DEFINE_FIELD( m_id, FIELD_INTEGER ), + DEFINE_FIELD( m_angle, FIELD_FLOAT ), + DEFINE_FIELD( m_angleChange, FIELD_FLOAT ), + DEFINE_FIELD( m_forward, FIELD_VECTOR ), + DEFINE_FIELD( m_perp, FIELD_VECTOR ), + DEFINE_FIELD( m_poolOrigin, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_waterLevel, FIELD_FLOAT ), + DEFINE_FIELD( m_speed, FIELD_FLOAT ), + DEFINE_FIELD( m_desiredSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_calmSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_panicSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_avoidRange, FIELD_FLOAT ), + DEFINE_FIELD( m_turnClockwise, FIELD_BOOLEAN ), +END_DATADESC() + + +//----------------------------------------------------------------------------------------------------- +/** + * Send fish position relative to pool origin + */ +void SendProxy_FishOriginX( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CFish *fish = (CFish *)pStruct; + Assert( fish ); + + const Vector &v = fish->GetAbsOrigin(); + Vector origin = fish->m_poolOrigin; + + pOut->m_Float = v.x - origin.x; +} + +void SendProxy_FishOriginY( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CFish *fish = (CFish *)pStruct; + Assert( fish ); + + const Vector &v = fish->GetAbsOrigin(); + Vector origin = fish->m_poolOrigin; + + pOut->m_Float = v.y - origin.y; +} + +// keep angle in normalized range when sending it +void SendProxy_FishAngle( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + float value = *((float *)pData); + + while( value > 360.0f ) + value -= 360.0f; + + while (value < 0.0f) + value += 360.0f; + + pOut->m_Float = value; +} + + +/** + * NOTE: Do NOT use SPROP_CHANGES_OFTEN, as it will reorder this list. + * The pool origin must arrive befoore m_x and m_y or the fish will + * respawn at the origin and zip back to their proper places. + */ +IMPLEMENT_SERVERCLASS_ST_NOBASE( CFish, DT_CFish ) + + SendPropVector( SENDINFO(m_poolOrigin), -1, SPROP_COORD, 0.0f, HIGH_DEFAULT ), // only sent once + + SendPropFloat( SENDINFO(m_angle), 7, 0 /*SPROP_CHANGES_OFTEN*/, 0.0f, 360.0f, SendProxy_FishAngle ), + + SendPropFloat( SENDINFO(m_x), 7, 0 /*SPROP_CHANGES_OFTEN*/, -255.0f, 255.0f ), + SendPropFloat( SENDINFO(m_y), 7, 0 /*SPROP_CHANGES_OFTEN*/, -255.0f, 255.0f ), + SendPropFloat( SENDINFO(m_z), -1, SPROP_COORD ), // only sent once + + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropInt( SENDINFO(m_lifeState) ), + + SendPropFloat( SENDINFO(m_waterLevel) ), // only sent once + +END_SEND_TABLE() + + + +//------------------------------------------------------------------------------------------------------------- +CFish::CFish( void ) +{ +} + + +//------------------------------------------------------------------------------------------------------------- +CFish::~CFish() +{ +} + + +//------------------------------------------------------------------------------------------------------------- +void CFish::Initialize( CFishPool *pool, unsigned int id ) +{ + m_pool = pool; + m_id = id; + + m_poolOrigin = pool->GetAbsOrigin(); + m_waterLevel = pool->GetWaterLevel(); + + // pass relative position to the client + Vector deltaPos = GetAbsOrigin() - m_poolOrigin; + m_x = deltaPos.x; + m_y = deltaPos.y; + m_z = m_poolOrigin->z; + + SetModel( pool->GetModelName().ToCStr() ); +} + + +//------------------------------------------------------------------------------------------------------------- +void CFish::Spawn( void ) +{ + Precache(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE | FSOLID_NOT_SOLID | FSOLID_TRIGGER ); + SetMoveType( MOVETYPE_FLY ); + + m_angle = RandomFloat( 0.0f, 360.0f ); + m_angleChange = 0.0f; + + m_forward = Vector( 1.0f, 0.0, 0.0f ); + m_perp.x = -m_forward.y; + m_perp.y = m_forward.x; + m_perp.z = 0.0f; + + m_speed = 0.0f; + m_calmSpeed = RandomFloat( 10.0f, 20.0f ); + m_panicSpeed = m_calmSpeed * RandomFloat( 4.0f, 5.0f ); + m_desiredSpeed = m_calmSpeed; + + m_turnClockwise = (RandomInt( 0, 100 ) < 50); + + m_avoidRange = RandomFloat( 40.0f, 75.0f ); + + m_iHealth = 1; + m_iMaxHealth = 1; + m_takedamage = DAMAGE_YES; + + // spread out a bit + m_disperseTimer.Start( RandomFloat( 0.0f, 10.0f ) ); + m_goTimer.Start( RandomFloat( 10.0f, 60.0f ) ); + m_moveTimer.Start( RandomFloat( 2.0f, 10.0 ) ); + m_desiredSpeed = m_calmSpeed; +} + + +//------------------------------------------------------------------------------------------------------------- +void CFish::Event_Killed( const CTakeDamageInfo &info ) +{ + m_takedamage = DAMAGE_NO; + m_lifeState = LIFE_DEAD; +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * In contact with "other" + */ +void CFish::Touch( CBaseEntity *other ) +{ + if (other && other->IsPlayer()) + { + // touched a Player - panic! + Panic(); + } +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Influence my motion to flock with other nearby fish + * 'amount' ranges from zero to one, representing the amount of flocking influence allowed + * If 'other' is NULL, flock to the center of the pool. + */ +void CFish::FlockTo( CFish *other, float amount ) +{ + // allow fish to disperse a bit at round start + if (!m_disperseTimer.IsElapsed()) + return; + + const float maxRange = (other) ? 100.0f : 300.0f; + + Vector to = (other) ? (other->GetAbsOrigin() - GetAbsOrigin()) : (m_pool->GetAbsOrigin() - GetAbsOrigin()); + float range = to.NormalizeInPlace(); + + if (range > maxRange) + return; + + // if they are close and we are moving together, avoid them + const float avoidRange = 25.0f; + if (other && range < avoidRange) + { + // compute their relative velocity to us + Vector relVel = other->GetAbsVelocity() - GetAbsVelocity(); + + if (DotProduct( to, relVel ) < 0.0f) + { + const float avoidPower = 5.0f; + + // their comin' right at us! - avoid + if (DotProduct( m_perp, to ) > 0.0f) + { + m_angleChange -= avoidPower * (1.0f - range/avoidRange); + } + else + { + m_angleChange += avoidPower * (1.0f - range/avoidRange); + } + return; + } + } + + // turn is 2 if 'other' is behind us, 1 perpendicular, and 0 straight ahead + float turn = 1.0f + DotProduct( -m_forward, to ); + + Vector perp( -m_forward.y, m_forward.x, 0.0f ); + float side = (DotProduct( perp, to ) > 1.0f) ? 1.0f : -1.0f; + + if (turn > 1.0f) + { + // always turn one way to avoid dithering if many fish are behind us + side = (m_turnClockwise) ? 1.0f : -1.0f; + } + + float power = 1.0f - (range / maxRange); + + const float flockInfluence = 0.7f; // 0.3f; // 0.3 + m_angleChange += amount * flockInfluence * power * side * turn; +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Returns a value between zero (no danger of hitting an obstacle) + * and one (extreme danger of hitting an obstacle). + * This is used to modulate later flocking behaviors. + */ +float CFish::Avoid( void ) +{ + const float avoidPower = 100.0f; // 50.0f; // 25.0f; + + // + // Stay within pool bounds. + // This may cause problems with pools with oddly concave portions + // right at the max range. + // + Vector toCenter = m_pool->GetAbsOrigin() - GetAbsOrigin(); + const float avoidZone = 20.0f; + if (toCenter.IsLengthGreaterThan( m_pool->GetMaxRange() - avoidZone )) + { + // turn away from edge + if (DotProduct( toCenter, m_forward ) < 0.0f) + { + m_angleChange += (m_turnClockwise) ? -avoidPower : avoidPower; + } + + // take total precedence over flocking + return 1.0f; + } + + trace_t result; + const float sideOffset = 0.2f; + + float rightDanger = 0.0f; + float leftDanger = 0.0f; + + // slightly right of forward + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + m_avoidRange * (m_forward + sideOffset * m_perp), MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + if (result.fraction < 1.0f) + { + rightDanger = 1.0f - result.fraction; + } + + // slightly left of forward + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + m_avoidRange * (m_forward - sideOffset * m_perp), MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + if (result.fraction < 1.0f) + { + // steer away + leftDanger = 1.0f - result.fraction; + } + + // steer away - prefer one side to avoid cul-de-sacs + if (m_turnClockwise) + { + if (rightDanger > 0.0f) + { + m_angleChange -= avoidPower * rightDanger; + } + else + { + m_angleChange += avoidPower * leftDanger; + } + } + else + { + if (leftDanger > 0.0f) + { + m_angleChange += avoidPower * leftDanger; + } + else + { + m_angleChange -= avoidPower * rightDanger; + } + } + + + return (leftDanger > rightDanger) ? leftDanger : rightDanger; +} + + +//------------------------------------------------------------------------------------------------------------- +void CFish::Panic( void ) +{ + // start to panic + m_panicTimer.Start( RandomFloat( 5.0f, 15.0f ) ); + m_moveTimer.Start( RandomFloat( 10.0f, 20.0f ) ); + m_desiredSpeed = m_panicSpeed; +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Invoked each server tick + */ +void CFish::Update( float deltaT ) +{ + Vector deltaPos = GetAbsOrigin() - m_poolOrigin; + const float safetyMargin = 5.0f; + + // pass relative position to the client + // clamp them here to cover the rare cases where a fish's high velocity skirts the range limit + m_x = clamp( deltaPos.x, -255.0f, 255.0f ); + m_y = clamp( deltaPos.y, -255.0f, 255.0f ); + m_z = m_poolOrigin->z; + + + // + // Dead fish just coast to a stop. All floating to the + // surface and bobbing motion is handled client-side. + // + if (m_lifeState == LIFE_DEAD) + { + // don't allow fish to leave maximum range of pool + if (deltaPos.IsLengthGreaterThan( m_pool->GetMaxRange() - safetyMargin )) + { + SetAbsVelocity( Vector( 0, 0, 0 ) ); + } + else + { + // decay movement speed to zero + Vector vel = GetAbsVelocity(); + + const float drag = 1.0f; + vel -= drag * vel * deltaT; + + SetAbsVelocity( vel ); + } + + return; + } + + + // + // Living fish behavior + // + + // periodically change our turning preference + if (m_turnTimer.IsElapsed()) + { + m_turnTimer.Start( RandomFloat( 10.0f, 30.0f ) ); + m_turnClockwise = !m_turnClockwise; + } + + if (m_panicTimer.GetRemainingTime() > 0.0f) + { + // panicking + m_desiredSpeed = m_panicSpeed; + } + else if (m_moveTimer.GetRemainingTime() > 0.0f) + { + // normal movement + m_desiredSpeed = m_calmSpeed; + } + else if (m_goTimer.IsElapsed()) + { + // move every so often + m_goTimer.Start( RandomFloat( 10.0f, 60.0f ) ); + m_moveTimer.Start( RandomFloat( 2.0f, 10.0 ) ); + m_desiredSpeed = m_calmSpeed; + } + + // avoid obstacles + float danger = Avoid(); + + // flock towards visible fish + for( int i=0; i maxAngleChange) + { + m_angleChange = maxAngleChange; + } + else if (m_angleChange < -maxAngleChange) + { + m_angleChange = -maxAngleChange; + } + + m_angle += m_angleChange; + m_angleChange = 0.0f; + + m_forward.x = cos( m_angle * M_PI/180.0f ); + m_forward.y = sin( m_angle * M_PI/180.0f ); + m_forward.z = 0.0f; + + m_perp.x = -m_forward.y; + m_perp.y = m_forward.x; + m_perp.z = 0.0f; + + // + // Update speed + // + const float rate = 2.0f; + m_speed += rate * (m_desiredSpeed - m_speed) * deltaT; + + // decay desired speed if done moving + if (m_moveTimer.IsElapsed()) + { + const float decayRate = 1.0f; + m_desiredSpeed -= decayRate * deltaT; + if (m_desiredSpeed < 0.0f) + { + m_desiredSpeed = 0.0f; + } + } + + Vector vel = m_speed * m_forward; + + // don't allow fish to leave maximum range of pool + if (deltaPos.IsLengthGreaterThan( m_pool->GetMaxRange() - safetyMargin )) + { + Vector toCenter = -deltaPos; + + float radial = DotProduct( toCenter, vel ); + if (radial < 0.0f) + { + // heading out of range, zero the radial velocity component + toCenter.NormalizeInPlace(); + Vector perp( -toCenter.y, toCenter.x, 0.0f ); + + float side = DotProduct( perp, vel ); + + vel = side * perp; + } + } + + SetAbsVelocity( vel ); + + m_flSpeed = m_speed; +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Zero the visible vector + */ +void CFish::ResetVisible( void ) +{ + m_visible.RemoveAll(); +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Add this fish to our visible vector + */ +void CFish::AddVisible( CFish *fish ) +{ + m_visible.AddToTail( fish ); +} + + +//------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------- +/** + * A CFishPool manages a collection of CFish, and defines where the "pool" is in the world. + */ + +LINK_ENTITY_TO_CLASS( func_fish_pool, CFishPool ); + +BEGIN_DATADESC( CFishPool ) + + DEFINE_FIELD( m_fishCount, FIELD_INTEGER ), + DEFINE_FIELD( m_maxRange, FIELD_FLOAT ), + DEFINE_FIELD( m_swimDepth, FIELD_FLOAT ), + DEFINE_FIELD( m_waterLevel, FIELD_FLOAT ), + DEFINE_FIELD( m_isDormant, FIELD_BOOLEAN ), + DEFINE_UTLVECTOR( m_fishes, FIELD_EHANDLE ), + +#ifdef MAPBASE + DEFINE_INPUT( m_nSkin, FIELD_INTEGER, "skin" ), + + DEFINE_KEYFIELD( m_flLoudPanicRange, FIELD_FLOAT, "LoudPanicRange" ), + DEFINE_KEYFIELD( m_flQuietPanicRange, FIELD_FLOAT, "QuietPanicRange" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "SpawnFish", InputSpawnFish ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "PanicLoudFromPoint", InputPanicLoudFromPoint ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "PanicQuietFromPoint", InputPanicQuietFromPoint ), + + DEFINE_OUTPUT( m_OnSpawnFish, "OnSpawnFish" ), +#endif + + DEFINE_THINKFUNC( Update ), + +END_DATADESC() + + +//------------------------------------------------------------------------------------------------------------- +CFishPool::CFishPool( void ) +{ + m_fishCount = 0; + m_maxRange = 255.0f; + m_swimDepth = 0.0f; + m_isDormant = false; + +#ifdef MAPBASE + m_nSkin = 0; + + // Original defaults + m_flLoudPanicRange = 500.0f; + m_flQuietPanicRange = 75.0f; +#endif + + m_visTimer.Start( 0.5f ); + + ListenForGameEvent( "player_shoot" ); + ListenForGameEvent( "player_footstep" ); + ListenForGameEvent( "weapon_fire" ); + ListenForGameEvent( "hegrenade_detonate" ); + ListenForGameEvent( "flashbang_detonate" ); + ListenForGameEvent( "smokegrenade_detonate" ); + ListenForGameEvent( "bomb_exploded" ); +} + +//------------------------------------------------------------------------------------------------------------- +/** + * Initialize the fish pool + */ +void CFishPool::Spawn() +{ + SetThink( &CFishPool::Update ); + SetNextThink( gpGlobals->curtime ); + + m_waterLevel = UTIL_WaterLevel( GetAbsOrigin(), GetAbsOrigin().z, GetAbsOrigin().z + 1000.0f ); + + trace_t result; + for( int i=0; iInitialize( this, i ); + + if (fish) + { + CHandle hFish; + hFish.Set( fish ); + m_fishes.AddToTail( hFish ); +#ifdef MAPBASE + fish->m_nSkin = m_nSkin; + m_OnSpawnFish.Set( hFish, fish, this ); +#endif + } + } +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Parse KeyValue pairs + */ +bool CFishPool::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq( szKeyName, "fish_count" )) + { + m_fishCount = atoi(szValue); + return true; + } + else if (FStrEq( szKeyName, "max_range" )) + { + m_maxRange = atof(szValue); + if (m_maxRange <= 1.0f) + { + m_maxRange = 1.0f; + } + else if (m_maxRange > 255.0f) + { + // stay within 8 bits range + m_maxRange = 255.0f; + } + + return true; + } + else if (FStrEq( szKeyName, "model" )) + { + PrecacheModel( szValue ); + SetModelName( AllocPooledString( szValue ) ); + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Game event processing + */ +void CFishPool::FireGameEvent( IGameEvent *event ) +{ + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + + // the fish panic +#ifdef MAPBASE + float range = (Q_strcmp( "player_footstep", event->GetName() )) ? m_flLoudPanicRange : m_flQuietPanicRange; +#else + const float loudRange = 500.0f; + const float quietRange = 75.0f; + + float range = (Q_strcmp( "player_footstep", event->GetName() )) ? loudRange : quietRange; +#endif + + for( int i=0; iGetAbsOrigin() - m_fishes[i]->GetAbsOrigin()).IsLengthGreaterThan( range )) + { + // event too far away to care + continue; + } + + m_fishes[i]->Panic(); + } +} + + +//------------------------------------------------------------------------------------------------------------- +/** + * Invoked each server tick + */ +void CFishPool::Update( void ) +{ + float deltaT = 0.1f; + SetNextThink( gpGlobals->curtime + deltaT ); + + /// @todo Go dormant when no players are around to see us + + if (fish_dormant.GetBool()) + { + if (!m_isDormant) + { + // stop all the fish + for( int i=0; iSetAbsVelocity( Vector( 0, 0, 0 ) ); + } + + m_isDormant = true; + } + + return; + } + else + { + m_isDormant = false; + } + + // update fish to fish visibility + if (m_visTimer.IsElapsed()) + { + m_visTimer.Reset(); + + int i, j; + trace_t result; + + // reset each fishes vis list + for( i=0; iResetVisible(); + } + + // build new vis lists - line of sight is symmetric + for( i=0; iIsAlive()) + continue; + + for( j=i+1; jIsAlive()) + continue; + + UTIL_TraceLine( m_fishes[i]->GetAbsOrigin(), m_fishes[j]->GetAbsOrigin(), MASK_PLAYERSOLID, m_fishes[i], COLLISION_GROUP_NONE, &result ); + if (result.fraction >= 1.0f) + { + // the fish can see each other + m_fishes[i]->AddVisible( m_fishes[j] ); + m_fishes[j]->AddVisible( m_fishes[i] ); + } + } + } + } + + // simulate the fishes behavior + for( int i=0; iUpdate( deltaT ); + } +} + +#ifdef MAPBASE +//------------------------------------------------------------------------------------------------------------- +/** + * Inputs + */ +void CFishPool::InputSpawnFish( inputdata_t &inputdata ) +{ + QAngle heading( 0.0f, RandomFloat( 0, 360.0f ), 0.0f ); + + CFish *fish = (CFish *)Create( "fish", GetAbsOrigin(), heading, this ); + fish->Initialize( this, m_fishes.Count() ); + + if (fish) + { + CHandle hFish; + hFish.Set( fish ); + m_fishes.AddToTail( hFish ); +#ifdef MAPBASE + m_OnSpawnFish.Set( hFish, fish, this ); +#endif + } +} + +void CFishPool::InputPanicLoudFromPoint( inputdata_t &inputdata ) +{ + // Make the fish panic from this point + Vector vecPoint; + inputdata.value.Vector3D( vecPoint ); + for( int i=0; iGetAbsOrigin()).IsLengthGreaterThan( m_flLoudPanicRange )) + { + // event too far away to care + continue; + } + + m_fishes[i]->Panic(); + } +} + +void CFishPool::InputPanicQuietFromPoint( inputdata_t &inputdata ) +{ + // Make the fish panic from this point + Vector vecPoint; + inputdata.value.Vector3D( vecPoint ); + for( int i=0; iGetAbsOrigin()).IsLengthGreaterThan( m_flQuietPanicRange )) + { + // event too far away to care + continue; + } + + m_fishes[i]->Panic(); + } +} +#endif + diff --git a/sp/src/game/server/fish.h b/sp/src/game/server/fish.h new file mode 100644 index 00000000..257bdbcd --- /dev/null +++ b/sp/src/game/server/fish.h @@ -0,0 +1,155 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// fish.h +// Simple fish behavior +// Author: Michael S. Booth, April 2005 + +#ifndef _FISH_H_ +#define _FISH_H_ + +#include "baseanimating.h" +#include "GameEventListener.h" + +class CFishPool; + +//---------------------------------------------------------------------------------------------- +/** + * Simple ambient fish + */ +class CFish : public CBaseAnimating +{ +public: + DECLARE_CLASS( CFish, CBaseAnimating ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CFish( void ); + virtual ~CFish(); + + void Initialize( CFishPool *pool, unsigned int id ); + + virtual void Spawn( void ); + + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void Touch( CBaseEntity *other ); ///< in contact with "other" + + void Update( float deltaT ); ///< invoked each server tick + + void FlockTo( CFish *other, float amount ); ///< influence my motion to flock with other nearby fish + float Avoid( void ); + void Panic( void ); ///< panic for awhile + + void ResetVisible( void ); ///< zero the visible vector + void AddVisible( CFish *fish ); ///< add this fish to our visible vector + +private: + friend void SendProxy_FishOriginX( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); + friend void SendProxy_FishOriginY( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); + + CHandle m_pool; ///< the pool we are in + unsigned int m_id; ///< our unique ID + + CNetworkVar( float, m_x ); ///< have to send position coordinates separately since Z is unused + CNetworkVar( float, m_y ); ///< have to send position coordinates separately since Z is unused + CNetworkVar( float, m_z ); ///< only sent once since fish always swim at the same depth + + CNetworkVar( float, m_angle ); ///< only yaw changes + float m_angleChange; + Vector m_forward; + Vector m_perp; + + CNetworkVar( Vector, m_poolOrigin ); ///< used to efficiently network our relative position + CNetworkVar( float, m_waterLevel ); + + float m_speed; + float m_desiredSpeed; + + float m_calmSpeed; ///< speed the fish moves when calm + float m_panicSpeed; ///< speed the fish moves when panicked + + float m_avoidRange; ///< range to avoid obstacles + + CountdownTimer m_turnTimer; ///< every so often our turn preference changes + bool m_turnClockwise; ///< if true this fish prefers to turn clockwise, else CCW + + CountdownTimer m_goTimer; ///< start the fish moving when timer elapses + CountdownTimer m_moveTimer; ///< dont decay speed while we are moving + CountdownTimer m_panicTimer; ///< if active, fish is panicked + CountdownTimer m_disperseTimer; ///< initial non-flocking time + + CUtlVector< CFish * > m_visible; ///< vector of fish that we can see +}; + + +//---------------------------------------------------------------------------------------------- +/** + * This class defines a volume of water where a number of CFish swim + */ +class CFishPool : public CBaseEntity, public CGameEventListener +{ +public: + DECLARE_CLASS( CFishPool, CBaseEntity ); + DECLARE_DATADESC(); + + CFishPool( void ); + + virtual void Spawn(); + + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + + virtual void FireGameEvent( IGameEvent *event ); + + void Update( void ); ///< invoked each server tick + + float GetWaterLevel( void ) const; ///< return Z coordinate of water in world coords + float GetMaxRange( void ) const; ///< return how far a fish is allowed to wander + +#ifdef MAPBASE + void InputSpawnFish( inputdata_t &inputdata ); + void InputPanicLoudFromPoint( inputdata_t &inputdata ); + void InputPanicQuietFromPoint( inputdata_t &inputdata ); +#endif + +private: + int m_fishCount; ///< number of fish in the pool + float m_maxRange; ///< how far a fish is allowed to wander + float m_swimDepth; ///< the depth the fish swim below the water surface + + float m_waterLevel; ///< Z of water surface + + bool m_isDormant; + + CUtlVector< CHandle > m_fishes; ///< vector of all fish in this pool + +#ifdef MAPBASE + int m_nSkin; // Sets the skin of spawned fish + + float m_flLoudPanicRange; + float m_flQuietPanicRange; + + COutputEHANDLE m_OnSpawnFish; +#endif + + CountdownTimer m_visTimer; ///< for throttling line of sight checks between all fish +}; + + +inline float CFishPool::GetMaxRange( void ) const +{ + return m_maxRange; +} + + +inline float CFishPool::GetWaterLevel( void ) const +{ + return m_waterLevel; +} + + +#endif // _FISH_H_ + diff --git a/sp/src/game/server/fogcontroller.cpp b/sp/src/game/server/fogcontroller.cpp new file mode 100644 index 00000000..3ed75060 --- /dev/null +++ b/sp/src/game/server/fogcontroller.cpp @@ -0,0 +1,403 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// An entity that allows level designer control over the fog parameters. +// +//============================================================================= + +#include "cbase.h" +#include "fogcontroller.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "player.h" +#include "world.h" +#include "ndebugoverlay.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CFogSystem s_FogSystem( "FogSystem" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CFogSystem *FogSystem( void ) +{ + return &s_FogSystem; +} + +LINK_ENTITY_TO_CLASS( env_fog_controller, CFogController ); + +BEGIN_DATADESC( CFogController ) + + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetStartDist", InputSetStartDist ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetEndDist", InputSetEndDist ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMaxDensity", InputSetMaxDensity ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetColor", InputSetColor ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetColorSecondary", InputSetColorSecondary ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetFarZ", InputSetFarZ ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetAngles", InputSetAngles ), + + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetColorLerpTo", InputSetColorLerpTo ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetColorSecondaryLerpTo", InputSetColorSecondaryLerpTo ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetStartDistLerpTo", InputSetStartDistLerpTo ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetEndDistLerpTo", InputSetEndDistLerpTo ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartFogTransition", InputStartFogTransition ), + + // Quiet classcheck + //DEFINE_EMBEDDED( m_fog ), + + DEFINE_KEYFIELD( m_bUseAngles, FIELD_BOOLEAN, "use_angles" ), + DEFINE_KEYFIELD( m_fog.colorPrimary, FIELD_COLOR32, "fogcolor" ), + DEFINE_KEYFIELD( m_fog.colorSecondary, FIELD_COLOR32, "fogcolor2" ), + DEFINE_KEYFIELD( m_fog.dirPrimary, FIELD_VECTOR, "fogdir" ), + DEFINE_KEYFIELD( m_fog.enable, FIELD_BOOLEAN, "fogenable" ), + DEFINE_KEYFIELD( m_fog.blend, FIELD_BOOLEAN, "fogblend" ), + DEFINE_KEYFIELD( m_fog.start, FIELD_FLOAT, "fogstart" ), + DEFINE_KEYFIELD( m_fog.end, FIELD_FLOAT, "fogend" ), + DEFINE_KEYFIELD( m_fog.maxdensity, FIELD_FLOAT, "fogmaxdensity" ), + DEFINE_KEYFIELD( m_fog.farz, FIELD_FLOAT, "farz" ), + DEFINE_KEYFIELD( m_fog.duration, FIELD_FLOAT, "foglerptime" ), + + DEFINE_THINKFUNC( SetLerpValues ), + + DEFINE_FIELD( m_iChangedVariables, FIELD_INTEGER ), + + DEFINE_FIELD( m_fog.lerptime, FIELD_TIME ), + DEFINE_FIELD( m_fog.colorPrimaryLerpTo, FIELD_COLOR32 ), + DEFINE_FIELD( m_fog.colorSecondaryLerpTo, FIELD_COLOR32 ), + DEFINE_FIELD( m_fog.startLerpTo, FIELD_FLOAT ), + DEFINE_FIELD( m_fog.endLerpTo, FIELD_FLOAT ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CFogController, DT_FogController ) +// fog data + SendPropInt( SENDINFO_STRUCTELEM( m_fog.enable ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_STRUCTELEM( m_fog.blend ), 1, SPROP_UNSIGNED ), + SendPropVector( SENDINFO_STRUCTELEM(m_fog.dirPrimary), -1, SPROP_COORD), + SendPropInt( SENDINFO_STRUCTELEM( m_fog.colorPrimary ), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_STRUCTELEM( m_fog.colorSecondary ), 32, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.start ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.end ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.maxdensity ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.farz ), 0, SPROP_NOSCALE ), + + SendPropInt( SENDINFO_STRUCTELEM( m_fog.colorPrimaryLerpTo ), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_STRUCTELEM( m_fog.colorSecondaryLerpTo ), 32, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.startLerpTo ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.endLerpTo ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.lerptime ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_fog.duration ), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + +CFogController::CFogController() +{ + // Make sure that old maps without fog fields don't get wacked out fog values. + m_fog.enable = false; + m_fog.maxdensity = 1.0f; +} + + +CFogController::~CFogController() +{ +} + +void CFogController::Spawn( void ) +{ + BaseClass::Spawn(); + + m_fog.colorPrimaryLerpTo = m_fog.colorPrimary; + m_fog.colorSecondaryLerpTo = m_fog.colorSecondary; +} + +//----------------------------------------------------------------------------- +// Activate! +//----------------------------------------------------------------------------- +void CFogController::Activate( ) +{ + BaseClass::Activate(); + + if ( m_bUseAngles ) + { + AngleVectors( GetAbsAngles(), &m_fog.dirPrimary.GetForModify() ); + m_fog.dirPrimary.GetForModify() *= -1.0f; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CFogController::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler for setting the fog start distance. +//------------------------------------------------------------------------------ +void CFogController::InputSetStartDist(inputdata_t &inputdata) +{ + // Get the world entity. + m_fog.start = inputdata.value.Float(); +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler for setting the fog end distance. +//------------------------------------------------------------------------------ +void CFogController::InputSetEndDist(inputdata_t &inputdata) +{ + // Get the world entity. + m_fog.end = inputdata.value.Float(); +} + +//------------------------------------------------------------------------------ +// Input handler for setting the maximum density of the fog. This lets us bring +// the start distance in without the scene fogging too much. +//------------------------------------------------------------------------------ +void CFogController::InputSetMaxDensity( inputdata_t &inputdata ) +{ + m_fog.maxdensity = inputdata.value.Float(); +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler for turning on the fog. +//------------------------------------------------------------------------------ +void CFogController::InputTurnOn(inputdata_t &inputdata) +{ + // Get the world entity. + m_fog.enable = true; +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler for turning off the fog. +//------------------------------------------------------------------------------ +void CFogController::InputTurnOff(inputdata_t &inputdata) +{ + // Get the world entity. + m_fog.enable = false; +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler for setting the primary fog color. +//------------------------------------------------------------------------------ +void CFogController::InputSetColor(inputdata_t &inputdata) +{ + // Get the world entity. + m_fog.colorPrimary = inputdata.value.Color32(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler for setting the secondary fog color. +//------------------------------------------------------------------------------ +void CFogController::InputSetColorSecondary(inputdata_t &inputdata) +{ + // Get the world entity. + m_fog.colorSecondary = inputdata.value.Color32(); +} + +void CFogController::InputSetFarZ(inputdata_t &inputdata) +{ + m_fog.farz = inputdata.value.Int(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Sets the angles to use for the secondary fog direction. +//------------------------------------------------------------------------------ +void CFogController::InputSetAngles( inputdata_t &inputdata ) +{ + const char *pAngles = inputdata.value.String(); + + QAngle angles; + UTIL_StringToVector( angles.Base(), pAngles ); + + Vector vTemp; + AngleVectors( angles, &vTemp ); + SetAbsAngles( angles ); + + AngleVectors( GetAbsAngles(), &m_fog.dirPrimary.GetForModify() ); + m_fog.dirPrimary.GetForModify() *= -1.0f; +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CFogController::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + Q_snprintf(tempstr,sizeof(tempstr),"State: %s",(m_fog.enable)?"On":"Off"); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Start: %3.0f",m_fog.start.Get()); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"End : %3.0f",m_fog.end.Get()); + EntityText(text_offset,tempstr,0); + text_offset++; + + color32 color = m_fog.colorPrimary; + Q_snprintf(tempstr,sizeof(tempstr),"1) Red : %i",color.r); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"1) Green: %i",color.g); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"1) Blue : %i",color.b); + EntityText(text_offset,tempstr,0); + text_offset++; + + color = m_fog.colorSecondary; + Q_snprintf(tempstr,sizeof(tempstr),"2) Red : %i",color.r); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"2) Green: %i",color.g); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"2) Blue : %i",color.b); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + +#define FOG_CONTROLLER_COLORPRIMARY_LERP 1 +#define FOG_CONTROLLER_COLORSECONDARY_LERP 2 +#define FOG_CONTROLLER_START_LERP 4 +#define FOG_CONTROLLER_END_LERP 8 + +void CFogController::InputSetColorLerpTo(inputdata_t &data) +{ + m_iChangedVariables |= FOG_CONTROLLER_COLORPRIMARY_LERP; + m_fog.colorPrimaryLerpTo = data.value.Color32(); +} + +void CFogController::InputSetColorSecondaryLerpTo(inputdata_t &data) +{ + m_iChangedVariables |= FOG_CONTROLLER_COLORSECONDARY_LERP; + m_fog.colorSecondaryLerpTo = data.value.Color32(); +} + +void CFogController::InputSetStartDistLerpTo(inputdata_t &data) +{ + m_iChangedVariables |= FOG_CONTROLLER_START_LERP; + m_fog.startLerpTo = data.value.Float(); +} + +void CFogController::InputSetEndDistLerpTo(inputdata_t &data) +{ + m_iChangedVariables |= FOG_CONTROLLER_END_LERP; + m_fog.endLerpTo = data.value.Float(); +} + +void CFogController::InputStartFogTransition(inputdata_t &data) +{ + SetThink( &CFogController::SetLerpValues ); + + m_fog.lerptime = gpGlobals->curtime + m_fog.duration + 0.1; + SetNextThink( gpGlobals->curtime + m_fog.duration ); +} + +void CFogController::SetLerpValues( void ) +{ + if ( m_iChangedVariables & FOG_CONTROLLER_COLORPRIMARY_LERP ) + { + m_fog.colorPrimary = m_fog.colorPrimaryLerpTo; + } + + if ( m_iChangedVariables & FOG_CONTROLLER_COLORSECONDARY_LERP ) + { + m_fog.colorSecondary = m_fog.colorSecondaryLerpTo; + } + + if ( m_iChangedVariables & FOG_CONTROLLER_START_LERP ) + { + m_fog.start = m_fog.startLerpTo; + } + + if ( m_iChangedVariables & FOG_CONTROLLER_END_LERP ) + { + m_fog.end = m_fog.endLerpTo; + } + + m_iChangedVariables = 0; + m_fog.lerptime = gpGlobals->curtime; +} + + +//----------------------------------------------------------------------------- +// Purpose: Clear out the fog controller. +//----------------------------------------------------------------------------- +void CFogSystem::LevelInitPreEntity( void ) +{ + m_pMasterController = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: On level load find the master fog controller. If no controller is +// set as Master, use the first fog controller found. +//----------------------------------------------------------------------------- +void CFogSystem::LevelInitPostEntity( void ) +{ + CFogController *pFogController = NULL; + do + { + pFogController = static_cast( gEntList.FindEntityByClassname( pFogController, "env_fog_controller" ) ); + if ( pFogController ) + { + if ( m_pMasterController == NULL ) + { + m_pMasterController = pFogController; + } + else + { + if ( pFogController->IsMaster() ) + { + m_pMasterController = pFogController; + } + } + } + } while ( pFogController ); + + // HACK: Singleplayer games don't get a call to CBasePlayer::Spawn on level transitions. + // CBasePlayer::Activate is called before this is called so that's too soon to set up the fog controller. + // We don't have a hook similar to Activate that happens after LevelInitPostEntity + // is called, or we could just do this in the player itself. + if ( gpGlobals->maxClients == 1 ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer && ( pPlayer->m_Local.m_PlayerFog.m_hCtrl.Get() == NULL ) ) + { + pPlayer->InitFogController(); + } + } + else + { + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( pPlayer && ( pPlayer->m_Local.m_PlayerFog.m_hCtrl.Get() == NULL ) ) + { + pPlayer->InitFogController(); + } + } + } +} + diff --git a/sp/src/game/server/fogcontroller.h b/sp/src/game/server/fogcontroller.h new file mode 100644 index 00000000..3a5b716d --- /dev/null +++ b/sp/src/game/server/fogcontroller.h @@ -0,0 +1,101 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef FOGCONTROLLER_H +#define FOGCONTROLLER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "playernet_vars.h" +#include "igamesystem.h" + +// Spawn Flags +#define SF_FOG_MASTER 0x0001 + +//============================================================================= +// +// Class Fog Controller: +// Compares a set of integer inputs to the one main input +// Outputs true if they are all equivalant, false otherwise +// +class CFogController : public CBaseEntity +{ +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + DECLARE_CLASS( CFogController, CBaseEntity ); + + CFogController(); + ~CFogController(); + + // Parse data from a map file + virtual void Activate(); + virtual int UpdateTransmitState(); + + // Input handlers + void InputSetStartDist(inputdata_t &data); + void InputSetEndDist(inputdata_t &data); + void InputTurnOn(inputdata_t &data); + void InputTurnOff(inputdata_t &data); + void InputSetColor(inputdata_t &data); + void InputSetColorSecondary(inputdata_t &data); + void InputSetFarZ( inputdata_t &data ); + void InputSetAngles( inputdata_t &inputdata ); + void InputSetMaxDensity( inputdata_t &inputdata ); + + void InputSetColorLerpTo(inputdata_t &data); + void InputSetColorSecondaryLerpTo(inputdata_t &data); + void InputSetStartDistLerpTo(inputdata_t &data); + void InputSetEndDistLerpTo(inputdata_t &data); + + void InputStartFogTransition(inputdata_t &data); + + int DrawDebugTextOverlays(void); + + void SetLerpValues( void ); + void Spawn( void ); + + bool IsMaster( void ) { return HasSpawnFlags( SF_FOG_MASTER ); } + +public: + + CNetworkVarEmbedded( fogparams_t, m_fog ); + bool m_bUseAngles; + int m_iChangedVariables; +}; + +//============================================================================= +// +// Fog Controller System. +// +class CFogSystem : public CAutoGameSystem +{ +public: + + // Creation/Init. + CFogSystem( char const *name ) : CAutoGameSystem( name ) + { + m_pMasterController = NULL; + } + + ~CFogSystem() + { + m_pMasterController = NULL; + } + + virtual void LevelInitPreEntity(); + virtual void LevelInitPostEntity(); + CFogController *GetMasterFogController( void ) { return m_pMasterController; } + +private: + + CFogController *m_pMasterController; +}; + +CFogSystem *FogSystem( void ); + +#endif // FOGCONTROLLER_H diff --git a/sp/src/game/server/forcefeedback.cpp b/sp/src/game/server/forcefeedback.cpp new file mode 100644 index 00000000..59f131e2 --- /dev/null +++ b/sp/src/game/server/forcefeedback.cpp @@ -0,0 +1,155 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "forcefeedback.h" +#include "igamesystem.h" + +class CForceFeedback : public IForceFeedback, public CAutoGameSystem +{ +public: + virtual bool Init(); + virtual void Shutdown(); + + // API + virtual void StopAllEffects( CBasePlayer *player ); + virtual void StopEffect( CBasePlayer *player, FORCEFEEDBACK_t effect ); + virtual void StartEffect( CBasePlayer *player, FORCEFEEDBACK_t effect, const FFBaseParams_t& params ); + + virtual void PauseAll( CBasePlayer *player ); + virtual void ResumeAll( CBasePlayer *player ); +}; + +static CForceFeedback g_ForceFeedbackSingleton; +IForceFeedback *forcefeedback = &g_ForceFeedbackSingleton; + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CForceFeedback::Init() +{ + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CForceFeedback::Shutdown() +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +//----------------------------------------------------------------------------- +void CForceFeedback::StopAllEffects( CBasePlayer *player ) +{ + if ( !player ) + return; + + CSingleUserRecipientFilter user( player ); + + UserMessageBegin( user, "ForceFeedback" ); + + WRITE_BYTE( FFMSG_STOPALL ); // Reset effects + + MessageEnd(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +// effect - +//----------------------------------------------------------------------------- +void CForceFeedback::StopEffect( CBasePlayer *player, FORCEFEEDBACK_t effect ) +{ + if ( !player ) + return; + + CSingleUserRecipientFilter user( player ); + + UserMessageBegin( user, "ForceFeedback" ); + + WRITE_BYTE( FFMSG_STOP ); // Reset effect + WRITE_BYTE( effect ); + + MessageEnd(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +// effect - +// params - +//----------------------------------------------------------------------------- +void CForceFeedback::StartEffect( CBasePlayer *player, FORCEFEEDBACK_t effect, const FFBaseParams_t& params ) +{ + if ( !player ) + { + return; + } + + CSingleUserRecipientFilter user( player ); + + UserMessageBegin( user, "ForceFeedback" ); + + WRITE_BYTE( FFMSG_START ); // Reset effects + WRITE_BYTE( effect ); + + // encode direction as a byte + int dir = (int)( ( params.m_flDirection / 360.0f ) * 255.0f ); + WRITE_BYTE( dir ); + + // encode duration as a signed int + int duration = (int)params.m_flDuration * 1000.0f; + WRITE_LONG( duration ); + + // encode gain as a byte + byte gain = (byte)clamp( params.m_flGain * 255.0f, 0.0f, 255.0f ); + + WRITE_BYTE( gain ); + WRITE_BYTE( params.m_nPriority ); + WRITE_BYTE( params.m_bSolo ? 1 : 0 ); + + MessageEnd(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +//----------------------------------------------------------------------------- +void CForceFeedback::PauseAll( CBasePlayer *player ) +{ + if ( !player ) + return; + + CSingleUserRecipientFilter user( player ); + + UserMessageBegin( user, "ForceFeedback" ); + + WRITE_BYTE( FFMSG_PAUSE ); // Pause effects + + MessageEnd(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +//----------------------------------------------------------------------------- +void CForceFeedback::ResumeAll( CBasePlayer *player ) +{ + if ( !player ) + return; + + CSingleUserRecipientFilter user( player ); + + UserMessageBegin( user, "ForceFeedback" ); + + WRITE_BYTE( FFMSG_RESUME ); // Resume effects + + MessageEnd(); +} \ No newline at end of file diff --git a/sp/src/game/server/fourwheelvehiclephysics.cpp b/sp/src/game/server/fourwheelvehiclephysics.cpp new file mode 100644 index 00000000..ba4a827a --- /dev/null +++ b/sp/src/game/server/fourwheelvehiclephysics.cpp @@ -0,0 +1,1482 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A moving vehicle that is used as a battering ram +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "fourwheelvehiclephysics.h" +#include "engine/IEngineSound.h" +#include "soundenvelope.h" +#include "in_buttons.h" +#include "player.h" +#include "IEffects.h" +#include "physics_saverestore.h" +#include "vehicle_base.h" +#include "isaverestore.h" +#include "movevars_shared.h" +#include "te_effect_dispatch.h" +#include "particle_parse.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define STICK_EXTENTS 400.0f + + +#define DUST_SPEED 5 // speed at which dust starts +#define REAR_AXLE 1 // indexes of axlex +#define FRONT_AXLE 0 +#define MAX_GUAGE_SPEED 100.0 // 100 mph is max speed shown on guage + +#define BRAKE_MAX_VALUE 1.0f +#define BRAKE_BACK_FORWARD_SCALAR 2.0f + +ConVar r_vehicleBrakeRate( "r_vehicleBrakeRate", "1.5", FCVAR_CHEAT ); + +ConVar xbox_throttlebias("xbox_throttlebias", "100", FCVAR_ARCHIVE ); +ConVar xbox_throttlespoof("xbox_throttlespoof", "200", FCVAR_ARCHIVE ); +ConVar xbox_autothrottle("xbox_autothrottle", "1", FCVAR_ARCHIVE ); +ConVar xbox_steering_deadzone( "xbox_steering_deadzone", "0.0" ); + +// remaps an angular variable to a 3 band function: +// 0 <= t < start : f(t) = 0 +// start <= t <= end : f(t) = end * spline(( t-start) / (end-start) ) // s curve between clamped and linear +// end < t : f(t) = t +float RemapAngleRange( float startInterval, float endInterval, float value ) +{ + // Fixup the roll + value = AngleNormalize( value ); + float absAngle = fabs(value); + + // beneath cutoff? + if ( absAngle < startInterval ) + { + value = 0; + } + // in spline range? + else if ( absAngle <= endInterval ) + { + float newAngle = SimpleSpline( (absAngle - startInterval) / (endInterval-startInterval) ) * endInterval; + // grab the sign from the initial value + if ( value < 0 ) + { + newAngle *= -1; + } + value = newAngle; + } + // else leave it alone, in linear range + + return value; +} + +enum vehicle_pose_params +{ + VEH_FL_WHEEL_HEIGHT=0, + VEH_FR_WHEEL_HEIGHT, + VEH_RL_WHEEL_HEIGHT, + VEH_RR_WHEEL_HEIGHT, + VEH_FL_WHEEL_SPIN, + VEH_FR_WHEEL_SPIN, + VEH_RL_WHEEL_SPIN, + VEH_RR_WHEEL_SPIN, + VEH_STEER, + VEH_ACTION, + VEH_SPEEDO, + +}; + + +BEGIN_DATADESC_NO_BASE( CFourWheelVehiclePhysics ) + +// These two are reset every time +// DEFINE_FIELD( m_pOuter, FIELD_EHANDLE ), +// m_pOuterServerVehicle; + + // Quiet down classcheck + // DEFINE_FIELD( m_controls, vehicle_controlparams_t ), + + // Controls + DEFINE_FIELD( m_controls.throttle, FIELD_FLOAT ), + DEFINE_FIELD( m_controls.steering, FIELD_FLOAT ), + DEFINE_FIELD( m_controls.brake, FIELD_FLOAT ), + DEFINE_FIELD( m_controls.boost, FIELD_FLOAT ), + DEFINE_FIELD( m_controls.handbrake, FIELD_BOOLEAN ), + DEFINE_FIELD( m_controls.handbrakeLeft, FIELD_BOOLEAN ), + DEFINE_FIELD( m_controls.handbrakeRight, FIELD_BOOLEAN ), + DEFINE_FIELD( m_controls.brakepedal, FIELD_BOOLEAN ), + DEFINE_FIELD( m_controls.bHasBrakePedal, FIELD_BOOLEAN ), + + // This has to be handled by the containing class owing to 'owner' issues +// DEFINE_PHYSPTR( m_pVehicle ), + + DEFINE_FIELD( m_nSpeed, FIELD_INTEGER ), + DEFINE_FIELD( m_nLastSpeed, FIELD_INTEGER ), + DEFINE_FIELD( m_nRPM, FIELD_INTEGER ), + DEFINE_FIELD( m_fLastBoost, FIELD_FLOAT ), + DEFINE_FIELD( m_nBoostTimeLeft, FIELD_INTEGER ), + DEFINE_FIELD( m_nHasBoost, FIELD_INTEGER ), + + DEFINE_FIELD( m_maxThrottle, FIELD_FLOAT ), + DEFINE_FIELD( m_flMaxRevThrottle, FIELD_FLOAT ), + DEFINE_FIELD( m_flMaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_actionSpeed, FIELD_FLOAT ), + + // This has to be handled by the containing class owing to 'owner' issues +// DEFINE_PHYSPTR_ARRAY( m_pWheels ), + + DEFINE_FIELD( m_wheelCount, FIELD_INTEGER ), + + DEFINE_ARRAY( m_wheelPosition, FIELD_VECTOR, 4 ), + DEFINE_ARRAY( m_wheelRotation, FIELD_VECTOR, 4 ), + DEFINE_ARRAY( m_wheelBaseHeight, FIELD_FLOAT, 4 ), + DEFINE_ARRAY( m_wheelTotalHeight, FIELD_FLOAT, 4 ), + DEFINE_ARRAY( m_poseParameters, FIELD_INTEGER, 12 ), + DEFINE_FIELD( m_actionValue, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_actionScale, FIELD_FLOAT, "actionScale" ), + DEFINE_FIELD( m_debugRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_throttleRate, FIELD_FLOAT ), + DEFINE_FIELD( m_throttleStartTime, FIELD_FLOAT ), + DEFINE_FIELD( m_throttleActiveTime, FIELD_FLOAT ), + DEFINE_FIELD( m_turboTimer, FIELD_FLOAT ), + + DEFINE_FIELD( m_flVehicleVolume, FIELD_FLOAT ), + DEFINE_FIELD( m_bIsOn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bLastThrottle, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bLastBoost, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bLastSkid, FIELD_BOOLEAN ), +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +BEGIN_SCRIPTDESC_ROOT( CFourWheelVehiclePhysics, "Handler for four-wheel vehicle physics." ) + + DEFINE_SCRIPTFUNC( SetThrottle, "Sets the throttle." ) + DEFINE_SCRIPTFUNC( SetMaxThrottle, "Sets the max throttle." ) + DEFINE_SCRIPTFUNC( SetMaxReverseThrottle, "Sets the max reverse throttle." ) + DEFINE_SCRIPTFUNC( SetSteering, "Sets the steering." ) + DEFINE_SCRIPTFUNC( SetSteeringDegrees, "Sets the degrees of steering." ) + DEFINE_SCRIPTFUNC( SetAction, "Sets the action." ) + DEFINE_SCRIPTFUNC( SetHandbrake, "Sets the handbrake." ) + DEFINE_SCRIPTFUNC( SetBoost, "Sets the boost." ) + DEFINE_SCRIPTFUNC( SetHasBrakePedal, "Sets whether a handbrake pedal exists." ) + + DEFINE_SCRIPTFUNC( SetDisableEngine, "Sets whether the engine is disabled." ) + DEFINE_SCRIPTFUNC( IsEngineDisabled, "Checks whether the engine is disabled." ) + + DEFINE_SCRIPTFUNC( EnableMotion, "Enables vehicle motion." ) + DEFINE_SCRIPTFUNC( DisableMotion, "Disables vehicle motion." ) + + DEFINE_SCRIPTFUNC( GetSpeed, "Gets the speed." ) + DEFINE_SCRIPTFUNC( GetMaxSpeed, "Gets the max speed." ) + DEFINE_SCRIPTFUNC( GetRPM, "Gets the RPM." ) + DEFINE_SCRIPTFUNC( GetThrottle, "Gets the throttle." ) + DEFINE_SCRIPTFUNC( HasBoost, "Checks if the vehicle has the ability to boost." ) + DEFINE_SCRIPTFUNC( BoostTimeLeft, "Gets how much time is left in any current boost." ) + DEFINE_SCRIPTFUNC( IsBoosting, "Checks if the vehicle is boosting." ) + DEFINE_SCRIPTFUNC( GetHLSpeed, "Gets HL speed." ) + DEFINE_SCRIPTFUNC( GetSteering, "Gets the steeering." ) + DEFINE_SCRIPTFUNC( GetSteeringDegrees, "Gets the degrees of steeering." ) + +END_SCRIPTDESC(); +#endif + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CFourWheelVehiclePhysics::CFourWheelVehiclePhysics( CBaseAnimating *pOuter ) +{ + m_flVehicleVolume = 0.5; + m_pOuter = NULL; + m_pOuterServerVehicle = NULL; + m_flMaxSpeed = 30; +} + +//----------------------------------------------------------------------------- +// Destructor +//----------------------------------------------------------------------------- +CFourWheelVehiclePhysics::~CFourWheelVehiclePhysics () +{ + physenv->DestroyVehicleController( m_pVehicle ); +} + +//----------------------------------------------------------------------------- +// A couple wrapper methods to perform common operations +//----------------------------------------------------------------------------- +inline int CFourWheelVehiclePhysics::LookupPoseParameter( const char *szName ) +{ + return m_pOuter->LookupPoseParameter( szName ); +} + +inline float CFourWheelVehiclePhysics::GetPoseParameter( int iParameter ) +{ + return m_pOuter->GetPoseParameter( iParameter ); +} + +inline float CFourWheelVehiclePhysics::SetPoseParameter( int iParameter, float flValue ) +{ + Assert(IsFinite(flValue)); + return m_pOuter->SetPoseParameter( iParameter, flValue ); +} + +inline bool CFourWheelVehiclePhysics::GetAttachment( const char *szName, Vector &origin, QAngle &angles ) +{ + return m_pOuter->GetAttachment( szName, origin, angles ); +} + +//----------------------------------------------------------------------------- +// Methods related to spawn +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::InitializePoseParameters() +{ + m_poseParameters[VEH_FL_WHEEL_HEIGHT] = LookupPoseParameter( "vehicle_wheel_fl_height" ); + m_poseParameters[VEH_FR_WHEEL_HEIGHT] = LookupPoseParameter( "vehicle_wheel_fr_height" ); + m_poseParameters[VEH_RL_WHEEL_HEIGHT] = LookupPoseParameter( "vehicle_wheel_rl_height" ); + m_poseParameters[VEH_RR_WHEEL_HEIGHT] = LookupPoseParameter( "vehicle_wheel_rr_height" ); + m_poseParameters[VEH_FL_WHEEL_SPIN] = LookupPoseParameter( "vehicle_wheel_fl_spin" ); + m_poseParameters[VEH_FR_WHEEL_SPIN] = LookupPoseParameter( "vehicle_wheel_fr_spin" ); + m_poseParameters[VEH_RL_WHEEL_SPIN] = LookupPoseParameter( "vehicle_wheel_rl_spin" ); + m_poseParameters[VEH_RR_WHEEL_SPIN] = LookupPoseParameter( "vehicle_wheel_rr_spin" ); + m_poseParameters[VEH_STEER] = LookupPoseParameter( "vehicle_steer" ); + m_poseParameters[VEH_ACTION] = LookupPoseParameter( "vehicle_action" ); + m_poseParameters[VEH_SPEEDO] = LookupPoseParameter( "vehicle_guage" ); + + + // move the wheels to a neutral position + SetPoseParameter( m_poseParameters[VEH_SPEEDO], 0 ); + SetPoseParameter( m_poseParameters[VEH_STEER], 0 ); + SetPoseParameter( m_poseParameters[VEH_FL_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_FR_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_RL_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_RR_WHEEL_HEIGHT], 0 ); + m_pOuter->InvalidateBoneCache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Parses the vehicle's script +//----------------------------------------------------------------------------- +bool CFourWheelVehiclePhysics::ParseVehicleScript( const char *pScriptName, solid_t &solid, vehicleparams_t &vehicle) +{ + // Physics keeps a cache of these to share among spawns of vehicles or flush for debugging + PhysFindOrAddVehicleScript( pScriptName, &vehicle, NULL ); + + m_debugRadius = vehicle.axles[0].wheels.radius; + CalcWheelData( vehicle ); + + PhysModelParseSolid( solid, m_pOuter, m_pOuter->GetModelIndex() ); + + // Allow the script to shift the center of mass + if ( vehicle.body.massCenterOverride != vec3_origin ) + { + solid.massCenterOverride = vehicle.body.massCenterOverride; + solid.params.massCenterOverride = &solid.massCenterOverride; + } + + // allow script to change the mass of the vehicle body + if ( vehicle.body.massOverride > 0 ) + { + solid.params.mass = vehicle.body.massOverride; + } + + return true; +} + +void CFourWheelVehiclePhysics::CalcWheelData( vehicleparams_t &vehicle ) +{ + const char *pWheelAttachments[4] = { "wheel_fl", "wheel_fr", "wheel_rl", "wheel_rr" }; + Vector left, right; + QAngle dummy; + SetPoseParameter( m_poseParameters[VEH_FL_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_FR_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_RL_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_RR_WHEEL_HEIGHT], 0 ); + m_pOuter->InvalidateBoneCache(); + if ( GetAttachment( "wheel_fl", left, dummy ) && GetAttachment( "wheel_fr", right, dummy ) ) + { + VectorITransform( left, m_pOuter->EntityToWorldTransform(), left ); + VectorITransform( right, m_pOuter->EntityToWorldTransform(), right ); + Vector center = (left + right) * 0.5; + vehicle.axles[0].offset = center; + vehicle.axles[0].wheelOffset = right - center; + // Cache the base height of the wheels in body space + m_wheelBaseHeight[0] = left.z; + m_wheelBaseHeight[1] = right.z; + } + + if ( GetAttachment( "wheel_rl", left, dummy ) && GetAttachment( "wheel_rr", right, dummy ) ) + { + VectorITransform( left, m_pOuter->EntityToWorldTransform(), left ); + VectorITransform( right, m_pOuter->EntityToWorldTransform(), right ); + Vector center = (left + right) * 0.5; + vehicle.axles[1].offset = center; + vehicle.axles[1].wheelOffset = right - center; + // Cache the base height of the wheels in body space + m_wheelBaseHeight[2] = left.z; + m_wheelBaseHeight[3] = right.z; + } + SetPoseParameter( m_poseParameters[VEH_FL_WHEEL_HEIGHT], 1 ); + SetPoseParameter( m_poseParameters[VEH_FR_WHEEL_HEIGHT], 1 ); + SetPoseParameter( m_poseParameters[VEH_RL_WHEEL_HEIGHT], 1 ); + SetPoseParameter( m_poseParameters[VEH_RR_WHEEL_HEIGHT], 1 ); + m_pOuter->InvalidateBoneCache(); + if ( GetAttachment( "wheel_fl", left, dummy ) && GetAttachment( "wheel_fr", right, dummy ) ) + { + VectorITransform( left, m_pOuter->EntityToWorldTransform(), left ); + VectorITransform( right, m_pOuter->EntityToWorldTransform(), right ); + // Cache the height range of the wheels in body space + m_wheelTotalHeight[0] = m_wheelBaseHeight[0] - left.z; + m_wheelTotalHeight[1] = m_wheelBaseHeight[1] - right.z; + vehicle.axles[0].wheels.springAdditionalLength = m_wheelTotalHeight[0]; + } + + if ( GetAttachment( "wheel_rl", left, dummy ) && GetAttachment( "wheel_rr", right, dummy ) ) + { + VectorITransform( left, m_pOuter->EntityToWorldTransform(), left ); + VectorITransform( right, m_pOuter->EntityToWorldTransform(), right ); + // Cache the height range of the wheels in body space + m_wheelTotalHeight[2] = m_wheelBaseHeight[0] - left.z; + m_wheelTotalHeight[3] = m_wheelBaseHeight[1] - right.z; + vehicle.axles[1].wheels.springAdditionalLength = m_wheelTotalHeight[2]; + } + for ( int i = 0; i < 4; i++ ) + { + if ( m_wheelTotalHeight[i] == 0.0f ) + { + DevWarning("Vehicle %s has invalid wheel attachment for %s - no movement\n", STRING(m_pOuter->GetModelName()), pWheelAttachments[i]); + m_wheelTotalHeight[i] = 1.0f; + } + } + + SetPoseParameter( m_poseParameters[VEH_FL_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_FR_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_RL_WHEEL_HEIGHT], 0 ); + SetPoseParameter( m_poseParameters[VEH_RR_WHEEL_HEIGHT], 0 ); + m_pOuter->InvalidateBoneCache(); + + // Get raytrace offsets if they exist. + if ( GetAttachment( "raytrace_fl", left, dummy ) && GetAttachment( "raytrace_fr", right, dummy ) ) + { + VectorITransform( left, m_pOuter->EntityToWorldTransform(), left ); + VectorITransform( right, m_pOuter->EntityToWorldTransform(), right ); + Vector center = ( left + right ) * 0.5; + vehicle.axles[0].raytraceCenterOffset = center; + vehicle.axles[0].raytraceOffset = right - center; + } + + if ( GetAttachment( "raytrace_rl", left, dummy ) && GetAttachment( "raytrace_rr", right, dummy ) ) + { + VectorITransform( left, m_pOuter->EntityToWorldTransform(), left ); + VectorITransform( right, m_pOuter->EntityToWorldTransform(), right ); + Vector center = ( left + right ) * 0.5; + vehicle.axles[1].raytraceCenterOffset = center; + vehicle.axles[1].raytraceOffset = right - center; + } +} + + +//----------------------------------------------------------------------------- +// Spawns the vehicle +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::Spawn( ) +{ + Assert( m_pOuter ); + + m_actionValue = 0; + m_actionSpeed = 0; + + m_bIsOn = false; + m_controls.handbrake = false; + m_controls.handbrakeLeft = false; + m_controls.handbrakeRight = false; + m_controls.bHasBrakePedal = true; + m_controls.bAnalogSteering = false; + + SetMaxThrottle( 1.0 ); + SetMaxReverseThrottle( -1.0f ); + + InitializePoseParameters(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Initializes the vehicle physics +// Called by our outer vehicle in it's Spawn() +//----------------------------------------------------------------------------- +bool CFourWheelVehiclePhysics::Initialize( const char *pVehicleScript, unsigned int nVehicleType ) +{ + // Ok, turn on the simulation now + // FIXME: Disabling collisions here is necessary because we seem to be + // getting a one-frame collision between the old + new collision models + if ( m_pOuter->VPhysicsGetObject() ) + { + m_pOuter->VPhysicsGetObject()->EnableCollisions(false); + } + m_pOuter->VPhysicsDestroyObject(); + + // Create the vphysics model + teleport it into position + solid_t solid; + vehicleparams_t vehicle; + if (!ParseVehicleScript( pVehicleScript, solid, vehicle )) + { + UTIL_Remove(m_pOuter); + return false; + } + + // NOTE: this needs to be greater than your max framerate (so zero is still instant) + m_throttleRate = 10000.0; + if ( vehicle.engine.throttleTime > 0 ) + { + m_throttleRate = 1.0 / vehicle.engine.throttleTime; + } + + m_flMaxSpeed = vehicle.engine.maxSpeed; + + IPhysicsObject *pBody = m_pOuter->VPhysicsInitNormal( SOLID_VPHYSICS, 0, false, &solid ); + PhysSetGameFlags( pBody, FVPHYSICS_NO_SELF_COLLISIONS | FVPHYSICS_MULTIOBJECT_ENTITY ); + m_pVehicle = physenv->CreateVehicleController( pBody, vehicle, nVehicleType, physgametrace ); + m_wheelCount = m_pVehicle->GetWheelCount(); + for ( int i = 0; i < m_wheelCount; i++ ) + { + m_pWheels[i] = m_pVehicle->GetWheel( i ); + } + return true; +} + + +//----------------------------------------------------------------------------- +// Various steering parameters +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetThrottle( float flThrottle ) +{ + m_controls.throttle = flThrottle; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetMaxThrottle( float flMaxThrottle ) +{ + m_maxThrottle = flMaxThrottle; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetMaxReverseThrottle( float flMaxThrottle ) +{ + m_flMaxRevThrottle = flMaxThrottle; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetSteering( float flSteering, float flSteeringRate ) +{ + if ( !flSteeringRate ) + { + m_controls.steering = flSteering; + } + else + { + m_controls.steering = Approach( flSteering, m_controls.steering, flSteeringRate ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetSteeringDegrees( float flDegrees ) +{ + vehicleparams_t &vehicleParams = m_pVehicle->GetVehicleParamsForChange(); + vehicleParams.steering.degreesSlow = flDegrees; + vehicleParams.steering.degreesFast = flDegrees; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetAction( float flAction ) +{ + m_actionSpeed = flAction; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::TurnOn( ) +{ + if ( IsEngineDisabled() ) + return; + + if ( !m_bIsOn ) + { + m_pOuterServerVehicle->SoundStart(); + m_bIsOn = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::TurnOff( ) +{ + ResetControls(); + + if ( m_bIsOn ) + { + m_pOuterServerVehicle->SoundShutdown(); + m_bIsOn = false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetBoost( float flBoost ) +{ + if ( !IsEngineDisabled() ) + { + m_controls.boost = flBoost; + } +} + +//------------------------------------------------------ +// UpdateBooster - Calls UpdateBooster() in the vphysics +// code to allow the timer to be updated +// +// Returns: false if timer has expired (can use again and +// can stop think +// true if timer still running +//------------------------------------------------------ +bool CFourWheelVehiclePhysics::UpdateBooster( void ) +{ + float retval = m_pVehicle->UpdateBooster(gpGlobals->frametime ); + return ( retval > 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetHasBrakePedal( bool bHasBrakePedal ) +{ + m_controls.bHasBrakePedal = bHasBrakePedal; +} + +//----------------------------------------------------------------------------- +// Teleport +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::Teleport( matrix3x4_t& relativeTransform ) +{ + // We basically just have to make sure the wheels are in the right place + // after teleportation occurs + + for ( int i = 0; i < m_wheelCount; i++ ) + { + matrix3x4_t matrix, newMatrix; + m_pWheels[i]->GetPositionMatrix( &matrix ); + ConcatTransforms( relativeTransform, matrix, newMatrix ); + m_pWheels[i]->SetPositionMatrix( newMatrix, true ); + } + + // Wake the vehicle back up after a teleport + if ( m_pOuterServerVehicle && m_pOuterServerVehicle->GetFourWheelVehicle() ) + { + IPhysicsObject *pObj = m_pOuterServerVehicle->GetFourWheelVehicle()->VPhysicsGetObject(); + if ( pObj ) + { + pObj->Wake(); + } + } +} + +#if 1 +// For the #if 0 debug code below! +#define HL2IVP_FACTOR METERS_PER_INCH +#define IVP2HL(x) (float)(x * (1.0f/HL2IVP_FACTOR)) +#define HL2IVP(x) (double)(x * HL2IVP_FACTOR) +#endif + +//----------------------------------------------------------------------------- +// Debugging methods +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::DrawDebugGeometryOverlays() +{ + for ( int iWheel = 0; iWheel < m_wheelCount; iWheel++ ) + { + IPhysicsObject *pWheel = m_pVehicle->GetWheel( iWheel ); + float radius = pWheel->GetSphereRadius(); + + Vector vecPos; + QAngle vecRot; + pWheel->GetPosition( &vecPos, &vecRot ); + // draw the physics object position/orientation + NDebugOverlay::Sphere( vecPos, vecRot, radius, 0, 255, 0, 0, false, 0 ); + // draw the animation position/orientation + NDebugOverlay::Sphere(m_wheelPosition[iWheel], m_wheelRotation[iWheel], radius, 255, 255, 0, 0, false, 0); + } + + // Render vehicle data. + IPhysicsObject *pBody = m_pOuter->VPhysicsGetObject(); + if ( pBody ) + { + const vehicleparams_t vehicleParams = m_pVehicle->GetVehicleParams(); + + // Draw a red cube as the "center" of the vehicle. + Vector vecBodyPosition; + QAngle angBodyDirection; + pBody->GetPosition( &vecBodyPosition, &angBodyDirection ); + NDebugOverlay::BoxAngles( vecBodyPosition, Vector( -5, -5, -5 ), Vector( 5, 5, 5 ), angBodyDirection, 255, 0, 0, 0 ,0 ); + + matrix3x4_t matrix; + AngleMatrix( angBodyDirection, vecBodyPosition, matrix ); + + // Draw green cubes at axle centers. + Vector vecAxlePositions[2], vecAxlePositionsHL[2]; + vecAxlePositions[0] = vehicleParams.axles[0].offset; + vecAxlePositions[1] = vehicleParams.axles[1].offset; + + VectorTransform( vecAxlePositions[0], matrix, vecAxlePositionsHL[0] ); + VectorTransform( vecAxlePositions[1], matrix, vecAxlePositionsHL[1] ); + + NDebugOverlay::BoxAngles( vecAxlePositionsHL[0], Vector( -3, -3, -3 ), Vector( 3, 3, 3 ), angBodyDirection, 0, 255, 0, 0 ,0 ); + NDebugOverlay::BoxAngles( vecAxlePositionsHL[1], Vector( -3, -3, -3 ), Vector( 3, 3, 3 ), angBodyDirection, 0, 255, 0, 0 ,0 ); + + // Draw wheel raycasts in yellow + vehicle_debugcarsystem_t debugCarSystem; + m_pVehicle->GetCarSystemDebugData( debugCarSystem ); + for ( int iWheel = 0; iWheel < 4; ++iWheel ) + { + Vector vecStart, vecEnd, vecImpact; + + // Hack for now. + float tmpY = IVP2HL( debugCarSystem.vecWheelRaycasts[iWheel][0].z ); + vecStart.z = -IVP2HL( debugCarSystem.vecWheelRaycasts[iWheel][0].y ); + vecStart.y = tmpY; + vecStart.x = IVP2HL( debugCarSystem.vecWheelRaycasts[iWheel][0].x ); + + tmpY = IVP2HL( debugCarSystem.vecWheelRaycasts[iWheel][1].z ); + vecEnd.z = -IVP2HL( debugCarSystem.vecWheelRaycasts[iWheel][1].y ); + vecEnd.y = tmpY; + vecEnd.x = IVP2HL( debugCarSystem.vecWheelRaycasts[iWheel][1].x ); + + tmpY = IVP2HL( debugCarSystem.vecWheelRaycastImpacts[iWheel].z ); + vecImpact.z = -IVP2HL( debugCarSystem.vecWheelRaycastImpacts[iWheel].y ); + vecImpact.y = tmpY; + vecImpact.x = IVP2HL( debugCarSystem.vecWheelRaycastImpacts[iWheel].x ); + + NDebugOverlay::BoxAngles( vecStart, Vector( -1 , -1, -1 ), Vector( 1, 1, 1 ), angBodyDirection, 0, 255, 0, 0, 0 ); + NDebugOverlay::Line( vecStart, vecEnd, 255, 255, 0, true, 0 ); + NDebugOverlay::BoxAngles( vecEnd, Vector( -1, -1, -1 ), Vector( 1, 1, 1 ), angBodyDirection, 255, 0, 0, 0, 0 ); + + NDebugOverlay::BoxAngles( vecImpact, Vector( -0.5f , -0.5f, -0.5f ), Vector( 0.5f, 0.5f, 0.5f ), angBodyDirection, 0, 0, 255, 0, 0 ); + DebugDrawContactPoints( m_pVehicle->GetWheel(iWheel) ); + } + } +} + +int CFourWheelVehiclePhysics::DrawDebugTextOverlays( int nOffset ) +{ + const vehicle_operatingparams_t ¶ms = m_pVehicle->GetOperatingParams(); + char tempstr[512]; + Q_snprintf( tempstr,sizeof(tempstr), "Speed %.1f T/S/B (%.0f/%.0f/%.1f)", params.speed, m_controls.throttle, m_controls.steering, m_controls.brake ); + m_pOuter->EntityText( nOffset, tempstr, 0 ); + nOffset++; + Msg( "%s", tempstr ); + + Q_snprintf( tempstr,sizeof(tempstr), "Gear: %d, RPM %4d", params.gear, (int)params.engineRPM ); + m_pOuter->EntityText( nOffset, tempstr, 0 ); + nOffset++; + Msg( " %s\n", tempstr ); + + return nOffset; +} + +//---------------------------------------------------- +// Place dust at vector passed in +//---------------------------------------------------- +void CFourWheelVehiclePhysics::PlaceWheelDust( int wheelIndex, bool ignoreSpeed ) +{ + // New vehicles handle this deeper into the base class + if ( hl2_episodic.GetBool() ) + return; + + // Old dust + Vector vecPos, vecVel; + m_pVehicle->GetWheelContactPoint( wheelIndex, &vecPos, NULL ); + + vecVel.Random( -1.0f, 1.0f ); + vecVel.z = random->RandomFloat( 0.3f, 1.0f ); + + VectorNormalize( vecVel ); + + // Higher speeds make larger dust clouds + float flSize; + if ( ignoreSpeed ) + { + flSize = 1.0f; + } + else + { + flSize = RemapValClamped( m_nSpeed, DUST_SPEED, m_flMaxSpeed, 0.0f, 1.0f ); + } + + if ( flSize ) + { + CEffectData data; + + data.m_vOrigin = vecPos; + data.m_vNormal = vecVel; + data.m_flScale = flSize; + + DispatchEffect( "WheelDust", data ); + } +} + +//----------------------------------------------------------------------------- +// Frame-based updating +//----------------------------------------------------------------------------- +bool CFourWheelVehiclePhysics::Think() +{ + if (!m_pVehicle) + return false; + + // Update sound + physics state + const vehicle_operatingparams_t &carState = m_pVehicle->GetOperatingParams(); + const vehicleparams_t &vehicleData = m_pVehicle->GetVehicleParams(); + + // Set save data. + float carSpeed = fabs( INS2MPH( carState.speed ) ); + m_nLastSpeed = m_nSpeed; + m_nSpeed = ( int )carSpeed; + m_nRPM = ( int )carState.engineRPM; + m_nHasBoost = vehicleData.engine.boostDelay; // if we have any boost delay, vehicle has boost ability + + m_pVehicle->Update( gpGlobals->frametime, m_controls); + + // boost sounds + if( IsBoosting() && !m_bLastBoost ) + { + m_bLastBoost = true; + m_turboTimer = gpGlobals->curtime + 2.75f; // min duration for turbo sound + } + else if( !IsBoosting() && m_bLastBoost ) + { + if ( gpGlobals->curtime >= m_turboTimer ) + { + m_bLastBoost = false; + } + } + + m_fLastBoost = carState.boostDelay; + m_nBoostTimeLeft = carState.boostTimeLeft; + + // UNDONE: Use skid info from the physics system? + // Only check wheels if we're not being carried by a dropship + if ( m_pOuter->VPhysicsGetObject() && !m_pOuter->VPhysicsGetObject()->GetShadowController() ) + { + const float skidFactor = 0.15f; + const float minSpeed = DEFAULT_SKID_THRESHOLD / skidFactor; + // we have to slide at least 15% of our speed at higher speeds to make the skid sound (otherwise it can be too frequent) + float skidThreshold = m_bLastSkid ? DEFAULT_SKID_THRESHOLD : (carState.speed * 0.15f); + if ( skidThreshold < DEFAULT_SKID_THRESHOLD ) + { + // otherwise, ramp in the skid threshold to avoid the sound at really low speeds unless really skidding + skidThreshold = RemapValClamped( fabs(carState.speed), 0, minSpeed, DEFAULT_SKID_THRESHOLD*8, DEFAULT_SKID_THRESHOLD ); + } + // check for skidding, if we're skidding, need to play the sound + if ( carState.skidSpeed > skidThreshold && m_bIsOn ) + { + if ( !m_bLastSkid ) // only play sound once + { + m_bLastSkid = true; + CPASAttenuationFilter filter( m_pOuter ); + m_pOuterServerVehicle->PlaySound( VS_SKID_FRICTION_NORMAL ); + } + + // kick up dust from the wheels while skidding + for ( int i = 0; i < 4; i++ ) + { + PlaceWheelDust( i, true ); + } + } + else if ( m_bLastSkid == true ) + { + m_bLastSkid = false; + m_pOuterServerVehicle->StopSound( VS_SKID_FRICTION_NORMAL ); + } + + // toss dust up from the wheels of the vehicle if we're moving fast enough + if ( m_nSpeed >= DUST_SPEED && vehicleData.steering.dustCloud && m_bIsOn ) + { + for ( int i = 0; i < 4; i++ ) + { + PlaceWheelDust( i ); + } + } + } + + // Make the steering wheel match the input, with a little dampening. + #define STEER_DAMPING 0.8 + float flSteer = GetPoseParameter( m_poseParameters[VEH_STEER] ); + float flPhysicsSteer = carState.steeringAngle / vehicleData.steering.degreesSlow; + SetPoseParameter( m_poseParameters[VEH_STEER], (STEER_DAMPING * flSteer) + ((1 - STEER_DAMPING) * flPhysicsSteer) ); + + m_actionValue += m_actionSpeed * m_actionScale * gpGlobals->frametime; + SetPoseParameter( m_poseParameters[VEH_ACTION], m_actionValue ); + + // setup speedometer + if ( m_bIsOn == true ) + { + float displaySpeed = m_nSpeed / MAX_GUAGE_SPEED; + SetPoseParameter( m_poseParameters[VEH_SPEEDO], displaySpeed ); + } + + return m_bIsOn; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFourWheelVehiclePhysics::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + // must be a wheel + if ( pPhysics == m_pOuter->VPhysicsGetObject() ) + return true; + + // This is here so we can make the pose parameters of the wheels + // reflect their current physics state + for ( int i = 0; i < m_wheelCount; i++ ) + { + if ( pPhysics == m_pWheels[i] ) + { + Vector tmp; + pPhysics->GetPosition( &m_wheelPosition[i], &m_wheelRotation[i] ); + + // transform the wheel into body space + VectorITransform( m_wheelPosition[i], m_pOuter->EntityToWorldTransform(), tmp ); + SetPoseParameter( m_poseParameters[VEH_FL_WHEEL_HEIGHT + i], (m_wheelBaseHeight[i] - tmp.z) / m_wheelTotalHeight[i] ); + SetPoseParameter( m_poseParameters[VEH_FL_WHEEL_SPIN + i], -m_wheelRotation[i].z ); + return false; + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Shared code to compute the vehicle view position +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::GetVehicleViewPosition( const char *pViewAttachment, float flPitchFactor, Vector *pAbsOrigin, QAngle *pAbsAngles ) +{ + matrix3x4_t vehicleEyePosToWorld; + Vector vehicleEyeOrigin; + QAngle vehicleEyeAngles; + GetAttachment( pViewAttachment, vehicleEyeOrigin, vehicleEyeAngles ); + AngleMatrix( vehicleEyeAngles, vehicleEyePosToWorld ); + +#ifdef HL2_DLL + // View dampening. + if ( r_VehicleViewDampen.GetInt() ) + { + m_pOuterServerVehicle->GetFourWheelVehicle()->DampenEyePosition( vehicleEyeOrigin, vehicleEyeAngles ); + } +#endif + + // Compute the relative rotation between the unperterbed eye attachment + the eye angles + matrix3x4_t cameraToWorld; + AngleMatrix( *pAbsAngles, cameraToWorld ); + + matrix3x4_t worldToEyePos; + MatrixInvert( vehicleEyePosToWorld, worldToEyePos ); + + matrix3x4_t vehicleCameraToEyePos; + ConcatTransforms( worldToEyePos, cameraToWorld, vehicleCameraToEyePos ); + + // Now perterb the attachment point + vehicleEyeAngles.x = RemapAngleRange( PITCH_CURVE_ZERO * flPitchFactor, PITCH_CURVE_LINEAR, vehicleEyeAngles.x ); + vehicleEyeAngles.z = RemapAngleRange( ROLL_CURVE_ZERO * flPitchFactor, ROLL_CURVE_LINEAR, vehicleEyeAngles.z ); + AngleMatrix( vehicleEyeAngles, vehicleEyeOrigin, vehicleEyePosToWorld ); + + // Now treat the relative eye angles as being relative to this new, perterbed view position... + matrix3x4_t newCameraToWorld; + ConcatTransforms( vehicleEyePosToWorld, vehicleCameraToEyePos, newCameraToWorld ); + + // output new view abs angles + MatrixAngles( newCameraToWorld, *pAbsAngles ); + + // UNDONE: *pOrigin would already be correct in single player if the HandleView() on the server ran after vphysics + MatrixGetColumn( newCameraToWorld, 3, *pAbsOrigin ); +} + + +//----------------------------------------------------------------------------- +// Control initialization +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::ResetControls() +{ + m_controls.handbrake = true; + m_controls.handbrakeLeft = false; + m_controls.handbrakeRight = false; + m_controls.boost = 0; + m_controls.brake = 0.0f; + m_controls.throttle = 0; + m_controls.steering = 0; +} + +void CFourWheelVehiclePhysics::ReleaseHandbrake() +{ + m_controls.handbrake = false; +} + +void CFourWheelVehiclePhysics::SetHandbrake( bool bBrake ) +{ + m_controls.handbrake = bBrake; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::EnableMotion( void ) +{ + for( int iWheel = 0; iWheel < m_wheelCount; ++iWheel ) + { + m_pWheels[iWheel]->EnableMotion( true ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::DisableMotion( void ) +{ + Vector vecZero( 0.0f, 0.0f, 0.0f ); + AngularImpulse angNone( 0.0f, 0.0f, 0.0f ); + + for( int iWheel = 0; iWheel < m_wheelCount; ++iWheel ) + { + m_pWheels[iWheel]->SetVelocity( &vecZero, &angNone ); + m_pWheels[iWheel]->EnableMotion( false ); + } +} + +float CFourWheelVehiclePhysics::GetHLSpeed() const +{ + const vehicle_operatingparams_t &carState = m_pVehicle->GetOperatingParams(); + return carState.speed; +} + +float CFourWheelVehiclePhysics::GetSteering() const +{ + return m_controls.steering; +} + +float CFourWheelVehiclePhysics::GetSteeringDegrees() const +{ + const vehicleparams_t vehicleParams = m_pVehicle->GetVehicleParams(); + return vehicleParams.steering.degreesSlow; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SteeringRest( float carSpeed, const vehicleparams_t &vehicleData ) +{ + float flSteeringRate = RemapValClamped( carSpeed, vehicleData.steering.speedSlow, vehicleData.steering.speedFast, + vehicleData.steering.steeringRestRateSlow, vehicleData.steering.steeringRestRateFast ); + m_controls.steering = Approach(0, m_controls.steering, flSteeringRate * gpGlobals->frametime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SteeringTurn( float carSpeed, const vehicleparams_t &vehicleData, bool bTurnLeft, bool bBrake, bool bThrottle ) +{ + float flTargetSteering = bTurnLeft ? -1.0f : 1.0f; + // steering speeds are stored in MPH + float flSteeringRestRate = RemapValClamped( carSpeed, vehicleData.steering.speedSlow, vehicleData.steering.speedFast, + vehicleData.steering.steeringRestRateSlow, vehicleData.steering.steeringRestRateFast ); + + float carSpeedIns = MPH2INS(carSpeed); + // engine speeds are stored in in/s + if ( carSpeedIns > vehicleData.engine.maxSpeed ) + { + flSteeringRestRate = RemapValClamped( carSpeedIns, vehicleData.engine.maxSpeed, vehicleData.engine.boostMaxSpeed, vehicleData.steering.steeringRestRateFast, vehicleData.steering.steeringRestRateFast*0.5f ); + } + + const vehicle_operatingparams_t &carState = m_pVehicle->GetOperatingParams(); + bool bIsBoosting = carState.isTorqueBoosting; + + // if you're recovering from a boost and still going faster than max, use the boost steering values + bool bIsBoostRecover = (carState.boostTimeLeft == 100 || carState.boostTimeLeft == 0) ? false : true; + float boostMinSpeed = vehicleData.engine.maxSpeed * vehicleData.engine.autobrakeSpeedGain; + if ( !bIsBoosting && bIsBoostRecover && carSpeedIns > boostMinSpeed ) + { + bIsBoosting = true; + } + + if ( bIsBoosting ) + { + flSteeringRestRate *= vehicleData.steering.boostSteeringRestRateFactor; + } + else if ( bThrottle ) + { + flSteeringRestRate *= vehicleData.steering.throttleSteeringRestRateFactor; + } + + float flSteeringRate = RemapValClamped( carSpeed, vehicleData.steering.speedSlow, vehicleData.steering.speedFast, + vehicleData.steering.steeringRateSlow, vehicleData.steering.steeringRateFast ); + + if ( fabs(flSteeringRate) < flSteeringRestRate ) + { + if ( Sign(flTargetSteering) != Sign(m_controls.steering) ) + { + flSteeringRate = flSteeringRestRate; + } + } + if ( bIsBoosting ) + { + flSteeringRate *= vehicleData.steering.boostSteeringRateFactor; + } + else if ( bBrake ) + { + flSteeringRate *= vehicleData.steering.brakeSteeringRateFactor; + } + flSteeringRate *= gpGlobals->frametime; + m_controls.steering = Approach( flTargetSteering, m_controls.steering, flSteeringRate ); + m_controls.bAnalogSteering = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SteeringTurnAnalog( float carSpeed, const vehicleparams_t &vehicleData, float sidemove ) +{ + + // OLD Code +#if 0 + float flSteeringRate = STEERING_BASE_RATE; + + float factor = clamp( fabs( sidemove ) / STICK_EXTENTS, 0.0f, 1.0f ); + + factor *= 30; + flSteeringRate *= log( factor ); + flSteeringRate *= gpGlobals->frametime; + + SetSteering( sidemove < 0.0f ? -1 : 1, flSteeringRate ); +#else + // This is tested with gamepads with analog sticks. It gives full analog control allowing the player to hold shallow turns. + float steering = ( sidemove / STICK_EXTENTS ); + + float flSign = ( steering > 0 ) ? 1.0f : -1.0f; + float flSteerAdj = RemapValClamped( fabs( steering ), xbox_steering_deadzone.GetFloat(), 1.0f, 0.0f, 1.0f ); + + float flSteeringRate = RemapValClamped( carSpeed, vehicleData.steering.speedSlow, vehicleData.steering.speedFast, + vehicleData.steering.steeringRateSlow, vehicleData.steering.steeringRateFast ); + flSteeringRate *= vehicleData.steering.throttleSteeringRestRateFactor; + + m_controls.bAnalogSteering = true; + SetSteering( flSign * flSteerAdj, flSteeringRate * gpGlobals->frametime ); +#endif +} + +//----------------------------------------------------------------------------- +// Methods related to actually driving the vehicle +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::UpdateDriverControls( CUserCmd *cmd, float flFrameTime ) +{ + const float SPEED_THROTTLE_AS_BRAKE = 2.0f; + int nButtons = cmd->buttons; + + // Get vehicle data. + const vehicle_operatingparams_t &carState = m_pVehicle->GetOperatingParams(); + const vehicleparams_t &vehicleData = m_pVehicle->GetVehicleParams(); + + // Get current speed in miles/hour. + float flCarSign = 0.0f; + if (carState.speed >= SPEED_THROTTLE_AS_BRAKE) + { + flCarSign = 1.0f; + } + else if ( carState.speed <= -SPEED_THROTTLE_AS_BRAKE ) + { + flCarSign = -1.0f; + } + float carSpeed = fabs(INS2MPH(carState.speed)); + + // If going forward and turning hard, keep the throttle applied. + if( xbox_autothrottle.GetBool() && cmd->forwardmove > 0.0f ) + { + if( carSpeed > GetMaxSpeed() * 0.75 ) + { + if( fabs(cmd->sidemove) > cmd->forwardmove ) + { + cmd->forwardmove = STICK_EXTENTS; + } + } + } + + //Msg("F: %4.1f \tS: %4.1f!\tSTEER: %3.1f\n", cmd->forwardmove, cmd->sidemove, carState.steeringAngle); + // If changing direction, use default "return to zero" speed to more quickly transition. + if ( ( nButtons & IN_MOVELEFT ) || ( nButtons & IN_MOVERIGHT ) ) + { + bool bTurnLeft = ( (nButtons & IN_MOVELEFT) != 0 ); + bool bBrake = ((nButtons & IN_BACK) != 0); + bool bThrottleDown = ( (nButtons & IN_FORWARD) != 0 ) && !bBrake; + SteeringTurn( carSpeed, vehicleData, bTurnLeft, bBrake, bThrottleDown ); + } + else if ( cmd->sidemove != 0.0f ) + { + SteeringTurnAnalog( carSpeed, vehicleData, cmd->sidemove ); + } + else + { + SteeringRest( carSpeed, vehicleData ); + } + + // Set vehicle control inputs. + m_controls.boost = 0; + m_controls.handbrake = false; + m_controls.handbrakeLeft = false; + m_controls.handbrakeRight = false; + m_controls.brakepedal = false; + bool bThrottle; + + //------------------------------------------------------------------------- + // Analog throttle biasing - This code gives the player a bit of control stick + // 'slop' in the opposite direction that they are driving. If a player is + // driving forward and makes a hard turn in which the stick actually goes + // below neutral (toward reverse), this code continues to propel the car + // forward unless the player makes a significant motion towards reverse. + // (The inverse is true when driving in reverse and the stick is moved slightly forward) + //------------------------------------------------------------------------- + CBaseEntity *pDriver = m_pOuterServerVehicle->GetDriver(); + CBasePlayer *pPlayerDriver; + float flBiasThreshold = xbox_throttlebias.GetFloat(); + + if( pDriver && pDriver->IsPlayer() ) + { + pPlayerDriver = dynamic_cast(pDriver); + + if( cmd->forwardmove == 0.0f && (fabs(cmd->sidemove) < 200.0f) ) + { + // If the stick goes neutral, clear out the bias. When the bias is neutral, it will begin biasing + // in whichever direction the user next presses the analog stick. + pPlayerDriver->SetVehicleAnalogControlBias( VEHICLE_ANALOG_BIAS_NONE ); + } + else if( cmd->forwardmove > 0.0f) + { + if( pPlayerDriver->GetVehicleAnalogControlBias() == VEHICLE_ANALOG_BIAS_REVERSE ) + { + // Player is pushing forward, but the controller is currently biased for reverse driving. + // Must pass a threshold to be accepted as forward input. Otherwise we just spoof a reduced reverse input + // to keep the car moving in the direction the player probably expects. + if( cmd->forwardmove < flBiasThreshold ) + { + cmd->forwardmove = -xbox_throttlespoof.GetFloat(); + } + else + { + // Passed the threshold. Allow the direction change to occur. + pPlayerDriver->SetVehicleAnalogControlBias( VEHICLE_ANALOG_BIAS_FORWARD ); + } + } + else if( pPlayerDriver->GetVehicleAnalogControlBias() == VEHICLE_ANALOG_BIAS_NONE ) + { + pPlayerDriver->SetVehicleAnalogControlBias( VEHICLE_ANALOG_BIAS_FORWARD ); + } + } + else if( cmd->forwardmove < 0.0f ) + { + if( pPlayerDriver->GetVehicleAnalogControlBias() == VEHICLE_ANALOG_BIAS_FORWARD ) + { + // Inverse of above logic + if( cmd->forwardmove > -flBiasThreshold ) + { + cmd->forwardmove = xbox_throttlespoof.GetFloat(); + } + else + { + pPlayerDriver->SetVehicleAnalogControlBias( VEHICLE_ANALOG_BIAS_REVERSE ); + } + } + else if( pPlayerDriver->GetVehicleAnalogControlBias() == VEHICLE_ANALOG_BIAS_NONE ) + { + pPlayerDriver->SetVehicleAnalogControlBias( VEHICLE_ANALOG_BIAS_REVERSE ); + } + } + } + + //========================= + // analog control + //========================= + if( cmd->forwardmove > 0.0f ) + { + float flAnalogThrottle = cmd->forwardmove / STICK_EXTENTS; + + flAnalogThrottle = clamp( flAnalogThrottle, 0.25f, 1.0f ); + + bThrottle = true; + if ( m_controls.throttle < 0 ) + { + m_controls.throttle = 0; + } + + float flMaxThrottle = MAX( 0.1, m_maxThrottle ); + if ( m_controls.steering != 0 ) + { + float flThrottleReduce = 0; + + // ramp this in, don't just start at the slow speed reduction (helps accelerate from a stop) + if ( carSpeed < vehicleData.steering.speedSlow ) + { + flThrottleReduce = RemapValClamped( carSpeed, 0, vehicleData.steering.speedSlow, + 0, vehicleData.steering.turnThrottleReduceSlow ); + } + else + { + flThrottleReduce = RemapValClamped( carSpeed, vehicleData.steering.speedSlow, vehicleData.steering.speedFast, + vehicleData.steering.turnThrottleReduceSlow, vehicleData.steering.turnThrottleReduceFast ); + } + + float limit = 1.0f - (flThrottleReduce * fabs(m_controls.steering)); + if ( limit < 0 ) + limit = 0; + flMaxThrottle = MIN( flMaxThrottle, limit ); + } + + m_controls.throttle = Approach( flMaxThrottle * flAnalogThrottle, m_controls.throttle, flFrameTime * m_throttleRate ); + + // Apply the brake. + if ( ( flCarSign < 0.0f ) && m_controls.bHasBrakePedal ) + { + m_controls.brake = Approach( BRAKE_MAX_VALUE, m_controls.brake, flFrameTime * r_vehicleBrakeRate.GetFloat() * BRAKE_BACK_FORWARD_SCALAR ); + m_controls.brakepedal = true; + m_controls.throttle = 0.0f; + bThrottle = false; + } + else + { + m_controls.brake = 0.0f; + } + } + else if( cmd->forwardmove < 0.0f ) + { + float flAnalogBrake = fabs(cmd->forwardmove / STICK_EXTENTS); + + flAnalogBrake = clamp( flAnalogBrake, 0.25f, 1.0f ); + + bThrottle = true; + if ( m_controls.throttle > 0 ) + { + m_controls.throttle = 0; + } + + float flMaxThrottle = MIN( -0.1, m_flMaxRevThrottle ); + m_controls.throttle = Approach( flMaxThrottle * flAnalogBrake, m_controls.throttle, flFrameTime * m_throttleRate ); + + // Apply the brake. + if ( ( flCarSign > 0.0f ) && m_controls.bHasBrakePedal ) + { + m_controls.brake = Approach( BRAKE_MAX_VALUE, m_controls.brake, flFrameTime * r_vehicleBrakeRate.GetFloat() ); + m_controls.brakepedal = true; + m_controls.throttle = 0.0f; + bThrottle = false; + } + else + { + m_controls.brake = 0.0f; + } + } + // digital control + else if ( nButtons & IN_FORWARD ) + { + bThrottle = true; + if ( m_controls.throttle < 0 ) + { + m_controls.throttle = 0; + } + + float flMaxThrottle = MAX( 0.1, m_maxThrottle ); + + if ( m_controls.steering != 0 ) + { + float flThrottleReduce = 0; + + // ramp this in, don't just start at the slow speed reduction (helps accelerate from a stop) + if ( carSpeed < vehicleData.steering.speedSlow ) + { + flThrottleReduce = RemapValClamped( carSpeed, 0, vehicleData.steering.speedSlow, + 0, vehicleData.steering.turnThrottleReduceSlow ); + } + else + { + flThrottleReduce = RemapValClamped( carSpeed, vehicleData.steering.speedSlow, vehicleData.steering.speedFast, + vehicleData.steering.turnThrottleReduceSlow, vehicleData.steering.turnThrottleReduceFast ); + } + + float limit = 1.0f - (flThrottleReduce * fabs(m_controls.steering)); + if ( limit < 0 ) + limit = 0; + flMaxThrottle = MIN( flMaxThrottle, limit ); + } + + m_controls.throttle = Approach( flMaxThrottle, m_controls.throttle, flFrameTime * m_throttleRate ); + + // Apply the brake. + if ( ( flCarSign < 0.0f ) && m_controls.bHasBrakePedal ) + { + m_controls.brake = Approach( BRAKE_MAX_VALUE, m_controls.brake, flFrameTime * r_vehicleBrakeRate.GetFloat() * BRAKE_BACK_FORWARD_SCALAR ); + m_controls.brakepedal = true; + m_controls.throttle = 0.0f; + bThrottle = false; + } + else + { + m_controls.brake = 0.0f; + } + } + else if ( nButtons & IN_BACK ) + { + bThrottle = true; + if ( m_controls.throttle > 0 ) + { + m_controls.throttle = 0; + } + + float flMaxThrottle = MIN( -0.1, m_flMaxRevThrottle ); + m_controls.throttle = Approach( flMaxThrottle, m_controls.throttle, flFrameTime * m_throttleRate ); + + // Apply the brake. + if ( ( flCarSign > 0.0f ) && m_controls.bHasBrakePedal ) + { + m_controls.brake = Approach( BRAKE_MAX_VALUE, m_controls.brake, flFrameTime * r_vehicleBrakeRate.GetFloat() ); + m_controls.brakepedal = true; + m_controls.throttle = 0.0f; + bThrottle = false; + } + else + { + m_controls.brake = 0.0f; + } + } + else + { + bThrottle = false; + m_controls.throttle = 0; + m_controls.brake = 0.0f; + } + + if ( ( nButtons & IN_SPEED ) && !IsEngineDisabled() && bThrottle ) + { + m_controls.boost = 1.0f; + } + + // Using has brakepedal for handbrake as well. + if ( ( nButtons & IN_JUMP ) && m_controls.bHasBrakePedal ) + { + m_controls.handbrake = true; + + if ( cmd->sidemove < -100 ) + { + m_controls.handbrakeLeft = true; + } + else if ( cmd->sidemove > 100 ) + { + m_controls.handbrakeRight = true; + } + + // Prevent playing of the engine revup when we're braking + bThrottle = false; + } + + if ( IsEngineDisabled() ) + { + m_controls.throttle = 0.0f; + m_controls.handbrake = true; + bThrottle = false; + } + + // throttle sounds + // If we dropped a bunch of speed, restart the throttle + if ( bThrottle && (m_nLastSpeed > m_nSpeed && (m_nLastSpeed - m_nSpeed > 10)) ) + { + m_bLastThrottle = false; + } + + // throttle down now but not before??? (or we're braking) + if ( !m_controls.handbrake && !m_controls.brakepedal && bThrottle && !m_bLastThrottle ) + { + m_throttleStartTime = gpGlobals->curtime; // need to track how long throttle is down + m_bLastThrottle = true; + } + // throttle up now but not before?? + else if ( !bThrottle && m_bLastThrottle && IsEngineDisabled() == false ) + { + m_throttleActiveTime = gpGlobals->curtime - m_throttleStartTime; + m_bLastThrottle = false; + } + + float flSpeedPercentage = clamp( m_nSpeed / m_flMaxSpeed, 0.f, 1.f ); + vbs_sound_update_t params; + params.Defaults(); + params.bReverse = (m_controls.throttle < 0); + params.bThrottleDown = bThrottle; + params.bTurbo = IsBoosting(); + params.bVehicleInWater = m_pOuterServerVehicle->IsVehicleBodyInWater(); + params.flCurrentSpeedFraction = flSpeedPercentage; + params.flFrameTime = flFrameTime; + params.flWorldSpaceSpeed = carState.speed; + m_pOuterServerVehicle->SoundUpdate( params ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFourWheelVehiclePhysics::IsBoosting( void ) +{ + const vehicleparams_t *pVehicleParams = &m_pVehicle->GetVehicleParams(); + const vehicle_operatingparams_t *pVehicleOperating = &m_pVehicle->GetOperatingParams(); + if ( pVehicleParams && pVehicleOperating ) + { + if ( ( pVehicleOperating->boostDelay - pVehicleParams->engine.boostDelay ) > 0.0f ) + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelVehiclePhysics::SetDisableEngine( bool bDisable ) +{ + // Set the engine state. + m_pVehicle->SetEngineDisabled( bDisable ); +} + +static int AddPhysToList( IPhysicsObject **pList, int listMax, int count, IPhysicsObject *pPhys ) +{ + if ( pPhys ) + { + if ( count < listMax ) + { + pList[count] = pPhys; + count++; + } + } + return count; +} + +int CFourWheelVehiclePhysics::VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ) +{ + int count = 0; + // add the body + count = AddPhysToList( pList, listMax, count, m_pOuter->VPhysicsGetObject() ); + for ( int i = 0; i < 4; i++ ) + { + count = AddPhysToList( pList, listMax, count, m_pWheels[i] ); + } + return count; +} diff --git a/sp/src/game/server/fourwheelvehiclephysics.h b/sp/src/game/server/fourwheelvehiclephysics.h new file mode 100644 index 00000000..ab9e8506 --- /dev/null +++ b/sp/src/game/server/fourwheelvehiclephysics.h @@ -0,0 +1,218 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A base class that deals with four-wheel vehicles +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef FOUR_WHEEL_VEHICLE_PHYSICS_H +#define FOUR_WHEEL_VEHICLE_PHYSICS_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "vphysics/vehicles.h" +#include "vcollide_parse.h" +#include "datamap.h" +#include "vehicle_sounds.h" + +// in/sec to miles/hour +#define INS2MPH_SCALE ( 3600 * (1/5280.0f) * (1/12.0f) ) +#define INS2MPH(x) ( (x) * INS2MPH_SCALE ) +#define MPH2INS(x) ( (x) * (1/INS2MPH_SCALE) ) + +class CBaseAnimating; +class CFourWheelServerVehicle; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CFourWheelVehiclePhysics +{ +public: + DECLARE_DATADESC(); + + CFourWheelVehiclePhysics( CBaseAnimating *pOuter ); + ~CFourWheelVehiclePhysics (); + + // Call Precache + Spawn from the containing entity's Precache + Spawn methods + void Spawn(); + void SetOuter( CBaseAnimating *pOuter, CFourWheelServerVehicle *pServerVehicle ); + + // Initializes the vehicle physics so we can drive it + bool Initialize( const char *pScriptName, unsigned int nVehicleType ); + + void Teleport( matrix3x4_t& relativeTransform ); + bool VPhysicsUpdate( IPhysicsObject *pPhysics ); + bool Think(); + void PlaceWheelDust( int wheelIndex, bool ignoreSpeed = false ); + + void DrawDebugGeometryOverlays(); + int DrawDebugTextOverlays( int nOffset ); + + // Updates the controls based on user input + void UpdateDriverControls( CUserCmd *cmd, float flFrameTime ); + + // Various steering parameters + void SetThrottle( float flThrottle ); + void SetMaxThrottle( float flMaxThrottle ); + void SetMaxReverseThrottle( float flMaxThrottle ); + void SetSteering( float flSteering, float flSteeringRate ); + void SetSteeringDegrees( float flDegrees ); + void SetAction( float flAction ); + void TurnOn( ); + void TurnOff(); + void ReleaseHandbrake(); + void SetHandbrake( bool bBrake ); + bool IsOn() const { return m_bIsOn; } + void ResetControls(); + void SetBoost( float flBoost ); + bool UpdateBooster( void ); + void SetHasBrakePedal( bool bHasBrakePedal ); + + // Engine + void SetDisableEngine( bool bDisable ); + bool IsEngineDisabled( void ) { return m_pVehicle->IsEngineDisabled(); } + + // Enable/Disable Motion + void EnableMotion( void ); + void DisableMotion( void ); + + // Shared code to compute the vehicle view position + void GetVehicleViewPosition( const char *pViewAttachment, float flPitchFactor, Vector *pAbsPosition, QAngle *pAbsAngles ); + + IPhysicsObject *GetWheel( int iWheel ) { return m_pWheels[iWheel]; } + + int GetSpeed() const; + int GetMaxSpeed() const; + int GetRPM() const; + float GetThrottle() const; + bool HasBoost() const; + int BoostTimeLeft() const; + bool IsBoosting( void ); + float GetHLSpeed() const; + float GetSteering() const; + float GetSteeringDegrees() const; + IPhysicsVehicleController* GetVehicle(void) { return m_pVehicle; } + float GetWheelBaseHeight(int wheelIndex) { return m_wheelBaseHeight[wheelIndex]; } + float GetWheelTotalHeight(int wheelIndex) { return m_wheelTotalHeight[wheelIndex]; } + + IPhysicsVehicleController *GetVehicleController() { return m_pVehicle; } + const vehicleparams_t &GetVehicleParams( void ) { return m_pVehicle->GetVehicleParams(); } + const vehicle_controlparams_t &GetVehicleControls( void ) { return m_controls; } + const vehicle_operatingparams_t &GetVehicleOperatingParams( void ) { return m_pVehicle->GetOperatingParams(); } + + int VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ); + +private: + // engine sounds + void CalcWheelData( vehicleparams_t &vehicle ); + + void SteeringRest( float carSpeed, const vehicleparams_t &vehicleData ); + void SteeringTurn( float carSpeed, const vehicleparams_t &vehicleData, bool bTurnLeft, bool bBrake, bool bThrottle ); + void SteeringTurnAnalog( float carSpeed, const vehicleparams_t &vehicleData, float sidemove ); + + // A couple wrapper methods to perform common operations + int LookupPoseParameter( const char *szName ); + float GetPoseParameter( int iParameter ); + float SetPoseParameter( int iParameter, float flValue ); + bool GetAttachment ( const char *szName, Vector &origin, QAngle &angles ); + + void InitializePoseParameters(); + bool ParseVehicleScript( const char *pScriptName, solid_t &solid, vehicleparams_t &vehicle ); + +private: + // This is the entity that contains this class + CHandle m_pOuter; + CFourWheelServerVehicle *m_pOuterServerVehicle; + + vehicle_controlparams_t m_controls; + IPhysicsVehicleController *m_pVehicle; + + // Vehicle state info + int m_nSpeed; + int m_nLastSpeed; + int m_nRPM; + float m_fLastBoost; + int m_nBoostTimeLeft; + int m_nHasBoost; + + float m_maxThrottle; + float m_flMaxRevThrottle; + float m_flMaxSpeed; + float m_actionSpeed; + IPhysicsObject *m_pWheels[4]; + + int m_wheelCount; + + Vector m_wheelPosition[4]; + QAngle m_wheelRotation[4]; + float m_wheelBaseHeight[4]; + float m_wheelTotalHeight[4]; + int m_poseParameters[12]; + float m_actionValue; + float m_actionScale; + float m_debugRadius; + float m_throttleRate; + float m_throttleStartTime; + float m_throttleActiveTime; + float m_turboTimer; + + float m_flVehicleVolume; // NPC driven vehicles used louder sounds + bool m_bIsOn; + bool m_bLastThrottle; + bool m_bLastBoost; + bool m_bLastSkid; +}; + + +//----------------------------------------------------------------------------- +// Physics state.. +//----------------------------------------------------------------------------- +inline int CFourWheelVehiclePhysics::GetSpeed() const +{ + return m_nSpeed; +} + +inline int CFourWheelVehiclePhysics::GetMaxSpeed() const +{ + return INS2MPH(m_pVehicle->GetVehicleParams().engine.maxSpeed); +} + +inline int CFourWheelVehiclePhysics::GetRPM() const +{ + return m_nRPM; +} + +inline float CFourWheelVehiclePhysics::GetThrottle() const +{ + return m_controls.throttle; +} + +inline bool CFourWheelVehiclePhysics::HasBoost() const +{ + return m_nHasBoost != 0; +} + +inline int CFourWheelVehiclePhysics::BoostTimeLeft() const +{ + return m_nBoostTimeLeft; +} + +inline void CFourWheelVehiclePhysics::SetOuter( CBaseAnimating *pOuter, CFourWheelServerVehicle *pServerVehicle ) +{ + m_pOuter = pOuter; + m_pOuterServerVehicle = pServerVehicle; +} + +float RemapAngleRange( float startInterval, float endInterval, float value ); + +#define ROLL_CURVE_ZERO 5 // roll less than this is clamped to zero +#define ROLL_CURVE_LINEAR 45 // roll greater than this is copied out + +#define PITCH_CURVE_ZERO 10 // pitch less than this is clamped to zero +#define PITCH_CURVE_LINEAR 45 // pitch greater than this is copied out + +#endif // FOUR_WHEEL_VEHICLE_PHYSICS_H diff --git a/sp/src/game/server/func_areaportal.cpp b/sp/src/game/server/func_areaportal.cpp new file mode 100644 index 00000000..d2e50a35 --- /dev/null +++ b/sp/src/game/server/func_areaportal.cpp @@ -0,0 +1,390 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: area portal entity: toggles visibility areas on/off +// +// NOTE: These are not really brush entities. They are brush entities from a +// designer/worldcraft perspective, but by the time they reach the game, the +// brush model is gone and this is, in effect, a point entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "func_areaportalbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +enum areaportal_state +{ + AREAPORTAL_CLOSED = 0, + AREAPORTAL_OPEN = 1, +}; + + +class CAreaPortal : public CFuncAreaPortalBase +{ +public: + DECLARE_CLASS( CAreaPortal, CFuncAreaPortalBase ); + + CAreaPortal(); + + virtual void Spawn( void ); + virtual void Precache( void ); + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual int UpdateTransmitState(); + + // Input handlers + void InputOpen( inputdata_t &inputdata ); + void InputClose( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + virtual bool UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ); + +#ifdef MAPBASE + // For func_areaportal_oneway. + int GetPortalState() { return m_state; } +#endif + + DECLARE_DATADESC(); + +private: + bool UpdateState( void ); + + int m_state; +}; + +LINK_ENTITY_TO_CLASS( func_areaportal, CAreaPortal ); + +BEGIN_DATADESC( CAreaPortal ) + + DEFINE_KEYFIELD( m_portalNumber, FIELD_INTEGER, "portalnumber" ), + DEFINE_FIELD( m_state, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Open", InputOpen ), + DEFINE_INPUTFUNC( FIELD_VOID, "Close", InputClose ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + + // TODO: obsolete! remove + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputClose ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputOpen ), + +END_DATADESC() + + + +CAreaPortal::CAreaPortal() +{ + m_state = AREAPORTAL_OPEN; +} + + +void CAreaPortal::Spawn( void ) +{ + AddEffects( EF_NORECEIVESHADOW | EF_NOSHADOW ); + Precache(); +} + + +//----------------------------------------------------------------------------- +// Purpose: notify the engine of the state at startup/restore +//----------------------------------------------------------------------------- +void CAreaPortal::Precache( void ) +{ + UpdateState(); +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CAreaPortal::InputClose( inputdata_t &inputdata ) +{ + m_state = AREAPORTAL_CLOSED; + UpdateState(); +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CAreaPortal::InputOpen( inputdata_t &inputdata ) +{ + m_state = AREAPORTAL_OPEN; + UpdateState(); +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CAreaPortal::InputToggle( inputdata_t &inputdata ) +{ + m_state = ((m_state == AREAPORTAL_OPEN) ? AREAPORTAL_CLOSED : AREAPORTAL_OPEN); + UpdateState(); +} + + +bool CAreaPortal::UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ) +{ + if ( m_state ) + { + // We're not closed, so give the base class a chance to close it. + return BaseClass::UpdateVisibility( vOrigin, fovDistanceAdjustFactor, bIsOpenOnClient ); + } + else + { + bIsOpenOnClient = false; + return false; + } +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CAreaPortal::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( useType == USE_ON ) + { + m_state = AREAPORTAL_OPEN; + } + else if ( useType == USE_OFF ) + { + m_state = AREAPORTAL_CLOSED; + } + else + { + return; + } + + UpdateState(); +} + + +bool CAreaPortal::KeyValue( const char *szKeyName, const char *szValue ) +{ + if( FStrEq( szKeyName, "StartOpen" ) ) + { + m_state = (atoi(szValue) != 0) ? AREAPORTAL_OPEN : AREAPORTAL_CLOSED; + + return true; + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } +} + + +bool CAreaPortal::UpdateState() +{ + engine->SetAreaPortalState( m_portalNumber, m_state ); + return !!m_state; +} + + +int CAreaPortal::UpdateTransmitState() +{ + // Our brushes are kept around so don't transmit anything since we don't want to draw them. + return SetTransmitState( FL_EDICT_DONTSEND ); +} + +#ifdef MAPBASE +// An areaportal that automatically closes and opens depending on the direction of the client. +// http://developer.valvesoftware.com/wiki/CAreaPortalOneWay +class CAreaPortalOneWay : public CAreaPortal // CAPOW! +{ + DECLARE_CLASS( CAreaPortalOneWay, CAreaPortal ); + DECLARE_DATADESC(); + +public: + Vector m_vecOpenVector; + bool m_bAvoidPop; + bool m_bOneWayActive; + + void Spawn(); + void Activate(); + int Restore(IRestore &restore); + bool UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ); + + void InputDisableOneWay( inputdata_t &inputdata ); + void InputEnableOneWay( inputdata_t &inputdata ); + void InputToggleOneWay( inputdata_t &inputdata ); + void InputInvertOneWay( inputdata_t &inputdata ); + +protected: + void RemoteUpdate( bool IsOpen ); + + bool m_bRemotelyUpdated; + bool m_bRemoteCalcWasOpen; + CHandle m_hNextPortal; // This get saved to disc... + CAreaPortalOneWay* m_pNextPortal; // ...while this gets used at runtime, avoiding loads of casts + +private: + void UpdateNextPortal( bool IsOpen ); + + // These two are irrelevant once the entity has established itself + string_t m_strGroupName; + Vector m_vecOrigin_; // The portal won't compile properly if vecOrigin itself has a value, but it's okay to move something in at runtime +}; + +LINK_ENTITY_TO_CLASS( func_areaportal_oneway, CAreaPortalOneWay ); + +BEGIN_DATADESC( CAreaPortalOneWay ) + DEFINE_KEYFIELD( m_vecOpenVector, FIELD_VECTOR, "onewayfacing" ), + DEFINE_KEYFIELD( m_bAvoidPop, FIELD_BOOLEAN, "avoidpop" ), + DEFINE_KEYFIELD_NOT_SAVED( m_vecOrigin_, FIELD_VECTOR, "origin_" ), + DEFINE_KEYFIELD_NOT_SAVED( m_strGroupName, FIELD_STRING, "group" ), + DEFINE_FIELD( m_bOneWayActive, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hNextPortal, FIELD_EHANDLE ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableOneWay", InputDisableOneWay ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableOneWay", InputEnableOneWay ), + DEFINE_INPUTFUNC( FIELD_VOID, "ToggleOneWay", InputToggleOneWay ), + DEFINE_INPUTFUNC( FIELD_VOID, "InvertOneWay", InputInvertOneWay ), +END_DATADESC() + +void CAreaPortalOneWay::Spawn() +{ + // Convert our angle from Hammer to a proper vector + QAngle angOpenDir = QAngle( m_vecOpenVector.x, m_vecOpenVector.y, m_vecOpenVector.z ); + AngleVectors( angOpenDir, &m_vecOpenVector ); + + SetLocalOrigin(m_vecOrigin_); + m_bOneWayActive = true; + m_bRemotelyUpdated = false; + + BaseClass::Spawn(); +} + +void CAreaPortalOneWay::Activate() +{ + // Optimisation: share open/closed value for CAPOWs with the same GroupName. + if (m_strGroupName != NULL_STRING) + { + for( unsigned short i = GetPortalListElement(); i != g_AreaPortals.InvalidIndex(); i = g_AreaPortals.Next(i) ) + { + CAreaPortalOneWay* pCur = dynamic_cast(g_AreaPortals[i]); + + if ( pCur && pCur != this && strcmp( STRING(m_strGroupName),STRING(pCur->m_strGroupName) ) == 0 ) + { + m_pNextPortal = pCur; + m_hNextPortal = pCur; + break; + } + } + } + + BaseClass::Activate(); +} + +int CAreaPortalOneWay::Restore(IRestore &restore) +{ + if ( m_hNextPortal.IsValid() ) + m_pNextPortal = m_hNextPortal.Get(); + + return BaseClass::Restore(restore); +} + +// Disable the CAPOW (becomes a normal AP) +void CAreaPortalOneWay::InputDisableOneWay( inputdata_t &inputdata ) +{ + m_bOneWayActive = false; +} + +// Re-enable the CAPOW +void CAreaPortalOneWay::InputEnableOneWay( inputdata_t &inputdata ) +{ + m_bOneWayActive = true; +} + +// Toggle CAPOW +void CAreaPortalOneWay::InputToggleOneWay( inputdata_t &inputdata ) +{ + m_bOneWayActive = !m_bOneWayActive; +} + +// Flip the one way direction +void CAreaPortalOneWay::InputInvertOneWay( inputdata_t &inputdata ) +{ + m_vecOpenVector.Negate(); +} + +// Recieve a shared state from another CAPOW, then pass it on to the next +void CAreaPortalOneWay::RemoteUpdate( bool IsOpen ) +{ + m_bRemotelyUpdated = true; + m_bRemoteCalcWasOpen = IsOpen; + UpdateNextPortal(IsOpen); +} + +// Inline func since this code is required three times +inline void CAreaPortalOneWay::UpdateNextPortal( bool IsOpen ) +{ + if (m_pNextPortal) + m_pNextPortal->RemoteUpdate(IsOpen); +} + +#define VIEWER_PADDING 80 // Value copied from func_areaportalbase.cpp + +bool CAreaPortalOneWay::UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ) +{ + if (!m_bOneWayActive) + return BaseClass::UpdateVisibility( vOrigin, fovDistanceAdjustFactor, bIsOpenOnClient ); + + if( m_portalNumber == -1 || GetPortalState() == AREAPORTAL_CLOSED ) + { + bIsOpenOnClient = false; + return false; + } + + // Has another CAPOW on our plane already done a calculation? + // Note that the CAPOW chain is traversed with new values in RemoteUpdate(), NOT here + if (m_bRemotelyUpdated) + { + m_bRemotelyUpdated = false; + return m_bRemoteCalcWasOpen ? BaseClass::UpdateVisibility( vOrigin, fovDistanceAdjustFactor, bIsOpenOnClient ) : false; + } + + // *********************** + // If we've got this far then we're the first CAPOW in the chain this frame + // and need to calculate a value and pass it along said chain ourselves + // *********************** + + float dist = VIEWER_PADDING; // Assume open for backfacing tests... + VPlane plane; + if( engine->GetAreaPortalPlane(vOrigin,m_portalNumber,&plane) ) + dist = plane.DistTo(vOrigin); // ...but if we find a plane, use a genuine figure instead. + // This is done because GetAreaPortalPlane only works for + // portals facing the current area. + + // We can use LocalOrigin here because APs never have parents. + float dot = DotProduct(m_vecOpenVector,vOrigin - GetLocalOrigin()); + + if( dot > 0 ) + { + // We are on the open side of the portal. Pass the result on! + UpdateNextPortal(true); + + // The following backfacing check is the inverse of CFuncAreaPortalBase's: + // it /closes/ the portal if the camera is /behind/ the plane. IsOpenOnClient + // is left alone as per func_areaportalbase.h + return dist < -VIEWER_PADDING ? false : true; + } + else // Closed side + { + // To avoid latency pop when crossing the portal's plane, it is only + // closed on the client if said client is outside the "padding zone". + if ( !m_bAvoidPop || (m_bAvoidPop && dist > VIEWER_PADDING) ) + bIsOpenOnClient = false; + + // We are definitely closed on the server, however. + UpdateNextPortal(false); + return false; + } +} +#endif diff --git a/sp/src/game/server/func_areaportalbase.cpp b/sp/src/game/server/func_areaportalbase.cpp new file mode 100644 index 00000000..88cb2a61 --- /dev/null +++ b/sp/src/game/server/func_areaportalbase.cpp @@ -0,0 +1,71 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "func_areaportalbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// A sphere around the player used for backface culling of areaportals. +#define VIEWER_PADDING 80 + + +CUtlLinkedList g_AreaPortals; + + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CFuncAreaPortalBase ) + + DEFINE_FIELD( m_portalNumber, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_iPortalVersion, FIELD_INTEGER, "PortalVersion" ) +// DEFINE_FIELD( m_AreaPortalsElement, FIELD_SHORT ), + +END_DATADESC() + + + + +CFuncAreaPortalBase::CFuncAreaPortalBase() +{ + m_portalNumber = -1; + m_AreaPortalsElement = g_AreaPortals.AddToTail( this ); + m_iPortalVersion = 0; +} + + +CFuncAreaPortalBase::~CFuncAreaPortalBase() +{ + g_AreaPortals.Remove( m_AreaPortalsElement ); +} + + +bool CFuncAreaPortalBase::UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ) +{ + // NOTE: We leave bIsOpenOnClient alone on purpose here. See the header for a description of why. + + if( m_portalNumber == -1 ) + return false; + + // See if the viewer is on the backside. + VPlane plane; + if( !engine->GetAreaPortalPlane( vOrigin, m_portalNumber, &plane ) ) + return true; // leave it open if there's an error here for some reason + + bool bOpen = false; + if( plane.DistTo( vOrigin ) + VIEWER_PADDING > 0 ) + bOpen = true; + + return bOpen; +} + + + + diff --git a/sp/src/game/server/func_areaportalbase.h b/sp/src/game/server/func_areaportalbase.h new file mode 100644 index 00000000..124ece47 --- /dev/null +++ b/sp/src/game/server/func_areaportalbase.h @@ -0,0 +1,120 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef FUNC_AREAPORTALBASE_H +#define FUNC_AREAPORTALBASE_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" +#include "utllinkedlist.h" + + +// Shared stuff between door portals and window portals. +class CFuncAreaPortalBase : public CBaseEntity +{ + DECLARE_CLASS( CFuncAreaPortalBase, CBaseEntity ); +public: + DECLARE_DATADESC(); + + CFuncAreaPortalBase(); + virtual ~CFuncAreaPortalBase(); + + // Areaportals must be placed in each map for preprocess, they can't use transitions + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + // This is called each frame for each client to all portals to close + // when the viewer is far enough away, or on the backside. + // + // The default implementation closes the portal if the viewer (plus some padding) + // is on the backside of the portal. Return false if you close the portal. + // + // Returns whether or not the (server part of) the engine was told to shut the + // portal off for purposes of flowing through portals to determine which areas to + // send to the client. + // + // + // bIsOpenOnClient is usually the same as the return value but NOT always. Here's why (explained + // here in depth because this case was discovered in a lengthy debugging session): + // + // - Each CFuncAreaPortalBase represents two dareaportal_t's (matched by CFuncAreaPortalBase::m_portalNumber + // and dareaportal_t::m_PortalKey). Each dareaportal_t leads into one of the connected areas + // and the dareaportal_t's have opposite-facing planes. + // + // - The engine's SetAreaPortalState function takes a portal key and closes BOTH dareaportal_t's associated with it. + // + // - UpdateVisibility may decide a portal leading out of the _area you're sitting in_ can be closed + // for purposes of flowing through areas because you're on the backside of the dareaportal_t that + // you would flow out of. + // + // - At the same time, you might be able to look through the other dareaportal_t attached to + // that portal key (right back into the area you're standing in). + // + // - An illustration: + // + // -------------------- + // | | + // | |--------|aaaaaa| + // | | |bbbbbb| <---- aaaa and bbbb area both for PortalKey1 + // |**| | | + // | | | area | + // |**| | 2 | + // | | |------| + // | | | | + // | ---------- area | + // | 1 | + // |------------------| + // + // "aaaa" and "bbbb" each represent a different dareaportal_t, (aaa leads into area 2 and + // bbb leads into area 1). They both represent the same portal key though (call it PortalKey1). + // + // When standing in area 1 (where the "area 1" text is), the engine will check aaaa and it'll notice + // that you're on the wrong side of aaaa to be looking through it, so it'll say that PortalKey1 is closed + // for purposes of flowing through areas on the server (it turns out you can get to area 2 through + // the portal right above the "area 1" text). + // + // If you told the client that PortalKey1 was closed, then you'd get a pop when moving from area 1 + // to area 2 because the client would think you couldn't see through PortalKey1 into area 1 UNTIL + // the server transmitted the new updated PortalKey bits, which can be lagged. + // + // That's why we have bIsOpenOnClient which doesn't include this backfacing test. + // + // .. and they all lived happily ever after. + // The End + // + // + // + // Note: when you're standing in the space between the **'s, then the server would stop transmitting + // the contents of area 2 because there would be no portal you were on the correct side of to + // see into area 2. + virtual bool UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ); + +#ifdef MAPBASE + // For func_areaportal_oneway. + unsigned short GetPortalListElement() { return m_AreaPortalsElement; } +#endif + +public: + + // This matches two dareaportal_t::m_PortalKeys. + int m_portalNumber; + + int m_iPortalVersion; + +private: + + unsigned short m_AreaPortalsElement; // link into g_AreaPortals. +}; + + +extern CUtlLinkedList g_AreaPortals; + + + +#endif // FUNC_AREAPORTALBASE_H diff --git a/sp/src/game/server/func_areaportalwindow.cpp b/sp/src/game/server/func_areaportalwindow.cpp new file mode 100644 index 00000000..4e60f9fb --- /dev/null +++ b/sp/src/game/server/func_areaportalwindow.cpp @@ -0,0 +1,128 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "func_areaportalwindow.h" +#include "entitylist.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// The server will still send entities through a window even after it opaque +// to allow for net lag. +#define FADE_DIST_BUFFER 10 + + +LINK_ENTITY_TO_CLASS( func_areaportalwindow, CFuncAreaPortalWindow ); + + +IMPLEMENT_SERVERCLASS_ST( CFuncAreaPortalWindow, DT_FuncAreaPortalWindow ) + SendPropFloat( SENDINFO(m_flFadeDist), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flFadeStartDist), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flTranslucencyLimit), 0, SPROP_NOSCALE ), + + SendPropModelIndex(SENDINFO(m_iBackgroundModelIndex) ), +END_SEND_TABLE() + + +BEGIN_DATADESC( CFuncAreaPortalWindow ) + + DEFINE_KEYFIELD( m_portalNumber, FIELD_INTEGER, "portalnumber" ), + DEFINE_KEYFIELD( m_flFadeStartDist, FIELD_FLOAT, "FadeStartDist" ), + DEFINE_KEYFIELD( m_flFadeDist, FIELD_FLOAT, "FadeDist" ), + DEFINE_KEYFIELD( m_flTranslucencyLimit, FIELD_FLOAT, "TranslucencyLimit" ), + DEFINE_KEYFIELD( m_iBackgroundBModelName,FIELD_STRING, "BackgroundBModel" ), +// DEFINE_KEYFIELD( m_iBackgroundModelIndex,FIELD_INTEGER ), + + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFadeStartDistance", InputSetFadeStartDistance ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFadeEndDistance", InputSetFadeEndDistance ), + +END_DATADESC() + + + + +CFuncAreaPortalWindow::CFuncAreaPortalWindow() +{ + m_iBackgroundModelIndex = -1; +} + + +CFuncAreaPortalWindow::~CFuncAreaPortalWindow() +{ +} + + +void CFuncAreaPortalWindow::Spawn() +{ + Precache(); + + engine->SetAreaPortalState( m_portalNumber, 1 ); +} + + +void CFuncAreaPortalWindow::Activate() +{ + BaseClass::Activate(); + + // Find our background model. + CBaseEntity *pBackground = gEntList.FindEntityByName( NULL, m_iBackgroundBModelName ); + if( pBackground ) + { + m_iBackgroundModelIndex = modelinfo->GetModelIndex( STRING( pBackground->GetModelName() ) ); + pBackground->AddEffects( EF_NODRAW ); // we will draw for it. + } + + // Find our target and steal its bmodel. + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_target ); + if( pTarget ) + { + SetModel( STRING(pTarget->GetModelName()) ); + SetAbsOrigin( pTarget->GetAbsOrigin() ); + pTarget->AddEffects( EF_NODRAW ); // we will draw for it. + } +} + + +bool CFuncAreaPortalWindow::IsWindowOpen( const Vector &vOrigin, float fovDistanceAdjustFactor ) +{ + float flDist = CollisionProp()->CalcDistanceFromPoint( vOrigin ); + flDist *= fovDistanceAdjustFactor; + return ( flDist <= (m_flFadeDist + FADE_DIST_BUFFER) ); +} + + +bool CFuncAreaPortalWindow::UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ) +{ + if ( IsWindowOpen( vOrigin, fovDistanceAdjustFactor ) ) + { + return BaseClass::UpdateVisibility( vOrigin, fovDistanceAdjustFactor, bIsOpenOnClient ); + } + else + { + bIsOpenOnClient = false; + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Changes the fade start distance +// Input: float distance in inches +//----------------------------------------------------------------------------- +void CFuncAreaPortalWindow::InputSetFadeStartDistance( inputdata_t &inputdata ) +{ + m_flFadeStartDist = inputdata.value.Float(); +} + +//----------------------------------------------------------------------------- +// Purpose: Changes the fade end distance +// Input: float distance in inches +//----------------------------------------------------------------------------- +void CFuncAreaPortalWindow::InputSetFadeEndDistance( inputdata_t &inputdata ) +{ + m_flFadeDist = inputdata.value.Float(); +} \ No newline at end of file diff --git a/sp/src/game/server/func_areaportalwindow.h b/sp/src/game/server/func_areaportalwindow.h new file mode 100644 index 00000000..d128c843 --- /dev/null +++ b/sp/src/game/server/func_areaportalwindow.h @@ -0,0 +1,67 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef FUNC_AREAPORTALWINDOW_H +#define FUNC_AREAPORTALWINDOW_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" +#include "utllinkedlist.h" +#include "func_areaportalbase.h" + + +class CFuncAreaPortalWindow : public CFuncAreaPortalBase +{ +public: + DECLARE_CLASS( CFuncAreaPortalWindow, CFuncAreaPortalBase ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CFuncAreaPortalWindow(); + ~CFuncAreaPortalWindow(); + + +// Overrides. +public: + + virtual void Spawn(); + virtual void Activate(); + + +// CFuncAreaPortalBase stuff. +public: + + virtual bool UpdateVisibility( const Vector &vOrigin, float fovDistanceAdjustFactor, bool &bIsOpenOnClient ); + + +public: + // Returns false if the viewer is past the fadeout distance. + bool IsWindowOpen( const Vector &vOrigin, float fovDistanceAdjustFactor ); + +public: + + CNetworkVar( float, m_flFadeStartDist ); // Distance at which it starts fading (when <= this, alpha=m_flTranslucencyLimit). + CNetworkVar( float, m_flFadeDist ); // Distance at which it becomes solid. + + // 0-1 value - minimum translucency it's allowed to get to. + CNetworkVar( float, m_flTranslucencyLimit ); + + string_t m_iBackgroundBModelName; // string name of background bmodel + CNetworkVar( int, m_iBackgroundModelIndex ); + + //Input handlers + void InputSetFadeStartDistance( inputdata_t &inputdata ); + void InputSetFadeEndDistance( inputdata_t &inputdata ); +}; + + + +#endif // FUNC_AREAPORTALWINDOW_H diff --git a/sp/src/game/server/func_break.cpp b/sp/src/game/server/func_break.cpp new file mode 100644 index 00000000..bb7dff50 --- /dev/null +++ b/sp/src/game/server/func_break.cpp @@ -0,0 +1,1326 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements breakables and pushables. func_breakable is a bmodel +// that breaks into pieces after taking damage. +// +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "filters.h" +#include "func_break.h" +#include "decals.h" +#include "explode.h" +#include "in_buttons.h" +#include "physics.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "globals.h" +#include "util.h" +#include "physics_impact_damage.h" +#include "tier0/icommandline.h" + +#ifdef PORTAL + #include "portal_shareddefs.h" + #include "portal_util_shared.h" + #include "prop_portal_shared.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar func_break_max_pieces( "func_break_max_pieces", "15", FCVAR_ARCHIVE | FCVAR_REPLICATED ); +ConVar func_break_reduction_factor( "func_break_reduction_factor", ".5" ); + +#ifdef HL1_DLL +extern void PlayerPickupObject( CBasePlayer *pPlayer, CBaseEntity *pObject ); +#endif + +extern Vector g_vecAttackDir; + +// Just add more items to the bottom of this array and they will automagically be supported +// This is done instead of just a classname in the FGD so we can control which entities can +// be spawned, and still remain fairly flexible + +#ifndef HL1_DLL + const char *CBreakable::pSpawnObjects[] = + { + NULL, // 0 + "item_battery", // 1 + "item_healthkit", // 2 + "item_ammo_pistol", // 3 + "item_ammo_pistol_large", // 4 + "item_ammo_smg1", // 5 + "item_ammo_smg1_large", // 6 + "item_ammo_ar2", // 7 + "item_ammo_ar2_large", // 8 + "item_box_buckshot", // 9 + "item_flare_round", // 10 + "item_box_flare_rounds", // 11 + "item_rpg_round", // 12 + "unused (item_smg1_grenade) 13",// 13 + "item_box_sniper_rounds", // 14 + "unused (???"") 15", // 15 - split into two strings to avoid trigraph warning + "weapon_stunstick", // 16 + "unused (weapon_ar1) 17", // 17 + "weapon_ar2", // 18 + "unused (???"") 19", // 19 - split into two strings to avoid trigraph warning + "weapon_rpg", // 20 + "weapon_smg1", // 21 + "unused (weapon_smg2) 22", // 22 + "unused (weapon_slam) 23", // 23 + "weapon_shotgun", // 24 + "unused (weapon_molotov) 25",// 25 + "item_dynamic_resupply", // 26 + }; +#else + // Half-Life 1 spawn objects! + const char *CBreakable::pSpawnObjects[] = + { + NULL, // 0 + "item_battery", // 1 + "item_healthkit", // 2 + "weapon_glock", // 3 + "ammo_9mmclip", // 4 + "weapon_mp5", // 5 + "ammo_9mmAR", // 6 + "ammo_ARgrenades", // 7 + "weapon_shotgun", // 8 + "ammo_buckshot", // 9 + "weapon_crossbow", // 10 + "ammo_crossbow", // 11 + "weapon_357", // 12 + "ammo_357", // 13 + "weapon_rpg", // 14 + "ammo_rpgclip", // 15 + "ammo_gaussclip", // 16 + "weapon_handgrenade",// 17 + "weapon_tripmine", // 18 + "weapon_satchel", // 19 + "weapon_snark", // 20 + "weapon_hornetgun", // 21 + }; +#endif + +const char *pFGDPropData[] = +{ + NULL, + "Wooden.Tiny", + "Wooden.Small", + "Wooden.Medium", + "Wooden.Large", + "Wooden.Huge", + "Metal.Small", + "Metal.Medium", + "Metal.Large", + "Cardboard.Small", + "Cardboard.Medium", + "Cardboard.Large", + "Stone.Small", + "Stone.Medium", + "Stone.Large", + "Stone.Huge", + "Glass.Small", + "Plastic.Small", + "Plastic.Medium", + "Plastic.Large", + "Pottery.Small", + "Pottery.Medium", + "Pottery.Large", + "Pottery.Huge", + "Glass.Window", +}; + +LINK_ENTITY_TO_CLASS( func_breakable, CBreakable ); +BEGIN_DATADESC( CBreakable ) + + DEFINE_FIELD( m_Material, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_Explosion, FIELD_INTEGER, "explosion" ), + DEFINE_KEYFIELD( m_GibDir, FIELD_VECTOR, "gibdir" ), + DEFINE_FIELD( m_hBreaker, FIELD_EHANDLE ), + + // Don't need to save/restore these because we precache after restore + //DEFINE_FIELD( m_idShard, FIELD_INTEGER ), + DEFINE_FIELD( m_angle, FIELD_FLOAT ), + DEFINE_FIELD( m_iszGibModel, FIELD_STRING ), + DEFINE_FIELD( m_iszSpawnObject, FIELD_STRING ), + DEFINE_KEYFIELD( m_ExplosionMagnitude, FIELD_INTEGER, "explodemagnitude" ), + DEFINE_KEYFIELD( m_flPressureDelay, FIELD_FLOAT, "PressureDelay" ), + DEFINE_KEYFIELD( m_iMinHealthDmg, FIELD_INTEGER, "minhealthdmg" ), + DEFINE_FIELD( m_bTookPhysicsDamage, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iszPropData, FIELD_STRING ), + DEFINE_INPUT( m_impactEnergyScale, FIELD_FLOAT, "physdamagescale" ), + DEFINE_KEYFIELD( m_PerformanceMode, FIELD_INTEGER, "PerformanceMode" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Break", InputBreak ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddHealth", InputAddHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveHealth", InputRemoveHealth ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMass", InputSetMass ), + + // Function Pointers + DEFINE_ENTITYFUNC( BreakTouch ), + DEFINE_THINKFUNC( Die ), + + // Outputs + DEFINE_OUTPUT(m_OnBreak, "OnBreak"), + DEFINE_OUTPUT(m_OnHealthChanged, "OnHealthChanged"), + + DEFINE_FIELD( m_flDmgModBullet, FIELD_FLOAT ), + DEFINE_FIELD( m_flDmgModClub, FIELD_FLOAT ), + DEFINE_FIELD( m_flDmgModExplosive, FIELD_FLOAT ), + DEFINE_FIELD( m_iszPhysicsDamageTableName, FIELD_STRING ), + DEFINE_FIELD( m_iszBreakableModel, FIELD_STRING ), + DEFINE_FIELD( m_iBreakableSkin, FIELD_INTEGER ), + DEFINE_FIELD( m_iBreakableCount, FIELD_INTEGER ), + DEFINE_FIELD( m_iMaxBreakableSize, FIELD_INTEGER ), + DEFINE_FIELD( m_iszBasePropData, FIELD_STRING ), + DEFINE_FIELD( m_iInteractions, FIELD_INTEGER ), + DEFINE_FIELD( m_explodeRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_iszModelName, FIELD_STRING ), + + // Physics Influence + DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBreakable::KeyValue( const char *szKeyName, const char *szValue ) +{ + // UNDONE_WC: explicitly ignoring these fields, but they shouldn't be in the map file! + if (FStrEq(szKeyName, "material")) + { + int i = atoi( szValue); + + // 0:glass, 1:metal, 2:flesh, 3:wood + + if ((i < 0) || (i >= matLastMaterial)) + m_Material = matWood; + else + m_Material = (Materials)i; + } + else if (FStrEq(szKeyName, "deadmodel")) + { + } + else if (FStrEq(szKeyName, "shards")) + { +// m_iShards = atof(szValue); + } + else if (FStrEq(szKeyName, "gibmodel") ) + { + m_iszGibModel = AllocPooledString(szValue); + } + else if (FStrEq(szKeyName, "spawnobject") ) + { + int object = atoi( szValue ); + if ( object > 0 && object < ARRAYSIZE(pSpawnObjects) ) + m_iszSpawnObject = MAKE_STRING( pSpawnObjects[object] ); +#ifdef MAPBASE + else + m_iszSpawnObject = AllocPooledString(szValue); +#endif + } + else if (FStrEq(szKeyName, "propdata") ) + { + int pdata = atoi( szValue ); + if ( pdata > 0 && pdata < ARRAYSIZE(pFGDPropData) ) + { + m_iszPropData = MAKE_STRING( pFGDPropData[pdata] ); + } + else if ( pdata ) + { + // If you've hit this warning, it's probably because someone's added a new + // propdata field to func_breakables in the .fgd, and not added it to the + // pFGDPropData list. + Warning("func_breakable with invalid propdata %d.\n", pdata ); + } + } + else if (FStrEq(szKeyName, "lip") ) + { + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBreakable::Spawn( void ) +{ + // Initialize damage modifiers. Must be done before baseclass spawn. + m_flDmgModBullet = func_breakdmg_bullet.GetFloat(); + m_flDmgModClub = func_breakdmg_club.GetFloat(); + m_flDmgModExplosive = func_breakdmg_explosive.GetFloat(); + + ParsePropData(); + + Precache( ); + + if ( !m_iHealth || FBitSet( m_spawnflags, SF_BREAK_TRIGGER_ONLY ) ) + { + // This allows people to shoot at the glass (since it's penetrable) + if ( m_Material == matGlass ) + { + m_iHealth = 1; + } + + m_takedamage = DAMAGE_NO; + } + else + { + m_takedamage = DAMAGE_YES; + } + + m_iMaxHealth = ( m_iHealth > 0 ) ? m_iHealth : 1; + + SetSolid( SOLID_BSP ); + SetMoveType( MOVETYPE_PUSH ); + + // this is a hack to shoot the gibs in a specific yaw/direction + m_angle = GetLocalAngles().y; + SetLocalAngles( vec3_angle ); + + SetModel( STRING( GetModelName() ) );//set size and link into world. + + SetTouch( &CBreakable::BreakTouch ); + if ( FBitSet( m_spawnflags, SF_BREAK_TRIGGER_ONLY ) ) // Only break on trigger + { + SetTouch( NULL ); + } + + // Flag unbreakable glass as "worldbrush" so it will block ALL tracelines + if ( !IsBreakable() && m_nRenderMode != kRenderNormal ) + AddFlag( FL_WORLDBRUSH ); + + if ( m_impactEnergyScale == 0 ) + { + m_impactEnergyScale = 1.0; + } + + CreateVPhysics(); +} + +//----------------------------------------------------------------------------- +// Purpose: Parse this prop's data, if it has a keyvalues section. +// Returns true only if this prop is using a model that has a prop_data section that's invalid. +//----------------------------------------------------------------------------- +void CBreakable::ParsePropData( void ) +{ + if ( m_iszPropData == NULL_STRING ) + return; + + if ( !Q_strncmp( STRING(m_iszPropData), "None", 4 ) ) + return; + + g_PropDataSystem.ParsePropFromBase( this, STRING(m_iszPropData) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBreakable::CreateVPhysics( void ) +{ + VPhysicsInitStatic(); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char *CBreakable::MaterialSound( Materials precacheMaterial ) +{ + switch ( precacheMaterial ) + { + case matWood: + return "Breakable.MatWood"; + case matFlesh: + case matWeb: + return "Breakable.MatFlesh"; + case matComputer: + return "Breakable.Computer"; + case matUnbreakableGlass: + case matGlass: + return "Breakable.MatGlass"; + case matMetal: + return "Breakable.MatMetal"; + case matCinderBlock: + case matRocks: + return "Breakable.MatConcrete"; + case matCeilingTile: + case matNone: + default: + break; + } + + return NULL; +} + + +void CBreakable::MaterialSoundRandom( int entindex, Materials soundMaterial, float volume ) +{ + const char *soundname; + soundname = MaterialSound( soundMaterial ); + if ( !soundname ) + return; + + CSoundParameters params; + if ( !GetParametersForSound( soundname, params, NULL ) ) + return; + + CPASAttenuationFilter filter( CBaseEntity::Instance( entindex ), params.soundlevel ); + + + EmitSound_t ep; + ep.m_nChannel = params.channel; + ep.m_pSoundName = params.soundname; + ep.m_flVolume = volume; + ep.m_SoundLevel = params.soundlevel; + + EmitSound( filter, entindex, ep ); +} + + +void CBreakable::Precache( void ) +{ + const char *pGibName = "WoodChunks"; + + switch (m_Material) + { + case matWood: + pGibName = "WoodChunks"; + break; + + case matUnbreakableGlass: + case matGlass: + pGibName = "GlassChunks"; + break; + + case matMetal: + pGibName = "MetalChunks"; + break; + + case matRocks: + pGibName = "ConcreteChunks"; + break; + +#ifdef HL1_DLL + case matComputer: + pGibName = "ComputerGibs"; + break; + + case matCeilingTile: + pGibName = "CeilingTile"; + break; + + case matFlesh: + pGibName = "FleshGibs"; + break; + + case matCinderBlock: + pGibName = "CinderBlocks"; + break; + + case matWeb: + pGibName = "WebGibs"; + break; +#else + + case matCinderBlock: + pGibName = "ConcreteChunks"; + break; + +#endif + +#if HL2_EPISODIC || MAPBASE + case matNone: + pGibName = ""; + break; +#endif + + default: + Warning("%s (%s) at (%.3f %.3f %.3f) using obsolete or unknown material type.\n", GetClassname(), GetDebugName(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + pGibName = "WoodChunks"; + break; + } + + if ( m_iszGibModel != NULL_STRING ) + { + pGibName = STRING(m_iszGibModel); + +#ifdef HL1_DLL + PrecacheModel( pGibName ); +#endif + } + + m_iszModelName = MAKE_STRING( pGibName ); + + // Precache the spawn item's data + if ( !CommandLine()->CheckParm("-makereslists")) + { + if ( m_iszSpawnObject != NULL_STRING ) + { + UTIL_PrecacheOther( STRING( m_iszSpawnObject ) ); + } + } + else + { + // Actually, precache all possible objects... + for ( int i = 0; i < ARRAYSIZE(pSpawnObjects) ; ++i ) + { + if ( !pSpawnObjects[ i ] ) + continue; + + if ( !Q_strnicmp( pSpawnObjects[ i ], "unused", Q_strlen( "unused" ) ) ) + continue; + + UTIL_PrecacheOther( pSpawnObjects[ i ] ); + } + } + + PrecacheScriptSound( "Breakable.MatGlass" ); + PrecacheScriptSound( "Breakable.MatWood" ); + PrecacheScriptSound( "Breakable.MatMetal" ); + PrecacheScriptSound( "Breakable.MatFlesh" ); + PrecacheScriptSound( "Breakable.MatConcrete" ); + PrecacheScriptSound( "Breakable.Computer" ); + PrecacheScriptSound( "Breakable.Crate" ); + PrecacheScriptSound( "Breakable.Glass" ); + PrecacheScriptSound( "Breakable.Metal" ); + PrecacheScriptSound( "Breakable.Flesh" ); + PrecacheScriptSound( "Breakable.Concrete" ); + PrecacheScriptSound( "Breakable.Ceiling" ); +} + +// play shard sound when func_breakable takes damage. +// the more damage, the louder the shard sound. + + +void CBreakable::DamageSound( void ) +{ + int pitch; + float fvol; + const char *soundname = NULL; + int material = m_Material; + + if (random->RandomInt(0,2)) + { + pitch = PITCH_NORM; + } + else + { + pitch = 95 + random->RandomInt(0,34); + } + + fvol = random->RandomFloat(0.75, 1.0); + + if (material == matComputer && random->RandomInt(0,1)) + { + material = matMetal; + } + + switch (material) + { + case matGlass: + case matUnbreakableGlass: + soundname = "Breakable.MatGlass"; + break; + + case matWood: + soundname = "Breakable.MatWood"; + break; + + case matMetal: + soundname = "Breakable.MatMetal"; + break; + + case matRocks: + case matCinderBlock: + soundname = "Breakable.MatConcrete"; + break; + + case matComputer: + soundname = "Breakable.Computer"; + break; + + default: + break; + } + + if ( soundname ) + { + CSoundParameters params; + if ( GetParametersForSound( soundname, params, NULL ) ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = params.channel; + ep.m_pSoundName = params.soundname; + ep.m_flVolume = fvol; + ep.m_SoundLevel = params.soundlevel; + ep.m_nPitch = pitch; + + EmitSound( filter, entindex(), ep ); + } + } +} + +void CBreakable::BreakTouch( CBaseEntity *pOther ) +{ + float flDamage; + + // only players can break these right now + if ( !pOther->IsPlayer() || !IsBreakable() ) + { + return; + } + + // can I be broken when run into? + if ( HasSpawnFlags( SF_BREAK_TOUCH ) ) + { + flDamage = pOther->GetSmoothedVelocity().Length() * 0.01; + + if (flDamage >= m_iHealth) + { + m_takedamage = DAMAGE_YES; + + SetTouch( NULL ); + OnTakeDamage( CTakeDamageInfo( pOther, pOther, flDamage, DMG_CRUSH ) ); + + // do a little damage to player if we broke glass or computer + CTakeDamageInfo info( pOther, pOther, flDamage/4, DMG_SLASH ); + CalculateMeleeDamageForce( &info, (pOther->GetAbsOrigin() - GetAbsOrigin()), GetAbsOrigin() ); + pOther->TakeDamage( info ); + } + } + + // can I be broken when stood upon? + if ( HasSpawnFlags( SF_BREAK_PRESSURE ) && pOther->GetGroundEntity() == this ) + { + // play creaking sound here. + DamageSound(); + + m_hBreaker = pOther; + + SetThink ( &CBreakable::Die ); + SetTouch( NULL ); + + // Add optional delay + SetNextThink( gpGlobals->curtime + m_flPressureDelay ); + + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for adding to the breakable's health. +// Input : Integer health points to add. +//----------------------------------------------------------------------------- +void CBreakable::InputAddHealth( inputdata_t &inputdata ) +{ + UpdateHealth( m_iHealth + inputdata.value.Int(), inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for breaking the breakable immediately. +//----------------------------------------------------------------------------- +void CBreakable::InputBreak( inputdata_t &inputdata ) +{ + Break( inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for removing health from the breakable. +// Input : Integer health points to remove. +//----------------------------------------------------------------------------- +void CBreakable::InputRemoveHealth( inputdata_t &inputdata ) +{ + UpdateHealth( m_iHealth - inputdata.value.Int(), inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the breakable's health. +//----------------------------------------------------------------------------- +void CBreakable::InputSetHealth( inputdata_t &inputdata ) +{ + UpdateHealth( inputdata.value.Int(), inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the breakable's mass. +//----------------------------------------------------------------------------- +void CBreakable::InputSetMass( inputdata_t &inputdata ) +{ + IPhysicsObject * vPhys = VPhysicsGetObject(); + if ( vPhys ) + { + float toMass = inputdata.value.Float(); + Assert(toMass > 0); + vPhys->SetMass( toMass ); + } + else + { + Warning( "Tried to call SetMass() on %s but it has no physics.\n", GetEntityName().ToCStr() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Choke point for changes to breakable health. Ensures outputs are fired. +// Input : iNewHealth - +// pActivator - +// Output : Returns true if the breakable survived, false if it died (broke). +//----------------------------------------------------------------------------- +bool CBreakable::UpdateHealth( int iNewHealth, CBaseEntity *pActivator ) +{ + if ( iNewHealth != m_iHealth ) + { + m_iHealth = iNewHealth; + + if ( m_iMaxHealth == 0 ) + { + Assert( false ); + m_iMaxHealth = 1; + } + + // Output the new health as a percentage of max health [0..1] + float flRatio = clamp( (float)m_iHealth / (float)m_iMaxHealth, 0.f, 1.f ); + m_OnHealthChanged.Set( flRatio, pActivator, this ); + + if ( m_iHealth <= 0 ) + { + Break( pActivator ); + return false; + } + else + { + if ( FBitSet( m_spawnflags, SF_BREAK_TRIGGER_ONLY ) ) + { + m_takedamage = DAMAGE_NO; + } + else + { + m_takedamage = DAMAGE_YES; + } + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Breaks the breakable if it can be broken. +// Input : pBreaker - The entity that caused us to break, either via an input, +// by shooting us, or by touching us. +//----------------------------------------------------------------------------- +void CBreakable::Break( CBaseEntity *pBreaker ) +{ + if ( IsBreakable() ) + { + QAngle angles = GetLocalAngles(); + angles.y = m_angle; + SetLocalAngles( angles ); + m_hBreaker = pBreaker; + Die(); + } +} + + +void CBreakable::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + // random spark if this is a 'computer' object + if (random->RandomInt(0,1) ) + { + switch( m_Material ) + { + case matComputer: + { + g_pEffects->Sparks( ptr->endpos ); + + EmitSound( "Breakable.Computer" ); + } + break; + + case matUnbreakableGlass: + g_pEffects->Ricochet( ptr->endpos, (vecDir*-1.0f) ); + break; + } + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Allows us to take damage from physics objects +//----------------------------------------------------------------------------- +void CBreakable::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + BaseClass::VPhysicsCollision( index, pEvent ); + + Vector damagePos; + pEvent->pInternalData->GetContactPoint( damagePos ); + Vector damageForce = pEvent->postVelocity[index] * pEvent->pObjects[index]->GetMass(); + if ( damageForce == vec3_origin ) + { + // This can happen if this entity is a func_breakable, and can't move. + // Use the velocity of the entity that hit us instead. + damageForce = pEvent->postVelocity[!index] * pEvent->pObjects[!index]->GetMass(); + } + + // If we're supposed to explode on collision, do so + if ( HasSpawnFlags( SF_BREAK_PHYSICS_BREAK_IMMEDIATELY ) ) + { + // We're toast + m_bTookPhysicsDamage = true; + CBaseEntity *pHitEntity = pEvent->pEntities[!index]; + + // HACKHACK: Reset mass to get correct collision response for the object breaking this glass + if ( m_Material == matGlass ) + { + pEvent->pObjects[index]->SetMass( 2.0f ); + } + CTakeDamageInfo dmgInfo( pHitEntity, pHitEntity, damageForce, damagePos, (m_iHealth + 1), DMG_CRUSH ); + PhysCallbackDamage( this, dmgInfo, *pEvent, index ); + } + else if ( !HasSpawnFlags( SF_BREAK_DONT_TAKE_PHYSICS_DAMAGE ) ) + { + int otherIndex = !index; + CBaseEntity *pOther = pEvent->pEntities[otherIndex]; + + // We're to take normal damage from this + int damageType; + IBreakableWithPropData *pBreakableInterface = assert_cast(this); + float damage = CalculateDefaultPhysicsDamage( index, pEvent, m_impactEnergyScale, true, damageType, pBreakableInterface->GetPhysicsDamageTable() ); + if ( damage > 0 ) + { + // HACKHACK: Reset mass to get correct collision response for the object breaking this glass + if ( m_Material == matGlass ) + { + pEvent->pObjects[index]->SetMass( 2.0f ); + } + CTakeDamageInfo dmgInfo( pOther, pOther, damageForce, damagePos, damage, damageType ); + PhysCallbackDamage( this, dmgInfo, *pEvent, index ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Allows us to make damage exceptions that are breakable-specific. +//----------------------------------------------------------------------------- +int CBreakable::OnTakeDamage( const CTakeDamageInfo &info ) +{ + Vector vecTemp; + + CTakeDamageInfo subInfo = info; + + // If attacker can't do at least the min required damage to us, don't take any damage from them + if ( m_takedamage == DAMAGE_NO || info.GetDamage() < m_iMinHealthDmg ) + return 0; + + // Check our damage filter + if ( !PassesDamageFilter(subInfo) ) + { + m_bTookPhysicsDamage = false; + return 1; + } + + vecTemp = subInfo.GetInflictor()->GetAbsOrigin() - WorldSpaceCenter(); + + if (!IsBreakable()) + return 0; + + float flPropDamage = GetBreakableDamage( subInfo, assert_cast(this) ); + subInfo.SetDamage( flPropDamage ); + + int iPrevHealth = m_iHealth; + BaseClass::OnTakeDamage( subInfo ); + + // HACK: slam health back to what it was so UpdateHealth can do its thing + int iNewHealth = m_iHealth; + m_iHealth = iPrevHealth; + if ( !UpdateHealth( iNewHealth, info.GetAttacker() ) ) + return 1; + + // Make a shard noise each time func breakable is hit, if it's capable of taking damage + if ( m_takedamage == DAMAGE_YES ) + { + // Don't play shard noise if being burned. + // Don't play shard noise if cbreakable actually died. + if ( ( subInfo.GetDamageType() & DMG_BURN ) == false ) + { + DamageSound(); + } + } + + return 1; +} + +//------------------------------------------------------------------------------ +// Purpose : Reset the OnGround flags for any entities that may have been +// resting on me +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakable::ResetOnGroundFlags(void) +{ + // !!! HACK This should work! + // Build a box above the entity that looks like an 9 inch high sheet + Vector mins, maxs; + CollisionProp()->WorldSpaceAABB( &mins, &maxs ); + mins.z -= 1; + maxs.z += 8; + + // BUGBUG -- can only find 256 entities on a breakable -- should be enough + CBaseEntity *pList[256]; + int count = UTIL_EntitiesInBox( pList, 256, mins, maxs, FL_ONGROUND ); + if ( count ) + { + for ( int i = 0; i < count; i++ ) + { + pList[i]->SetGroundEntity( (CBaseEntity *)NULL ); + } + } + +#ifdef PORTAL + // !!! HACK This should work! + // Tell touching portals to fizzle + int iPortalCount = CProp_Portal_Shared::AllPortals.Count(); + if( iPortalCount != 0 ) + { + Vector vMin, vMax; + CollisionProp()->WorldSpaceAABB( &vMin, &vMax ); + + Vector vBoxCenter = ( vMin + vMax ) * 0.5f; + Vector vBoxExtents = ( vMax - vMin ) * 0.5f; + + CProp_Portal **pPortals = CProp_Portal_Shared::AllPortals.Base(); + for( int i = 0; i != iPortalCount; ++i ) + { + CProp_Portal *pTempPortal = pPortals[i]; + if( UTIL_IsBoxIntersectingPortal( vBoxCenter, vBoxExtents, pTempPortal ) ) + { + pTempPortal->DoFizzleEffect( PORTAL_FIZZLE_KILLED, false ); + pTempPortal->Fizzle(); + } + } + } +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Breaks the breakable. m_hBreaker is the entity that caused us to break. +//----------------------------------------------------------------------------- +void CBreakable::Die( void ) +{ + Vector vecVelocity;// shard velocity + char cFlag = 0; + int pitch; + float fvol; + + pitch = 95 + random->RandomInt(0,29); + + if (pitch > 97 && pitch < 103) + { + pitch = 100; + } + + // The more negative m_iHealth, the louder + // the sound should be. + + fvol = random->RandomFloat(0.85, 1.0) + (abs(m_iHealth) / 100.0); + if (fvol > 1.0) + { + fvol = 1.0; + } + + const char *soundname = NULL; + + switch (m_Material) + { + default: + break; + + case matGlass: + soundname = "Breakable.Glass"; + cFlag = BREAK_GLASS; + break; + + case matWood: + soundname = "Breakable.Crate"; + cFlag = BREAK_WOOD; + break; + + case matComputer: + soundname = "Breakable.Computer"; + cFlag = BREAK_METAL; + break; + + case matMetal: + soundname = "Breakable.Metal"; + cFlag = BREAK_METAL; + break; + + case matFlesh: + case matWeb: + soundname = "Breakable.Flesh"; + cFlag = BREAK_FLESH; + break; + + case matRocks: + case matCinderBlock: + soundname = "Breakable.Concrete"; + cFlag = BREAK_CONCRETE; + break; + + case matCeilingTile: + soundname = "Breakable.Ceiling"; + break; + } + + if ( soundname ) + { + if ( m_hBreaker && m_hBreaker->IsPlayer() ) + { + IGameEvent * event = gameeventmanager->CreateEvent( "break_breakable" ); + if ( event ) + { + event->SetInt( "userid", ToBasePlayer( m_hBreaker )->GetUserID() ); + event->SetInt( "entindex", entindex() ); + event->SetInt( "material", cFlag ); + gameeventmanager->FireEvent( event ); + } + } + + CSoundParameters params; + if ( GetParametersForSound( soundname, params, NULL ) ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = params.channel; + ep.m_pSoundName = params.soundname; + ep.m_flVolume = fvol; + ep.m_SoundLevel = params.soundlevel; + ep.m_nPitch = pitch; + + EmitSound( filter, entindex(), ep ); + } + } + + switch( m_Explosion ) + { + case expDirected: + vecVelocity = g_vecAttackDir * -200; + break; + + case expUsePrecise: + { + AngleVectors( m_GibDir, &vecVelocity, NULL, NULL ); + vecVelocity *= 200; + } + break; + + case expRandom: + vecVelocity.x = 0; + vecVelocity.y = 0; + vecVelocity.z = 0; + break; + + default: + DevMsg("**ERROR - Unspecified gib dir method in func_breakable!\n"); + break; + } + + Vector vecSpot = WorldSpaceCenter(); + CPVSFilter filter2( vecSpot ); + + int iModelIndex = 0; + CCollisionProperty *pCollisionProp = CollisionProp(); + + Vector vSize = pCollisionProp->OBBSize(); + int iCount = ( vSize[0] * vSize[1] + vSize[1] * vSize[2] + vSize[2] * vSize[0] ) / ( 3 * 12 * 12 ); + + if ( iCount > func_break_max_pieces.GetInt() ) + { + iCount = func_break_max_pieces.GetInt(); + } + + ConVarRef breakable_disable_gib_limit( "breakable_disable_gib_limit" ); + if ( !breakable_disable_gib_limit.GetBool() && iCount ) + { + if ( m_PerformanceMode == PM_NO_GIBS ) + { + iCount = 0; + } + else if ( m_PerformanceMode == PM_REDUCED_GIBS ) + { + int iNewCount = iCount * func_break_reduction_factor.GetFloat(); + iCount = MAX( iNewCount, 1 ); + } + } + + if ( m_iszModelName != NULL_STRING ) + { + for ( int i = 0; i < iCount; i++ ) + { + + #ifdef HL1_DLL + // Use the passed model instead of the propdata type + const char *modelName = STRING( m_iszModelName ); + + // if the map specifies a model by name + if( strstr( modelName, ".mdl" ) != NULL ) + { + iModelIndex = modelinfo->GetModelIndex( modelName ); + } + else // do the hl2 / normal way + #endif + + iModelIndex = modelinfo->GetModelIndex( g_PropDataSystem.GetRandomChunkModel( STRING( m_iszModelName ) ) ); + + // All objects except the first one in this run are marked as slaves... + int slaveFlag = 0; + if ( i != 0 ) + { + slaveFlag = BREAK_SLAVE; + } + + te->BreakModel( filter2, 0.0, + vecSpot, pCollisionProp->GetCollisionAngles(), vSize, + vecVelocity, iModelIndex, 100, 1, 2.5, cFlag | slaveFlag ); + } + } + + ResetOnGroundFlags(); + + // Don't fire something that could fire myself + SetName( NULL_STRING ); + + AddSolidFlags( FSOLID_NOT_SOLID ); + + // Fire targets on break + m_OnBreak.FireOutput( m_hBreaker, this ); + + VPhysicsDestroyObject(); + SetThink( &CBreakable::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + if ( m_iszSpawnObject != NULL_STRING ) + { + CBaseEntity::Create( STRING(m_iszSpawnObject), vecSpot, pCollisionProp->GetCollisionAngles(), this ); + } + + if ( Explodable() ) + { + ExplosionCreate( vecSpot, pCollisionProp->GetCollisionAngles(), this, GetExplosiveDamage(), GetExplosiveRadius(), true ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns whether this object can be broken. +//----------------------------------------------------------------------------- +bool CBreakable::IsBreakable( void ) +{ + return m_Material != matUnbreakableGlass; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +char const *CBreakable::DamageDecal( int bitsDamageType, int gameMaterial ) +{ + if ( m_Material == matGlass ) + return "GlassBreak"; + + if ( m_Material == matUnbreakableGlass ) + return "BulletProof"; + + return BaseClass::DamageDecal( bitsDamageType, gameMaterial ); +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CBreakable::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + if ( GetMaxHealth() ) + { + char tempstr[512]; + Q_snprintf(tempstr,sizeof(tempstr),"Health: %i",GetHealth()); + EntityText(text_offset,tempstr,0); + text_offset++; + } + + if ( m_iszBasePropData != NULL_STRING ) + { + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr),"Base PropData: %s", STRING(m_iszBasePropData) ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + } + + return text_offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Keep track of physgun influence +//----------------------------------------------------------------------------- + +void CBreakable::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + m_hPhysicsAttacker = pPhysGunUser; + m_flLastPhysicsInfluenceTime = gpGlobals->curtime; +} + +void CBreakable::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ) +{ + m_hPhysicsAttacker = pPhysGunUser; + m_flLastPhysicsInfluenceTime = gpGlobals->curtime; +} + +CBasePlayer *CBreakable::HasPhysicsAttacker( float dt ) +{ + if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) + { + return m_hPhysicsAttacker; + } + return NULL; +} + + +//============================================================================================================================= +// PUSHABLE +//============================================================================================================================= + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPushable : public CBreakable +{ +public: + DECLARE_CLASS( CPushable, CBreakable ); + + void Spawn ( void ); + bool CreateVPhysics( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_ONOFF_USE; } + + // breakables use an overridden takedamage + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + unsigned int PhysicsSolidMaskForEntity( void ) const { return MASK_PLAYERSOLID; } +}; + + +LINK_ENTITY_TO_CLASS( func_pushable, CPushable ); + + +void CPushable::Spawn( void ) +{ + if ( HasSpawnFlags( SF_PUSH_BREAKABLE ) ) + { + BaseClass::Spawn(); + } + else + { + Precache(); + + SetSolid( SOLID_VPHYSICS ); + + SetMoveType( MOVETYPE_PUSH ); + SetModel( STRING( GetModelName() ) ); + + CreateVPhysics(); + } + +#ifdef HL1_DLL + // Force HL1 Pushables to stay axially aligned. + VPhysicsGetObject()->SetInertia( Vector( 1e30, 1e30, 1e30 ) ); +#endif//HL1_DLL +} + + +bool CPushable::CreateVPhysics( void ) +{ + VPhysicsInitNormal( SOLID_VPHYSICS, 0, false ); + IPhysicsObject *pPhysObj = VPhysicsGetObject(); + if ( pPhysObj ) + { + pPhysObj->SetMass( 30 ); +// Vector vecInertia = Vector(800, 800, 800); +// pPhysObj->SetInertia( vecInertia ); + } + + return true; +} + +// Pull the func_pushable +void CPushable::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ +#ifdef HL1_DLL + if( m_spawnflags & SF_PUSH_NO_USE ) + return; + + // Allow pushables to be dragged by player + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( pPlayer ) + { + if ( useType == USE_ON ) + { + PlayerPickupObject( pPlayer, this ); + } + } +#else + BaseClass::Use( pActivator, pCaller, useType, value ); +#endif +} + + +int CPushable::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( m_spawnflags & SF_PUSH_BREAKABLE ) + return BaseClass::OnTakeDamage( info ); + + return 1; +} + +//----------------------------------------------------------------------------- +// Purpose: Allows us to take damage from physics objects +//----------------------------------------------------------------------------- +void CPushable::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + int otherIndex = !index; + CBaseEntity *pOther = pEvent->pEntities[otherIndex]; + if ( pOther->IsPlayer() ) + { + // Pushables don't take damage from impacts with the player + // We call all the way back to the baseclass to get the physics effects. + CBaseEntity::VPhysicsCollision( index, pEvent ); + return; + } + + BaseClass::VPhysicsCollision( index, pEvent ); +} + diff --git a/sp/src/game/server/func_break.h b/sp/src/game/server/func_break.h new file mode 100644 index 00000000..d423c953 --- /dev/null +++ b/sp/src/game/server/func_break.h @@ -0,0 +1,174 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Defines a class for objects that break after taking a certain amount +// of damage. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef FUNC_BREAK_H +#define FUNC_BREAK_H +#pragma once + +#include "entityoutput.h" +#include "props.h" + +typedef enum { expRandom = 0, expDirected, expUsePrecise} Explosions; +typedef enum { matGlass = 0, matWood, matMetal, matFlesh, matCinderBlock, matCeilingTile, matComputer, matUnbreakableGlass, matRocks, matWeb, matNone, matLastMaterial } Materials; + + +#define NUM_SHARDS 6 // this many shards spawned when breakable objects break; + +// Spawnflags for func breakable +#define SF_BREAK_TRIGGER_ONLY 0x0001 // may only be broken by trigger +#define SF_BREAK_TOUCH 0x0002 // can be 'crashed through' by running player (plate glass) +#define SF_BREAK_PRESSURE 0x0004 // can be broken by a player standing on it +#define SF_BREAK_PHYSICS_BREAK_IMMEDIATELY 0x0200 // the first physics collision this breakable has will immediately break it +#define SF_BREAK_DONT_TAKE_PHYSICS_DAMAGE 0x0400 // this breakable doesn't take damage from physics collisions +#define SF_BREAK_NO_BULLET_PENETRATION 0x0800 // don't allow bullets to penetrate + +// Spawnflags for func_pushable (it's also func_breakable, so don't collide with those flags) +#define SF_PUSH_BREAKABLE 0x0080 +#define SF_PUSH_NO_USE 0x0100 // player cannot +use pickup this ent + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CBreakable : public CBaseEntity, public IBreakableWithPropData, public CDefaultPlayerPickupVPhysics +{ +public: + DECLARE_CLASS( CBreakable, CBaseEntity ); + + // basic functions + virtual void Spawn( void ); + void ParsePropData( void ); + bool CreateVPhysics( void ); + virtual void Precache( void ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + + void BreakTouch( CBaseEntity *pOther ); + void DamageSound( void ); + void Break( CBaseEntity *pBreaker ); + + // Input handlers + void InputAddHealth( inputdata_t &inputdata ); + void InputBreak( inputdata_t &inputdata ); + void InputRemoveHealth( inputdata_t &inputdata ); + void InputSetHealth( inputdata_t &inputdata ); + void InputSetMass( inputdata_t &inputdata ); + + + // breakables use an overridden takedamage + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + // To spark when hit + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + bool IsBreakable( void ); + bool SparkWhenHit( void ); + + char const *DamageDecal( int bitsDamageType, int gameMaterial ); + + virtual void Die( void ); + void ResetOnGroundFlags(void); + + inline bool Explodable( void ) { return GetExplosiveRadius() > 0; } + + Materials GetMaterialType( void ) { return m_Material; } + static void MaterialSoundRandom( int entindex, Materials soundMaterial, float volume ); + static const char *MaterialSound( Materials precacheMaterial ); + + static const char *pSpawnObjects[]; + + int DrawDebugTextOverlays(void); + + DECLARE_DATADESC(); + +public: +// IBreakableWithPropData + void SetDmgModBullet( float flDmgMod ) { m_flDmgModBullet = flDmgMod; } + void SetDmgModClub( float flDmgMod ) { m_flDmgModClub = flDmgMod; } + void SetDmgModExplosive( float flDmgMod ) { m_flDmgModExplosive = flDmgMod; } + float GetDmgModBullet( void ) { return m_flDmgModBullet; } + float GetDmgModClub( void ) { return m_flDmgModClub; } + float GetDmgModExplosive( void ) { return m_flDmgModExplosive; } + void SetExplosiveRadius( float flRadius ) { m_explodeRadius = flRadius; } + void SetExplosiveDamage( float flDamage ) { m_ExplosionMagnitude = flDamage; } + float GetExplosiveRadius( void ) { return m_explodeRadius; } + float GetExplosiveDamage( void ) { return m_ExplosionMagnitude; } + void SetPhysicsDamageTable( string_t iszTableName ) { m_iszPhysicsDamageTableName = iszTableName; } + string_t GetPhysicsDamageTable( void ) { return m_iszPhysicsDamageTableName; } + void SetBreakableModel( string_t iszModel ) { m_iszBreakableModel = iszModel; } + string_t GetBreakableModel( void ) { return m_iszBreakableModel; } + void SetBreakableSkin( int iSkin ) { m_iBreakableSkin = iSkin; } + int GetBreakableSkin( void ) { return m_iBreakableSkin; } + void SetBreakableCount( int iCount ) { m_iBreakableCount = iCount; } + int GetBreakableCount( void ) { return m_iBreakableCount; } + void SetMaxBreakableSize( int iSize ) { m_iMaxBreakableSize = iSize; } + int GetMaxBreakableSize( void ) { return m_iMaxBreakableSize; } + void SetPropDataBlocksLOS( bool bBlocksLOS ) { SetBlocksLOS( bBlocksLOS ); } + void SetPropDataIsAIWalkable( bool bBlocksLOS ) { SetAIWalkable( bBlocksLOS ); } + void SetBasePropData( string_t iszBase ) { m_iszBasePropData = iszBase; } + string_t GetBasePropData( void ) { return m_iszBasePropData; } + void SetInteraction( propdata_interactions_t Interaction ) { m_iInteractions |= (1 << Interaction); } + bool HasInteraction( propdata_interactions_t Interaction ) { return ( m_iInteractions & (1 << Interaction) ) != 0; } + void SetPhysicsMode(int iMode){} + int GetPhysicsMode() { return PHYSICS_MULTIPLAYER_SOLID; } + void SetMultiplayerBreakMode( mp_break_t mode ) {} + mp_break_t GetMultiplayerBreakMode( void ) const { return MULTIPLAYER_BREAK_DEFAULT; } + +protected: + float m_angle; + Materials m_Material; + EHANDLE m_hBreaker; // The entity that broke us. Held as a data member because sometimes breaking is delayed. + +private: + + Explosions m_Explosion; + QAngle m_GibDir; + string_t m_iszGibModel; + string_t m_iszSpawnObject; + int m_ExplosionMagnitude; + float m_flPressureDelay; // Delay before breaking when destoyed by pressure + int m_iMinHealthDmg; // minimum damage attacker must have to cause damage + bool m_bTookPhysicsDamage; + + string_t m_iszPropData; + string_t m_iszModelName; + +protected: + + bool UpdateHealth( int iNewHealth, CBaseEntity *pActivator ); + + float m_impactEnergyScale; + + COutputEvent m_OnBreak; + COutputFloat m_OnHealthChanged; + + // Prop data storage + float m_flDmgModBullet; + float m_flDmgModClub; + float m_flDmgModExplosive; + string_t m_iszPhysicsDamageTableName; + string_t m_iszBreakableModel; + int m_iBreakableSkin; + int m_iBreakableCount; + int m_iMaxBreakableSize; + string_t m_iszBasePropData; + int m_iInteractions; + PerformanceMode_t m_PerformanceMode; + + float m_explodeRadius; + +public: + // IPlayerPickupVPhysics + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ); + virtual CBasePlayer *HasPhysicsAttacker( float dt ); +private: + CHandle m_hPhysicsAttacker; + float m_flLastPhysicsInfluenceTime; +}; + +#endif // FUNC_BREAK_H diff --git a/sp/src/game/server/func_breakablesurf.cpp b/sp/src/game/server/func_breakablesurf.cpp new file mode 100644 index 00000000..ddd8542f --- /dev/null +++ b/sp/src/game/server/func_breakablesurf.cpp @@ -0,0 +1,1308 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A planar textured surface that breaks into increasingly smaller fragments +// as it takes damage. Undamaged pieces remain attached to the world +// until they are damaged. Used for window panes. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ndebugoverlay.h" +#include "filters.h" +#include "player.h" +#include "func_breakablesurf.h" +#include "shattersurfacetypes.h" +#include "materialsystem/imaterialsystem.h" +#include "materialsystem/imaterial.h" +#include "materialsystem/imaterialvar.h" +#include "globals.h" +#include "physics_impact_damage.h" +#include "te_effect_dispatch.h" + +//============================================================================= +// HPE_BEGIN +// [dwenger] Necessary for stats tracking +//============================================================================= +#include "gamestats.h" +//============================================================================= +// HPE_END +//============================================================================= + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Spawn flags +#define SF_BREAKABLESURF_CRACK_DECALS 0x00000001 +#define SF_BREAKABLESURF_DAMAGE_FROM_HELD_OBJECTS 0x00000002 +#ifdef MAPBASE +#define SF_BREAKABLESURF_PLAY_BREAK_SOUND 0x00000004 +#endif + +//############################################################################# +// > CWindowPane +//############################################################################# +#define WINDOW_PANEL_SIZE 12 +#define WINDOW_SMALL_SHARD_SIZE 4 +#define WINDOW_LARGE_SHARD_SIZE 7 +#define WINDOW_MAX_SUPPORT 6.75 +#define WINDOW_BREAK_SUPPORT 0.20 + +#define WINDOW_PANE_BROKEN -1 +#define WINDOW_PANE_HEALTHY 1 + +// Also defined in WC +#define QUAD_ERR_NONE 0 +#define QUAD_ERR_MULT_FACES 1 +#define QUAD_ERR_NOT_QUAD 2 + +// +// func_breakable - bmodel that breaks into pieces after taking damage +// +LINK_ENTITY_TO_CLASS( window_pane, CWindowPane ); +BEGIN_DATADESC( CWindowPane ) + + // Function Pointers + DEFINE_FUNCTION( Die ), + DEFINE_FUNCTION( PaneTouch ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CWindowPane::Spawn( void ) +{ + Precache( ); + + SetSolid( SOLID_BBOX ); + SetMoveType( MOVETYPE_FLYGRAVITY ); + m_takedamage = DAMAGE_YES; + + SetCollisionGroup( COLLISION_GROUP_BREAKABLE_GLASS ); + + SetModel( "models/brokenglass_piece.mdl" );//set size and link into world. +} + +void CWindowPane::Precache( void ) +{ + PrecacheModel( "models/brokenglass_piece.mdl" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pOther - +//----------------------------------------------------------------------------- +void CWindowPane::PaneTouch( CBaseEntity *pOther ) +{ + if (pOther && + pOther->GetCollisionGroup() != COLLISION_GROUP_BREAKABLE_GLASS) + { + Die(); + } +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CWindowPane::Die( void ) +{ + Vector flForce = -1 * GetAbsVelocity(); + + CPASFilter filter( GetAbsOrigin() ); + te->ShatterSurface( filter, 0.0, + &GetAbsOrigin(), &GetAbsAngles(), + &GetAbsVelocity(), &GetAbsOrigin(), + WINDOW_PANEL_SIZE, WINDOW_PANEL_SIZE,WINDOW_SMALL_SHARD_SIZE,SHATTERSURFACE_GLASS, + 255,255,255,255,255,255); + + UTIL_Remove(this); +} + +///------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +CWindowPane* CWindowPane::CreateWindowPane( const Vector &vecOrigin, const QAngle &vecAngles ) +{ + CWindowPane *pGlass = (CWindowPane*)CreateEntityByName( "window_pane" ); + if ( !pGlass ) + { + Msg( "NULL Ent in CreateWindowPane!\n" ); + return NULL; + } + + if ( pGlass->edict() ) + { + pGlass->SetLocalOrigin( vecOrigin ); + pGlass->SetLocalAngles( vecAngles ); + pGlass->Spawn(); + pGlass->SetTouch(&CWindowPane::PaneTouch); + pGlass->SetLocalAngularVelocity( RandomAngle(-50,50) ); + pGlass->m_nBody = random->RandomInt(0,2); + } + return pGlass; +} + + +//#################################################################################### +// > CBreakableSurface +//#################################################################################### +LINK_ENTITY_TO_CLASS( func_breakable_surf, CBreakableSurface ); + +BEGIN_DATADESC( CBreakableSurface ) + + DEFINE_KEYFIELD( m_nSurfaceType, FIELD_INTEGER, "surfacetype"), + DEFINE_KEYFIELD( m_nFragility, FIELD_INTEGER, "fragility"), + DEFINE_KEYFIELD( m_vLLVertex, FIELD_VECTOR, "lowerleft" ), + DEFINE_KEYFIELD( m_vULVertex, FIELD_VECTOR, "upperleft" ), + DEFINE_KEYFIELD( m_vLRVertex, FIELD_VECTOR, "lowerright" ), + DEFINE_KEYFIELD( m_vURVertex, FIELD_VECTOR, "upperright" ), + DEFINE_KEYFIELD( m_nQuadError, FIELD_INTEGER, "error" ), + + DEFINE_FIELD( m_nNumWide, FIELD_INTEGER), + DEFINE_FIELD( m_nNumHigh, FIELD_INTEGER), + DEFINE_FIELD( m_flPanelWidth, FIELD_FLOAT), + DEFINE_FIELD( m_flPanelHeight, FIELD_FLOAT), + DEFINE_FIELD( m_vNormal, FIELD_VECTOR), + DEFINE_FIELD( m_vCorner, FIELD_POSITION_VECTOR), + DEFINE_FIELD( m_bIsBroken, FIELD_BOOLEAN), + DEFINE_FIELD( m_nNumBrokenPanes, FIELD_INTEGER), + + // UNDONE: How to load save this? Need a way to update + // the client about the state of the window upon load... + // We should use client-side save/load to fix this problem. + DEFINE_AUTO_ARRAY2D( m_flSupport, FIELD_FLOAT), + DEFINE_ARRAY( m_RawPanelBitVec, FIELD_BOOLEAN, MAX_NUM_PANELS*MAX_NUM_PANELS ), + + // Function Pointers + DEFINE_THINKFUNC( BreakThink ), + DEFINE_ENTITYFUNC( SurfaceTouch ), + + DEFINE_INPUTFUNC( FIELD_VECTOR, "Shatter", InputShatter ), + + // DEFINE_FIELD( m_ForceUpdateClientData, CBitVec < MAX_PLAYERS > ), // No need to save/restore this, it's just a temporary flag field +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST(CBreakableSurface, DT_BreakableSurface) + SendPropInt(SENDINFO(m_nNumWide), 8, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_nNumHigh), 8, SPROP_UNSIGNED), + SendPropFloat(SENDINFO(m_flPanelWidth), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_flPanelHeight), 0, SPROP_NOSCALE), + SendPropVector(SENDINFO(m_vNormal), -1, SPROP_COORD), + SendPropVector(SENDINFO(m_vCorner), -1, SPROP_COORD), + SendPropInt(SENDINFO(m_bIsBroken), 1, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_nSurfaceType), 2, SPROP_UNSIGNED), + SendPropArray3(SENDINFO_ARRAY3(m_RawPanelBitVec), SendPropInt( SENDINFO_ARRAY( m_RawPanelBitVec ), 1, SPROP_UNSIGNED ) ), +END_SEND_TABLE() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBreakableSurface::Precache(void) +{ + UTIL_PrecacheOther( "window_pane" ); + + // Load the edge types and styles for the specific surface type + if (m_nSurfaceType == SHATTERSURFACE_TILE) + { + PrecacheMaterial( "models/brokentile/tilebroken_03a" ); + PrecacheMaterial( "models/brokentile/tilebroken_03b" ); + PrecacheMaterial( "models/brokentile/tilebroken_03c" ); + PrecacheMaterial( "models/brokentile/tilebroken_03d" ); + + PrecacheMaterial( "models/brokentile/tilebroken_02a" ); + PrecacheMaterial( "models/brokentile/tilebroken_02b" ); + PrecacheMaterial( "models/brokentile/tilebroken_02c" ); + PrecacheMaterial( "models/brokentile/tilebroken_02d" ); + + PrecacheMaterial( "models/brokentile/tilebroken_01a" ); + PrecacheMaterial( "models/brokentile/tilebroken_01b" ); + PrecacheMaterial( "models/brokentile/tilebroken_01c" ); + PrecacheMaterial( "models/brokentile/tilebroken_01d" ); + } + else + { + PrecacheMaterial( "models/brokenglass/glassbroken_solid" ); + PrecacheMaterial( "models/brokenglass/glassbroken_01a" ); + PrecacheMaterial( "models/brokenglass/glassbroken_01b" ); + PrecacheMaterial( "models/brokenglass/glassbroken_01c" ); + PrecacheMaterial( "models/brokenglass/glassbroken_01d" ); + PrecacheMaterial( "models/brokenglass/glassbroken_02a" ); + PrecacheMaterial( "models/brokenglass/glassbroken_02b" ); + PrecacheMaterial( "models/brokenglass/glassbroken_02c" ); + PrecacheMaterial( "models/brokenglass/glassbroken_02d" ); + PrecacheMaterial( "models/brokenglass/glassbroken_03a" ); + PrecacheMaterial( "models/brokenglass/glassbroken_03b" ); + PrecacheMaterial( "models/brokenglass/glassbroken_03c" ); + PrecacheMaterial( "models/brokenglass/glassbroken_03d" ); + } + + BaseClass::Precache(); +} + + +//------------------------------------------------------------------------------ +// Purpose : Window has been touched. Break out pieces based on touching +// entity's bounding box +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakableSurface::SurfaceTouch( CBaseEntity *pOther ) +{ + // If tile only break if object is moving fast + if (m_nSurfaceType == SHATTERSURFACE_TILE) + { + Vector vVel; + pOther->GetVelocity( &vVel, NULL ); + if (vVel.Length() < 500) + { + return; + } + } + + // Find nearest point on plane for max + Vector vecAbsMins, vecAbsMaxs; + pOther->CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); + Vector vToPlane = (vecAbsMaxs - m_vCorner); + float vDistToPlane = DotProduct(m_vNormal,vToPlane); + Vector vTouchPos = vecAbsMaxs + vDistToPlane*m_vNormal; + + float flMinsWidth,flMinsHeight; + PanePos(vTouchPos, &flMinsWidth, &flMinsHeight); + + // Find nearest point on plane for mins + vToPlane = (vecAbsMins - m_vCorner); + vDistToPlane = DotProduct(m_vNormal,vToPlane); + vTouchPos = vecAbsMins + vDistToPlane*m_vNormal; + + float flMaxsWidth,flMaxsHeight; + PanePos(vTouchPos, &flMaxsWidth, &flMaxsHeight); + + int nMinWidth = Floor2Int(MAX(0, MIN(flMinsWidth,flMaxsWidth))); + int nMaxWidth = Ceil2Int(MIN(m_nNumWide,MAX(flMinsWidth,flMaxsWidth))); + + int nMinHeight = Floor2Int(MAX(0, MIN(flMinsHeight,flMaxsHeight))); + int nMaxHeight = Ceil2Int(MIN(m_nNumHigh,MAX(flMinsHeight,flMaxsHeight))); + + Vector vHitVel; + pOther->GetVelocity( &vHitVel, NULL ); + + // Move faster then penetrating object so can see shards + vHitVel *= 5; + + // If I'm not broken yet, break me + if ( !m_bIsBroken ) + { + Die( pOther, vHitVel ); + } + + for (int height=nMinHeight;heightRandomInt(0,1)) + { + ShatterPane(nMinWidth-1, height,vHitVel,pOther->GetLocalOrigin()); + } + for (int width=nMinWidth;widthGetLocalOrigin()); + } + // Randomly break the one after so it doesn't look square + if (random->RandomInt(0,1)) + { + ShatterPane(nMaxWidth+1, height,vHitVel,pOther->GetLocalOrigin()); + } + } +} + +//------------------------------------------------------------------------------ +// Purpose : Only take damage in trace attack +// Input : +// Output : +//------------------------------------------------------------------------------ +int CBreakableSurface::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( !m_bIsBroken && info.GetDamageType() == DMG_CRUSH ) + { + // physics will kill me now + Die( info.GetAttacker(), info.GetDamageForce() ); + return 0; + } + + if ( m_nSurfaceType == SHATTERSURFACE_GLASS && info.GetDamageType() & DMG_BLAST ) + { + Vector vecDir = info.GetInflictor()->GetAbsOrigin() - WorldSpaceCenter(); + VectorNormalize( vecDir ); + Die( info.GetAttacker(), vecDir ); + return 0; + } + + // Accept slash damage, too. Manhacks and such. + if ( m_nSurfaceType == SHATTERSURFACE_GLASS && (info.GetDamageType() & DMG_SLASH) ) + { + Die( info.GetAttacker(), info.GetDamageForce() ); + return 0; + } + + + return 0; +} + + +//------------------------------------------------------------------------------ +// Purpose: Accepts damage and breaks if health drops below zero. +//------------------------------------------------------------------------------ +void CBreakableSurface::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + //============================================================================= + // HPE_BEGIN: + // [dwenger] Window break stat tracking + //============================================================================= + + // Make sure this pane has not already been shattered + bool bWasBroken = m_bIsBroken; + + //============================================================================= + // HPE_END + //============================================================================= + + // Decrease health + m_iHealth -= info.GetDamage(); + m_OnHealthChanged.Set( m_iHealth, info.GetAttacker(), this ); + + // If I'm not broken yet, break me + if (!m_bIsBroken ) + { + Vector vSurfDir = ptr->endpos - ptr->startpos; + Die( info.GetAttacker(), vSurfDir ); + } + + if (info.GetDamageType() & (DMG_BULLET | DMG_CLUB)) + { + // Figure out which panel has taken the damage and break it + float flWidth,flHeight; + PanePos(ptr->endpos,&flWidth,&flHeight); + int nWidth = flWidth; + int nHeight = flHeight; + + if ( ShatterPane(nWidth, nHeight,vecDir*500,ptr->endpos) ) + { + //============================================================================= + // HPE_BEGIN: + // [dwenger] Window break stat tracking + //============================================================================= + + CBasePlayer* pAttacker = ToBasePlayer(info.GetAttacker()); + if ( ( pAttacker ) && ( !bWasBroken ) ) + { + gamestats->Event_WindowShattered( pAttacker ); + } + + //============================================================================= + // HPE_END + //============================================================================= + + // Do an impact hit + CEffectData data; + + data.m_vNormal = ptr->plane.normal; + data.m_vOrigin = ptr->endpos; + + CPASFilter filter( data.m_vOrigin ); + + // client cannot trace against triggers + filter.SetIgnorePredictionCull( true ); + + te->DispatchEffect( filter, 0.0, data.m_vOrigin, "GlassImpact", data ); + } + + if (m_nSurfaceType == SHATTERSURFACE_GLASS) + { + // Break nearby panes if damages was near pane edge + float flWRem = flWidth - nWidth; + float flHRem = flHeight - nHeight; + + if (flWRem > 0.8 && nWidth != m_nNumWide-1) + { + ShatterPane(nWidth+1, nHeight,vecDir*500,ptr->endpos); + } + else if (flWRem < 0.2 && nWidth != 0) + { + ShatterPane(nWidth-1, nHeight,vecDir*500,ptr->endpos); + } + if (flHRem > 0.8 && nHeight != m_nNumHigh-1) + { + ShatterPane(nWidth, nHeight+1,vecDir*500,ptr->endpos); + } + else if (flHRem < 0.2 && nHeight != 0) + { + ShatterPane(nWidth, nHeight-1,vecDir*500,ptr->endpos); + } + + // Occasionally break the pane above me + if (random->RandomInt(0,1)==0) + { + ShatterPane(nWidth, nHeight+1,vecDir*1000,ptr->endpos); + // Occasionally break the pane above that + if (random->RandomInt(0,1)==0) + { + ShatterPane(nWidth, nHeight+2,vecDir*1000,ptr->endpos); + } + } + } + } + else if (info.GetDamageType() & (DMG_SONIC | DMG_BLAST)) + { + // ---------------------------------------- + // If it's tile blow out nearby tiles + // ---------------------------------------- + if (m_nSurfaceType == SHATTERSURFACE_TILE) + { + // Figure out which panel has taken the damage and break it + float flWidth,flHeight; + if (info.GetAttacker()) + { + PanePos(info.GetAttacker()->GetAbsOrigin(),&flWidth,&flHeight); + } + else + { + PanePos(ptr->endpos,&flWidth,&flHeight); + } + int nWidth = flWidth; + int nHeight = flHeight; + + // Blow out a roughly circular patch of tile with some randomness + for (int width =nWidth-4;widthRandomInt(2,5)) + { + ShatterPane(width, height,vecDir*500,ptr->endpos); + } + } + } + } + // ---------------------------------------- + // If it's glass blow out the whole window + // ---------------------------------------- + else + { + //============================================================================= + // HPE_BEGIN: + // [pfreese] Window break stat tracking + //============================================================================= + + CBasePlayer* pAttacker = ToBasePlayer(info.GetAttacker()); + if ( ( pAttacker ) && ( !bWasBroken ) ) + { + gamestats->Event_WindowShattered( pAttacker ); + } + + //============================================================================= + // HPE_END + //============================================================================= + + float flDot = DotProduct(m_vNormal,vecDir); + +#ifdef CSTRIKE_DLL + float damageMultiplier = info.GetDamage(); +#else + float damageMultiplier = 1.0f; +#endif + + Vector vBlastDir; + if (flDot > 0) + { + vBlastDir = damageMultiplier * 3000 * m_vNormal; + } + else + { + vBlastDir = damageMultiplier * -3000 * m_vNormal; + } + + // Has the window already been destroyed? + if (m_nNumBrokenPanes >= m_nNumWide*m_nNumHigh) + { + return; + } + // --------------------------------------------------------------- + // If less than 10% of my panels have been broken, blow me + // up in one large glass shatter + // --------------------------------------------------------------- + else if ( m_nNumBrokenPanes < 0.1*(m_nNumWide*m_nNumHigh)) + { + QAngle vAngles; + VectorAngles(-1*m_vNormal,vAngles); + + CreateShards(m_vCorner, vAngles,vBlastDir, ptr->endpos, + m_nNumWide*m_flPanelWidth, m_nNumHigh*m_flPanelHeight, WINDOW_LARGE_SHARD_SIZE); + } + // --------------------------------------------------------------- + // Otherwise break in the longest vertical strips possible + // (to cut down on the network bandwidth) + // --------------------------------------------------------------- + else + { + QAngle vAngles; + VectorAngles(-1*m_vNormal,vAngles); + Vector vWidthDir,vHeightDir; + AngleVectors(vAngles,NULL,&vWidthDir,&vHeightDir); + + for (int width=0;width 0) + { + Vector vBreakPos = m_vCorner + + (width*vWidthDir*m_flPanelWidth) + + ((height-nHCount)*vHeightDir*m_flPanelHeight); + + CreateShards(vBreakPos, vAngles, + vBlastDir, ptr->endpos, + m_flPanelWidth, nHCount*m_flPanelHeight, + WINDOW_LARGE_SHARD_SIZE); + + nHCount = 0; + } + } + if (nHCount) + { + Vector vBreakPos = m_vCorner + + (width*vWidthDir*m_flPanelWidth) + + ((height-nHCount)*vHeightDir*m_flPanelHeight); + + CreateShards(vBreakPos, vAngles, + vBlastDir, ptr->endpos, + m_flPanelWidth,nHCount*m_flPanelHeight, + WINDOW_LARGE_SHARD_SIZE); + } + } + } + + BreakAllPanes(); + } + } +} + +//------------------------------------------------------------------------------ +// Purpose: Break into panels +// Input : pBreaker - +// vDir - +//----------------------------------------------------------------------------- +void CBreakableSurface::Die( CBaseEntity *pBreaker, const Vector &vAttackDir ) +{ + if ( m_bIsBroken ) + return; + + // Play a break sound +#ifdef MAPBASE + if ( HasSpawnFlags(SF_BREAKABLESURF_PLAY_BREAK_SOUND) ) + { + Vector centerPos = (m_vLLVertex + m_vURVertex) / 2; + PhysBreakSound( this, VPhysicsGetObject(), centerPos ); + } +#else + PhysBreakSound( this, VPhysicsGetObject(), GetAbsOrigin() ); +#endif + + m_bIsBroken = true; + m_iHealth = 0.0f; + + if (pBreaker) + { + m_OnBreak.FireOutput( pBreaker, this ); + } + else + { + m_OnBreak.FireOutput( this, this ); + } + + float flDir = -1; + + if ( vAttackDir.LengthSqr() > 0.001 ) + { + float flDot = DotProduct( m_vNormal, vAttackDir ); + if (flDot < 0) + { + m_vLLVertex += m_vNormal; + m_vLRVertex += m_vNormal; + m_vULVertex += m_vNormal; + m_vURVertex += m_vNormal; + m_vNormal *= -1; + flDir = 1; + } + } + + // ------------------------------------------------------- + // The surface has two sides, when we are killed pick + // the side that the damage came from + // ------------------------------------------------------- + Vector vWidth = m_vLLVertex - m_vLRVertex; + Vector vHeight = m_vLLVertex - m_vULVertex; + CrossProduct( vWidth, vHeight, m_vNormal.GetForModify() ); + VectorNormalize(m_vNormal.GetForModify()); + + // --------------------------------------------------- + // Make sure width and height are oriented correctly + // --------------------------------------------------- + QAngle vAngles; + VectorAngles(-1*m_vNormal,vAngles); + Vector vWidthDir,vHeightDir; + AngleVectors(vAngles,NULL,&vWidthDir,&vHeightDir); + + float flWDist = DotProduct(vWidthDir,vWidth); + if (fabs(flWDist)<0.5) + { + Vector vSaveHeight = vHeight; + vHeight = vWidth * flDir; + vWidth = vSaveHeight * flDir; + } + + // ------------------------------------------------- + // Find which corner to use + // ------------------------------------------------- + bool bLeft = (DotProduct(vWidthDir,vWidth) < 0); + bool bLower = (DotProduct(vHeightDir,vHeight) < 0); + if (bLeft) + { + m_vCorner = bLower ? m_vLLVertex : m_vULVertex; + } + else + { + m_vCorner = bLower ? m_vLRVertex : m_vURVertex; + } + + // ------------------------------------------------- + // Calculate the number of panels + // ------------------------------------------------- + float flWidth = vWidth.Length(); + float flHeight = vHeight.Length(); + m_nNumWide = flWidth / WINDOW_PANEL_SIZE; + m_nNumHigh = flHeight / WINDOW_PANEL_SIZE; + + // If to many panels make panel size bigger + if (m_nNumWide > MAX_NUM_PANELS) m_nNumWide = MAX_NUM_PANELS; + if (m_nNumHigh > MAX_NUM_PANELS) m_nNumHigh = MAX_NUM_PANELS; + + m_flPanelWidth = flWidth / m_nNumWide; + m_flPanelHeight = flHeight / m_nNumHigh; + + // Initialize panels + for (int w=0;w m_nNumWide) + nMaxX = m_nNumWide; + + int nMinY = (int)(flCenterY - vecShatterInfo.z / m_flPanelHeight); + int nMaxY = (int)(flCenterY + vecShatterInfo.z / m_flPanelHeight) + 1; + + if (nMinY < 0) + nMinY = 0; + if (nMaxY > m_nNumHigh) + nMaxY = m_nNumHigh; + + QAngle vAngles; + VectorAngles(-1*m_vNormal,vAngles); + Vector vWidthDir,vHeightDir; + AngleVectors(vAngles,NULL,&vWidthDir,&vHeightDir); + + // Blow out a roughly circular of tile with some randomness + Vector2D vecActualCenter( flCenterX * m_flPanelWidth, flCenterY * m_flPanelHeight ); + for (int width = nMinX; width < nMaxX; width++) + { + for (int height = nMinY; height < nMaxY; height++) + { + Vector2D pt( (width + 0.5f) * m_flPanelWidth, (height + 0.5f) * m_flPanelWidth ); + if ( pt.DistToSqr(vecActualCenter) <= vecShatterInfo.z * vecShatterInfo.z ) + { + Vector vBreakPos = m_vCorner + + (width*vWidthDir*m_flPanelWidth) + + (height*vHeightDir*m_flPanelHeight); + + ShatterPane( width, height, m_vNormal * 500, vBreakPos ); + } + } + } +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakableSurface::Event_Killed( CBaseEntity *pInflictor, CBaseEntity *pAttacker, float flDamage, int bitsDamageType ) +{ + return; +} + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +bool CBreakableSurface::IsBroken(int nWidth, int nHeight) +{ + if (nWidth < 0 || nWidth >= m_nNumWide) return true; + if (nHeight < 0 || nHeight >= m_nNumHigh) return true; + + return (m_flSupport[nWidth][nHeight]==WINDOW_PANE_BROKEN); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : w - +// h - +// support - +//----------------------------------------------------------------------------- +void CBreakableSurface::SetSupport( int w, int h, float support ) +{ + m_flSupport[ w ][ h ] = support; + + int offset = w + h * m_nNumWide; + + bool prevval = m_RawPanelBitVec.Get( offset ); + bool curval = prevval; + + if ( support < 0.0f ) + { + curval = false; + } + else + { + curval = true; + } + if ( curval != prevval ) + { + m_RawPanelBitVec.Set( offset, curval ); + m_RawPanelBitVec.GetForModify( offset ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +float CBreakableSurface::GetSupport(int nWidth, int nHeight) +{ + return MAX(0,m_flSupport[nWidth][nHeight]); +} + +//------------------------------------------------------------------------------ +// Purpose : Return the structural support for this pane. Assumes window +// is upright. Still works for windows parallel to the ground +// but simulation isn't quite as good +// Input : +// Output : +//------------------------------------------------------------------------------ +float CBreakableSurface::RecalcSupport(int nWidth, int nHeight) +{ + // Always has some support. Zero signifies that it has been broken + float flSupport = 0.01; + + // ------------ + // Top support + // ------------ + if (nHeight == m_nNumHigh-1) + { + flSupport += 1.0; + } + else + { + flSupport += GetSupport(nWidth,nHeight+1); + } + + // ------------ + // Bottom Support + // ------------ + if (nHeight == 0) + { + flSupport += 1.25; + } + else + { + flSupport += 1.25 * GetSupport(nWidth,nHeight-1); + } + + // ------------ + // Left Support + // ------------ + if (nWidth == 0) + { + flSupport += 1.0; + } + else + { + flSupport += GetSupport(nWidth-1,nHeight); + } + + // -------------- + // Right Support + // -------------- + if (nWidth == m_nNumWide-1) + { + flSupport += 1.0; + } + else + { + flSupport += GetSupport(nWidth+1,nHeight); + } + + // -------------------- + // Bottom Left Support + // -------------------- + if (nHeight == 0 || nWidth == 0) + { + flSupport += 1.0; + } + else + { + flSupport += GetSupport(nWidth-1,nHeight-1); + } + + // --------------------- + // Bottom Right Support + // --------------------- + if (nHeight == 0 || nWidth == m_nNumWide-1) + { + flSupport += 1.0; + } + else + { + flSupport += GetSupport(nWidth+1,nHeight-1); + } + + // ----------------- + // Top Right Support + // ----------------- + if (nHeight == m_nNumHigh-1 || nWidth == m_nNumWide-1) + { + flSupport += 0.25; + } + else + { + flSupport += 0.25 * GetSupport(nWidth+1,nHeight+1); + } + + // ----------------- + // Top Left Support + // ----------------- + if (nHeight == m_nNumHigh-1 || nWidth == 0) + { + flSupport += 0.25; + } + else + { + flSupport += 0.25 * GetSupport(nWidth-1,nHeight+1); + } + + return flSupport; +} + + +//------------------------------------------------------------------------------ +// Purpose : Itterate through the panels and make sure none have become +// unstable +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakableSurface::BreakThink(void) +{ + // Don't calculate support if I'm tile + if (m_nSurfaceType == SHATTERSURFACE_TILE) + { + return; + } + + // ----------------------- + // Recalculate all support + // ----------------------- + int w; + float flSupport[MAX_NUM_PANELS][MAX_NUM_PANELS]; + for (w=0;wRandomInt(0,1)) + { + DropPane(w,h); + } + // Otherwise just shatter the glass + else + { + ShatterPane(w,h,vec3_origin,vec3_origin); + } + SetNextThink( gpGlobals->curtime ); + } + } + } + } +} + +//------------------------------------------------------------------------------ +// Purpose : Given a 3D position on the window in space return the height and +// width of the position from the window's corner +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakableSurface::PanePos(const Vector &vPos, float *flWidth, float *flHeight) +{ + Vector vAttackVec = vPos - m_vCorner; + QAngle vAngles; + VectorAngles(-1*m_vNormal,vAngles); + Vector vWidthDir,vHeightDir; + AngleVectors(vAngles,NULL,&vWidthDir,&vHeightDir); + float flWDist = DotProduct(vWidthDir,vAttackVec); + float flHDist = DotProduct(vHeightDir,vAttackVec); + + // Figure out which quadrent I'm in + *flWidth = flWDist/m_flPanelWidth; + *flHeight = flHDist/m_flPanelHeight; +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakableSurface::BreakAllPanes(void) +{ + // Now tell the client all the panes have been broken + for (int width=0;width= m_nNumWide) return; + if (nHeight < 0 || nHeight >= m_nNumHigh) return; + + // Count how many panes have been broken or dropped + m_nNumBrokenPanes++; + SetSupport( nWidth, nHeight, WINDOW_PANE_BROKEN ); + + SetThink(&CBreakableSurface::BreakThink); + SetNextThink( gpGlobals->curtime ); +} + +//------------------------------------------------------------------------------ +// Purpose : Drop a window pane entity +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakableSurface::DropPane(int nWidth, int nHeight) +{ + // Check parameter range + if (nWidth < 0 || nWidth >= m_nNumWide) return; + if (nHeight < 0 || nHeight >= m_nNumHigh) return; + + if (!IsBroken(nWidth,nHeight)) + { + BreakPane(nWidth,nHeight); + + QAngle vAngles; + VectorAngles(-1*m_vNormal,vAngles); + + Vector vWidthDir,vHeightDir; + AngleVectors(vAngles,NULL,&vWidthDir,&vHeightDir); + Vector vBreakPos = m_vCorner + + (nWidth*vWidthDir*m_flPanelWidth) + + (nHeight*vHeightDir*m_flPanelHeight); + + CreateShards(vBreakPos, vAngles, vec3_origin, vec3_origin, + WINDOW_PANEL_SIZE, WINDOW_PANEL_SIZE, + WINDOW_SMALL_SHARD_SIZE); + + DamageSound(); + + CWindowPane *pPane = CWindowPane::CreateWindowPane(vBreakPos, vAngles); + if (pPane) + { + pPane->SetLocalAngularVelocity( RandomAngle(-120,120) ); + } + } +} + +void CBreakableSurface::CreateShards(const Vector &vBreakPos, const QAngle &vAngles, + const Vector &vForce, const Vector &vForcePos, + float flWidth, float flHeight, + int nShardSize) +{ + Vector vAdjustedBreakPos = vBreakPos; + Vector vAdjustedForce = vForce; + int front_r,front_g,front_b; + int back_r,back_g,back_b; + + + // UNDONE: For now hardcode these colors. Later when used by more textures + // we'll automate this process or expose the colors in WC + if (m_nSurfaceType == SHATTERSURFACE_TILE) + { + // If tile shoot shards back from the shattered surface and offset slightly + // from the surface. + vAdjustedBreakPos -= 8*m_vNormal; + vAdjustedForce = -0.75*vForce; + front_r = 89; + front_g = 120; + front_b = 83; + back_r = 99; + back_g = 76; + back_b = 21; + } + else + { + front_r = 255; + front_g = 255; + front_b = 255; + back_r = 255; + back_g = 255; + back_b = 255; + } + + CPASFilter filter( vAdjustedBreakPos ); + te->ShatterSurface(filter, 0.0, + &vAdjustedBreakPos, &vAngles, + &vAdjustedForce, &vForcePos, + flWidth, flHeight,WINDOW_SMALL_SHARD_SIZE,m_nSurfaceType, + front_r,front_g,front_b,back_r,back_g,back_b);//4); +} + +//------------------------------------------------------------------------------ +// Purpose : Break a panel +// Input : +// Output : +//------------------------------------------------------------------------------ +bool CBreakableSurface::ShatterPane(int nWidth, int nHeight, const Vector &vForce, const Vector &vForcePos) +{ + // Check parameter range + if (nWidth < 0 || nWidth >= m_nNumWide) return false; + if (nHeight < 0 || nHeight >= m_nNumHigh) return false; + + if ( IsBroken(nWidth,nHeight) ) + return false; + + BreakPane(nWidth,nHeight); + + QAngle vAngles; + VectorAngles(-1*m_vNormal,vAngles); + Vector vWidthDir,vHeightDir; + AngleVectors(vAngles,NULL,&vWidthDir,&vHeightDir); + Vector vBreakPos = m_vCorner + + (nWidth*vWidthDir*m_flPanelWidth) + + (nHeight*vHeightDir*m_flPanelHeight); + + CreateShards(vBreakPos, vAngles,vForce, vForcePos, m_flPanelWidth, m_flPanelHeight, WINDOW_SMALL_SHARD_SIZE); + + DamageSound(); + return true; +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBreakableSurface::Spawn(void) +{ + BaseClass::Spawn(); + SetCollisionGroup( COLLISION_GROUP_BREAKABLE_GLASS ); + m_bIsBroken = false; + + if (m_nQuadError == QUAD_ERR_MULT_FACES) + { + Warning("Rejecting func_breakablesurf. Has multiple faces that aren't NODRAW.\n"); + UTIL_Remove(this); + } + else if (m_nQuadError == QUAD_ERR_NOT_QUAD) + { + Warning("Rejecting func_breakablesurf. Drawn face isn't a quad.\n"); + UTIL_Remove(this); + } + + int materialCount = modelinfo->GetModelMaterialCount( const_cast(GetModel()) ); + if( materialCount != 1 ) + { + Warning( "Encountered func_breakablesurf that has a material applied to more than one surface!\n" ); + UTIL_Remove(this); + } + + // Get at the first material; even if there are more than one. + IMaterial* pMaterial; + modelinfo->GetModelMaterials( const_cast(GetModel()), 1, &pMaterial ); + + // The material should point to a cracked version of itself + bool foundVar; + IMaterialVar* pCrackName = pMaterial->FindVar( "$crackmaterial", &foundVar, false ); + if (foundVar) + { + PrecacheMaterial( pCrackName->GetStringValue() ); + } + + // Init the Panel bit vector to all true. ( no panes are broken ) + int bitVecLength = MAX_NUM_PANELS * MAX_NUM_PANELS; + + for( int i=0;i 10 ) + { + // HACKHACK: Reset mass to get correct collision response for the object breaking this + pEvent->pObjects[index]->SetMass( 2.0f ); + + Vector normal, damagePos; + pEvent->pInternalData->GetSurfaceNormal( normal ); + if ( index == 0 ) + { + normal *= -1.0f; + } + pEvent->pInternalData->GetContactPoint( damagePos ); + int otherIndex = !index; + CBaseEntity *pInflictor = pEvent->pEntities[otherIndex]; + CTakeDamageInfo info( pInflictor, pInflictor, normal, damagePos, damage, damageType ); + PhysCallbackDamage( this, info, *pEvent, index ); + } + else if ( damage > 0 ) + { + if ( m_spawnflags & SF_BREAKABLESURF_CRACK_DECALS ) + { + + Vector normal, damagePos; + pEvent->pInternalData->GetSurfaceNormal( normal ); + if ( index == 0 ) + { + normal *= -1.0f; + } + pEvent->pInternalData->GetContactPoint( damagePos ); + + trace_t tr; + UTIL_TraceLine ( damagePos - normal, damagePos + normal, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + + // Only place decals and draw effects if we hit something valid + if ( tr.m_pEnt && tr.m_pEnt == this ) + { + // Build the impact data + CEffectData data; + data.m_vOrigin = tr.endpos; + data.m_vStart = tr.startpos; + data.m_nSurfaceProp = tr.surface.surfaceProps; + data.m_nDamageType = DMG_CLUB; + data.m_nHitBox = tr.hitbox; + data.m_nEntIndex = entindex(); + + // Send it on its way + DispatchEffect( "Impact", data ); + } + } + } + } + BaseClass::VPhysicsCollision( index, pEvent ); +} + diff --git a/sp/src/game/server/func_breakablesurf.h b/sp/src/game/server/func_breakablesurf.h new file mode 100644 index 00000000..58e4f0f2 --- /dev/null +++ b/sp/src/game/server/func_breakablesurf.h @@ -0,0 +1,102 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef FUNC_BREAKABLESURF_H +#define FUNC_BREAKABLESURF_H +#ifdef _WIN32 +#pragma once +#endif + + +#define MAX_NUM_PANELS 16 //Must match client + +#include "func_break.h" + +//############################################################################# +// > CWindowPane +// +// A piece that falls out of the window +//############################################################################# +class CWindowPane : public CBaseAnimating +{ +public: + DECLARE_CLASS( CWindowPane, CBaseAnimating ); + + static CWindowPane* CreateWindowPane( const Vector &vecOrigin, const QAngle &vecAngles ); + + void Spawn( void ); + void Precache( void ); + void PaneTouch( CBaseEntity *pOther ); + void Die( void ); + DECLARE_DATADESC(); +}; + +//############################################################################# +// > CBreakableSurface +// +// A breakable surface +//############################################################################# +class CBreakableSurface : public CBreakable +{ + DECLARE_CLASS( CBreakableSurface, CBreakable ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + +public: + CNetworkVar( int, m_nNumWide ); + CNetworkVar( int, m_nNumHigh ); + CNetworkVar( float, m_flPanelWidth ); + CNetworkVar( float, m_flPanelHeight ); + CNetworkVector( m_vNormal ); + CNetworkVector( m_vCorner ); + CNetworkVar( bool, m_bIsBroken ); + CNetworkVar( ShatterSurface_t, m_nSurfaceType ); + int m_nNumBrokenPanes; + float m_flSupport[MAX_NUM_PANELS][MAX_NUM_PANELS]; //UNDONE: allocate dynamically? + + int m_nFragility; + Vector m_vLLVertex; + Vector m_vULVertex; + Vector m_vLRVertex; + Vector m_vURVertex; + int m_nQuadError; + + void SurfaceTouch( CBaseEntity *pOther ); + void PanePos(const Vector &vPos, float *flWidth, float *flHeight); + + bool IsBroken(int nWidth, int nHeight); + void SetSupport(int w, int h, float support); + + float GetSupport(int nWidth, int nHeight); + float RecalcSupport(int nWidth, int nHeight); + + void BreakPane(int nWidth, int nHeight); + void DropPane(int nWidth, int nHeight); + bool ShatterPane(int nWidth, int nHeight, const Vector &force, const Vector &vForcePos); + void BreakAllPanes(void); + + void CreateShards(const Vector &vBreakPos, const QAngle &vAngles, + const Vector &vForce, const Vector &vForcePos, + float flWidth, float flHeight, + int nShardSize); + + void Spawn(void); + void Precache(void); + void Die( CBaseEntity *pBreaker, const Vector &vAttackDir ); + void BreakThink(void); + void Event_Killed( CBaseEntity *pInflictor, CBaseEntity *pAttacker, float flDamage, int bitsDamageType ); + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + int OnTakeDamage( const CTakeDamageInfo &info ); + void InputShatter( inputdata_t &inputdata ); + void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); +private: + // One bit per pane + CNetworkArray( bool, m_RawPanelBitVec, MAX_NUM_PANELS * MAX_NUM_PANELS ); +}; + +#endif // FUNC_BREAKABLESURF_H + diff --git a/sp/src/game/server/func_dust.cpp b/sp/src/game/server/func_dust.cpp new file mode 100644 index 00000000..3e3fcb21 --- /dev/null +++ b/sp/src/game/server/func_dust.cpp @@ -0,0 +1,332 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Volumetric dust motes. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "func_dust_shared.h" +#include "te_particlesystem.h" +#include "IEffects.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CFunc_Dust : public CBaseEntity +{ +public: + DECLARE_CLASS( CFunc_Dust, CBaseEntity ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CFunc_Dust(); + virtual ~CFunc_Dust(); + + +// CBaseEntity overrides. +public: + + virtual void Spawn(); + virtual void Activate(); + virtual void Precache(); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + + +// Input handles. +public: + + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + + +// FGD properties. +public: + + CNetworkVar( color32, m_Color ); + CNetworkVar( int, m_SpawnRate ); + + CNetworkVar( float, m_flSizeMin ); + CNetworkVar( float, m_flSizeMax ); + + CNetworkVar( int, m_SpeedMax ); + + CNetworkVar( int, m_LifetimeMin ); + CNetworkVar( int, m_LifetimeMax ); + + CNetworkVar( int, m_DistMax ); + + CNetworkVar( float, m_FallSpeed ); + +public: + + CNetworkVar( int, m_DustFlags ); // Combination of DUSTFLAGS_ + +private: + int m_iAlpha; + +}; + + +class CFunc_DustMotes : public CFunc_Dust +{ + DECLARE_CLASS( CFunc_DustMotes, CFunc_Dust ); +public: + CFunc_DustMotes(); +}; + + +class CFunc_DustCloud : public CFunc_Dust +{ + DECLARE_CLASS( CFunc_DustCloud, CFunc_Dust ); +public: +}; + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CFunc_Dust, DT_Func_Dust ) + SendPropInt( SENDINFO(m_Color), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_SpawnRate), 12, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_SpeedMax), 12, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_flSizeMin), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flSizeMax), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO(m_DistMax), 16, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_LifetimeMin), 4, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_LifetimeMax), 4, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_DustFlags), DUST_NUMFLAGS, SPROP_UNSIGNED ), + + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropFloat( SENDINFO(m_FallSpeed), 0, SPROP_NOSCALE ), + SendPropDataTable( SENDINFO_DT( m_Collision ), &REFERENCE_SEND_TABLE(DT_CollisionProperty) ), +END_SEND_TABLE() + + +BEGIN_DATADESC( CFunc_Dust ) + + DEFINE_FIELD( m_DustFlags,FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_Color, FIELD_COLOR32, "Color" ), + DEFINE_KEYFIELD( m_SpawnRate, FIELD_INTEGER, "SpawnRate" ), + DEFINE_KEYFIELD( m_flSizeMin, FIELD_FLOAT, "SizeMin" ), + DEFINE_KEYFIELD( m_flSizeMax, FIELD_FLOAT, "SizeMax" ), + DEFINE_KEYFIELD( m_SpeedMax, FIELD_INTEGER, "SpeedMax" ), + DEFINE_KEYFIELD( m_LifetimeMin, FIELD_INTEGER, "LifetimeMin" ), + DEFINE_KEYFIELD( m_LifetimeMax, FIELD_INTEGER, "LifetimeMax" ), + DEFINE_KEYFIELD( m_DistMax, FIELD_INTEGER, "DistMax" ), + DEFINE_FIELD( m_iAlpha, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_FallSpeed, FIELD_FLOAT, "FallSpeed" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ) + + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_dustmotes, CFunc_DustMotes ); +LINK_ENTITY_TO_CLASS( func_dustcloud, CFunc_DustCloud ); + + +// ------------------------------------------------------------------------------------- // +// CFunc_DustMotes implementation. +// ------------------------------------------------------------------------------------- // + +CFunc_DustMotes::CFunc_DustMotes() +{ + m_DustFlags |= DUSTFLAGS_SCALEMOTES; +} + + + +// ------------------------------------------------------------------------------------- // +// CFunc_Dust implementation. +// ------------------------------------------------------------------------------------- // + +CFunc_Dust::CFunc_Dust() +{ + m_DustFlags = DUSTFLAGS_ON; + m_FallSpeed = 0.0f; +} + + +CFunc_Dust::~CFunc_Dust() +{ +} + + +void CFunc_Dust::Spawn() +{ + Precache(); + + // Bind to our bmodel. + SetModel( STRING( GetModelName() ) ); + //AddSolidFlags( FSOLID_NOT_SOLID ); + AddSolidFlags( FSOLID_VOLUME_CONTENTS ); + + //Since keyvalues can arrive in any order, and UTIL_StringToColor32 stomps alpha, + //install the alpha value here. + color32 clr = { m_Color.m_Value.r, m_Color.m_Value.g, m_Color.m_Value.b, m_iAlpha }; + m_Color.Set( clr ); + + BaseClass::Spawn(); +} + + +void CFunc_Dust::Precache() +{ + PrecacheMaterial( "particle/sparkles" ); +} + +void CFunc_Dust::Activate() +{ + BaseClass::Activate(); +} + + +bool CFunc_Dust::KeyValue( const char *szKeyName, const char *szValue ) +{ + if( stricmp( szKeyName, "StartDisabled" ) == 0 ) + { + if( szValue[0] == '1' ) + m_DustFlags &= ~DUSTFLAGS_ON; + else + m_DustFlags |= DUSTFLAGS_ON; + + return true; + } + else if( stricmp( szKeyName, "Alpha" ) == 0 ) + { + m_iAlpha = atoi( szValue ); + return true; + } + else if( stricmp( szKeyName, "Frozen" ) == 0 ) + { + if( szValue[0] == '1' ) + m_DustFlags |= DUSTFLAGS_FROZEN; + else + m_DustFlags &= ~DUSTFLAGS_FROZEN; + + return true; + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } +} + + +void CFunc_Dust::InputTurnOn( inputdata_t &inputdata ) +{ + if( !(m_DustFlags & DUSTFLAGS_ON) ) + { + m_DustFlags |= DUSTFLAGS_ON; + } +} + + +void CFunc_Dust::InputTurnOff( inputdata_t &inputdata ) +{ + if( m_DustFlags & DUSTFLAGS_ON ) + { + m_DustFlags &= ~DUSTFLAGS_ON; + } +} + +// +// Dust +// + +class CTEDust : public CTEParticleSystem +{ +public: + DECLARE_CLASS( CTEDust, CTEParticleSystem ); + DECLARE_SERVERCLASS(); + + CTEDust( const char *name ); + virtual ~CTEDust( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ) { }; + + CNetworkVar( float, m_flSize ); + CNetworkVar( float, m_flSpeed ); + CNetworkVector( m_vecDirection ); +}; + +CTEDust::CTEDust( const char *name ) : BaseClass( name ) +{ + m_flSize = 1.0f; + m_flSpeed = 1.0f; + m_vecDirection.Init(); +} + +CTEDust::~CTEDust( void ) +{ +} + +IMPLEMENT_SERVERCLASS_ST( CTEDust, DT_TEDust ) + SendPropFloat( SENDINFO(m_flSize), -1, SPROP_COORD ), + SendPropFloat( SENDINFO(m_flSpeed), -1, SPROP_COORD ), + SendPropVector( SENDINFO(m_vecDirection), 4, 0, -1.0f, 1.0f ), // cheap normal +END_SEND_TABLE() + +static CTEDust g_TEDust( "Dust" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &pos - +// &angles - +//----------------------------------------------------------------------------- +void TE_Dust( IRecipientFilter& filter, float delay, + const Vector &pos, const Vector &dir, float size, float speed ) +{ + g_TEDust.m_vecOrigin = pos; + g_TEDust.m_vecDirection = dir; + g_TEDust.m_flSize = size; + g_TEDust.m_flSpeed = speed; + + Assert( dir.Length() < 1.01 ); // make sure it's a normal + + //Send it + g_TEDust.Create( filter, delay ); +} + +class CEnvDustPuff : public CPointEntity +{ + DECLARE_CLASS( CEnvDustPuff, CPointEntity ); + +public: + + DECLARE_DATADESC(); + +protected: + + // Input handlers + void InputSpawnDust( inputdata_t &inputdata ); + + float m_flScale; + color32 m_rgbaColor; +}; + +LINK_ENTITY_TO_CLASS( env_dustpuff, CEnvDustPuff ); + +BEGIN_DATADESC( CEnvDustPuff ) + + DEFINE_KEYFIELD( m_flScale, FIELD_FLOAT, "scale" ), + DEFINE_KEYFIELD( m_rgbaColor, FIELD_COLOR32, "color" ), + + // Function Pointers + DEFINE_INPUTFUNC( FIELD_VOID, "SpawnDust", InputSpawnDust ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CEnvDustPuff::InputSpawnDust( inputdata_t &inputdata ) +{ + Vector dir; + AngleVectors( GetAbsAngles(), &dir ); + + VectorNormalize( dir ); + + g_pEffects->Dust( GetAbsOrigin(), dir, m_flScale, m_flSpeed ); +} diff --git a/sp/src/game/server/func_ladder_endpoint.cpp b/sp/src/game/server/func_ladder_endpoint.cpp new file mode 100644 index 00000000..73d29c52 --- /dev/null +++ b/sp/src/game/server/func_ladder_endpoint.cpp @@ -0,0 +1,68 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// +#include "cbase.h" +#include "func_ladder.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: A transient entity used to construct a true CFuncLadder +// FIXME: THIS ENTITY IS OBSOLETE NOW, SHOULD BE REMOVED FROM HERE AND .FGD AT SOME POINT!!! +//----------------------------------------------------------------------------- +class CFuncLadderEndPoint : public CBaseEntity +{ +public: + DECLARE_CLASS( CFuncLadderEndPoint, CBaseEntity ); + + virtual void Activate(); + +private: + bool Validate(); +}; + +LINK_ENTITY_TO_CLASS( func_ladderendpoint, CFuncLadderEndPoint ); + +void CFuncLadderEndPoint::Activate() +{ + BaseClass::Activate(); + + if ( IsMarkedForDeletion() ) + return; + + Validate(); +} + +bool CFuncLadderEndPoint::Validate() +{ + // Find the the other end + Vector startPos = GetAbsOrigin(); + + CFuncLadderEndPoint *other = dynamic_cast< CFuncLadderEndPoint * >( GetNextTarget() ); + if ( !other ) + { + DevMsg( 1, "func_ladderendpoint(%s) without matching target\n", GetEntityName().ToCStr() ); + return false; + } + + Vector endPos = other->GetAbsOrigin(); + + CFuncLadder *ladder = ( CFuncLadder * )CreateEntityByName( "func_useableladder" ); + if ( ladder ) + { + ladder->SetEndPoints( startPos, endPos ); + ladder->SetAbsOrigin( GetAbsOrigin() ); + ladder->SetParent( GetParent() ); + ladder->SetName( GetEntityName() ); + ladder->Spawn(); + } + + // Delete both endpoints + UTIL_Remove( other ); + UTIL_Remove( this ); + + return true; +} diff --git a/sp/src/game/server/func_lod.cpp b/sp/src/game/server/func_lod.cpp new file mode 100644 index 00000000..e1c41c12 --- /dev/null +++ b/sp/src/game/server/func_lod.cpp @@ -0,0 +1,130 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CFunc_LOD : public CBaseEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CFunc_LOD, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + + CFunc_LOD(); + virtual ~CFunc_LOD(); + + + // When the viewer is between: + // (0 and m_fNonintrusiveDist): the bmodel is forced to be visible + // (m_fNonintrusiveDist and m_fDisappearDist): the bmodel is trying to appear or disappear nonintrusively + // (waits until it's out of the view frustrum or until there's a lot of motion) + // (m_fDisappearDist+): the bmodel is forced to be invisible + CNetworkVar( float, m_fDisappearDist ); +#ifdef MAPBASE + CNetworkVar( float, m_fDisappearMaxDist ); +#endif + +// CBaseEntity overrides. +public: + + virtual void Spawn(); + bool CreateVPhysics(); + virtual void Activate(); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); +}; + + +IMPLEMENT_SERVERCLASS_ST(CFunc_LOD, DT_Func_LOD) + SendPropFloat(SENDINFO(m_fDisappearDist), 0, SPROP_NOSCALE), +#ifdef MAPBASE + SendPropFloat(SENDINFO(m_fDisappearMaxDist), 0, SPROP_NOSCALE), +#endif +END_SEND_TABLE() + + +LINK_ENTITY_TO_CLASS(func_lod, CFunc_LOD); + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CFunc_LOD ) + + DEFINE_FIELD( m_fDisappearDist, FIELD_FLOAT ), +#ifdef MAPBASE + DEFINE_FIELD( m_fDisappearMaxDist, FIELD_FLOAT ), +#endif + +END_DATADESC() + + +// ------------------------------------------------------------------------------------- // +// CFunc_LOD implementation. +// ------------------------------------------------------------------------------------- // +CFunc_LOD::CFunc_LOD() +{ +} + + +CFunc_LOD::~CFunc_LOD() +{ +} + + +void CFunc_LOD::Spawn() +{ + // Bind to our bmodel. + SetModel( STRING( GetModelName() ) ); + SetSolid( SOLID_BSP ); + BaseClass::Spawn(); + + CreateVPhysics(); +} + +bool CFunc_LOD::CreateVPhysics() +{ + VPhysicsInitStatic(); + return true; +} + +void CFunc_LOD::Activate() +{ + BaseClass::Activate(); +} + + +bool CFunc_LOD::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "DisappearDist")) + { + m_fDisappearDist = (float)atof(szValue); + } +#ifdef MAPBASE + else if (FStrEq(szKeyName, "DisappearMaxDist")) + { + m_fDisappearMaxDist = (float)atof(szValue); + } +#endif + else if (FStrEq(szKeyName, "Solid")) + { + if (atoi(szValue) != 0) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + } + else + { + return BaseClass::KeyValue(szKeyName, szValue); + } + + return true; +} + diff --git a/sp/src/game/server/func_movelinear.cpp b/sp/src/game/server/func_movelinear.cpp new file mode 100644 index 00000000..0fd92a82 --- /dev/null +++ b/sp/src/game/server/func_movelinear.cpp @@ -0,0 +1,476 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements a brush model entity that moves along a linear path. +// Water whose level can be changed is implemented using the same entity. +// +//=============================================================================// + +#include "cbase.h" +#include "func_movelinear.h" +#include "entitylist.h" +#include "locksounds.h" +#include "ndebugoverlay.h" +#include "engine/IEngineSound.h" +#include "physics_saverestore.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// ------------------------------- +// SPAWN_FLAGS +// ------------------------------- +#define SF_MOVELINEAR_NOTSOLID 8 + +LINK_ENTITY_TO_CLASS( func_movelinear, CFuncMoveLinear ); +LINK_ENTITY_TO_CLASS( momentary_door, CFuncMoveLinear ); // For backward compatibility + +// +// func_water_analog is implemented as a linear mover so we can raise/lower the water level. +// +LINK_ENTITY_TO_CLASS( func_water_analog, CFuncMoveLinear ); + + +BEGIN_DATADESC( CFuncMoveLinear ) + + DEFINE_KEYFIELD( m_vecMoveDir, FIELD_VECTOR, "movedir" ), + DEFINE_KEYFIELD( m_soundStart, FIELD_SOUNDNAME, "StartSound" ), + DEFINE_KEYFIELD( m_soundStop, FIELD_SOUNDNAME, "StopSound" ), + DEFINE_FIELD( m_currentSound, FIELD_SOUNDNAME ), + DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "BlockDamage"), + DEFINE_KEYFIELD( m_flStartPosition, FIELD_FLOAT, "StartPosition"), + DEFINE_KEYFIELD( m_flMoveDistance, FIELD_FLOAT, "MoveDistance"), +#ifdef MAPBASE + DEFINE_FIELD( m_vecReference, FIELD_VECTOR ), +#endif +// DEFINE_PHYSPTR( m_pFluidController ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Open", InputOpen ), + DEFINE_INPUTFUNC( FIELD_VOID, "Close", InputClose ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPosition", InputSetPosition ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), + + // Outputs + DEFINE_OUTPUT( m_OnFullyOpen, "OnFullyOpen" ), + DEFINE_OUTPUT( m_OnFullyClosed, "OnFullyClosed" ), + + // Functions + DEFINE_FUNCTION( StopMoveSound ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose: Called before spawning, after keyvalues have been parsed. +//------------------------------------------------------------------------------ +void CFuncMoveLinear::Spawn( void ) +{ + // Convert movedir from angles to a vector + QAngle angMoveDir = QAngle( m_vecMoveDir.x, m_vecMoveDir.y, m_vecMoveDir.z ); + AngleVectors( angMoveDir, &m_vecMoveDir ); + + SetMoveType( MOVETYPE_PUSH ); + SetModel( STRING( GetModelName() ) ); + + // Don't allow zero or negative speeds + if (m_flSpeed <= 0) + { + m_flSpeed = 100; + } + + // If move distance is set to zero, use with width of the + // brush to determine the size of the move distance + if (m_flMoveDistance <= 0) + { + Vector vecOBB = CollisionProp()->OBBSize(); + vecOBB -= Vector( 2, 2, 2 ); + m_flMoveDistance = DotProductAbs( m_vecMoveDir, vecOBB ) - m_flLip; + } + +#ifdef MAPBASE + m_vecPosition1 = GetLocalOrigin() - (m_vecMoveDir * m_flMoveDistance * m_flStartPosition); + m_vecPosition2 = m_vecPosition1 + (m_vecMoveDir * m_flMoveDistance); + m_vecFinalDest = GetLocalOrigin(); + m_vecReference = GetLocalOrigin(); +#else + m_vecPosition1 = GetAbsOrigin() - (m_vecMoveDir * m_flMoveDistance * m_flStartPosition); + m_vecPosition2 = m_vecPosition1 + (m_vecMoveDir * m_flMoveDistance); + m_vecFinalDest = GetAbsOrigin(); +#endif + + SetTouch( NULL ); + + Precache(); + + // It is solid? + SetSolid( SOLID_VPHYSICS ); + + if ( FClassnameIs( this, "func_water_analog" ) ) + { + AddSolidFlags( FSOLID_VOLUME_CONTENTS ); + } + + if ( !FClassnameIs( this, "func_water_analog" ) && FBitSet (m_spawnflags, SF_MOVELINEAR_NOTSOLID) ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + CreateVPhysics(); +} + + +bool CFuncMoveLinear::ShouldSavePhysics( void ) +{ + // don't save physics for func_water_analog, regen + return !FClassnameIs( this, "func_water_analog" ); + +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Sets the movement parent of this entity. This entity will be moved +// to a local coordinate calculated from its current absolute offset +// from the parent entity and will then follow the parent entity. +// Input : pParentEntity - This entity's new parent in the movement hierarchy. +//----------------------------------------------------------------------------- +void CFuncMoveLinear::SetParent( CBaseEntity *pParentEntity, int iAttachment ) +{ + Vector oldLocal = GetLocalOrigin(); + + BaseClass::SetParent( pParentEntity, iAttachment ); + + // SOLID_NONE indicates we haven't spawned yet + if (GetSolid() != SOLID_NONE) + { + m_vecReference = ((m_vecReference - oldLocal) + GetLocalOrigin()); + m_vecPosition1 = m_vecReference - (m_vecMoveDir * m_flMoveDistance * m_flStartPosition); + m_vecPosition2 = m_vecPosition1 + (m_vecMoveDir * m_flMoveDistance); + m_vecFinalDest = m_vecReference - m_vecFinalDest; + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFuncMoveLinear::CreateVPhysics( void ) +{ + if ( !FClassnameIs( this, "func_water_analog" ) ) + { + //normal door + if ( !IsSolidFlagSet( FSOLID_NOT_SOLID ) ) + { + VPhysicsInitShadow( false, false ); + } + } + else + { + // special contents + AddSolidFlags( FSOLID_VOLUME_CONTENTS ); + //SETBITS( m_spawnflags, SF_DOOR_SILENT ); // water is silent for now + + IPhysicsObject *pPhysics = VPhysicsInitShadow( false, false ); + fluidparams_t fluid; + + Assert( CollisionProp()->GetCollisionAngles() == vec3_angle ); + fluid.damping = 0.01f; + fluid.surfacePlane[0] = 0; + fluid.surfacePlane[1] = 0; + fluid.surfacePlane[2] = 1; + fluid.surfacePlane[3] = CollisionProp()->GetCollisionOrigin().z + CollisionProp()->OBBMaxs().z - 1; + fluid.currentVelocity.Init(0,0,0); + fluid.torqueFactor = 0.1f; + fluid.viscosityFactor = 0.01f; + fluid.pGameData = static_cast(this); + + //FIXME: Currently there's no way to specify that you want slime + fluid.contents = CONTENTS_WATER; + + m_pFluidController = physenv->CreateFluidController( pPhysics, &fluid ); + } + + return true; +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CFuncMoveLinear::Precache( void ) +{ + if (m_soundStart != NULL_STRING) + { + PrecacheScriptSound( (char *) STRING(m_soundStart) ); + } + if (m_soundStop != NULL_STRING) + { + PrecacheScriptSound( (char *) STRING(m_soundStop) ); + } + m_currentSound = NULL_STRING; +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CFuncMoveLinear::MoveTo(Vector vPosition, float flSpeed) +{ + if ( flSpeed != 0 ) + { + if ( m_soundStart != NULL_STRING ) + { + if (m_currentSound == m_soundStart) + { + StopSound(entindex(), CHAN_BODY, (char*)STRING(m_soundStop)); + } + else + { + m_currentSound = m_soundStart; + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_BODY; + ep.m_pSoundName = (char*)STRING(m_soundStart); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + } + + LinearMove( vPosition, flSpeed ); + + if ( m_pFluidController ) + { + m_pFluidController->WakeAllSleepingObjects(); + } + + // Clear think (that stops sounds) + SetThink(NULL); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CFuncMoveLinear::StopMoveSound( void ) +{ + if ( m_soundStart != NULL_STRING && ( m_currentSound == m_soundStart ) ) + { + StopSound(entindex(), CHAN_BODY, (char*)STRING(m_soundStart) ); + } + + if ( m_soundStop != NULL_STRING && ( m_currentSound != m_soundStop ) ) + { + m_currentSound = m_soundStop; + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_BODY; + ep.m_pSoundName = (char*)STRING(m_soundStop); + ep.m_flVolume = 1; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + SetThink(NULL); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CFuncMoveLinear::MoveDone( void ) +{ + // Stop sounds at the next think, rather than here as another + // SetPosition call might immediately follow the end of this move + SetThink(&CFuncMoveLinear::StopMoveSound); + SetNextThink( gpGlobals->curtime + 0.1f ); + BaseClass::MoveDone(); + +#ifdef MAPBASE + if ( GetLocalOrigin() == m_vecPosition2 ) + { + m_OnFullyOpen.FireOutput( this, this ); + } + else if ( GetLocalOrigin() == m_vecPosition1 ) + { + m_OnFullyClosed.FireOutput( this, this ); + } +#else + if ( GetAbsOrigin() == m_vecPosition2 ) + { + m_OnFullyOpen.FireOutput( this, this ); + } + else if ( GetAbsOrigin() == m_vecPosition1 ) + { + m_OnFullyClosed.FireOutput( this, this ); + } +#endif +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CFuncMoveLinear::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( useType != USE_SET ) // Momentary buttons will pass down a float in here + return; + + if ( value > 1.0 ) + value = 1.0; + + Vector move = m_vecPosition1 + (value * (m_vecPosition2 - m_vecPosition1)); + + Vector delta = move - GetLocalOrigin(); + float speed = delta.Length() * 10; + + MoveTo(move, speed); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the position as a value from [0..1]. +//----------------------------------------------------------------------------- +void CFuncMoveLinear::SetPosition( float flPosition ) +{ + Vector vTargetPos = m_vecPosition1 + ( flPosition * (m_vecPosition2 - m_vecPosition1)); + if ((vTargetPos - GetLocalOrigin()).Length() > 0.001) + { + MoveTo(vTargetPos, m_flSpeed); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CFuncMoveLinear::InputOpen( inputdata_t &inputdata ) +{ + if (GetLocalOrigin() != m_vecPosition2) + { + MoveTo(m_vecPosition2, m_flSpeed); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CFuncMoveLinear::InputClose( inputdata_t &inputdata ) +{ + if (GetLocalOrigin() != m_vecPosition1) + { + MoveTo(m_vecPosition1, m_flSpeed); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler for setting the position from [0..1]. +// Input : Float position. +//----------------------------------------------------------------------------- +void CFuncMoveLinear::InputSetPosition( inputdata_t &inputdata ) +{ + SetPosition( inputdata.value.Float() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called every frame when the bruch is blocked while moving +// Input : pOther - The blocking entity. +//----------------------------------------------------------------------------- +void CFuncMoveLinear::Blocked( CBaseEntity *pOther ) +{ + // Hurt the blocker + if ( m_flBlockDamage ) + { + if ( pOther->m_takedamage == DAMAGE_EVENTS_ONLY ) + { + if ( FClassnameIs( pOther, "gib" ) ) + UTIL_Remove( pOther ); + } + else + pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CFuncMoveLinear::InputSetSpeed( inputdata_t &inputdata ) +{ + // Set the new speed + m_flSpeed = inputdata.value.Float(); + + // FIXME: This is a little questionable. Do we want to fix the speed, or let it continue on at the old speed? + float flDistToGoalSqr = ( m_vecFinalDest - GetAbsOrigin() ).LengthSqr(); + if ( flDistToGoalSqr > Square( FLT_EPSILON ) ) + { + // NOTE: We do NOT want to call sound functions here, just vanilla position changes + LinearMove( m_vecFinalDest, m_flSpeed ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CFuncMoveLinear::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { +#ifdef MAPBASE + if (GetMoveParent()) + { + Vector vecReference, vecPosition1, vecPosition2; + QAngle angReference; + + vecReference = m_vecFinalDest + GetMoveParent()->GetAbsOrigin(); + angReference = GetAbsAngles(); + vecPosition1 = vecReference + m_vecPosition1; + vecPosition2 = vecReference + m_vecPosition2; + + NDebugOverlay::Axis( vecReference, angReference, 12.0f, true, 0.15f ); + NDebugOverlay::Axis( vecPosition1, angReference, 5.0f, true, 0.15f ); + NDebugOverlay::Axis( vecPosition2, angReference, 2.5f, true, 0.15f ); + + char tempstr[512]; + float flTravelDist = (vecPosition1 - vecPosition2).Length(); + float flCurDist = (vecPosition1 - GetAbsOrigin()).Length(); + Q_snprintf(tempstr,sizeof(tempstr),"Current Pos: %3.3f",flCurDist/flTravelDist); + EntityText(text_offset,tempstr,0); + text_offset++; + + float flTargetDist = (vecPosition1 - m_vecFinalDest).Length(); + Q_snprintf(tempstr,sizeof(tempstr),"Target Pos: %3.3f",flTargetDist/flTravelDist); + EntityText(text_offset,tempstr,0); + text_offset++; + } + else + { +#else + char tempstr[512]; + float flTravelDist = (m_vecPosition1 - m_vecPosition2).Length(); + float flCurDist = (m_vecPosition1 - GetLocalOrigin()).Length(); + Q_snprintf(tempstr,sizeof(tempstr),"Current Pos: %3.3f",flCurDist/flTravelDist); + EntityText(text_offset,tempstr,0); + text_offset++; + + float flTargetDist = (m_vecPosition1 - m_vecFinalDest).Length(); + Q_snprintf(tempstr,sizeof(tempstr),"Target Pos: %3.3f",flTargetDist/flTravelDist); + EntityText(text_offset,tempstr,0); + text_offset++; +#endif +#ifdef MAPBASE + } +#endif + } + return text_offset; +} diff --git a/sp/src/game/server/func_movelinear.h b/sp/src/game/server/func_movelinear.h new file mode 100644 index 00000000..ac58f963 --- /dev/null +++ b/sp/src/game/server/func_movelinear.h @@ -0,0 +1,72 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef FUNC_MOVELINEAR_H +#define FUNC_MOVELINEAR_H + +#pragma once + +#include "basetoggle.h" +#include "entityoutput.h" + + +class IPhysicsFluidController; + + +class CFuncMoveLinear : public CBaseToggle +{ +public: + DECLARE_CLASS( CFuncMoveLinear, CBaseToggle ); + + void Spawn( void ); + void Precache( void ); + bool CreateVPhysics( void ); + bool ShouldSavePhysics( void ); + +#ifdef MAPBASE + void SetParent( CBaseEntity* pNewParent, int iAttachment = -1 ); +#endif + + void MoveTo(Vector vPosition, float flSpeed); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void MoveDone( void ); + void StopMoveSound( void ); + void Blocked( CBaseEntity *pOther ); + void SetPosition( float flPosition ); + + int DrawDebugTextOverlays(void); + + // Input handlers + void InputOpen( inputdata_t &inputdata ); + void InputClose( inputdata_t &inputdata ); + void InputSetPosition( inputdata_t &inputdata ); + void InputSetSpeed( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + Vector m_vecMoveDir; // Move direction. + + string_t m_soundStart; // start and looping sound + string_t m_soundStop; // stop sound + string_t m_currentSound; // sound I'm playing + + float m_flBlockDamage; // Damage inflicted when blocked. + float m_flStartPosition; // Position of brush when spawned + float m_flMoveDistance; // Total distance the brush can move +#ifdef MAPBASE + // For the parenting fix. + // Prevents position inconsistencies when changing parent. + Vector m_vecReference; +#endif + + IPhysicsFluidController *m_pFluidController; + + // Outputs + COutputEvent m_OnFullyOpen; + COutputEvent m_OnFullyClosed; +}; +#endif // FUNC_MOVELINEAR_H diff --git a/sp/src/game/server/func_occluder.cpp b/sp/src/game/server/func_occluder.cpp new file mode 100644 index 00000000..37308df6 --- /dev/null +++ b/sp/src/game/server/func_occluder.cpp @@ -0,0 +1,123 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: area portal entity: toggles visibility areas on/off +// +// NOTE: These are not really brush entities. They are brush entities from a +// designer/worldcraft perspective, but by the time they reach the game, the +// brush model is gone and this is, in effect, a point entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CFuncOccluder : public CBaseEntity +{ +public: + DECLARE_CLASS( CFuncOccluder, CBaseEntity ); + + CFuncOccluder(); + + virtual void Spawn( void ); + virtual int UpdateTransmitState( void ); + + // Input handlers + void InputActivate( inputdata_t &inputdata ); + void InputDeactivate( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + +private: + CNetworkVar( bool, m_bActive ); + CNetworkVar( int, m_nOccluderIndex ); +}; + +LINK_ENTITY_TO_CLASS( func_occluder, CFuncOccluder ); + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CFuncOccluder, DT_FuncOccluder) + SendPropBool( SENDINFO(m_bActive) ), + SendPropInt(SENDINFO(m_nOccluderIndex), 10, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +BEGIN_DATADESC( CFuncOccluder ) + + DEFINE_KEYFIELD( m_bActive, FIELD_BOOLEAN, "StartActive" ), + + // NOTE: This keyfield is computed + inserted by VBSP + DEFINE_KEYFIELD( m_nOccluderIndex, FIELD_INTEGER, "occludernumber" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Occluder : +//------------------------------------------------------------------------------ +CFuncOccluder::CFuncOccluder() +{ + m_bActive = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncOccluder::Spawn( void ) +{ + Precache( ); + + m_takedamage = DAMAGE_NO; + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + // set size and link into world. + SetModel( STRING( GetModelName() ) ); +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CFuncOccluder::InputDeactivate( inputdata_t &inputdata ) +{ + m_bActive = false; +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CFuncOccluder::InputActivate( inputdata_t &inputdata ) +{ + m_bActive = true; +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CFuncOccluder::InputToggle( inputdata_t &inputdata ) +{ + m_bActive = !m_bActive; +} + + +//------------------------------------------------------------------------------ +// We always want to transmit these bad boys +//------------------------------------------------------------------------------ +int CFuncOccluder::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + diff --git a/sp/src/game/server/func_reflective_glass.cpp b/sp/src/game/server/func_reflective_glass.cpp new file mode 100644 index 00000000..187556b5 --- /dev/null +++ b/sp/src/game/server/func_reflective_glass.cpp @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "cbase.h" +#include "modelentities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CFuncReflectiveGlass : public CFuncBrush +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CFuncReflectiveGlass, CFuncBrush ); + DECLARE_SERVERCLASS(); + + CFuncReflectiveGlass() + { +#ifdef MAPBASE + m_iszReflectRenderTarget = AllocPooledString( "_rt_WaterReflection" ); + m_iszRefractRenderTarget = AllocPooledString( "_rt_WaterRefraction" ); +#endif + } + +#ifdef MAPBASE + void InputSetReflectRenderTarget( inputdata_t &inputdata ) { m_iszReflectRenderTarget = inputdata.value.StringID(); } + void InputSetRefractRenderTarget( inputdata_t &inputdata ) { m_iszRefractRenderTarget = inputdata.value.StringID(); } + + CNetworkVar( string_t, m_iszReflectRenderTarget ); + CNetworkVar( string_t, m_iszRefractRenderTarget ); +#endif +}; + +// automatically hooks in the system's callbacks +BEGIN_DATADESC( CFuncReflectiveGlass ) + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iszReflectRenderTarget, FIELD_STRING, "ReflectRenderTarget" ), + DEFINE_KEYFIELD( m_iszRefractRenderTarget, FIELD_STRING, "RefractRenderTarget" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetReflectRenderTarget", InputSetReflectRenderTarget ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetRefractRenderTarget", InputSetRefractRenderTarget ), +#endif + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_reflective_glass, CFuncReflectiveGlass ); + +IMPLEMENT_SERVERCLASS_ST( CFuncReflectiveGlass, DT_FuncReflectiveGlass ) + +#ifdef MAPBASE + SendPropStringT( SENDINFO( m_iszReflectRenderTarget ) ), + SendPropStringT( SENDINFO( m_iszRefractRenderTarget ) ), +#endif + +END_SEND_TABLE() diff --git a/sp/src/game/server/func_smokevolume.cpp b/sp/src/game/server/func_smokevolume.cpp new file mode 100644 index 00000000..c1e0e8b8 --- /dev/null +++ b/sp/src/game/server/func_smokevolume.cpp @@ -0,0 +1,105 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseparticleentity.h" +#include "sendproxy.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CFuncSmokeVolume : public CBaseParticleEntity +{ +public: + DECLARE_CLASS( CFuncSmokeVolume, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CFuncSmokeVolume(); + void Spawn(); + void Activate( void ); + + // Set the times it fades out at. + void SetDensity( float density ); + +private: + CNetworkVar( color32, m_Color1 ); + CNetworkVar( color32, m_Color2 ); + CNetworkString( m_MaterialName, 255 ); + string_t m_String_tMaterialName; + CNetworkVar( float, m_ParticleDrawWidth ); + CNetworkVar( float, m_ParticleSpacingDistance ); + CNetworkVar( float, m_DensityRampSpeed ); + CNetworkVar( float, m_RotationSpeed ); + CNetworkVar( float, m_MovementSpeed ); + CNetworkVar( float, m_Density ); +}; + +BEGIN_DATADESC( CFuncSmokeVolume ) + + // Save/restore Keyvalue fields + DEFINE_KEYFIELD( m_Color1, FIELD_COLOR32, "Color1" ), + DEFINE_KEYFIELD( m_Color2, FIELD_COLOR32, "Color2" ), +// DEFINE_ARRAY( m_MaterialName, FIELD_STRING, 255 ), + DEFINE_KEYFIELD( m_String_tMaterialName, FIELD_STRING, "Material" ), + DEFINE_KEYFIELD( m_ParticleDrawWidth, FIELD_FLOAT, "ParticleDrawWidth" ), + DEFINE_KEYFIELD( m_ParticleSpacingDistance, FIELD_FLOAT, "ParticleSpacingDistance" ), + DEFINE_KEYFIELD( m_DensityRampSpeed, FIELD_FLOAT, "DensityRampSpeed" ), + DEFINE_KEYFIELD( m_RotationSpeed, FIELD_FLOAT, "RotationSpeed" ), + DEFINE_KEYFIELD( m_MovementSpeed, FIELD_FLOAT, "MovementSpeed" ), + DEFINE_KEYFIELD( m_Density, FIELD_FLOAT, "Density" ), + // inputs + DEFINE_INPUT( m_RotationSpeed, FIELD_FLOAT, "SetRotationSpeed"), + DEFINE_INPUT( m_MovementSpeed, FIELD_FLOAT, "SetMovementSpeed"), + DEFINE_INPUT( m_Density, FIELD_FLOAT, "SetDensity"), + +END_DATADESC() + + + + +IMPLEMENT_SERVERCLASS_ST( CFuncSmokeVolume, DT_FuncSmokeVolume ) + SendPropInt( SENDINFO( m_Color1 ), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), + SendPropInt( SENDINFO( m_Color2 ), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), + SendPropString( SENDINFO( m_MaterialName ) ), + SendPropFloat( SENDINFO( m_ParticleDrawWidth ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_ParticleSpacingDistance ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_DensityRampSpeed ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_RotationSpeed ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_MovementSpeed ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_Density ), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO(m_spawnflags), 8, SPROP_UNSIGNED ) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( func_smokevolume, CFuncSmokeVolume ); + +CFuncSmokeVolume::CFuncSmokeVolume() +{ + m_Density = 1.0f; +} + +void CFuncSmokeVolume::SetDensity( float density ) +{ + m_Density = density; +} + +void CFuncSmokeVolume::Spawn() +{ + memset( m_MaterialName.GetForModify(), 0, sizeof( m_MaterialName ) ); + + // Bind to our bmodel. + SetModel( STRING( GetModelName() ) ); + + BaseClass::Spawn(); +} + +void CFuncSmokeVolume::Activate( void ) +{ + BaseClass::Activate(); + Q_strncpy( m_MaterialName.GetForModify(), STRING( m_String_tMaterialName ), 255 ); +} + diff --git a/sp/src/game/server/functorutils.h b/sp/src/game/server/functorutils.h new file mode 100644 index 00000000..4816664b --- /dev/null +++ b/sp/src/game/server/functorutils.h @@ -0,0 +1,450 @@ +// FunctorUtils.h +// Useful functors +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef _FUNCTOR_UTILS_H_ +#define _FUNCTOR_UTILS_H_ + +#ifdef NEXT_BOT +#include "NextBotInterface.h" +#include "NextBotManager.h" +#endif // NEXT_BOT + +//-------------------------------------------------------------------------------------------------------- +/** + * NOTE: The functors in this file should ideally be game-independent, + * and work for any Source based game + */ +//-------------------------------------------------------------------------------------------------------- + + +//-------------------------------------------------------------------------------------------------------- +/** + * Count the number of living players on a given team (or TEAM_ANY) + */ +class LivePlayerCounter +{ +public: + static const bool EXCLUDE_BOTS = false; + LivePlayerCounter( int team, bool includeBots = true ) + { + m_team = team; + m_includeBots = includeBots; + m_count = 0; + } + + bool operator() ( CBasePlayer *player ) + { + if (player->IsAlive() && (m_team == TEAM_ANY || player->GetTeamNumber() == m_team)) + { + if (m_includeBots || !player->IsBot()) + { + ++m_count; + } + } + return true; + } + + int GetCount( void ) const + { + return m_count; + } + + int m_team; + bool m_includeBots; + int m_count; +}; + + +//-------------------------------------------------------------------------------------------------------- +/** +* Count the number of dead players on a given team (or TEAM_ANY) +*/ +class DeadPlayerCounter +{ +public: + static const bool EXCLUDE_BOTS = false; + DeadPlayerCounter( int team, bool includeBots = true ) + { + m_team = team; + m_includeBots = includeBots; + m_count = 0; + } + + bool operator() ( CBasePlayer *player ) + { + if (!player->IsAlive() && (m_team == TEAM_ANY || player->GetTeamNumber() == m_team)) + { + if (m_includeBots || !player->IsBot()) + { + ++m_count; + } + } + return true; + } + + int GetCount( void ) const + { + return m_count; + } + + int m_team; + bool m_includeBots; + int m_count; +}; + + +//-------------------------------------------------------------------------------------------------------- +/** +* Count the number of players on a given team (or TEAM_ANY) +*/ +class PlayerCounter +{ +public: + static const bool EXCLUDE_BOTS = false; + PlayerCounter( int team, int lifeState = -1, bool includeBots = true ) + { + m_team = team; + m_includeBots = includeBots; + m_count = 0; + m_lifeState = lifeState; + } + + bool operator() ( CBasePlayer *player ) + { + if ((player->m_lifeState == m_lifeState || m_lifeState == -1) && (m_team == TEAM_ANY || player->GetTeamNumber() == m_team)) + { + if (m_includeBots || !player->IsBot()) + { + ++m_count; + } + } + return true; + } + + int GetCount( void ) const + { + return m_count; + } + + int m_lifeState; + int m_team; + bool m_includeBots; + int m_count; +}; + + +//-------------------------------------------------------------------------------------------------------- +/** + * Return the closest living player on the given team (or TEAM_ANY) + */ +class ClosestPlayerScan +{ +public: + static const bool EXCLUDE_BOTS = false; + ClosestPlayerScan( const Vector &spot, int team, float maxRange = 0.0f, CBasePlayer *ignore = NULL, bool includeBots = true ) + { + m_spot = spot; + m_team = team; + m_includeBots = includeBots; + m_close = NULL; + + if ( maxRange > 0.0f ) + { + m_closeRangeSq = maxRange * maxRange; + } + else + { + m_closeRangeSq = 999999999.9f; + } + + m_ignore = ignore; + } + + bool operator() ( CBasePlayer *player ) + { + if (player == m_ignore) + return true; + + if (player->IsAlive() && (m_team == TEAM_ANY || player->GetTeamNumber() == m_team)) + { + if ( !m_includeBots && player->IsBot() ) + return true; + + Vector to = player->WorldSpaceCenter() - m_spot; + float rangeSq = to.LengthSqr(); + if (rangeSq < m_closeRangeSq) + { + m_closeRangeSq = rangeSq; + m_close = player; + } + } + return true; + } + + CBasePlayer *GetPlayer( void ) const + { + return m_close; + } + + bool IsCloserThan( float range ) + { + return (m_closeRangeSq < (range * range)); + } + + bool IsFartherThan( float range ) + { + return (m_closeRangeSq > (range * range)); + } + + Vector m_spot; + int m_team; + bool m_includeBots; + CBasePlayer *m_close; + float m_closeRangeSq; + CBasePlayer *m_ignore; +}; + + +//-------------------------------------------------------------------------------------------------------- +/** +* Return the closest living BaseCombatCharacter on the given team (or TEAM_ANY) +*/ +class ClosestActorScan +{ +public: + ClosestActorScan( const Vector &spot, int team, float maxRange = 0.0f, CBaseCombatCharacter *ignore = NULL ) + { + m_spot = spot; + m_team = team; + m_close = NULL; + + if ( maxRange > 0.0f ) + { + m_closeRangeSq = maxRange * maxRange; + } + else + { + m_closeRangeSq = 999999999.9f; + } + + m_ignore = ignore; + } + + bool operator() ( CBaseCombatCharacter *actor ) + { + if (actor == m_ignore) + return true; + + if (actor->IsAlive() && (m_team == TEAM_ANY || actor->GetTeamNumber() == m_team)) + { + Vector to = actor->WorldSpaceCenter() - m_spot; + float rangeSq = to.LengthSqr(); + if (rangeSq < m_closeRangeSq) + { + m_closeRangeSq = rangeSq; + m_close = actor; + } + } + return true; + } + + CBaseCombatCharacter *GetClosestActor( void ) const + { + return m_close; + } + + bool IsClosestActorCloserThan( float range ) + { + return (m_closeRangeSq < (range * range)); + } + + bool IsClosestActorFartherThan( float range ) + { + return (m_closeRangeSq > (range * range)); + } + + Vector m_spot; + int m_team; + CBaseCombatCharacter *m_close; + float m_closeRangeSq; + CBaseCombatCharacter *m_ignore; +}; + + +//-------------------------------------------------------------------------------------------------------- +class CShowViewportPanel +{ + int m_team; + const char *m_panelName; + bool m_show; + KeyValues *m_data; + +public: + CShowViewportPanel( int team, const char *panelName, bool show, KeyValues *data = NULL ) + { + m_team = team; + m_panelName = panelName; + m_show = show; + m_data = data; + } + + bool operator() ( CBasePlayer *player ) + { + if ( m_team != TEAM_ANY && m_team != player->GetTeamNumber() ) + return true; + + player->ShowViewPortPanel( m_panelName, m_show, m_data ); + return true; + } +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Iterate each "actor" in the game, where an actor is a Player or NextBot + */ +template < typename Functor > +inline bool ForEachActor( Functor &func ) +{ + // iterate all non-bot players + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = UTIL_PlayerByIndex( i ); + + if ( player == NULL ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( !player->IsPlayer() ) + continue; + + if ( !player->IsConnected() ) + continue; + +#ifdef NEXT_BOT + // skip bots - ForEachCombatCharacter will catch them + INextBot *bot = player->MyNextBotPointer(); + if ( bot ) + { + continue; + } +#endif // NEXT_BOT + + if ( func( player ) == false ) + { + return false; + } + } + +#ifdef NEXT_BOT + // iterate all NextBots + return TheNextBots().ForEachCombatCharacter( func ); +#else + return true; +#endif // NEXT_BOT +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * The interface for functors for use with ForEachActor() that + * want notification before iteration starts and after interation + * is complete (successful or not). + */ +class IActorFunctor +{ +public: + virtual void OnBeginIteration( void ) { } // invoked once before iteration begins + + virtual bool operator() ( CBaseCombatCharacter *them ) = 0; + + virtual void OnEndIteration( bool allElementsIterated ) { } // invoked once after iteration is complete whether successful or not +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Iterate each "actor" in the game, where an actor is a Player or NextBot + * Template specialization for IActorFunctors. + */ +template <> +inline bool ForEachActor( IActorFunctor &func ) +{ + func.OnBeginIteration(); + + bool isComplete = true; + + // iterate all non-bot players + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = UTIL_PlayerByIndex( i ); + + if ( player == NULL ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( !player->IsPlayer() ) + continue; + + if ( !player->IsConnected() ) + continue; + +#ifdef NEXT_BOT + // skip bots - ForEachCombatCharacter will catch them + INextBot *bot = dynamic_cast< INextBot * >( player ); + if ( bot ) + { + continue; + } +#endif // NEXT_BOT + + if ( func( player ) == false ) + { + isComplete = false; + break; + } + } + +#ifdef NEXT_BOT + if ( !isComplete ) + { + // iterate all NextBots + isComplete = TheNextBots().ForEachCombatCharacter( func ); + } +#endif // NEXT_BOT + + func.OnEndIteration( isComplete ); + + return isComplete; +} + + +//-------------------------------------------------------------------------------------------------------- +class CTraceFilterOnlyClassname : public CTraceFilterSimple +{ +public: + CTraceFilterOnlyClassname( const IHandleEntity *passentity, const char *pchClassname, int collisionGroup ) : + CTraceFilterSimple( passentity, collisionGroup ), m_pchClassname( pchClassname ) + { + } + + virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity ); + if ( !pEntity ) + return false; + + return FClassnameIs( pEntity, m_pchClassname ) && CTraceFilterSimple::ShouldHitEntity( pHandleEntity, contentsMask ); + } + +private: + + const char *m_pchClassname; +}; + + +#endif // _FUNCTOR_UTILS_H_ diff --git a/sp/src/game/server/game.cpp b/sp/src/game/server/game.cpp new file mode 100644 index 00000000..c22f7ebd --- /dev/null +++ b/sp/src/game/server/game.cpp @@ -0,0 +1,111 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "game.h" +#include "physics.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void MapCycleFileChangedCallback( IConVar *var, const char *pOldString, float flOldValue ) +{ + if ( Q_stricmp( pOldString, mapcyclefile.GetString() ) != 0 ) + { + if ( GameRules() ) + { + // For multiplayer games, forces the mapcyclefile to be reloaded + GameRules()->ResetMapCycleTimeStamp(); + } + } +} + +ConVar displaysoundlist( "displaysoundlist","0" ); +ConVar mapcyclefile( "mapcyclefile", "mapcycle.txt", FCVAR_NONE, "Name of the .txt file used to cycle the maps on multiplayer servers ", MapCycleFileChangedCallback ); +ConVar servercfgfile( "servercfgfile","server.cfg" ); +ConVar lservercfgfile( "lservercfgfile","listenserver.cfg" ); + +// multiplayer server rules +ConVar teamplay( "mp_teamplay","0", FCVAR_NOTIFY ); +ConVar falldamage( "mp_falldamage","0", FCVAR_NOTIFY ); +ConVar weaponstay( "mp_weaponstay","0", FCVAR_NOTIFY ); +ConVar forcerespawn( "mp_forcerespawn","1", FCVAR_NOTIFY ); +ConVar footsteps( "mp_footsteps","1", FCVAR_NOTIFY ); +#ifdef CSTRIKE +ConVar flashlight( "mp_flashlight","1", FCVAR_NOTIFY ); +#else +ConVar flashlight( "mp_flashlight","0", FCVAR_NOTIFY ); +#endif +ConVar aimcrosshair( "mp_autocrosshair","1", FCVAR_NOTIFY ); +ConVar decalfrequency( "decalfrequency","10", FCVAR_NOTIFY ); +ConVar teamlist( "mp_teamlist","hgrunt;scientist", FCVAR_NOTIFY ); +ConVar teamoverride( "mp_teamoverride","1" ); +ConVar defaultteam( "mp_defaultteam","0" ); +ConVar allowNPCs( "mp_allowNPCs","1", FCVAR_NOTIFY ); + +// Engine Cvars +const ConVar *g_pDeveloper = NULL; + + +ConVar suitvolume( "suitvolume", "0.25", FCVAR_ARCHIVE ); + +class CGameDLL_ConVarAccessor : public IConCommandBaseAccessor +{ +public: + virtual bool RegisterConCommandBase( ConCommandBase *pCommand ) + { + // Remember "unlinked" default value for replicated cvars + bool replicated = pCommand->IsFlagSet( FCVAR_REPLICATED ); + const char *defvalue = NULL; + if ( replicated && !pCommand->IsCommand() ) + { + defvalue = ( ( ConVar * )pCommand)->GetDefault(); + } + + // Link to engine's list instead + cvar->RegisterConCommand( pCommand ); + + // Apply any command-line values. + const char *pValue = cvar->GetCommandLineValue( pCommand->GetName() ); + if( pValue ) + { + if ( !pCommand->IsCommand() ) + { + ( ( ConVar * )pCommand )->SetValue( pValue ); + } + } + else + { + // NOTE: If not overridden at the command line, then if it's a replicated cvar, make sure that it's + // value is the server's value. This solves a problem where think_limit is defined in shared + // code but the value is inside and #if defined( _DEBUG ) block and if you have a debug game .dll + // and a release client, then the limiit was coming from the client even though the server value + // was the one that was important during debugging. Now the server trumps the client value for + // replicated ConVars by setting the value here after the ConVar has been linked. + if ( replicated && defvalue && !pCommand->IsCommand() ) + { + ConVar *var = ( ConVar * )pCommand; + var->SetValue( defvalue ); + } + } + + return true; + } +}; + +static CGameDLL_ConVarAccessor g_ConVarAccessor; + +// Register your console variables here +// This gets called one time when the game is initialied +void InitializeCvars( void ) +{ + // Register cvars here: + ConVar_Register( FCVAR_GAMEDLL, &g_ConVarAccessor ); + + g_pDeveloper = cvar->FindVar( "developer" ); +} + diff --git a/sp/src/game/server/game.h b/sp/src/game/server/game.h new file mode 100644 index 00000000..3377baa6 --- /dev/null +++ b/sp/src/game/server/game.h @@ -0,0 +1,41 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef GAME_H +#define GAME_H + + +#include "globals.h" + +extern void GameDLLInit( void ); + +extern ConVar displaysoundlist; +extern ConVar mapcyclefile; +extern ConVar servercfgfile; +extern ConVar lservercfgfile; + +// multiplayer server rules +extern ConVar teamplay; +extern ConVar fraglimit; +extern ConVar falldamage; +extern ConVar weaponstay; +extern ConVar forcerespawn; +extern ConVar footsteps; +extern ConVar flashlight; +extern ConVar aimcrosshair; +extern ConVar decalfrequency; +extern ConVar teamlist; +extern ConVar teamoverride; +extern ConVar defaultteam; +extern ConVar allowNPCs; + +extern ConVar suitvolume; + +// Engine Cvars +extern const ConVar *g_pDeveloper; +#endif // GAME_H diff --git a/sp/src/game/server/game_ui.cpp b/sp/src/game/server/game_ui.cpp new file mode 100644 index 00000000..dd2e2032 --- /dev/null +++ b/sp/src/game/server/game_ui.cpp @@ -0,0 +1,578 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entities that capture the player's UI and move it into game design +// as outputs. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "entitylist.h" +#include "util.h" +#include "physics.h" +#include "entityoutput.h" +#include "player.h" +#include "in_buttons.h" +#include "basecombatweapon.h" +#include "baseviewmodel.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//---------------------------------------------------------------- +// Spawn flags +//---------------------------------------------------------------- +#define SF_GAMEUI_FREEZE_PLAYER 32 +#define SF_GAMEUI_HIDE_WEAPON 64 +#define SF_GAMEUI_USE_DEACTIVATES 128 +#define SF_GAMEUI_JUMP_DEACTIVATES 256 + + +class CGameUI : public CBaseEntity +{ +public: + DECLARE_CLASS( CGameUI, CBaseEntity ); + + DECLARE_DATADESC(); + + // Input handlers + void InputDeactivate( inputdata_t &inputdata ); + void InputActivate( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputGetButtons( inputdata_t &inputdata ); +#endif + + void Think( void ); + void Deactivate( CBaseEntity *pActivator ); + + float m_flFieldOfView; + CHandle m_hSaveWeapon; + + COutputEvent m_playerOn; + COutputEvent m_playerOff; + + COutputEvent m_pressedMoveLeft; + COutputEvent m_pressedMoveRight; + COutputEvent m_pressedForward; + COutputEvent m_pressedBack; + COutputEvent m_pressedAttack; + COutputEvent m_pressedAttack2; +#ifdef MAPBASE + COutputEvent m_pressedUse; + COutputEvent m_pressedJump; + COutputEvent m_pressedCrouch; + COutputEvent m_pressedAttack3; + COutputEvent m_pressedSprint; + COutputEvent m_pressedReload; +#endif + + COutputEvent m_unpressedMoveLeft; + COutputEvent m_unpressedMoveRight; + COutputEvent m_unpressedForward; + COutputEvent m_unpressedBack; + COutputEvent m_unpressedAttack; + COutputEvent m_unpressedAttack2; +#ifdef MAPBASE + COutputEvent m_unpressedUse; + COutputEvent m_unpressedJump; + COutputEvent m_unpressedCrouch; + COutputEvent m_unpressedAttack3; + COutputEvent m_unpressedSprint; + COutputEvent m_unpressedReload; +#endif + + COutputFloat m_xaxis; + COutputFloat m_yaxis; + COutputFloat m_attackaxis; + COutputFloat m_attack2axis; + +#ifdef MAPBASE + COutputInt m_OutButtons; +#endif + + bool m_bForceUpdate; + int m_nLastButtonState; + + CHandle m_player; +}; + + +BEGIN_DATADESC( CGameUI ) + + DEFINE_KEYFIELD( m_flFieldOfView, FIELD_FLOAT, "FieldOfView" ), + DEFINE_FIELD( m_hSaveWeapon, FIELD_EHANDLE ), + DEFINE_FIELD( m_bForceUpdate, FIELD_BOOLEAN ), + DEFINE_FIELD( m_player, FIELD_EHANDLE ), + DEFINE_FIELD( m_nLastButtonState, FIELD_INTEGER ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + DEFINE_INPUTFUNC( FIELD_STRING, "Activate", InputActivate ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "GetButtons", InputGetButtons ), +#endif + + DEFINE_OUTPUT( m_playerOn, "PlayerOn" ), + DEFINE_OUTPUT( m_playerOff, "PlayerOff" ), + + DEFINE_OUTPUT( m_pressedMoveLeft, "PressedMoveLeft" ), + DEFINE_OUTPUT( m_pressedMoveRight, "PressedMoveRight" ), + DEFINE_OUTPUT( m_pressedForward, "PressedForward" ), + DEFINE_OUTPUT( m_pressedBack, "PressedBack" ), + DEFINE_OUTPUT( m_pressedAttack, "PressedAttack" ), + DEFINE_OUTPUT( m_pressedAttack2, "PressedAttack2" ), +#ifdef MAPBASE + DEFINE_OUTPUT( m_pressedUse, "PressedUse" ), + DEFINE_OUTPUT( m_pressedJump, "PressedJump" ), + DEFINE_OUTPUT( m_pressedCrouch, "PressedCrouch" ), + DEFINE_OUTPUT( m_pressedAttack3, "PressedAttack3" ), + DEFINE_OUTPUT( m_pressedSprint, "PressedSprint" ), + DEFINE_OUTPUT( m_pressedReload, "PressedReload" ), +#endif + + DEFINE_OUTPUT( m_unpressedMoveLeft, "UnpressedMoveLeft" ), + DEFINE_OUTPUT( m_unpressedMoveRight, "UnpressedMoveRight" ), + DEFINE_OUTPUT( m_unpressedForward, "UnpressedForward" ), + DEFINE_OUTPUT( m_unpressedBack, "UnpressedBack" ), + DEFINE_OUTPUT( m_unpressedAttack, "UnpressedAttack" ), + DEFINE_OUTPUT( m_unpressedAttack2, "UnpressedAttack2" ), +#ifdef MAPBASE + DEFINE_OUTPUT( m_unpressedUse, "UnpressedUse" ), + DEFINE_OUTPUT( m_unpressedJump, "UnpressedJump" ), + DEFINE_OUTPUT( m_unpressedCrouch, "UnpressedCrouch" ), + DEFINE_OUTPUT( m_unpressedAttack3, "UnpressedAttack3" ), + DEFINE_OUTPUT( m_unpressedSprint, "UnpressedSprint" ), + DEFINE_OUTPUT( m_unpressedReload, "UnpressedReload" ), + + DEFINE_OUTPUT( m_OutButtons, "OutButtons" ), +#endif + + DEFINE_OUTPUT( m_xaxis, "XAxis" ), + DEFINE_OUTPUT( m_yaxis, "YAxis" ), + DEFINE_OUTPUT( m_attackaxis, "AttackAxis" ), + DEFINE_OUTPUT( m_attack2axis, "Attack2Axis" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( game_ui, CGameUI ); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGameUI::InputDeactivate( inputdata_t &inputdata ) +{ + Deactivate( inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGameUI::Deactivate( CBaseEntity *pActivator ) +{ + CBasePlayer *pPlayer = m_player; + + AssertMsg(pPlayer, "CGameUI deactivated without a player!"); + + if (pPlayer) + { + // Re-enable player motion + if ( FBitSet( m_spawnflags, SF_GAMEUI_FREEZE_PLAYER ) ) + { + m_player->RemoveFlag( FL_ATCONTROLS ); + } + + // Restore weapons + if ( FBitSet( m_spawnflags, SF_GAMEUI_HIDE_WEAPON ) ) + { + // Turn the hud back on + pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_WEAPONSELECTION; + + if ( m_hSaveWeapon.Get() ) + { + m_player->Weapon_Switch( m_hSaveWeapon.Get() ); + m_hSaveWeapon = NULL; + } + + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->Deploy(); + } + } + + // Announce that the player is no longer controlling through us + m_playerOff.FireOutput( pPlayer, this, 0 ); + + // Clear out the axis controls + m_xaxis.Set( 0, pPlayer, this ); + m_yaxis.Set( 0, pPlayer, this ); + m_attackaxis.Set( 0, pPlayer, this ); + m_attack2axis.Set( 0, pPlayer, this ); + m_nLastButtonState = 0; + m_player = NULL; + } + else + { +#ifdef MAPBASE + Warning("%s Deactivate(): I have no player when called by %s!\n", GetEntityName().ToCStr(), pActivator ? pActivator->GetEntityName().ToCStr() : "(null)"); +#else + Warning("%s Deactivate(): I have no player when called by %s!\n", GetEntityName().ToCStr(), pActivator->GetEntityName().ToCStr()); +#endif + } + + // Stop thinking + SetNextThink( TICK_NEVER_THINK ); +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CGameUI::InputActivate( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer; + + // Determine if we're specifying this as an override parameter + if ( inputdata.value.StringID() != NULL_STRING ) + { + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, inputdata.value.String(), this, inputdata.pActivator, inputdata.pCaller ); + if ( pEntity == NULL || pEntity->IsPlayer() == false ) + { + Warning( "%s InputActivate: entity %s not found or is not a player!\n", GetEntityName().ToCStr(), inputdata.value.String() ); + return; + } + + pPlayer = ToBasePlayer( pEntity ); + } + else + { + // Otherwise try to use the activator + if ( inputdata.pActivator == NULL || inputdata.pActivator->IsPlayer() == false ) + { + Warning( "%s InputActivate: invalid or missing !activator!\n", GetEntityName().ToCStr() ); + return; + } + + pPlayer = ToBasePlayer( inputdata.pActivator ); + } + + // If another player is already using these controls3, ignore this activation + if ( m_player.Get() != NULL && pPlayer != m_player.Get() ) + { + // TODO: We could allow this by calling Deactivate() at this point and continuing on -- jdw + return; + } + + // Setup our internal data + m_player = pPlayer; + m_playerOn.FireOutput( pPlayer, this, 0 ); + + // Turn the hud off + SetNextThink( gpGlobals->curtime ); + + // Disable player's motion + if ( FBitSet( m_spawnflags, SF_GAMEUI_FREEZE_PLAYER ) ) + { + m_player->AddFlag( FL_ATCONTROLS ); + } + + // Store off and hide the currently held weapon + if ( FBitSet( m_spawnflags, SF_GAMEUI_HIDE_WEAPON ) ) + { + m_player->m_Local.m_iHideHUD |= HIDEHUD_WEAPONSELECTION; + + if ( m_player->GetActiveWeapon() ) + { + m_hSaveWeapon = m_player->GetActiveWeapon(); + + m_player->GetActiveWeapon()->Holster(); + m_player->ClearActiveWeapon(); + m_player->HideViewModels(); + } + } + + // We must update our state + m_bForceUpdate = true; +} + + +//------------------------------------------------------------------------------ +// Purpose: Samples the player's inputs and fires outputs based on what buttons +// are currently held down. +//------------------------------------------------------------------------------ +void CGameUI::Think( void ) +{ + CBasePlayer *pPlayer = m_player; + + // If player is gone, stop thinking + if (pPlayer == NULL) + { + SetNextThink( TICK_NEVER_THINK ); + return; + } + + // If we're forcing an update, state with a clean button state + if ( m_bForceUpdate ) + { + m_nLastButtonState = pPlayer->m_nButtons; + } + + // ------------------------------------------------ + // Check that toucher is facing the UI within + // the field of view tolerance. If not disconnect + // ------------------------------------------------ + if (m_flFieldOfView > -1) + { + Vector vPlayerFacing; + pPlayer->EyeVectors( &vPlayerFacing ); + Vector vPlayerToUI = GetAbsOrigin() - pPlayer->WorldSpaceCenter(); + VectorNormalize(vPlayerToUI); + + float flDotPr = DotProduct(vPlayerFacing,vPlayerToUI); + if (flDotPr < m_flFieldOfView) + { + Deactivate( pPlayer ); + return; + } + } + + pPlayer->AddFlag( FL_ONTRAIN ); + SetNextThink( gpGlobals->curtime ); + + // Deactivate if they jump or press +use. + // FIXME: prevent the use from going through in player.cpp + if ((( pPlayer->m_afButtonPressed & IN_USE ) && ( m_spawnflags & SF_GAMEUI_USE_DEACTIVATES )) || + (( pPlayer->m_afButtonPressed & IN_JUMP ) && ( m_spawnflags & SF_GAMEUI_JUMP_DEACTIVATES ))) + { +#ifdef MAPBASE + if (pPlayer->m_afButtonPressed & IN_USE) + m_pressedUse.FireOutput( pPlayer, this, 0 ); + if (pPlayer->m_afButtonPressed & IN_JUMP) + m_pressedJump.FireOutput( pPlayer, this, 0 ); +#endif + + Deactivate( pPlayer ); + return; + } + + // Determine what's different + int nButtonsChanged = ( pPlayer->m_nButtons ^ m_nLastButtonState ); + + // + // Handle all our possible input triggers + // + + if ( nButtonsChanged & IN_MOVERIGHT ) + { + if ( m_nLastButtonState & IN_MOVERIGHT ) + { + m_unpressedMoveRight.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedMoveRight.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_MOVELEFT ) + { + if ( m_nLastButtonState & IN_MOVELEFT ) + { + m_unpressedMoveLeft.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedMoveLeft.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_FORWARD ) + { + if ( m_nLastButtonState & IN_FORWARD ) + { + m_unpressedForward.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedForward.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_BACK ) + { + if ( m_nLastButtonState & IN_BACK ) + { + m_unpressedBack.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedBack.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_ATTACK ) + { + if ( m_nLastButtonState & IN_ATTACK ) + { + m_unpressedAttack.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedAttack.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_ATTACK2 ) + { + if ( m_nLastButtonState & IN_ATTACK2 ) + { + m_unpressedAttack2.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedAttack2.FireOutput( pPlayer, this, 0 ); + } + } + +#ifdef MAPBASE + if ( nButtonsChanged & IN_USE ) + { + if ( m_nLastButtonState & IN_USE ) + { + m_unpressedUse.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedUse.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_JUMP ) + { + if ( m_nLastButtonState & IN_JUMP ) + { + m_unpressedJump.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedJump.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_DUCK ) + { + if ( m_nLastButtonState & IN_DUCK ) + { + m_unpressedCrouch.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedCrouch.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_ATTACK3 ) + { + if ( m_nLastButtonState & IN_ATTACK3 ) + { + m_unpressedAttack3.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedAttack3.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_SPEED ) + { + if ( m_nLastButtonState & IN_SPEED ) + { + m_unpressedSprint.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedSprint.FireOutput( pPlayer, this, 0 ); + } + } + + if ( nButtonsChanged & IN_RELOAD ) + { + if ( m_nLastButtonState & IN_RELOAD ) + { + m_unpressedReload.FireOutput( pPlayer, this, 0 ); + } + else + { + m_pressedReload.FireOutput( pPlayer, this, 0 ); + } + } +#endif + + // Setup for the next frame + m_nLastButtonState = pPlayer->m_nButtons; + + float x = 0, y = 0, attack = 0, attack2 = 0; + if ( pPlayer->m_nButtons & IN_MOVERIGHT ) + { + x = 1; + } + else if ( pPlayer->m_nButtons & IN_MOVELEFT ) + { + x = -1; + } + + if ( pPlayer->m_nButtons & IN_FORWARD ) + { + y = 1; + } + else if ( pPlayer->m_nButtons & IN_BACK ) + { + y = -1; + } + + if ( pPlayer->m_nButtons & IN_ATTACK ) + { + attack = 1; + } + + if ( pPlayer->m_nButtons & IN_ATTACK2 ) + { + attack2 = 1; + } + + // + // Fire the analog outputs if they changed. + // + if ( m_bForceUpdate || ( m_xaxis.Get() != x ) ) + { + m_xaxis.Set( x, pPlayer, this ); + } + + if ( m_bForceUpdate || ( m_yaxis.Get() != y ) ) + { + m_yaxis.Set( y, pPlayer, this ); + } + + if ( m_bForceUpdate || ( m_attackaxis.Get() != attack ) ) + { + m_attackaxis.Set( attack, pPlayer, this ); + } + + if ( m_bForceUpdate || ( m_attack2axis.Get() != attack2 ) ) + { + m_attack2axis.Set( attack2, pPlayer, this ); + } + + m_bForceUpdate = false; +} + +#ifdef MAPBASE +//------------------------------------------------------------------------------ +// Purpose: Gets and outputs the player's current buttons +//------------------------------------------------------------------------------ +void CGameUI::InputGetButtons( inputdata_t &inputdata ) +{ + m_OutButtons.Set(m_player ? m_player->m_nButtons : m_nLastButtonState, m_player, this); +} +#endif diff --git a/sp/src/game/server/gamedll_replay.cpp b/sp/src/game/server/gamedll_replay.cpp new file mode 100644 index 00000000..e9ccde1d --- /dev/null +++ b/sp/src/game/server/gamedll_replay.cpp @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" + +#if defined( REPLAY_ENABLED ) + +#include "replay/iserverreplay.h" +#include "replay_gamestats_shared.h" + +//---------------------------------------------------------------------------------------- + +class CServerReplayImp : public IServerReplay +{ +public: + virtual void UploadOgsData( KeyValues *pData, bool bIncludeTimeField ) + { + GetReplayGameStatsHelper().UploadError( pData, bIncludeTimeField ); + } +}; + +static CServerReplayImp s_ServerReplayImp; +EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CServerReplayImp, IServerReplay, SERVER_REPLAY_INTERFACE_VERSION, s_ServerReplayImp ); + +#endif // #if defined( REPLAY_ENABLED ) \ No newline at end of file diff --git a/sp/src/game/server/gamehandle.cpp b/sp/src/game/server/gamehandle.cpp new file mode 100644 index 00000000..2ffcdc2e --- /dev/null +++ b/sp/src/game/server/gamehandle.cpp @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: returns the module handle of the game dll +// this is in its own file to protect it from tier0 PROTECTED_THINGS +//=============================================================================// + + +#if defined(_WIN32) +#include "winlite.h" +extern HMODULE win32DLLHandle; +#elif defined(POSIX) +#include +#include "tier0/dbg.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void *GetGameModuleHandle() +{ +#if defined(_WIN32) + return (void *)win32DLLHandle; +#elif defined(POSIX) + Assert(0); + return NULL; // NOT implemented +#else +#error "GetGameModuleHandle() needs to be implemented" +#endif +} + diff --git a/sp/src/game/server/gameinterface.cpp b/sp/src/game/server/gameinterface.cpp new file mode 100644 index 00000000..8f3df0a7 --- /dev/null +++ b/sp/src/game/server/gameinterface.cpp @@ -0,0 +1,3597 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: encapsulates and implements all the accessing of the game dll from external +// sources (only the engine at the time of writing) +// This files ONLY contains functions and data necessary to build an interface +// to external modules +//===========================================================================// + +#include "cbase.h" +#include "gamestringpool.h" +#include "mapentities_shared.h" +#include "game.h" +#include "entityapi.h" +#include "client.h" +#include "saverestore.h" +#include "entitylist.h" +#include "gamerules.h" +#include "soundent.h" +#include "player.h" +#include "server_class.h" +#include "ai_node.h" +#include "ai_link.h" +#include "ai_saverestore.h" +#include "ai_networkmanager.h" +#include "ndebugoverlay.h" +#include "ivoiceserver.h" +#include +#include "movehelper_server.h" +#include "networkstringtable_gamedll.h" +#include "filesystem.h" +#include "func_areaportalwindow.h" +#include "igamesystem.h" +#include "init_factory.h" +#include "vstdlib/random.h" +#include "env_wind_shared.h" +#include "engine/IEngineSound.h" +#include "ispatialpartition.h" +#include "textstatsmgr.h" +#include "bitbuf.h" +#include "saverestoretypes.h" +#include "physics_saverestore.h" +#include "achievement_saverestore.h" +#include "tier0/vprof.h" +#include "effect_dispatch_data.h" +#include "engine/IStaticPropMgr.h" +#include "TemplateEntities.h" +#include "ai_speech.h" +#include "soundenvelope.h" +#include "usermessages.h" +#include "physics.h" +#include "igameevents.h" +#include "EventLog.h" +#include "datacache/idatacache.h" +#include "engine/ivdebugoverlay.h" +#include "shareddefs.h" +#include "props.h" +#include "timedeventmgr.h" +#include "gameinterface.h" +#include "eventqueue.h" +#include "hltvdirector.h" +#if defined( REPLAY_ENABLED ) +#include "replay/iserverreplaycontext.h" +#endif +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "AI_ResponseSystem.h" +#include "saverestore_stringtable.h" +#include "util.h" +#include "tier0/icommandline.h" +#include "datacache/imdlcache.h" +#include "engine/iserverplugin.h" +#ifdef _WIN32 +#include "ienginevgui.h" +#endif +#include "ragdoll_shared.h" +#include "toolframework/iserverenginetools.h" +#include "sceneentity.h" +#include "appframework/IAppSystemGroup.h" +#include "scenefilecache/ISceneFileCache.h" +#include "tier2/tier2.h" +#include "particles/particles.h" +#include "gamestats.h" +#include "ixboxsystem.h" +#include "engine/imatchmaking.h" +#include "hl2orange.spa.h" +#include "particle_parse.h" +#ifndef NO_STEAM +#include "steam/steam_gameserver.h" +#endif +#include "tier3/tier3.h" +#include "serverbenchmark_base.h" +#include "querycache.h" +#ifdef MAPBASE +#include "world.h" +#endif + +#include "vscript/ivscript.h" +#include "vscript_server.h" + + +#ifdef TF_DLL +#include "gc_clientsystem.h" +#include "econ_item_inventory.h" +#include "steamworks_gamestats.h" +#include "tf/tf_gc_server.h" +#include "tf_gamerules.h" +#include "tf_lobby.h" +#include "player_vs_environment/tf_population_manager.h" + +extern ConVar tf_mm_trusted; +extern ConVar tf_mm_servermode; +#endif + +#ifdef USE_NAV_MESH +#include "nav_mesh.h" +#endif + +#ifdef NEXT_BOT +#include "NextBotManager.h" +#endif + +#ifdef USES_ECON_ITEMS +#include "econ_item_system.h" +#endif // USES_ECON_ITEMS + +#ifdef CSTRIKE_DLL // BOTPORT: TODO: move these ifdefs out +#include "bot/bot.h" +#endif + +#ifdef PORTAL +#include "prop_portal_shared.h" +#include "portal_player.h" +#endif + +#if defined( REPLAY_ENABLED ) +#include "replay/ireplaysystem.h" +#endif + +extern IToolFrameworkServer *g_pToolFrameworkServer; +extern IParticleSystemQuery *g_pParticleSystemQuery; + +extern ConVar commentary; + +#ifndef NO_STEAM +// this context is not available on dedicated servers +// WARNING! always check if interfaces are available before using +static CSteamAPIContext s_SteamAPIContext; +CSteamAPIContext *steamapicontext = &s_SteamAPIContext; + +// this context is not available on a pure client connected to a remote server. +// WARNING! always check if interfaces are available before using +static CSteamGameServerAPIContext s_SteamGameServerAPIContext; +CSteamGameServerAPIContext *steamgameserverapicontext = &s_SteamGameServerAPIContext; +#endif + +IUploadGameStats *gamestatsuploader = NULL; + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CTimedEventMgr g_NetworkPropertyEventMgr; + +ISaveRestoreBlockHandler *GetEventQueueSaveRestoreBlockHandler(); +ISaveRestoreBlockHandler *GetCommentarySaveRestoreBlockHandler(); + +CUtlLinkedList g_MapEntityRefs; + +// Engine interfaces. +IVEngineServer *engine = NULL; +IVoiceServer *g_pVoiceServer = NULL; +#if !defined(_STATIC_LINKED) +IFileSystem *filesystem = NULL; +#else +extern IFileSystem *filesystem; +#endif +INetworkStringTableContainer *networkstringtable = NULL; +IStaticPropMgrServer *staticpropmgr = NULL; +IUniformRandomStream *random = NULL; +IEngineSound *enginesound = NULL; +ISpatialPartition *partition = NULL; +IVModelInfo *modelinfo = NULL; +IEngineTrace *enginetrace = NULL; +IGameEventManager2 *gameeventmanager = NULL; +IDataCache *datacache = NULL; +IVDebugOverlay * debugoverlay = NULL; +ISoundEmitterSystemBase *soundemitterbase = NULL; +IServerPluginHelpers *serverpluginhelpers = NULL; +IServerEngineTools *serverenginetools = NULL; +ISceneFileCache *scenefilecache = NULL; +IXboxSystem *xboxsystem = NULL; // Xbox 360 only +IMatchmaking *matchmaking = NULL; // Xbox 360 only +IScriptManager *scriptmanager = NULL; +#if defined( REPLAY_ENABLED ) +IReplaySystem *g_pReplay = NULL; +IServerReplayContext *g_pReplayServerContext = NULL; +#endif + +IGameSystem *SoundEmitterSystem(); + +bool ModelSoundsCacheInit(); +void ModelSoundsCacheShutdown(); + +void SceneManager_ClientActive( CBasePlayer *player ); + +class IMaterialSystem; +class IStudioRender; + +#ifdef _DEBUG +static ConVar s_UseNetworkVars( "UseNetworkVars", "1", FCVAR_CHEAT, "For profiling, toggle network vars." ); +#endif + +extern ConVar sv_noclipduringpause; +ConVar sv_massreport( "sv_massreport", "0" ); +ConVar sv_force_transmit_ents( "sv_force_transmit_ents", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "Will transmit all entities to client, regardless of PVS conditions (will still skip based on transmit flags, however)." ); + +ConVar sv_autosave( "sv_autosave", "1", 0, "Set to 1 to autosave game on level transition. Does not affect autosave triggers." ); +ConVar *sv_maxreplay = NULL; +static ConVar *g_pcv_commentary = NULL; +static ConVar *g_pcv_ThreadMode = NULL; +static ConVar *g_pcv_hideServer = NULL; + +// String tables +INetworkStringTable *g_pStringTableParticleEffectNames = NULL; +INetworkStringTable *g_pStringTableEffectDispatch = NULL; +INetworkStringTable *g_pStringTableVguiScreen = NULL; +INetworkStringTable *g_pStringTableMaterials = NULL; +INetworkStringTable *g_pStringTableInfoPanel = NULL; +INetworkStringTable *g_pStringTableClientSideChoreoScenes = NULL; +INetworkStringTable *g_pStringTableServerMapCycle = NULL; + +#ifdef TF_DLL +INetworkStringTable *g_pStringTableServerPopFiles = NULL; +INetworkStringTable *g_pStringTableServerMapCycleMvM = NULL; +#endif + +CStringTableSaveRestoreOps g_VguiScreenStringOps; + +// Holds global variables shared between engine and game. +CGlobalVars *gpGlobals; +edict_t *g_pDebugEdictBase = 0; +static int g_nCommandClientIndex = 0; + +// The chapter number of the current +static int g_nCurrentChapterIndex = -1; + +#ifdef _DEBUG +static ConVar sv_showhitboxes( "sv_showhitboxes", "-1", FCVAR_CHEAT, "Send server-side hitboxes for specified entity to client (NOTE: this uses lots of bandwidth, use on listen server only)." ); +#endif + +void PrecachePointTemplates(); + +static ClientPutInServerOverrideFn g_pClientPutInServerOverride = NULL; +static void UpdateChapterRestrictions( const char *mapname ); + +static void UpdateRichPresence ( void ); + + +#if !defined( _XBOX ) // Don't doubly define this symbol. +CSharedEdictChangeInfo *g_pSharedChangeInfo = NULL; + +#endif + +IChangeInfoAccessor *CBaseEdict::GetChangeAccessor() +{ + return engine->GetChangeAccessor( (const edict_t *)this ); +} + +const IChangeInfoAccessor *CBaseEdict::GetChangeAccessor() const +{ + return engine->GetChangeAccessor( (const edict_t *)this ); +} + +const char *GetHintTypeDescription( CAI_Hint *pHint ); + +void ClientPutInServerOverride( ClientPutInServerOverrideFn fn ) +{ + g_pClientPutInServerOverride = fn; +} + +ConVar ai_post_frame_navigation( "ai_post_frame_navigation", "0" ); +class CPostFrameNavigationHook; +extern CPostFrameNavigationHook *PostFrameNavigationSystem( void ); + +//----------------------------------------------------------------------------- +// Purpose: +// Output : int +//----------------------------------------------------------------------------- +int UTIL_GetCommandClientIndex( void ) +{ + // -1 == unknown,dedicated server console + // 0 == player 1 + + // Convert to 1 based offset + return (g_nCommandClientIndex+1); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : CBasePlayer +//----------------------------------------------------------------------------- +CBasePlayer *UTIL_GetCommandClient( void ) +{ + int idx = UTIL_GetCommandClientIndex(); + if ( idx > 0 ) + { + return UTIL_PlayerByIndex( idx ); + } + + // HLDS console issued command + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Retrieves the MOD directory for the active game (ie. "hl2") +//----------------------------------------------------------------------------- + +bool UTIL_GetModDir( char *lpszTextOut, unsigned int nSize ) +{ + // Must pass in a buffer at least large enough to hold the desired string + const char *pGameDir = CommandLine()->ParmValue( "-game", "hl2" ); + Assert( strlen(pGameDir) <= nSize ); + if ( strlen(pGameDir) > nSize ) + return false; + + Q_strncpy( lpszTextOut, pGameDir, nSize ); + if ( Q_strnchr( lpszTextOut, '/', nSize ) || Q_strnchr( lpszTextOut, '\\', nSize ) ) + { + // Strip the last directory off (which will be our game dir) + Q_StripLastDir( lpszTextOut, nSize ); + + // Find the difference in string lengths and take that difference from the original string as the mod dir + int dirlen = Q_strlen( lpszTextOut ); + Q_strncpy( lpszTextOut, pGameDir + dirlen, Q_strlen( pGameDir ) - dirlen + 1 ); + } + + return true; +} + +extern void InitializeCvars( void ); + +CBaseEntity* FindPickerEntity( CBasePlayer* pPlayer ); +CAI_Node* FindPickerAINode( CBasePlayer* pPlayer, NodeType_e nNodeType ); +CAI_Link* FindPickerAILink( CBasePlayer* pPlayer ); +float GetFloorZ(const Vector &origin); +void UpdateAllClientData( void ); +void DrawMessageEntities(); + +#include "ai_network.h" + +// For now just using one big AI network +extern ConVar think_limit; + + +#if 0 +//----------------------------------------------------------------------------- +// Purpose: Draw output overlays for any measure sections +// Input : +//----------------------------------------------------------------------------- +void DrawMeasuredSections(void) +{ + int row = 1; + float rowheight = 0.025; + + CMeasureSection *p = CMeasureSection::GetList(); + while ( p ) + { + char str[256]; + Q_snprintf(str,sizeof(str),"%s",p->GetName()); + NDebugOverlay::ScreenText( 0.01,0.51+(row*rowheight),str, 255,255,255,255, 0.0 ); + + Q_snprintf(str,sizeof(str),"%5.2f\n",p->GetTime().GetMillisecondsF()); + //Q_snprintf(str,sizeof(str),"%3.3f\n",p->GetTime().GetSeconds() * 100.0 / engine->Time()); + NDebugOverlay::ScreenText( 0.28,0.51+(row*rowheight),str, 255,255,255,255, 0.0 ); + + Q_snprintf(str,sizeof(str),"%5.2f\n",p->GetMaxTime().GetMillisecondsF()); + //Q_snprintf(str,sizeof(str),"%3.3f\n",p->GetTime().GetSeconds() * 100.0 / engine->Time()); + NDebugOverlay::ScreenText( 0.34,0.51+(row*rowheight),str, 255,255,255,255, 0.0 ); + + + row++; + + p = p->GetNext(); + } + + bool sort_reset = false; + + // Time to redo sort? + if ( measure_resort.GetFloat() > 0.0 && + engine->Time() >= CMeasureSection::m_dNextResort ) + { + // Redo it + CMeasureSection::SortSections(); + // Set next time + CMeasureSection::m_dNextResort = engine->Time() + measure_resort.GetFloat(); + // Flag to reset sort accumulator, too + sort_reset = true; + } + + // Iterate through the sections now + p = CMeasureSection::GetList(); + while ( p ) + { + // Update max + p->UpdateMax(); + + // Reset regular accum. + p->Reset(); + // Reset sort accum less often + if ( sort_reset ) + { + p->SortReset(); + } + p = p->GetNext(); + } + +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void DrawAllDebugOverlays( void ) +{ + // If in debug select mode print the selection entities name or classname + if (CBaseEntity::m_bInDebugSelect) + { + CBasePlayer* pPlayer = UTIL_PlayerByIndex( CBaseEntity::m_nDebugPlayer ); + + if (pPlayer) + { + // First try to trace a hull to an entity + CBaseEntity *pEntity = FindPickerEntity( pPlayer ); + + if ( pEntity ) + { + pEntity->DrawDebugTextOverlays(); + pEntity->DrawBBoxOverlay(); + pEntity->SendDebugPivotOverlay(); + } + } + } + + // -------------------------------------------------------- + // Draw debug overlay lines + // -------------------------------------------------------- + UTIL_DrawOverlayLines(); + + // ------------------------------------------------------------------------ + // If in wc_edit mode draw a box to highlight which node I'm looking at + // ------------------------------------------------------------------------ + if (engine->IsInEditMode()) + { + CBasePlayer* pPlayer = UTIL_PlayerByIndex( CBaseEntity::m_nDebugPlayer ); + if (pPlayer) + { + if (g_pAINetworkManager->GetEditOps()->m_bLinkEditMode) + { + CAI_Link* pAILink = FindPickerAILink(pPlayer); + if (pAILink) + { + // For now just using one big AI network + Vector startPos = g_pBigAINet->GetNode(pAILink->m_iSrcID)->GetPosition(g_pAINetworkManager->GetEditOps()->m_iHullDrawNum); + Vector endPos = g_pBigAINet->GetNode(pAILink->m_iDestID)->GetPosition(g_pAINetworkManager->GetEditOps()->m_iHullDrawNum); + Vector linkDir = startPos-endPos; + float linkLen = VectorNormalize( linkDir ); + + // Draw in green if link that's been turned off + if (pAILink->m_LinkInfo & bits_LINK_OFF) + { + NDebugOverlay::BoxDirection(startPos, Vector(-4,-4,-4), Vector(-linkLen,4,4), linkDir, 0,255,0,40,0); + } + else + { + NDebugOverlay::BoxDirection(startPos, Vector(-4,-4,-4), Vector(-linkLen,4,4), linkDir, 255,0,0,40,0); + } + } + } + else + { + CAI_Node* pAINode; + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + pAINode = FindPickerAINode(pPlayer,NODE_AIR); + } + else + { + pAINode = FindPickerAINode(pPlayer,NODE_GROUND); + } + + if (pAINode) + { + Vector vecPos = pAINode->GetPosition(g_pAINetworkManager->GetEditOps()->m_iHullDrawNum); + NDebugOverlay::Box( vecPos, Vector(-8,-8,-8), Vector(8,8,8), 255,0,0,40,0); + + if ( pAINode->GetHint() ) + { + CBaseEntity *pEnt = (CBaseEntity *)pAINode->GetHint(); + if ( pEnt->GetEntityName() != NULL_STRING ) + { + NDebugOverlay::Text( vecPos + Vector(0,0,6), STRING(pEnt->GetEntityName()), false, 0 ); + } + NDebugOverlay::Text( vecPos, GetHintTypeDescription( pAINode->GetHint() ), false, 0 ); + } + } + } + // ------------------------------------ + // If in air edit mode draw guide line + // ------------------------------------ + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + UTIL_DrawPositioningOverlay(g_pAINetworkManager->GetEditOps()->m_flAirEditDistance); + } + else + { + NDebugOverlay::DrawGroundCrossHairOverlay(); + } + } + } + + // For not just using one big AI Network + if ( g_pAINetworkManager ) + { + g_pAINetworkManager->GetEditOps()->DrawAINetworkOverlay(); + } + + // PERFORMANCE: only do this in developer mode + if ( g_pDeveloper->GetInt() && !engine->IsDedicatedServer() ) + { + // iterate through all objects for debug overlays + const CEntInfo *pInfo = gEntList.FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + // HACKHACK: to flag off these calls + if ( ent->m_debugOverlays || ent->m_pTimedOverlay ) + { + MDLCACHE_CRITICAL_SECTION(); + ent->DrawDebugGeometryOverlays(); + } + } + } + + if ( sv_massreport.GetInt() ) + { + // iterate through all objects for debug overlays + const CEntInfo *pInfo = gEntList.FirstEntInfo(); + + for ( ;pInfo; pInfo = pInfo->m_pNext ) + { + CBaseEntity *ent = (CBaseEntity *)pInfo->m_pEntity; + if (!ent->VPhysicsGetObject()) + continue; + + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr),"%s: Mass: %.2f kg / %.2f lb (%s)", + STRING( ent->GetModelName() ), ent->VPhysicsGetObject()->GetMass(), + kg2lbs(ent->VPhysicsGetObject()->GetMass()), + GetMassEquivalent(ent->VPhysicsGetObject()->GetMass())); + ent->EntityText(0, tempstr, 0); + } + } + + // A hack to draw point_message entities w/o developer required + DrawMessageEntities(); +} + +CServerGameDLL g_ServerGameDLL; +// INTERFACEVERSION_SERVERGAMEDLL_VERSION_8 is compatible with the latest since we're only adding things to the end, so expose that as well. +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerGameDLL, IServerGameDLL008, INTERFACEVERSION_SERVERGAMEDLL_VERSION_8, g_ServerGameDLL ); +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerGameDLL, IServerGameDLL, INTERFACEVERSION_SERVERGAMEDLL, g_ServerGameDLL); + +// When bumping the version to this interface, check that our assumption is still valid and expose the older version in the same way +COMPILE_TIME_ASSERT( INTERFACEVERSION_SERVERGAMEDLL_INT == 9 ); + +bool CServerGameDLL::DLLInit( CreateInterfaceFn appSystemFactory, + CreateInterfaceFn physicsFactory, CreateInterfaceFn fileSystemFactory, + CGlobalVars *pGlobals) +{ + ConnectTier1Libraries( &appSystemFactory, 1 ); + ConnectTier2Libraries( &appSystemFactory, 1 ); + ConnectTier3Libraries( &appSystemFactory, 1 ); + + // Connected in ConnectTier1Libraries + if ( cvar == NULL ) + return false; + +#ifndef _X360 + s_SteamAPIContext.Init(); + s_SteamGameServerAPIContext.Init(); +#endif + + // init each (seperated for ease of debugging) + if ( (engine = (IVEngineServer*)appSystemFactory(INTERFACEVERSION_VENGINESERVER, NULL)) == NULL ) + return false; + if ( (g_pVoiceServer = (IVoiceServer*)appSystemFactory(INTERFACEVERSION_VOICESERVER, NULL)) == NULL ) + return false; + if ( (networkstringtable = (INetworkStringTableContainer *)appSystemFactory(INTERFACENAME_NETWORKSTRINGTABLESERVER,NULL)) == NULL ) + return false; + if ( (staticpropmgr = (IStaticPropMgrServer *)appSystemFactory(INTERFACEVERSION_STATICPROPMGR_SERVER,NULL)) == NULL ) + return false; + if ( (random = (IUniformRandomStream *)appSystemFactory(VENGINE_SERVER_RANDOM_INTERFACE_VERSION, NULL)) == NULL ) + return false; + if ( (enginesound = (IEngineSound *)appSystemFactory(IENGINESOUND_SERVER_INTERFACE_VERSION, NULL)) == NULL ) + return false; + if ( (partition = (ISpatialPartition *)appSystemFactory(INTERFACEVERSION_SPATIALPARTITION, NULL)) == NULL ) + return false; + if ( (modelinfo = (IVModelInfo *)appSystemFactory(VMODELINFO_SERVER_INTERFACE_VERSION, NULL)) == NULL ) + return false; + if ( (enginetrace = (IEngineTrace *)appSystemFactory(INTERFACEVERSION_ENGINETRACE_SERVER,NULL)) == NULL ) + return false; + if ( (filesystem = (IFileSystem *)fileSystemFactory(FILESYSTEM_INTERFACE_VERSION,NULL)) == NULL ) + return false; + if ( (gameeventmanager = (IGameEventManager2 *)appSystemFactory(INTERFACEVERSION_GAMEEVENTSMANAGER2,NULL)) == NULL ) + return false; + if ( (datacache = (IDataCache*)appSystemFactory(DATACACHE_INTERFACE_VERSION, NULL )) == NULL ) + return false; + if ( (soundemitterbase = (ISoundEmitterSystemBase *)appSystemFactory(SOUNDEMITTERSYSTEM_INTERFACE_VERSION, NULL)) == NULL ) + return false; +#ifndef _XBOX + if ( (gamestatsuploader = (IUploadGameStats *)appSystemFactory( INTERFACEVERSION_UPLOADGAMESTATS, NULL )) == NULL ) + return false; +#endif + if ( !mdlcache ) + return false; + if ( (serverpluginhelpers = (IServerPluginHelpers *)appSystemFactory(INTERFACEVERSION_ISERVERPLUGINHELPERS, NULL)) == NULL ) + return false; + if ( (scenefilecache = (ISceneFileCache *)appSystemFactory( SCENE_FILE_CACHE_INTERFACE_VERSION, NULL )) == NULL ) + return false; + if ( IsX360() && (xboxsystem = (IXboxSystem *)appSystemFactory( XBOXSYSTEM_INTERFACE_VERSION, NULL )) == NULL ) + return false; + if ( IsX360() && (matchmaking = (IMatchmaking *)appSystemFactory( VENGINE_MATCHMAKING_VERSION, NULL )) == NULL ) + return false; + + if (!CommandLine()->CheckParm("-noscripting")) + { + scriptmanager = (IScriptManager*)appSystemFactory(VSCRIPT_INTERFACE_VERSION, NULL); + + if (scriptmanager == nullptr) + { + scriptmanager = (IScriptManager*)Sys_GetFactoryThis()(VSCRIPT_INTERFACE_VERSION, NULL); + } + } + + // If not running dedicated, grab the engine vgui interface + if ( !engine->IsDedicatedServer() ) + { +#ifdef _WIN32 + // This interface is optional, and is only valid when running with -tools + serverenginetools = ( IServerEngineTools * )appSystemFactory( VSERVERENGINETOOLS_INTERFACE_VERSION, NULL ); +#endif + } + + // Yes, both the client and game .dlls will try to Connect, the soundemittersystem.dll will handle this gracefully + if ( !soundemitterbase->Connect( appSystemFactory ) ) + return false; + + // cache the globals + gpGlobals = pGlobals; + + g_pSharedChangeInfo = engine->GetSharedEdictChangeInfo(); + + MathLib_Init( 2.2f, 2.2f, 0.0f, 2.0f ); + + // save these in case other system inits need them + factorylist_t factories; + factories.engineFactory = appSystemFactory; + factories.fileSystemFactory = fileSystemFactory; + factories.physicsFactory = physicsFactory; + FactoryList_Store( factories ); + + // load used game events + gameeventmanager->LoadEventsFromFile("resource/gameevents.res"); + + // init the cvar list first in case inits want to reference them + InitializeCvars(); + + // Initialize the particle system + if ( !g_pParticleSystemMgr->Init( g_pParticleSystemQuery ) ) + { + return false; + } + + sv_cheats = g_pCVar->FindVar( "sv_cheats" ); + if ( !sv_cheats ) + return false; + + g_pcv_commentary = g_pCVar->FindVar( "commentary" ); + g_pcv_ThreadMode = g_pCVar->FindVar( "host_thread_mode" ); + g_pcv_hideServer = g_pCVar->FindVar( "hide_server" ); + + sv_maxreplay = g_pCVar->FindVar( "sv_maxreplay" ); + + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetEntitySaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetPhysSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetAISaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetTemplateSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetDefaultResponseSystemSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetCommentarySaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetEventQueueSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetAchievementSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->AddBlockHandler( GetVScriptSaveRestoreBlockHandler() ); + + // The string system must init first + shutdown last + IGameSystem::Add( GameStringSystem() ); + + // Physics must occur before the sound envelope manager + IGameSystem::Add( PhysicsGameSystem() ); + + // Used to service deferred navigation queries for NPCs + IGameSystem::Add( (IGameSystem *) PostFrameNavigationSystem() ); + + // Add game log system + IGameSystem::Add( GameLogSystem() ); +#ifndef _XBOX + // Add HLTV director + IGameSystem::Add( HLTVDirectorSystem() ); +#endif + // Add sound emitter + IGameSystem::Add( SoundEmitterSystem() ); + + // load Mod specific game events ( MUST be before InitAllSystems() so it can pickup the mod specific events) +#ifdef MAPBASE + gameeventmanager->LoadEventsFromFile("resource/MapbaseEvents.res"); +#endif + gameeventmanager->LoadEventsFromFile("resource/ModEvents.res"); + +#ifdef CSTRIKE_DLL // BOTPORT: TODO: move these ifdefs out + InstallBotControl(); +#endif + + if ( !IGameSystem::InitAllSystems() ) + return false; + +#if defined( REPLAY_ENABLED ) + if ( gameeventmanager->LoadEventsFromFile( "resource/replayevents.res" ) <= 0 ) + { + Warning( "\n*\n* replayevents.res MISSING.\n*\n\n" ); + return false; + } +#endif + + // Due to dependencies, these are not autogamesystems + if ( !ModelSoundsCacheInit() ) + { + return false; + } + + InvalidateQueryCache(); + + // Parse the particle manifest file & register the effects within it + ParseParticleEffects( false, false ); + + // try to get debug overlay, may be NULL if on HLDS + debugoverlay = (IVDebugOverlay *)appSystemFactory( VDEBUG_OVERLAY_INTERFACE_VERSION, NULL ); + +#ifndef _XBOX +#ifdef USE_NAV_MESH + // create the Navigation Mesh interface + TheNavMesh = NavMeshFactory(); +#endif + + // init the gamestatsupload connection + gamestatsuploader->InitConnection(); +#endif + + return true; +} + +void CServerGameDLL::PostInit() +{ + IGameSystem::PostInitAllSystems(); +} + +void CServerGameDLL::DLLShutdown( void ) +{ + + // Due to dependencies, these are not autogamesystems + ModelSoundsCacheShutdown(); + + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetVScriptSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetAchievementSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetCommentarySaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetEventQueueSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetDefaultResponseSystemSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetTemplateSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetAISaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetPhysSaveRestoreBlockHandler() ); + g_pGameSaveRestoreBlockSet->RemoveBlockHandler( GetEntitySaveRestoreBlockHandler() ); + + char *pFilename = g_TextStatsMgr.GetStatsFilename(); + if ( !pFilename || !pFilename[0] ) + { + g_TextStatsMgr.SetStatsFilename( "stats.txt" ); + } + g_TextStatsMgr.WriteFile( filesystem ); + + IGameSystem::ShutdownAllSystems(); + +#ifdef CSTRIKE_DLL // BOTPORT: TODO: move these ifdefs out + RemoveBotControl(); +#endif + +#ifndef _XBOX +#ifdef USE_NAV_MESH + // destroy the Navigation Mesh interface + if ( TheNavMesh ) + { + delete TheNavMesh; + TheNavMesh = NULL; + } +#endif + // reset (shutdown) the gamestatsupload connection + gamestatsuploader->InitConnection(); +#endif + +#ifndef _X360 + s_SteamAPIContext.Clear(); // Steam API context shutdown + s_SteamGameServerAPIContext.Clear(); +#endif + + gameeventmanager = NULL; + + DisconnectTier3Libraries(); + DisconnectTier2Libraries(); + ConVar_Unregister(); + DisconnectTier1Libraries(); +} + +bool CServerGameDLL::ReplayInit( CreateInterfaceFn fnReplayFactory ) +{ +#if defined( REPLAY_ENABLED ) + if ( !IsPC() ) + return false; + if ( (g_pReplay = ( IReplaySystem *)fnReplayFactory( REPLAY_INTERFACE_VERSION, NULL )) == NULL ) + return false; + if ( (g_pReplayServerContext = g_pReplay->SV_GetContext()) == NULL ) + return false; + return true; +#else + return false; +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: See shareddefs.h for redefining this. Don't even think about it, though, for HL2. Or you will pay. ywb 9/22/03 +// Output : float +//----------------------------------------------------------------------------- +float CServerGameDLL::GetTickInterval( void ) const +{ + float tickinterval = DEFAULT_TICK_INTERVAL; + +//============================================================================= +// HPE_BEGIN: +// [Forrest] For Counter-Strike, set default tick rate of 66 and removed -tickrate command line parameter. +//============================================================================= +// Ignoring this for now, server ops are abusing it +#if !defined( TF_DLL ) && !defined( CSTRIKE_DLL ) && !defined( DOD_DLL ) +//============================================================================= +// HPE_END +//============================================================================= + // override if tick rate specified in command line + if ( CommandLine()->CheckParm( "-tickrate" ) ) + { + float tickrate = CommandLine()->ParmValue( "-tickrate", 0 ); + if ( tickrate > 10 ) + tickinterval = 1.0f / tickrate; + } +#endif + + return tickinterval; +} + +// This is called when a new game is started. (restart, map) +bool CServerGameDLL::GameInit( void ) +{ + ResetGlobalState(); + engine->ServerCommand( "exec game.cfg\n" ); + engine->ServerExecute( ); + CBaseEntity::sm_bAccurateTriggerBboxChecks = true; + + IGameEvent *event = gameeventmanager->CreateEvent( "game_init" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + return true; +} + +// This is called when a game ends (server disconnect, death, restart, load) +// NOT on level transitions within a game +void CServerGameDLL::GameShutdown( void ) +{ + ResetGlobalState(); +} + +static bool g_OneWayTransition = false; +void Game_SetOneWayTransition( void ) +{ + g_OneWayTransition = true; +} + +static CUtlVector g_RestoredEntities; +// just for debugging, assert that this is the only time this function is called +static bool g_InRestore = false; + +void AddRestoredEntity( CBaseEntity *pEntity ) +{ + Assert(g_InRestore); + if ( !pEntity ) + return; + + g_RestoredEntities.AddToTail( EHANDLE(pEntity) ); +} + +void EndRestoreEntities() +{ + if ( !g_InRestore ) + return; + + // The entire hierarchy is restored, so we can call GetAbsOrigin again. + //CBaseEntity::SetAbsQueriesValid( true ); + + // Call all entities' OnRestore handlers + for ( int i = g_RestoredEntities.Count()-1; i >=0; --i ) + { + CBaseEntity *pEntity = g_RestoredEntities[i].Get(); + if ( pEntity && !pEntity->IsDormant() ) + { + MDLCACHE_CRITICAL_SECTION(); + pEntity->OnRestore(); + } + } + + g_RestoredEntities.Purge(); + + IGameSystem::OnRestoreAllSystems(); + + g_InRestore = false; + gEntList.CleanupDeleteList(); + + // HACKHACK: UNDONE: We need to redesign the main loop with respect to save/load/server activate + g_ServerGameDLL.ServerActivate( NULL, 0, 0 ); + CBaseEntity::SetAllowPrecache( false ); +} + +void BeginRestoreEntities() +{ + if ( g_InRestore ) + { + DevMsg( "BeginRestoreEntities without previous EndRestoreEntities.\n" ); + gEntList.CleanupDeleteList(); + } + g_RestoredEntities.Purge(); + g_InRestore = true; + + CBaseEntity::SetAllowPrecache( true ); + + // No calls to GetAbsOrigin until the entire hierarchy is restored! + //CBaseEntity::SetAbsQueriesValid( false ); +} + +//----------------------------------------------------------------------------- +// Purpose: This prevents sv.tickcount/gpGlobals->tickcount from advancing during restore which +// would cause a lot of the NPCs to fast forward their think times to the same +// tick due to some ticks being elapsed during restore where there was no simulation going on +//----------------------------------------------------------------------------- +bool CServerGameDLL::IsRestoring() +{ + return g_InRestore; +} + +// Called any time a new level is started (after GameInit() also on level transitions within a game) +bool CServerGameDLL::LevelInit( const char *pMapName, char const *pMapEntities, char const *pOldLevel, char const *pLandmarkName, bool loadGame, bool background ) +{ + VPROF("CServerGameDLL::LevelInit"); + +#ifdef USES_ECON_ITEMS + GameItemSchema_t *pItemSchema = ItemSystem()->GetItemSchema(); + if ( pItemSchema ) + { + pItemSchema->BInitFromDelayedBuffer(); + } +#endif // USES_ECON_ITEMS + + ResetWindspeed(); + UpdateChapterRestrictions( pMapName ); + + if ( IsX360() && !background && (gpGlobals->maxClients == 1) && (g_nCurrentChapterIndex >= 0) ) + { + // Single player games tell xbox live what game & chapter the user is playing + UpdateRichPresence(); + } + + //Tony; parse custom manifest if exists! + ParseParticleEffectsMap( pMapName, false ); + + // IGameSystem::LevelInitPreEntityAllSystems() is called when the world is precached + // That happens either in LoadGameState() or in MapEntity_ParseAllEntities() + if ( loadGame ) + { + if ( pOldLevel ) + { + gpGlobals->eLoadType = MapLoad_Transition; + } + else + { + gpGlobals->eLoadType = MapLoad_LoadGame; + } + + BeginRestoreEntities(); + if ( !engine->LoadGameState( pMapName, 1 ) ) + { + if ( pOldLevel ) + { + MapEntity_ParseAllEntities( pMapEntities ); + } + else + { + // Regular save load case + return false; + } + } + + if ( pOldLevel ) + { + engine->LoadAdjacentEnts( pOldLevel, pLandmarkName ); + } + + if ( g_OneWayTransition ) + { + engine->ClearSaveDirAfterClientLoad(); + } + + if ( pOldLevel && sv_autosave.GetBool() == true ) + { + // This is a single-player style level transition. + // Queue up an autosave one second into the level + CBaseEntity *pAutosave = CBaseEntity::Create( "logic_autosave", vec3_origin, vec3_angle, NULL ); + if ( pAutosave ) + { + g_EventQueue.AddEvent( pAutosave, "Save", 1.0, NULL, NULL ); + g_EventQueue.AddEvent( pAutosave, "Kill", 1.1, NULL, NULL ); + } + } + } + else + { + if ( background ) + { + gpGlobals->eLoadType = MapLoad_Background; + } + else + { + gpGlobals->eLoadType = MapLoad_NewGame; + } + + // Clear out entity references, and parse the entities into it. + g_MapEntityRefs.Purge(); + CMapLoadEntityFilter filter; + MapEntity_ParseAllEntities( pMapEntities, &filter ); + + g_pServerBenchmark->StartBenchmark(); + + // Now call the mod specific parse + LevelInit_ParseAllEntities( pMapEntities ); + } + + // Check low violence settings for this map + g_RagdollLVManager.SetLowViolence( pMapName ); + + // Now that all of the active entities have been loaded in, precache any entities who need point_template parameters + // to be parsed (the above code has loaded all point_template entities) + PrecachePointTemplates(); + + // load MOTD from file into stringtable + LoadMessageOfTheDay(); + + // Sometimes an ent will Remove() itself during its precache, so RemoveImmediate won't happen. + // This makes sure those ents get cleaned up. + gEntList.CleanupDeleteList(); + + g_AIFriendliesTalkSemaphore.Release(); + g_AIFoesTalkSemaphore.Release(); + g_OneWayTransition = false; + + // clear any pending autosavedangerous + m_fAutoSaveDangerousTime = 0.0f; + m_fAutoSaveDangerousMinHealthToCommit = 0.0f; + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: called after every level change and load game, iterates through all the +// active entities and gives them a chance to fix up their state +//----------------------------------------------------------------------------- +#ifdef DEBUG +bool g_bReceivedChainedActivate; +bool g_bCheckForChainedActivate; +#define BeginCheckChainedActivate() if (0) ; else { g_bCheckForChainedActivate = true; g_bReceivedChainedActivate = false; } +#define EndCheckChainedActivate( bCheck ) \ + if (0) ; else \ + { \ + if ( bCheck ) \ + { \ + AssertMsg( g_bReceivedChainedActivate == true, "Entity (%i/%s/%s) failed to call base class Activate()\n", pClass->entindex(), pClass->GetClassname(), STRING( pClass->GetEntityName() ) ); \ + } \ + g_bCheckForChainedActivate = false; \ + } +#else +#define BeginCheckChainedActivate() ((void)0) +#define EndCheckChainedActivate( bCheck ) ((void)0) +#endif + +void CServerGameDLL::ServerActivate( edict_t *pEdictList, int edictCount, int clientMax ) +{ + // HACKHACK: UNDONE: We need to redesign the main loop with respect to save/load/server activate + if ( g_InRestore ) + return; + + if ( gEntList.ResetDeleteList() != 0 ) + { + Msg( "ERROR: Entity delete queue not empty on level start!\n" ); + } + + for ( CBaseEntity *pClass = gEntList.FirstEnt(); pClass != NULL; pClass = gEntList.NextEnt(pClass) ) + { + if ( pClass && !pClass->IsDormant() ) + { + MDLCACHE_CRITICAL_SECTION(); + + BeginCheckChainedActivate(); + pClass->Activate(); + + // We don't care if it finished activating if it decided to remove itself. + EndCheckChainedActivate( !( pClass->GetEFlags() & EFL_KILLME ) ); + } + } + + IGameSystem::LevelInitPostEntityAllSystems(); + // No more precaching after PostEntityAllSystems!!! + CBaseEntity::SetAllowPrecache( false ); + + // only display the think limit when the game is run with "developer" mode set + if ( !g_pDeveloper->GetInt() ) + { + think_limit.SetValue( 0 ); + } + +#ifndef _XBOX +#ifdef USE_NAV_MESH + // load the Navigation Mesh for this map + TheNavMesh->Load(); + TheNavMesh->OnServerActivate(); +#endif +#endif + +#ifdef CSTRIKE_DLL // BOTPORT: TODO: move these ifdefs out + TheBots->ServerActivate(); +#endif + +#ifdef NEXT_BOT + TheNextBots().OnMapLoaded(); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Called after the steam API has been activated post-level startup +//----------------------------------------------------------------------------- +void CServerGameDLL::GameServerSteamAPIActivated( void ) +{ +#ifndef NO_STEAM + steamgameserverapicontext->Init(); + if ( steamgameserverapicontext->SteamGameServer() && engine->IsDedicatedServer() ) + { + steamgameserverapicontext->SteamGameServer()->GetGameplayStats(); + } +#endif + +#ifdef TF_DLL + GCClientSystem()->GameServerActivate(); + InventoryManager()->GameServerSteamAPIActivated(); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Called after the steam API has been activated post-level startup +//----------------------------------------------------------------------------- +void CServerGameDLL::GameServerSteamAPIShutdown( void ) +{ +#if !defined( NO_STEAM ) + if ( steamgameserverapicontext ) + { + steamgameserverapicontext->Clear(); + } +#endif +#ifdef TF_DLL + GCClientSystem()->Shutdown(); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Called at the start of every game frame +//----------------------------------------------------------------------------- +ConVar trace_report( "trace_report", "0" ); + +void CServerGameDLL::GameFrame( bool simulating ) +{ + VPROF( "CServerGameDLL::GameFrame" ); + + // Don't run frames until fully restored + if ( g_InRestore ) + return; + + if ( CBaseEntity::IsSimulatingOnAlternateTicks() ) + { + // only run simulation on even numbered ticks + if ( gpGlobals->tickcount & 1 ) + { + UpdateAllClientData(); + return; + } + // If we're skipping frames, then the frametime is 2x the normal tick + gpGlobals->frametime *= 2.0f; + } + + float oldframetime = gpGlobals->frametime; + +#ifdef _DEBUG + // For profiling.. let them enable/disable the networkvar manual mode stuff. + g_bUseNetworkVars = s_UseNetworkVars.GetBool(); +#endif + + extern void GameStartFrame( void ); + extern void ServiceEventQueue( void ); + extern void Physics_RunThinkFunctions( bool simulating ); + + // Delete anything that was marked for deletion + // outside of server frameloop (e.g., in response to concommand) + gEntList.CleanupDeleteList(); + + IGameSystem::FrameUpdatePreEntityThinkAllSystems(); + GameStartFrame(); + +#ifndef _XBOX +#ifdef USE_NAV_MESH + TheNavMesh->Update(); +#endif + +#ifdef NEXT_BOT + TheNextBots().Update(); +#endif + + gamestatsuploader->UpdateConnection(); +#endif + + UpdateQueryCache(); + g_pServerBenchmark->UpdateBenchmark(); + + Physics_RunThinkFunctions( simulating ); + + IGameSystem::FrameUpdatePostEntityThinkAllSystems(); + + // UNDONE: Make these systems IGameSystems and move these calls into FrameUpdatePostEntityThink() + // service event queue, firing off any actions whos time has come + ServiceEventQueue(); + + // free all ents marked in think functions + gEntList.CleanupDeleteList(); + + // FIXME: Should this only occur on the final tick? + UpdateAllClientData(); + + if ( g_pGameRules ) + { + g_pGameRules->EndGameFrame(); + } + + if ( trace_report.GetBool() ) + { + int total = 0, totals[3]; + for ( int i = 0; i < 3; i++ ) + { + totals[i] = enginetrace->GetStatByIndex( i, true ); + if ( totals[i] > 0 ) + { + total += totals[i]; + } + } + + if ( total ) + { + Msg("Trace: %d, contents %d, enumerate %d\n", totals[0], totals[1], totals[2] ); + } + } + + // Any entities that detect network state changes on a timer do it here. + g_NetworkPropertyEventMgr.FireEvents(); + + gpGlobals->frametime = oldframetime; +} + +//----------------------------------------------------------------------------- +// Purpose: Called every frame even if not ticking +// Input : simulating - +//----------------------------------------------------------------------------- +void CServerGameDLL::PreClientUpdate( bool simulating ) +{ + if ( !simulating ) + return; + + /* + if (game_speeds.GetInt()) + { + DrawMeasuredSections(); + } + */ + +//#ifdef _DEBUG - allow this in release for now + DrawAllDebugOverlays(); +//#endif + + IGameSystem::PreClientUpdateAllSystems(); + +#ifdef _DEBUG + if ( sv_showhitboxes.GetInt() == -1 ) + return; + + if ( sv_showhitboxes.GetInt() == 0 ) + { + // assume it's text + CBaseEntity *pEntity = NULL; + + while (1) + { + pEntity = gEntList.FindEntityByName( pEntity, sv_showhitboxes.GetString() ); + if ( !pEntity ) + break; + + CBaseAnimating *anim = dynamic_cast< CBaseAnimating * >( pEntity ); + + if (anim) + { + anim->DrawServerHitboxes(); + } + } + return; + } + + CBaseAnimating *anim = dynamic_cast< CBaseAnimating * >( CBaseEntity::Instance( engine->PEntityOfEntIndex( sv_showhitboxes.GetInt() ) ) ); + if ( !anim ) + return; + + anim->DrawServerHitboxes(); +#endif +} + +void CServerGameDLL::Think( bool finalTick ) +{ + if ( m_fAutoSaveDangerousTime != 0.0f && m_fAutoSaveDangerousTime < gpGlobals->curtime ) + { + // The safety timer for a dangerous auto save has expired + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + + if ( pPlayer && ( pPlayer->GetDeathTime() == 0.0f || pPlayer->GetDeathTime() > gpGlobals->curtime ) + && !pPlayer->IsSinglePlayerGameEnding() + ) + { + if( pPlayer->GetHealth() >= m_fAutoSaveDangerousMinHealthToCommit ) + { + // The player isn't dead, so make the dangerous auto save safe + engine->ServerCommand( "autosavedangerousissafe\n" ); + } + } + + m_fAutoSaveDangerousTime = 0.0f; + m_fAutoSaveDangerousMinHealthToCommit = 0.0f; + } +} + +void CServerGameDLL::OnQueryCvarValueFinished( QueryCvarCookie_t iCookie, edict_t *pPlayerEntity, EQueryCvarValueStatus eStatus, const char *pCvarName, const char *pCvarValue ) +{ +} + + +// Called when a level is shutdown (including changing levels) +void CServerGameDLL::LevelShutdown( void ) +{ +#ifndef NO_STEAM + IGameSystem::LevelShutdownPreClearSteamAPIContextAllSystems(); + + steamgameserverapicontext->Clear(); +#endif + + g_pServerBenchmark->EndBenchmark(); + + MDLCACHE_CRITICAL_SECTION(); + IGameSystem::LevelShutdownPreEntityAllSystems(); + + // YWB: + // This entity pointer is going away now and is corrupting memory on level transitions/restarts + CSoundEnt::ShutdownSoundEnt(); + + gEntList.Clear(); + + InvalidateQueryCache(); + + IGameSystem::LevelShutdownPostEntityAllSystems(); + + // In case we quit out during initial load + CBaseEntity::SetAllowPrecache( false ); + + g_nCurrentChapterIndex = -1; + +#ifndef _XBOX +#ifdef USE_NAV_MESH + // reset the Navigation Mesh + if ( TheNavMesh ) + { + TheNavMesh->Reset(); + } +#endif +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : ServerClass* +//----------------------------------------------------------------------------- +ServerClass* CServerGameDLL::GetAllServerClasses() +{ + return g_pServerClassHead; +} + + +const char *CServerGameDLL::GetGameDescription( void ) +{ + return ::GetGameDescription(); +} + +void CServerGameDLL::CreateNetworkStringTables( void ) +{ + // Create any shared string tables here (and only here!) + // E.g.: xxx = networkstringtable->CreateStringTable( "SceneStrings", 512 ); + g_pStringTableParticleEffectNames = networkstringtable->CreateStringTable( "ParticleEffectNames", MAX_PARTICLESYSTEMS_STRINGS ); + g_pStringTableEffectDispatch = networkstringtable->CreateStringTable( "EffectDispatch", MAX_EFFECT_DISPATCH_STRINGS ); + g_pStringTableVguiScreen = networkstringtable->CreateStringTable( "VguiScreen", MAX_VGUI_SCREEN_STRINGS ); + g_pStringTableMaterials = networkstringtable->CreateStringTable( "Materials", MAX_MATERIAL_STRINGS ); + g_pStringTableInfoPanel = networkstringtable->CreateStringTable( "InfoPanel", MAX_INFOPANEL_STRINGS ); + g_pStringTableClientSideChoreoScenes = networkstringtable->CreateStringTable( "Scenes", MAX_CHOREO_SCENES_STRINGS ); + g_pStringTableServerMapCycle = networkstringtable->CreateStringTable( "ServerMapCycle", 128 ); + +#ifdef TF_DLL + g_pStringTableServerPopFiles = networkstringtable->CreateStringTable( "ServerPopFiles", 128 ); + g_pStringTableServerMapCycleMvM = networkstringtable->CreateStringTable( "ServerMapCycleMvM", 128 ); +#endif + + bool bPopFilesValid = true; + (void)bPopFilesValid; // Avoid unreferenced variable warning + +#ifdef TF_DLL + bPopFilesValid = ( g_pStringTableServerPopFiles != NULL ); +#endif + + Assert( g_pStringTableParticleEffectNames && + g_pStringTableEffectDispatch && + g_pStringTableVguiScreen && + g_pStringTableMaterials && + g_pStringTableInfoPanel && + g_pStringTableClientSideChoreoScenes && + g_pStringTableServerMapCycle && + bPopFilesValid + ); + + // Need this so we have the error material always handy + PrecacheMaterial( "debug/debugempty" ); + Assert( GetMaterialIndex( "debug/debugempty" ) == 0 ); + + PrecacheParticleSystem( "error" ); // ensure error particle system is handy + Assert( GetParticleSystemIndex( "error" ) == 0 ); + + CreateNetworkStringTables_GameRules(); + + // Set up save/load utilities for string tables + g_VguiScreenStringOps.Init( g_pStringTableVguiScreen ); +} + +CSaveRestoreData *CServerGameDLL::SaveInit( int size ) +{ + return ::SaveInit(size); +} + +//----------------------------------------------------------------------------- +// Purpose: Saves data from a struct into a saverestore object, to be saved to disk +// Input : *pSaveData - the saverestore object +// char *pname - the name of the data to write +// *pBaseData - the struct into which the data is to be read +// *pFields - pointer to an array of data field descriptions +// fieldCount - the size of the array (number of field descriptions) +//----------------------------------------------------------------------------- +void CServerGameDLL::SaveWriteFields( CSaveRestoreData *pSaveData, const char *pname, void *pBaseData, datamap_t *pMap, typedescription_t *pFields, int fieldCount ) +{ + CSave saveHelper( pSaveData ); + saveHelper.WriteFields( pname, pBaseData, pMap, pFields, fieldCount ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Reads data from a save/restore block into a structure +// Input : *pSaveData - the saverestore object +// char *pname - the name of the data to extract from +// *pBaseData - the struct into which the data is to be restored +// *pFields - pointer to an array of data field descriptions +// fieldCount - the size of the array (number of field descriptions) +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- + +void CServerGameDLL::SaveReadFields( CSaveRestoreData *pSaveData, const char *pname, void *pBaseData, datamap_t *pMap, typedescription_t *pFields, int fieldCount ) +{ + CRestore restoreHelper( pSaveData ); + restoreHelper.ReadFields( pname, pBaseData, pMap, pFields, fieldCount ); +} + +//----------------------------------------------------------------------------- + +void CServerGameDLL::SaveGlobalState( CSaveRestoreData *s ) +{ + ::SaveGlobalState(s); +} + +void CServerGameDLL::RestoreGlobalState(CSaveRestoreData *s) +{ + ::RestoreGlobalState(s); +} + +void CServerGameDLL::Save( CSaveRestoreData *s ) +{ + CSave saveHelper( s ); + g_pGameSaveRestoreBlockSet->Save( &saveHelper ); +} + +void CServerGameDLL::Restore( CSaveRestoreData *s, bool b) +{ + CRestore restore(s); + g_pGameSaveRestoreBlockSet->Restore( &restore, b ); + g_pGameSaveRestoreBlockSet->PostRestore(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_type - +// *name - +// size - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- + +bool CServerGameDLL::GetUserMessageInfo( int msg_type, char *name, int maxnamelength, int& size ) +{ + if ( !usermessages->IsValidIndex( msg_type ) ) + return false; + + Q_strncpy( name, usermessages->GetUserMessageName( msg_type ), maxnamelength ); + size = usermessages->GetUserMessageSize( msg_type ); + return true; +} + +CStandardSendProxies* CServerGameDLL::GetStandardSendProxies() +{ + return &g_StandardSendProxies; +} + +int CServerGameDLL::CreateEntityTransitionList( CSaveRestoreData *s, int a) +{ + CRestore restoreHelper( s ); + // save off file base + int base = restoreHelper.GetReadPos(); + + int movedCount = ::CreateEntityTransitionList(s, a); + if ( movedCount ) + { + g_pGameSaveRestoreBlockSet->CallBlockHandlerRestore( GetPhysSaveRestoreBlockHandler(), base, &restoreHelper, false ); + g_pGameSaveRestoreBlockSet->CallBlockHandlerRestore( GetAISaveRestoreBlockHandler(), base, &restoreHelper, false ); + } + + GetPhysSaveRestoreBlockHandler()->PostRestore(); + GetAISaveRestoreBlockHandler()->PostRestore(); + + return movedCount; +} + +void CServerGameDLL::PreSave( CSaveRestoreData *s ) +{ + g_pGameSaveRestoreBlockSet->PreSave( s ); +} + +#include "client_textmessage.h" + +// This little hack lets me marry BSP names to messages in titles.txt +typedef struct +{ + const char *pBSPName; + const char *pTitleName; +} TITLECOMMENT; + +// this list gets searched for the first partial match, so some are out of order +static TITLECOMMENT gTitleComments[] = +{ +#ifdef HL1_DLL + { "t0a0", "#T0A0TITLE" }, + { "c0a0", "#HL1_Chapter1_Title" }, + { "c1a0", "#HL1_Chapter2_Title" }, + { "c1a1", "#HL1_Chapter3_Title" }, + { "c1a2", "#HL1_Chapter4_Title" }, + { "c1a3", "#HL1_Chapter5_Title" }, + { "c1a4", "#HL1_Chapter6_Title" }, + { "c2a1", "#HL1_Chapter7_Title" }, + { "c2a2", "#HL1_Chapter8_Title" }, + { "c2a3", "#HL1_Chapter9_Title" }, + { "c2a4d", "#HL1_Chapter11_Title" }, // These must appear before "C2A4" so all other map names starting with C2A4 get that title + { "c2a4e", "#HL1_Chapter11_Title" }, + { "c2a4f", "#HL1_Chapter11_Title" }, + { "c2a4g", "#HL1_Chapter11_Title" }, + { "c2a4", "#HL1_Chapter10_Title" }, + { "c2a5", "#HL1_Chapter12_Title" }, + { "c3a1", "#HL1_Chapter13_Title" }, + { "c3a2", "#HL1_Chapter14_Title" }, + { "c4a1a", "#HL1_Chapter17_Title" }, // Order is important, see above + { "c4a1b", "#HL1_Chapter17_Title" }, + { "c4a1c", "#HL1_Chapter17_Title" }, + { "c4a1d", "#HL1_Chapter17_Title" }, + { "c4a1e", "#HL1_Chapter17_Title" }, + { "c4a1", "#HL1_Chapter15_Title" }, + { "c4a2", "#HL1_Chapter16_Title" }, + { "c4a3", "#HL1_Chapter18_Title" }, + { "c5a1", "#HL1_Chapter19_Title" }, +#elif defined PORTAL + { "testchmb_a_00", "#Portal_Chapter1_Title" }, + { "testchmb_a_01", "#Portal_Chapter1_Title" }, + { "testchmb_a_02", "#Portal_Chapter2_Title" }, + { "testchmb_a_03", "#Portal_Chapter2_Title" }, + { "testchmb_a_04", "#Portal_Chapter3_Title" }, + { "testchmb_a_05", "#Portal_Chapter3_Title" }, + { "testchmb_a_06", "#Portal_Chapter4_Title" }, + { "testchmb_a_07", "#Portal_Chapter4_Title" }, + { "testchmb_a_08_advanced", "#Portal_Chapter5_Title" }, + { "testchmb_a_08", "#Portal_Chapter5_Title" }, + { "testchmb_a_09_advanced", "#Portal_Chapter6_Title" }, + { "testchmb_a_09", "#Portal_Chapter6_Title" }, + { "testchmb_a_10_advanced", "#Portal_Chapter7_Title" }, + { "testchmb_a_10", "#Portal_Chapter7_Title" }, + { "testchmb_a_11_advanced", "#Portal_Chapter8_Title" }, + { "testchmb_a_11", "#Portal_Chapter8_Title" }, + { "testchmb_a_13_advanced", "#Portal_Chapter9_Title" }, + { "testchmb_a_13", "#Portal_Chapter9_Title" }, + { "testchmb_a_14_advanced", "#Portal_Chapter10_Title" }, + { "testchmb_a_14", "#Portal_Chapter10_Title" }, + { "testchmb_a_15", "#Portal_Chapter11_Title" }, + { "escape_", "#Portal_Chapter11_Title" }, + { "background2", "#Portal_Chapter12_Title" }, +#else + { "intro", "#HL2_Chapter1_Title" }, + + { "d1_trainstation_05", "#HL2_Chapter2_Title" }, + { "d1_trainstation_06", "#HL2_Chapter2_Title" }, + + { "d1_trainstation_", "#HL2_Chapter1_Title" }, + + { "d1_canals_06", "#HL2_Chapter4_Title" }, + { "d1_canals_07", "#HL2_Chapter4_Title" }, + { "d1_canals_08", "#HL2_Chapter4_Title" }, + { "d1_canals_09", "#HL2_Chapter4_Title" }, + { "d1_canals_1", "#HL2_Chapter4_Title" }, + + { "d1_canals_0", "#HL2_Chapter3_Title" }, + + { "d1_eli_", "#HL2_Chapter5_Title" }, + + { "d1_town_", "#HL2_Chapter6_Title" }, + + { "d2_coast_09", "#HL2_Chapter8_Title" }, + { "d2_coast_1", "#HL2_Chapter8_Title" }, + { "d2_prison_01", "#HL2_Chapter8_Title" }, + + { "d2_coast_", "#HL2_Chapter7_Title" }, + + { "d2_prison_06", "#HL2_Chapter9a_Title" }, + { "d2_prison_07", "#HL2_Chapter9a_Title" }, + { "d2_prison_08", "#HL2_Chapter9a_Title" }, + + { "d2_prison_", "#HL2_Chapter9_Title" }, + + { "d3_c17_01", "#HL2_Chapter9a_Title" }, + { "d3_c17_09", "#HL2_Chapter11_Title" }, + { "d3_c17_1", "#HL2_Chapter11_Title" }, + + { "d3_c17_", "#HL2_Chapter10_Title" }, + + { "d3_citadel_", "#HL2_Chapter12_Title" }, + + { "d3_breen_", "#HL2_Chapter13_Title" }, + { "credits", "#HL2_Chapter14_Title" }, + + { "ep1_citadel_00", "#episodic_Chapter1_Title" }, + { "ep1_citadel_01", "#episodic_Chapter1_Title" }, + { "ep1_citadel_02b", "#episodic_Chapter1_Title" }, + { "ep1_citadel_02", "#episodic_Chapter1_Title" }, + { "ep1_citadel_03", "#episodic_Chapter2_Title" }, + { "ep1_citadel_04", "#episodic_Chapter2_Title" }, + { "ep1_c17_00a", "#episodic_Chapter3_Title" }, + { "ep1_c17_00", "#episodic_Chapter3_Title" }, + { "ep1_c17_01", "#episodic_Chapter4_Title" }, + { "ep1_c17_02b", "#episodic_Chapter4_Title" }, + { "ep1_c17_02", "#episodic_Chapter4_Title" }, + { "ep1_c17_05", "#episodic_Chapter5_Title" }, + { "ep1_c17_06", "#episodic_Chapter5_Title" }, + + { "ep2_outland_01a", "#ep2_Chapter1_Title" }, + { "ep2_outland_01", "#ep2_Chapter1_Title" }, + { "ep2_outland_02", "#ep2_Chapter2_Title" }, + { "ep2_outland_03", "#ep2_Chapter2_Title" }, + { "ep2_outland_04", "#ep2_Chapter2_Title" }, + { "ep2_outland_05", "#ep2_Chapter3_Title" }, + + { "ep2_outland_06a", "#ep2_Chapter4_Title" }, + { "ep2_outland_06", "#ep2_Chapter3_Title" }, + + { "ep2_outland_07", "#ep2_Chapter4_Title" }, + { "ep2_outland_08", "#ep2_Chapter4_Title" }, + { "ep2_outland_09", "#ep2_Chapter5_Title" }, + + { "ep2_outland_10a", "#ep2_Chapter5_Title" }, + { "ep2_outland_10", "#ep2_Chapter5_Title" }, + + { "ep2_outland_11a", "#ep2_Chapter6_Title" }, + { "ep2_outland_11", "#ep2_Chapter6_Title" }, + + { "ep2_outland_12a", "#ep2_Chapter7_Title" }, + { "ep2_outland_12", "#ep2_Chapter6_Title" }, +#endif +}; + +#ifdef MAPBASE +extern CUtlVector *Mapbase_GetChapterMaps(); +extern CUtlVector *Mapbase_GetChapterList(); +#endif + +#ifdef _XBOX +void CServerGameDLL::GetTitleName( const char *pMapName, char* pTitleBuff, int titleBuffSize ) +{ +#ifdef MAPBASE + // Check the world entity for a chapter title + if ( CWorld *pWorld = GetWorldEntity() ) + { + const char *pWorldChapter = pWorld->GetChapterTitle(); + if ( pWorldChapter && pWorldChapter[0] != '\0' ) + { + Q_strncpy( chapterTitle, pWorldChapter, sizeof( chapterTitle ) ); + return; + } + } + + // Look in the mod's chapter list + CUtlVector *ModChapterComments = Mapbase_GetChapterMaps(); + if (ModChapterComments->Count() > 0) + { + for ( int i = 0; i < ModChapterComments->Count(); i++ ) + { + if ( !Q_strnicmp( mapname, ModChapterComments->Element(i).pBSPName, strlen(ModChapterComments->Element(i).pBSPName) ) ) + { + Q_strncpy( pTitleBuff, ModChapterComments->Element(i).pTitleName, titleBuffSize ); + return; + } + } + } +#endif + + // Try to find a matching title comment for this mapname + for ( int i = 0; i < ARRAYSIZE(gTitleComments); i++ ) + { + if ( !Q_strnicmp( pMapName, gTitleComments[i].pBSPName, strlen(gTitleComments[i].pBSPName) ) ) + { + Q_strncpy( pTitleBuff, gTitleComments[i].pTitleName, titleBuffSize ); + return; + } + } + + Q_strncpy( pTitleBuff, pMapName, titleBuffSize ); +} +#endif + +void CServerGameDLL::GetSaveComment( char *text, int maxlength, float flMinutes, float flSeconds, bool bNoTime ) +{ + char comment[64]; + const char *pName; + int i; + + char const *mapname = STRING( gpGlobals->mapname ); + + pName = NULL; + + // Try to find a matching title comment for this mapname + for ( i = 0; i < ARRAYSIZE(gTitleComments) && !pName; i++ ) + { + if ( !Q_strnicmp( mapname, gTitleComments[i].pBSPName, strlen(gTitleComments[i].pBSPName) ) ) + { + // found one + int j; + + // Got a message, post-process it to be save name friendly + Q_strncpy( comment, gTitleComments[i].pTitleName, sizeof( comment ) ); + pName = comment; + j = 0; + // Strip out CRs + while ( j < 64 && comment[j] ) + { + if ( comment[j] == '\n' || comment[j] == '\r' ) + comment[j] = 0; + else + j++; + } + break; + } + } + +#ifdef MAPBASE + // Look in the mod's chapter list + CUtlVector *ModChapterComments = Mapbase_GetChapterMaps(); + if (ModChapterComments->Count() > 0) + { + for ( int i = 0; i < ModChapterComments->Count(); i++ ) + { + if ( !Q_strnicmp( mapname, ModChapterComments->Element(i).pBSPName, strlen(ModChapterComments->Element(i).pBSPName) ) ) + { + // found one + int j; + + // Got a message, post-process it to be save name friendly + Q_strncpy( comment, ModChapterComments->Element(i).pTitleName, sizeof( comment ) ); + pName = comment; + j = 0; + // Strip out CRs + while ( j < 64 && comment[j] ) + { + if ( comment[j] == '\n' || comment[j] == '\r' ) + comment[j] = 0; + else + j++; + } + break; + } + } + } + + // Check the world entity for a chapter title + if ( CWorld *pWorld = GetWorldEntity() ) + { + const char *pWorldChapter = pWorld->GetChapterTitle(); + if ( pWorldChapter && pWorldChapter[0] != '\0' ) + pName = pWorldChapter; + } +#endif + + // If we didn't get one, use the designer's map name, or the BSP name itself + if ( !pName ) + { + pName = mapname; + } + + if ( bNoTime ) + { + Q_snprintf( text, maxlength, "%-64.64s", pName ); + } + else + { + int minutes = flMinutes; + int seconds = flSeconds; + + // Wow, this guy/gal must suck...! + if ( minutes >= 1000 ) + { + minutes = 999; + seconds = 59; + } + + int minutesAdd = ( seconds / 60 ); + seconds %= 60; + + // add the elapsed time at the end of the comment, for the ui to parse out + Q_snprintf( text, maxlength, "%-64.64s %03d:%02d", pName, (minutes + minutesAdd), seconds ); + } +} + +void CServerGameDLL::WriteSaveHeaders( CSaveRestoreData *s ) +{ + CSave saveHelper( s ); + g_pGameSaveRestoreBlockSet->WriteSaveHeaders( &saveHelper ); + g_pGameSaveRestoreBlockSet->PostSave(); +} + +void CServerGameDLL::ReadRestoreHeaders( CSaveRestoreData *s ) +{ + CRestore restoreHelper( s ); + g_pGameSaveRestoreBlockSet->PreRestore(); + g_pGameSaveRestoreBlockSet->ReadRestoreHeaders( &restoreHelper ); +} + +void CServerGameDLL::PreSaveGameLoaded( char const *pSaveName, bool bInGame ) +{ + gamestats->Event_PreSaveGameLoaded( pSaveName, bInGame ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the game DLL wants the server not to be made public. +// Used by commentary system to hide multiplayer commentary servers from the master. +//----------------------------------------------------------------------------- +bool CServerGameDLL::ShouldHideServer( void ) +{ + if ( g_pcv_commentary && g_pcv_commentary->GetBool() ) + return true; + + if ( g_pcv_hideServer && g_pcv_hideServer->GetBool() ) + return true; + + if ( gpGlobals->eLoadType == MapLoad_Background ) + return true; + + #if defined( TF_DLL ) + if ( GTFGCClientSystem()->ShouldHideServer() ) + return true; + #endif + return false; +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CServerGameDLL::InvalidateMdlCache() +{ + CBaseAnimating *pAnimating; + for ( CBaseEntity *pEntity = gEntList.FirstEnt(); pEntity != NULL; pEntity = gEntList.NextEnt(pEntity) ) + { + pAnimating = dynamic_cast(pEntity); + if ( pAnimating ) + { + pAnimating->InvalidateMdlCache(); + } + } +} + +// interface to the new GC based lobby system +IServerGCLobby *CServerGameDLL::GetServerGCLobby() +{ +#ifdef TF_DLL + return GTFGCClientSystem(); +#else + return NULL; +#endif +} + + +void CServerGameDLL::SetServerHibernation( bool bHibernating ) +{ + m_bIsHibernating = bHibernating; + +#ifdef INFESTED_DLL + if ( engine && engine->IsDedicatedServer() && m_bIsHibernating && ASWGameRules() ) + { + ASWGameRules()->OnServerHibernating(); + } +#endif + +#ifdef TF_DLL + GTFGCClientSystem()->SetHibernation( bHibernating ); +#endif +} + +const char *CServerGameDLL::GetServerBrowserMapOverride() +{ +#ifdef TF_DLL + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + const char *pszFilenameShort = g_pPopulationManager ? g_pPopulationManager->GetPopulationFilenameShort() : NULL; + if ( pszFilenameShort && pszFilenameShort[0] ) + { + return pszFilenameShort; + } + } +#endif + return NULL; +} + +const char *CServerGameDLL::GetServerBrowserGameData() +{ + CUtlString sResult; + +#ifdef TF_DLL + sResult.Format( "tf_mm_trusted:%d,tf_mm_servermode:%d", tf_mm_trusted.GetInt(), tf_mm_servermode.GetInt() ); + + CTFLobby *pLobby = GTFGCClientSystem()->GetLobby(); + if ( pLobby == NULL ) + { + sResult.Append( ",lobby:0" ); + } + else + { + sResult.Append( CFmtStr( ",lobby:%016llx", pLobby->GetGroupID() ) ); + } + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + sResult.Append( CFmtStr( ",mannup:%d", ( pLobby && pLobby->GetPlayingForBraggingRights() ) ? 1 : 0 ) ); + } +#endif + + static char rchResult[2048]; + V_strcpy_safe( rchResult, sResult ); + return rchResult; +} + +//----------------------------------------------------------------------------- +// Purpose: Called during a transition, to build a map adjacency list +//----------------------------------------------------------------------------- +void CServerGameDLL::BuildAdjacentMapList( void ) +{ + // retrieve the pointer to the save data + CSaveRestoreData *pSaveData = gpGlobals->pSaveData; + + if ( pSaveData ) + pSaveData->levelInfo.connectionCount = BuildChangeList( pSaveData->levelInfo.levelList, MAX_LEVEL_CONNECTIONS ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sanity-check to verify that a path is a relative path inside the game dir +// Taken From: engine/cmd.cpp +//----------------------------------------------------------------------------- +static bool IsValidPath( const char *pszFilename ) +{ + if ( !pszFilename ) + { + return false; + } + + if ( Q_strlen( pszFilename ) <= 0 || + Q_IsAbsolutePath( pszFilename ) || // to protect absolute paths + Q_strstr( pszFilename, ".." ) ) // to protect relative paths + { + return false; + } + + return true; +} + +static void ValidateMOTDFilename( IConVar *pConVar, const char *oldValue, float flOldValue ) +{ + ConVarRef var( pConVar ); + if ( !IsValidPath( var.GetString() ) ) + { + var.SetValue( var.GetDefault() ); + } +} + +static ConVar motdfile( "motdfile", "motd.txt", 0, "The MOTD file to load.", ValidateMOTDFilename ); +static ConVar motdfile_text( "motdfile_text", "motd_text.txt", 0, "The text-only MOTD file to use for clients that have disabled HTML MOTDs.", ValidateMOTDFilename ); +void CServerGameDLL::LoadMessageOfTheDay() +{ + LoadSpecificMOTDMsg( motdfile, "motd" ); + LoadSpecificMOTDMsg( motdfile_text, "motd_text" ); +} + +void CServerGameDLL::LoadSpecificMOTDMsg( const ConVar &convar, const char *pszStringName ) +{ +#ifndef _XBOX + CUtlBuffer buf; + + // Generate preferred filename, which is in the cfg folder. + char szPreferredFilename[ MAX_PATH ]; + V_sprintf_safe( szPreferredFilename, "cfg/%s", convar.GetString() ); + + // Check the preferred filename first + char szResolvedFilename[ MAX_PATH ]; + V_strcpy_safe( szResolvedFilename, szPreferredFilename ); + bool bFound = filesystem->ReadFile( szResolvedFilename, "GAME", buf ); + + // Not found? Try in the root, which is the old place it used to go. + if ( !bFound ) + { + + V_strcpy_safe( szResolvedFilename, convar.GetString() ); + bFound = filesystem->ReadFile( szResolvedFilename, "GAME", buf ); + } + + // Still not found? See if we can try the default. + if ( !bFound && !V_stricmp( convar.GetString(), convar.GetDefault() ) ) + { + V_strcpy_safe( szResolvedFilename, szPreferredFilename ); + char *dotTxt = V_stristr( szResolvedFilename, ".txt" ); + Assert ( dotTxt != NULL ); + if ( dotTxt ) V_strcpy( dotTxt, "_default.txt" ); + bFound = filesystem->ReadFile( szResolvedFilename, "GAME", buf ); + } + + if ( !bFound ) + { + Msg( "'%s' not found; not loaded\n", szPreferredFilename ); + return; + } + + if ( buf.TellPut() > 2048 ) + { + Warning("'%s' is too big; not loaded\n", szResolvedFilename ); + return; + } + buf.PutChar( '\0' ); + + if ( V_stricmp( szPreferredFilename, szResolvedFilename ) == 0) + { + Msg( "Set %s from file '%s'\n", pszStringName, szResolvedFilename ); + } + else + { + Msg( "Set %s from file '%s'. ('%s' was not found.)\n", pszStringName, szResolvedFilename, szPreferredFilename ); + } + + g_pStringTableInfoPanel->AddString( CBaseEntity::IsServer(), pszStringName, buf.TellPut(), buf.Base() ); +#endif +} + +// keeps track of which chapters the user has unlocked +ConVar sv_unlockedchapters( "sv_unlockedchapters", "1", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX ); + +//----------------------------------------------------------------------------- +// Purpose: Updates which chapters are unlocked +//----------------------------------------------------------------------------- +void UpdateChapterRestrictions( const char *mapname ) +{ + // look at the chapter for this map + char chapterTitle[64]; + chapterTitle[0] = 0; + for ( int i = 0; i < ARRAYSIZE(gTitleComments); i++ ) + { + if ( !Q_strnicmp( mapname, gTitleComments[i].pBSPName, strlen(gTitleComments[i].pBSPName) ) ) + { + // found + Q_strncpy( chapterTitle, gTitleComments[i].pTitleName, sizeof( chapterTitle ) ); + int j = 0; + while ( j < 64 && chapterTitle[j] ) + { + if ( chapterTitle[j] == '\n' || chapterTitle[j] == '\r' ) + chapterTitle[j] = 0; + else + j++; + } + + break; + } + } + +#ifdef MAPBASE + // Look in the mod's chapter list + CUtlVector *ModChapterComments = Mapbase_GetChapterMaps(); + if (ModChapterComments->Count() > 0) + { + for ( int i = 0; i < ModChapterComments->Count(); i++ ) + { + if ( !Q_strnicmp( mapname, ModChapterComments->Element(i).pBSPName, strlen(ModChapterComments->Element(i).pBSPName) ) ) + { + // found + Q_strncpy( chapterTitle, ModChapterComments->Element(i).pTitleName, sizeof( chapterTitle ) ); + int j = 0; + while ( j < 64 && chapterTitle[j] ) + { + if ( chapterTitle[j] == '\n' || chapterTitle[j] == '\r' ) + chapterTitle[j] = 0; + else + j++; + } + + // Mods can order their own custom chapter names, + // allowing for more flexible string name usage, multiple names in one chapter, etc. + CUtlVector *ModChapterList = Mapbase_GetChapterList(); + for ( int i = 0; i < ModChapterList->Count(); i++ ) + { + if ( !Q_strnicmp( chapterTitle, ModChapterList->Element(i).pChapterName, strlen(chapterTitle) ) ) + { + // ok we have the string, see if it's newer + int nNewChapter = ModChapterList->Element(i).iChapter; + int nUnlockedChapter = sv_unlockedchapters.GetInt(); + + if ( nUnlockedChapter < nNewChapter ) + { + // ok we're at a higher chapter, unlock + sv_unlockedchapters.SetValue( nNewChapter ); + + // HACK: Call up through a better function than this? 7/23/07 - jdw + if ( IsX360() ) + { + engine->ServerCommand( "host_writeconfig\n" ); + } + } + + g_nCurrentChapterIndex = nNewChapter; + return; + } + } + + break; + } + } + } + + // Check the world entity for a chapter title. + if ( CWorld *pWorld = GetWorldEntity() ) + { + const char *pWorldChapter = pWorld->GetChapterTitle(); + if ( pWorldChapter && pWorldChapter[0] != '\0' ) + Q_strncpy( chapterTitle, pWorldChapter, sizeof( chapterTitle ) ); + } +#endif + + if ( !chapterTitle[0] ) + return; + + // make sure the specified chapter title is unlocked + strlwr( chapterTitle ); + + // Get our active mod directory name + char modDir[MAX_PATH]; + if ( UTIL_GetModDir( modDir, sizeof(modDir) ) == false ) + return; + + char chapterNumberPrefix[64]; + Q_snprintf(chapterNumberPrefix, sizeof(chapterNumberPrefix), "#%s_chapter", modDir); + + const char *newChapterNumber = strstr( chapterTitle, chapterNumberPrefix ); + if ( newChapterNumber ) + { + // cut off the front + newChapterNumber += strlen( chapterNumberPrefix ); + char newChapter[32]; + Q_strncpy( newChapter, newChapterNumber, sizeof(newChapter) ); + + // cut off the end + char *end = strstr( newChapter, "_title" ); + if ( end ) + { + *end = 0; + } + + int nNewChapter = atoi( newChapter ); + + // HACK: HL2 added a zany chapter "9a" which wreaks + // havoc in this stupid atoi-based chapter code. + if ( !Q_stricmp( modDir, "hl2" ) ) + { + if ( !Q_stricmp( newChapter, "9a" ) ) + { + nNewChapter = 10; + } + else if ( nNewChapter > 9 ) + { + nNewChapter++; + } + } + + // ok we have the string, see if it's newer + const char *unlockedChapter = sv_unlockedchapters.GetString(); + int nUnlockedChapter = atoi( unlockedChapter ); + + if ( nUnlockedChapter < nNewChapter ) + { + // ok we're at a higher chapter, unlock + sv_unlockedchapters.SetValue( nNewChapter ); + + // HACK: Call up through a better function than this? 7/23/07 - jdw + if ( IsX360() ) + { + engine->ServerCommand( "host_writeconfig\n" ); + } + } + + g_nCurrentChapterIndex = nNewChapter; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Update xbox live data for the user's presence +//----------------------------------------------------------------------------- +void UpdateRichPresence ( void ) +{ + // This assumes we're playing a single player game + Assert ( gpGlobals->maxClients == 1 ); + + // Shouldn't get here unless we're playing a map and we've updated sv_unlockedchapters + Assert ( g_nCurrentChapterIndex >= 0 ); + + // Get our active mod directory name + char modDir[MAX_PATH]; + if ( UTIL_GetModDir( modDir, sizeof(modDir) ) == false ) + return; + + // Get presence data based on the game we're playing + uint iGameID, iChapterIndex, iChapterID, iGamePresenceID; + iGameID = iChapterIndex = iChapterID = iGamePresenceID = 0; + if ( Q_stristr( modDir, "hl2" ) ) + { + iGameID = CONTEXT_GAME_GAME_HALF_LIFE_2; + iChapterID = CONTEXT_CHAPTER_HL2; + iChapterIndex = g_nCurrentChapterIndex - 1; + iGamePresenceID = CONTEXT_PRESENCE_HL2_INGAME; + } + else if ( Q_stristr( modDir, "episodic" ) ) + { + iGameID = CONTEXT_GAME_GAME_EPISODE_ONE; + iChapterID = CONTEXT_CHAPTER_EP1; + iChapterIndex = g_nCurrentChapterIndex - 1; + iGamePresenceID = CONTEXT_PRESENCE_EP1_INGAME; + } + else if ( Q_stristr( modDir, "ep2" ) ) + { + iGameID = CONTEXT_GAME_GAME_EPISODE_TWO; + iChapterID = CONTEXT_CHAPTER_EP2; + iChapterIndex = g_nCurrentChapterIndex - 1; + iGamePresenceID = CONTEXT_PRESENCE_EP2_INGAME; + } + else if ( Q_stristr( modDir, "portal" ) ) + { + iGameID = CONTEXT_GAME_GAME_PORTAL; + iChapterID = CONTEXT_CHAPTER_PORTAL; + iChapterIndex = g_nCurrentChapterIndex - 1; + iGamePresenceID = CONTEXT_PRESENCE_PORTAL_INGAME; + } + else + { + Warning( "UpdateRichPresence failed in GameInterface. Didn't recognize -game parameter." ); + } + +#if defined( _X360 ) + + // Set chapter context based on mapname + if ( !xboxsystem->UserSetContext( XBX_GetPrimaryUserId(), iChapterID, iChapterIndex, true ) ) + { + Warning( "GameInterface: UserSetContext failed.\n" ); + } + + if ( commentary.GetBool() ) + { + // Set presence to show the user is playing developer commentary + if ( !xboxsystem->UserSetContext( XBX_GetPrimaryUserId(), X_CONTEXT_PRESENCE, CONTEXT_PRESENCE_COMMENTARY, true ) ) + { + Warning( "GameInterface: UserSetContext failed.\n" ); + } + } + else + { + // Set presence to show the user is in-game + if ( !xboxsystem->UserSetContext( XBX_GetPrimaryUserId(), X_CONTEXT_PRESENCE, iGamePresenceID, true ) ) + { + Warning( "GameInterface: UserSetContext failed.\n" ); + } + } + + // Set which game the user is playing + if ( !xboxsystem->UserSetContext( XBX_GetPrimaryUserId(), CONTEXT_GAME, iGameID, true ) ) + { + Warning( "GameInterface: UserSetContext failed.\n" ); + } + + if ( !xboxsystem->UserSetContext( XBX_GetPrimaryUserId(), X_CONTEXT_GAME_TYPE, X_CONTEXT_GAME_TYPE_STANDARD, true ) ) + { + Warning( "GameInterface: UserSetContext failed.\n" ); + } + + if ( !xboxsystem->UserSetContext( XBX_GetPrimaryUserId(), X_CONTEXT_GAME_MODE, CONTEXT_GAME_MODE_SINGLEPLAYER, true ) ) + { + Warning( "GameInterface: UserSetContext failed.\n" ); + } +#endif +} + +//----------------------------------------------------------------------------- +// Precaches a vgui screen overlay material +//----------------------------------------------------------------------------- +void PrecacheMaterial( const char *pMaterialName ) +{ + Assert( pMaterialName && pMaterialName[0] ); + g_pStringTableMaterials->AddString( CBaseEntity::IsServer(), pMaterialName ); +} + + +//----------------------------------------------------------------------------- +// Converts a previously precached material into an index +//----------------------------------------------------------------------------- +int GetMaterialIndex( const char *pMaterialName ) +{ + if (pMaterialName) + { + int nIndex = g_pStringTableMaterials->FindStringIndex( pMaterialName ); + + if (nIndex != INVALID_STRING_INDEX ) + { + return nIndex; + } + else + { + DevMsg("Warning! GetMaterialIndex: couldn't find material %s\n ", pMaterialName ); + return 0; + } + } + + // This is the invalid string index + return 0; +} + +//----------------------------------------------------------------------------- +// Converts a previously precached material index into a string +//----------------------------------------------------------------------------- +const char *GetMaterialNameFromIndex( int nMaterialIndex ) +{ + return g_pStringTableMaterials->GetString( nMaterialIndex ); +} + + +//----------------------------------------------------------------------------- +// Precaches a vgui screen overlay material +//----------------------------------------------------------------------------- +void PrecacheParticleSystem( const char *pParticleSystemName ) +{ + Assert( pParticleSystemName && pParticleSystemName[0] ); + g_pStringTableParticleEffectNames->AddString( CBaseEntity::IsServer(), pParticleSystemName ); +} + + +//----------------------------------------------------------------------------- +// Converts a previously precached material into an index +//----------------------------------------------------------------------------- +int GetParticleSystemIndex( const char *pParticleSystemName ) +{ + if ( pParticleSystemName ) + { + int nIndex = g_pStringTableParticleEffectNames->FindStringIndex( pParticleSystemName ); + if (nIndex != INVALID_STRING_INDEX ) + return nIndex; + + DevWarning("Server: Missing precache for particle system \"%s\"!\n", pParticleSystemName ); + } + + // This is the invalid string index + return 0; +} + +//----------------------------------------------------------------------------- +// Converts a previously precached material index into a string +//----------------------------------------------------------------------------- +const char *GetParticleSystemNameFromIndex( int nMaterialIndex ) +{ + if ( nMaterialIndex < g_pStringTableParticleEffectNames->GetMaxStrings() ) + return g_pStringTableParticleEffectNames->GetString( nMaterialIndex ); + return "error"; +} + +//----------------------------------------------------------------------------- +// Returns true if host_thread_mode is set to non-zero (and engine is running in threaded mode) +//----------------------------------------------------------------------------- +bool IsEngineThreaded() +{ + if ( g_pcv_ThreadMode ) + { + return g_pcv_ThreadMode->GetBool(); + } + return false; +} + +class CServerGameEnts : public IServerGameEnts +{ +public: + virtual void SetDebugEdictBase(edict_t *base); + virtual void MarkEntitiesAsTouching( edict_t *e1, edict_t *e2 ); + virtual void FreeContainingEntity( edict_t * ); + virtual edict_t* BaseEntityToEdict( CBaseEntity *pEnt ); + virtual CBaseEntity* EdictToBaseEntity( edict_t *pEdict ); + virtual void CheckTransmit( CCheckTransmitInfo *pInfo, const unsigned short *pEdictIndices, int nEdicts ); +}; +EXPOSE_SINGLE_INTERFACE(CServerGameEnts, IServerGameEnts, INTERFACEVERSION_SERVERGAMEENTS); + +void CServerGameEnts::SetDebugEdictBase(edict_t *base) +{ + g_pDebugEdictBase = base; +} + +//----------------------------------------------------------------------------- +// Purpose: Marks entities as touching +// Input : *e1 - +// *e2 - +//----------------------------------------------------------------------------- +void CServerGameEnts::MarkEntitiesAsTouching( edict_t *e1, edict_t *e2 ) +{ + CBaseEntity *entity = GetContainingEntity( e1 ); + CBaseEntity *entityTouched = GetContainingEntity( e2 ); + if ( entity && entityTouched ) + { + // HACKHACK: UNDONE: Pass in the trace here??!?!? + trace_t tr; + UTIL_ClearTrace( tr ); + tr.endpos = (entity->GetAbsOrigin() + entityTouched->GetAbsOrigin()) * 0.5; + entity->PhysicsMarkEntitiesAsTouching( entityTouched, tr ); + } +} + +void CServerGameEnts::FreeContainingEntity( edict_t *e ) +{ + ::FreeContainingEntity(e); +} + +edict_t* CServerGameEnts::BaseEntityToEdict( CBaseEntity *pEnt ) +{ + if ( pEnt ) + return pEnt->edict(); + else + return NULL; +} + +CBaseEntity* CServerGameEnts::EdictToBaseEntity( edict_t *pEdict ) +{ + if ( pEdict ) + return CBaseEntity::Instance( pEdict ); + else + return NULL; +} + + +/* Yuck.. ideally this would be in CServerNetworkProperty's header, but it requires CBaseEntity and +// inlining it gives a nice speedup. +inline void CServerNetworkProperty::CheckTransmit( CCheckTransmitInfo *pInfo ) +{ + // If we have a transmit proxy, let it hook our ShouldTransmit return value. + if ( m_pTransmitProxy ) + { + nShouldTransmit = m_pTransmitProxy->ShouldTransmit( pInfo, nShouldTransmit ); + } + + if ( m_pOuter->ShouldTransmit( pInfo ) ) + { + m_pOuter->SetTransmit( pInfo ); + } +} */ + +void CServerGameEnts::CheckTransmit( CCheckTransmitInfo *pInfo, const unsigned short *pEdictIndices, int nEdicts ) +{ + // NOTE: for speed's sake, this assumes that all networkables are CBaseEntities and that the edict list + // is consecutive in memory. If either of these things change, then this routine needs to change, but + // ideally we won't be calling any virtual from this routine. This speedy routine was added as an + // optimization which would be nice to keep. + edict_t *pBaseEdict = engine->PEntityOfEntIndex( 0 ); + + // get recipient player's skybox: + CBaseEntity *pRecipientEntity = CBaseEntity::Instance( pInfo->m_pClientEnt ); + + Assert( pRecipientEntity && pRecipientEntity->IsPlayer() ); + if ( !pRecipientEntity ) + return; + + MDLCACHE_CRITICAL_SECTION(); + CBasePlayer *pRecipientPlayer = static_cast( pRecipientEntity ); + const int skyBoxArea = pRecipientPlayer->m_Local.m_skybox3d.area; + +#ifndef _X360 + const bool bIsHLTV = pRecipientPlayer->IsHLTV(); + const bool bIsReplay = pRecipientPlayer->IsReplay(); + + // m_pTransmitAlways must be set if HLTV client + Assert( bIsHLTV == ( pInfo->m_pTransmitAlways != NULL) || + bIsReplay == ( pInfo->m_pTransmitAlways != NULL) ); +#endif + + for ( int i=0; i < nEdicts; i++ ) + { + int iEdict = pEdictIndices[i]; + + edict_t *pEdict = &pBaseEdict[iEdict]; + Assert( pEdict == engine->PEntityOfEntIndex( iEdict ) ); + int nFlags = pEdict->m_fStateFlags & (FL_EDICT_DONTSEND|FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK|FL_EDICT_FULLCHECK); + + // entity needs no transmit + if ( nFlags & FL_EDICT_DONTSEND ) + continue; + + // entity is already marked for sending + if ( pInfo->m_pTransmitEdict->Get( iEdict ) ) + continue; + + if ( nFlags & FL_EDICT_ALWAYS ) + { + // FIXME: Hey! Shouldn't this be using SetTransmit so as + // to also force network down dependent entities? + while ( true ) + { + // mark entity for sending + pInfo->m_pTransmitEdict->Set( iEdict ); + +#ifndef _X360 + if ( bIsHLTV || bIsReplay ) + { + pInfo->m_pTransmitAlways->Set( iEdict ); + } +#endif + CServerNetworkProperty *pEnt = static_cast( pEdict->GetNetworkable() ); + if ( !pEnt ) + break; + + CServerNetworkProperty *pParent = pEnt->GetNetworkParent(); + if ( !pParent ) + break; + + pEdict = pParent->edict(); + iEdict = pParent->entindex(); + } + continue; + } + + // FIXME: Would like to remove all dependencies + CBaseEntity *pEnt = ( CBaseEntity * )pEdict->GetUnknown(); + Assert( dynamic_cast< CBaseEntity* >( pEdict->GetUnknown() ) == pEnt ); + + if ( nFlags == FL_EDICT_FULLCHECK ) + { + // do a full ShouldTransmit() check, may return FL_EDICT_CHECKPVS + nFlags = pEnt->ShouldTransmit( pInfo ); + + Assert( !(nFlags & FL_EDICT_FULLCHECK) ); + + if ( nFlags & FL_EDICT_ALWAYS ) + { + pEnt->SetTransmit( pInfo, true ); + continue; + } + } + + // don't send this entity + if ( !( nFlags & FL_EDICT_PVSCHECK ) ) + continue; + + CServerNetworkProperty *netProp = static_cast( pEdict->GetNetworkable() ); + +#ifndef _X360 + if ( bIsHLTV || bIsReplay ) + { + // for the HLTV/Replay we don't cull against PVS + if ( netProp->AreaNum() == skyBoxArea ) + { + pEnt->SetTransmit( pInfo, true ); + } + else + { + pEnt->SetTransmit( pInfo, false ); + } + continue; + } +#endif + + // Always send entities in the player's 3d skybox. + // Sidenote: call of AreaNum() ensures that PVS data is up to date for this entity + bool bSameAreaAsSky = netProp->AreaNum() == skyBoxArea; + if ( bSameAreaAsSky ) + { + pEnt->SetTransmit( pInfo, true ); + continue; + } + + bool bInPVS = netProp->IsInPVS( pInfo ); + if ( bInPVS || sv_force_transmit_ents.GetBool() ) + { + // only send if entity is in PVS + pEnt->SetTransmit( pInfo, false ); + continue; + } + + // If the entity is marked "check PVS" but it's in hierarchy, walk up the hierarchy looking for the + // for any parent which is also in the PVS. If none are found, then we don't need to worry about sending ourself + CBaseEntity *orig = pEnt; + CServerNetworkProperty *check = netProp->GetNetworkParent(); + + // BUG BUG: I think it might be better to build up a list of edict indices which "depend" on other answers and then + // resolve them in a second pass. Not sure what happens if an entity has two parents who both request PVS check? + while ( check ) + { + int checkIndex = check->entindex(); + + // Parent already being sent + if ( pInfo->m_pTransmitEdict->Get( checkIndex ) ) + { + orig->SetTransmit( pInfo, true ); + break; + } + + edict_t *checkEdict = check->edict(); + int checkFlags = checkEdict->m_fStateFlags & (FL_EDICT_DONTSEND|FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK|FL_EDICT_FULLCHECK); + if ( checkFlags & FL_EDICT_DONTSEND ) + break; + + if ( checkFlags & FL_EDICT_ALWAYS ) + { + orig->SetTransmit( pInfo, true ); + break; + } + + if ( checkFlags == FL_EDICT_FULLCHECK ) + { + // do a full ShouldTransmit() check, may return FL_EDICT_CHECKPVS + CBaseEntity *pCheckEntity = check->GetBaseEntity(); + nFlags = pCheckEntity->ShouldTransmit( pInfo ); + Assert( !(nFlags & FL_EDICT_FULLCHECK) ); + if ( nFlags & FL_EDICT_ALWAYS ) + { + pCheckEntity->SetTransmit( pInfo, true ); + orig->SetTransmit( pInfo, true ); + } + break; + } + + if ( checkFlags & FL_EDICT_PVSCHECK ) + { + // Check pvs + check->RecomputePVSInformation(); + bool bMoveParentInPVS = check->IsInPVS( pInfo ); + if ( bMoveParentInPVS ) + { + orig->SetTransmit( pInfo, true ); + break; + } + } + + // Continue up chain just in case the parent itself has a parent that's in the PVS... + check = check->GetNetworkParent(); + } + } + +// Msg("A:%i, N:%i, F: %i, P: %i\n", always, dontSend, fullCheck, PVS ); +} + + +CServerGameClients g_ServerGameClients; +// INTERFACEVERSION_SERVERGAMECLIENTS_VERSION_3 is compatible with the latest since we're only adding things to the end, so expose that as well. +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerGameClients, IServerGameClients003, INTERFACEVERSION_SERVERGAMECLIENTS_VERSION_3, g_ServerGameClients ); +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerGameClients, IServerGameClients, INTERFACEVERSION_SERVERGAMECLIENTS, g_ServerGameClients ); + + +//----------------------------------------------------------------------------- +// Purpose: called when a player tries to connect to the server +// Input : *pEdict - the new player +// char *pszName - the players name +// char *pszAddress - the IP address of the player +// reject - output - fill in with the reason why +// maxrejectlen -- sizeof output buffer +// the player was not allowed to connect. +// Output : Returns TRUE if player is allowed to join, FALSE if connection is denied. +//----------------------------------------------------------------------------- +bool CServerGameClients::ClientConnect( edict_t *pEdict, const char *pszName, const char *pszAddress, char *reject, int maxrejectlen ) +{ + if ( !g_pGameRules ) + return false; + + return g_pGameRules->ClientConnected( pEdict, pszName, pszAddress, reject, maxrejectlen ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called when a player is fully active (i.e. ready to receive messages) +// Input : *pEntity - the player +//----------------------------------------------------------------------------- +void CServerGameClients::ClientActive( edict_t *pEdict, bool bLoadGame ) +{ + MDLCACHE_CRITICAL_SECTION(); + + ::ClientActive( pEdict, bLoadGame ); + + // If we just loaded from a save file, call OnRestore on valid entities + EndRestoreEntities(); + + if ( gpGlobals->eLoadType != MapLoad_LoadGame ) + { + // notify all entities that the player is now in the game + for ( CBaseEntity *pEntity = gEntList.FirstEnt(); pEntity != NULL; pEntity = gEntList.NextEnt(pEntity) ) + { + pEntity->PostClientActive(); + } + } + + // Tell the sound controller to check looping sounds + CBasePlayer *pPlayer = ( CBasePlayer * )CBaseEntity::Instance( pEdict ); + CSoundEnvelopeController::GetController().CheckLoopingSoundsForPlayer( pPlayer ); + SceneManager_ClientActive( pPlayer ); + + #if defined( TF_DLL ) + Assert( pPlayer ); + if ( pPlayer && !pPlayer->IsFakeClient() ) + { + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) ) + { + GTFGCClientSystem()->ClientActive( steamID ); + } + else + { + Log("WARNING: ClientActive, but we don't know his SteamID?\n"); + } + } + #endif +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - the player +//----------------------------------------------------------------------------- +void CServerGameClients::ClientSpawned( edict_t *pPlayer ) +{ + if ( g_pGameRules ) + { + g_pGameRules->ClientSpawned( pPlayer ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: called when a player disconnects from a server +// Input : *pEdict - the player +//----------------------------------------------------------------------------- +void CServerGameClients::ClientDisconnect( edict_t *pEdict ) +{ + extern bool g_fGameOver; + + CBasePlayer *player = ( CBasePlayer * )CBaseEntity::Instance( pEdict ); + if ( player ) + { + if ( !g_fGameOver ) + { + player->SetMaxSpeed( 0.0f ); + + CSound *pSound; + pSound = CSoundEnt::SoundPointerForIndex( CSoundEnt::ClientSoundIndex( pEdict ) ); + { + // since this client isn't around to think anymore, reset their sound. + if ( pSound ) + { + pSound->Reset(); + } + } + + // since the edict doesn't get deleted, fix it so it doesn't interfere. + player->RemoveFlag( FL_AIMTARGET ); // don't attract autoaim + player->AddFlag( FL_DONTTOUCH ); // stop it touching anything + player->AddFlag( FL_NOTARGET ); // stop NPCs noticing it + player->AddSolidFlags( FSOLID_NOT_SOLID ); // nonsolid + + if ( g_pGameRules ) + { + g_pGameRules->ClientDisconnected( pEdict ); + gamestats->Event_PlayerDisconnected( player ); + } + } + + // Make sure all Untouch()'s are called for this client leaving + CBaseEntity::PhysicsRemoveTouchedList( player ); + CBaseEntity::PhysicsRemoveGroundList( player ); + +#if !defined( NO_ENTITY_PREDICTION ) + // Make sure anything we "own" is simulated by the server from now on + player->ClearPlayerSimulationList(); +#endif + #if defined( TF_DLL ) + if ( !player->IsFakeClient() ) + { + CSteamID steamID; + if ( player->GetSteamID( &steamID ) ) + { + GTFGCClientSystem()->ClientDisconnected( steamID ); + } + else + { + Log("WARNING: ClientDisconnected, but we don't know his SteamID?\n"); + } + } + #endif + } +} + +void CServerGameClients::ClientPutInServer( edict_t *pEntity, const char *playername ) +{ + if ( g_pClientPutInServerOverride ) + g_pClientPutInServerOverride( pEntity, playername ); + else + ::ClientPutInServer( pEntity, playername ); +} + +void CServerGameClients::ClientCommand( edict_t *pEntity, const CCommand &args ) +{ + CBasePlayer *pPlayer = ToBasePlayer( GetContainingEntity( pEntity ) ); + ::ClientCommand( pPlayer, args ); +} + +//----------------------------------------------------------------------------- +// Purpose: called after the player changes userinfo - gives dll a chance to modify +// it before it gets sent into the rest of the engine-> +// Input : *pEdict - the player +// *infobuffer - their infobuffer +//----------------------------------------------------------------------------- +void CServerGameClients::ClientSettingsChanged( edict_t *pEdict ) +{ + // Is the client spawned yet? + if ( !pEdict->GetUnknown() ) + return; + + CBasePlayer *player = ( CBasePlayer * )CBaseEntity::Instance( pEdict ); + + if ( !player ) + return; + + bool bAllowNetworkingClientSettingsChange = g_pGameRules->IsConnectedUserInfoChangeAllowed( player ); + if ( bAllowNetworkingClientSettingsChange ) + { + +#define QUICKGETCVARVALUE(v) (engine->GetClientConVarValue( player->entindex(), v )) + + // get network setting for prediction & lag compensation + + // Unfortunately, we have to duplicate the code in cdll_bounded_cvars.cpp here because the client + // doesn't send the virtualized value up (because it has no way to know when the virtualized value + // changes). Possible todo: put the responsibility on the bounded cvar to notify the engine when + // its virtualized value has changed. + + player->m_nUpdateRate = Q_atoi( QUICKGETCVARVALUE("cl_updaterate") ); + static const ConVar *pMinUpdateRate = g_pCVar->FindVar( "sv_minupdaterate" ); + static const ConVar *pMaxUpdateRate = g_pCVar->FindVar( "sv_maxupdaterate" ); + if ( pMinUpdateRate && pMaxUpdateRate ) + player->m_nUpdateRate = clamp( player->m_nUpdateRate, (int) pMinUpdateRate->GetFloat(), (int) pMaxUpdateRate->GetFloat() ); + + bool useInterpolation = Q_atoi( QUICKGETCVARVALUE("cl_interpolate") ) != 0; + if ( useInterpolation ) + { + float flLerpRatio = Q_atof( QUICKGETCVARVALUE("cl_interp_ratio") ); + if ( flLerpRatio == 0 ) + flLerpRatio = 1.0f; + float flLerpAmount = Q_atof( QUICKGETCVARVALUE("cl_interp") ); + + static const ConVar *pMin = g_pCVar->FindVar( "sv_client_min_interp_ratio" ); + static const ConVar *pMax = g_pCVar->FindVar( "sv_client_max_interp_ratio" ); + if ( pMin && pMax && pMin->GetFloat() != -1 ) + { + flLerpRatio = clamp( flLerpRatio, pMin->GetFloat(), pMax->GetFloat() ); + } + else + { + if ( flLerpRatio == 0 ) + flLerpRatio = 1.0f; + } + // #define FIXME_INTERP_RATIO + player->m_fLerpTime = MAX( flLerpAmount, flLerpRatio / player->m_nUpdateRate ); + } + else + { + player->m_fLerpTime = 0.0f; + } + +#if !defined( NO_ENTITY_PREDICTION ) + bool usePrediction = Q_atoi( QUICKGETCVARVALUE("cl_predict")) != 0; + + if ( usePrediction ) + { + player->m_bPredictWeapons = Q_atoi( QUICKGETCVARVALUE("cl_predictweapons")) != 0; + player->m_bLagCompensation = Q_atoi( QUICKGETCVARVALUE("cl_lagcompensation")) != 0; + } + else +#endif + { + player->m_bPredictWeapons = false; + player->m_bLagCompensation = false; + } + + +#undef QUICKGETCVARVALUE + } + + g_pGameRules->ClientSettingsChanged( player ); +} + + +#ifdef PORTAL +//----------------------------------------------------------------------------- +// Purpose: Runs CFuncAreaPortalBase::UpdateVisibility on each portal +// Input : pAreaPortal - The Area portal to test for visibility from portals +// Output : int - 1 if any portal needs this area portal open, 0 otherwise. +//----------------------------------------------------------------------------- +int TestAreaPortalVisibilityThroughPortals ( CFuncAreaPortalBase* pAreaPortal, edict_t *pViewEntity, unsigned char *pvs, int pvssize ) +{ + int iPortalCount = CProp_Portal_Shared::AllPortals.Count(); + if( iPortalCount == 0 ) + return 0; + + CProp_Portal **pPortals = CProp_Portal_Shared::AllPortals.Base(); + + for ( int i = 0; i != iPortalCount; ++i ) + { + CProp_Portal* pLocalPortal = pPortals[ i ]; + if ( pLocalPortal && pLocalPortal->m_bActivated ) + { + CProp_Portal* pRemotePortal = pLocalPortal->m_hLinkedPortal.Get(); + + // Make sure this portal's linked portal is in the PVS before we add what it can see + if ( pRemotePortal && pRemotePortal->m_bActivated && pRemotePortal->NetworkProp() && + pRemotePortal->NetworkProp()->IsInPVS( pViewEntity, pvs, pvssize ) ) + { + bool bIsOpenOnClient = true; + float fovDistanceAdjustFactor = 1.0f; + Vector portalOrg = pLocalPortal->GetAbsOrigin(); + int iPortalNeedsThisPortalOpen = pAreaPortal->UpdateVisibility( portalOrg, fovDistanceAdjustFactor, bIsOpenOnClient ); + + // Stop checking on success, this portal needs to be open + if ( iPortalNeedsThisPortalOpen ) + { + return iPortalNeedsThisPortalOpen; + } + } + } + } + + return 0; +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: A client can have a separate "view entity" indicating that his/her view should depend on the origin of that +// view entity. If that's the case, then pViewEntity will be non-NULL and will be used. Otherwise, the current +// entity's origin is used. Either is offset by the m_vecViewOffset to get the eye position. +// From the eye position, we set up the PAS and PVS to use for filtering network messages to the client. At this point, we could +// override the actual PAS or PVS values, or use a different origin. +// NOTE: Do not cache the values of pas and pvs, as they depend on reusable memory in the engine, they are only good for this one frame +// Input : *pViewEntity - +// *pClient - +// **pvs - +// **pas - +//----------------------------------------------------------------------------- +void CServerGameClients::ClientSetupVisibility( edict_t *pViewEntity, edict_t *pClient, unsigned char *pvs, int pvssize ) +{ + Vector org; + + // Reset the PVS!!! + engine->ResetPVS( pvs, pvssize ); + + g_pToolFrameworkServer->PreSetupVisibility(); + + // Find the client's PVS + CBaseEntity *pVE = NULL; + if ( pViewEntity ) + { + pVE = GetContainingEntity( pViewEntity ); + // If we have a viewentity, it overrides the player's origin + if ( pVE ) + { + org = pVE->EyePosition(); + engine->AddOriginToPVS( org ); + } + } + + float fovDistanceAdjustFactor = 1; + + CBasePlayer *pPlayer = ( CBasePlayer * )GetContainingEntity( pClient ); + if ( pPlayer ) + { + org = pPlayer->EyePosition(); + pPlayer->SetupVisibility( pVE, pvs, pvssize ); + UTIL_SetClientVisibilityPVS( pClient, pvs, pvssize ); + fovDistanceAdjustFactor = pPlayer->GetFOVDistanceAdjustFactorForNetworking(); + } + + unsigned char portalBits[MAX_AREA_PORTAL_STATE_BYTES]; + memset( portalBits, 0, sizeof( portalBits ) ); + + int portalNums[512]; + int isOpen[512]; + int iOutPortal = 0; + + for( unsigned short i = g_AreaPortals.Head(); i != g_AreaPortals.InvalidIndex(); i = g_AreaPortals.Next(i) ) + { + CFuncAreaPortalBase *pCur = g_AreaPortals[i]; + + bool bIsOpenOnClient = true; + + // Update our array of which portals are open and flush it if necessary. + portalNums[iOutPortal] = pCur->m_portalNumber; + isOpen[iOutPortal] = pCur->UpdateVisibility( org, fovDistanceAdjustFactor, bIsOpenOnClient ); + +#ifdef PORTAL + // If the client doesn't need this open, test if portals might need this area portal open + if ( isOpen[iOutPortal] == 0 ) + { + isOpen[iOutPortal] = TestAreaPortalVisibilityThroughPortals( pCur, pViewEntity, pvs, pvssize ); + } +#endif + + ++iOutPortal; + if ( iOutPortal >= ARRAYSIZE( portalNums ) ) + { + engine->SetAreaPortalStates( portalNums, isOpen, iOutPortal ); + iOutPortal = 0; + } + + // Version 0 portals (ie: shipping Half-Life 2 era) are always treated as open + // for purposes of the m_chAreaPortalBits array on the client. + if ( pCur->m_iPortalVersion == 0 ) + bIsOpenOnClient = true; + + if ( bIsOpenOnClient ) + { + if ( pCur->m_portalNumber < 0 ) + continue; + else if ( pCur->m_portalNumber >= sizeof( portalBits ) * 8 ) + Error( "ClientSetupVisibility: portal number (%d) too large", pCur->m_portalNumber ); + else + portalBits[pCur->m_portalNumber >> 3] |= (1 << (pCur->m_portalNumber & 7)); + } + } + + // Flush the remaining areaportal states. + engine->SetAreaPortalStates( portalNums, isOpen, iOutPortal ); + + if ( pPlayer ) + { + // Update the area bits that get sent to the client. + pPlayer->m_Local.UpdateAreaBits( pPlayer, portalBits ); + +#ifdef PORTAL + // *After* the player's view has updated its area bits, add on any other areas seen by portals + CPortal_Player* pPortalPlayer = dynamic_cast( pPlayer ); + if ( pPortalPlayer ) + { + pPortalPlayer->UpdatePortalViewAreaBits( pvs, pvssize ); + } +#endif //PORTAL + } +} + + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +// *buf - +// numcmds - +// totalcmds - +// dropped_packets - +// ignore - +// paused - +// Output : float +//----------------------------------------------------------------------------- +#define CMD_MAXBACKUP 64 + +float CServerGameClients::ProcessUsercmds( edict_t *player, bf_read *buf, int numcmds, int totalcmds, + int dropped_packets, bool ignore, bool paused ) +{ + int i; + CUserCmd *from, *to; + + // We track last three command in case we drop some + // packets but get them back. + CUserCmd cmds[ CMD_MAXBACKUP ]; + + CUserCmd cmdNull; // For delta compression + + Assert( numcmds >= 0 ); + Assert( ( totalcmds - numcmds ) >= 0 ); + + CBasePlayer *pPlayer = NULL; + CBaseEntity *pEnt = CBaseEntity::Instance(player); + if ( pEnt && pEnt->IsPlayer() ) + { + pPlayer = static_cast< CBasePlayer * >( pEnt ); + } + // Too many commands? + if ( totalcmds < 0 || totalcmds >= ( CMD_MAXBACKUP - 1 ) ) + { + const char *name = "unknown"; + if ( pPlayer ) + { + name = pPlayer->GetPlayerName(); + } + + Msg("CBasePlayer::ProcessUsercmds: too many cmds %i sent for player %s\n", totalcmds, name ); + // FIXME: Need a way to drop the client from here + //SV_DropClient ( host_client, false, "CMD_MAXBACKUP hit" ); + buf->SetOverflowFlag(); + return 0.0f; + } + + // Initialize for reading delta compressed usercmds + cmdNull.Reset(); + from = &cmdNull; + for ( i = totalcmds - 1; i >= 0; i-- ) + { + to = &cmds[ i ]; +#if defined( MAPBASE_VSCRIPT ) + ReadUsercmd( buf, to, from, pPlayer ); // Tell whose UserCmd it is +#else + ReadUsercmd( buf, to, from ); +#endif + from = to; + } + + // Client not fully connected or server has gone inactive or is paused, just ignore + if ( ignore || !pPlayer ) + { + return 0.0f; + } + + MDLCACHE_CRITICAL_SECTION(); + pPlayer->ProcessUsercmds( cmds, numcmds, totalcmds, dropped_packets, paused ); + + return TICK_INTERVAL; +} + + +void CServerGameClients::PostClientMessagesSent_DEPRECIATED( void ) +{ +} + +// Sets the client index for the client who typed the command into his/her console +void CServerGameClients::SetCommandClient( int index ) +{ + g_nCommandClientIndex = index; +} + +int CServerGameClients::GetReplayDelay( edict_t *pEdict, int &entity ) +{ + CBasePlayer *pPlayer = ( CBasePlayer * )CBaseEntity::Instance( pEdict ); + + if ( !pPlayer ) + return 0; + + entity = pPlayer->GetReplayEntity(); + + return pPlayer->GetDelayTicks(); +} + + +//----------------------------------------------------------------------------- +// The client's userinfo data lump has changed +//----------------------------------------------------------------------------- +void CServerGameClients::ClientEarPosition( edict_t *pEdict, Vector *pEarOrigin ) +{ + CBasePlayer *pPlayer = ( CBasePlayer * )CBaseEntity::Instance( pEdict ); + if (pPlayer) + { + *pEarOrigin = pPlayer->EarPosition(); + } + else + { + // Shouldn't happen + Assert(0); + *pEarOrigin = vec3_origin; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +// Output : CPlayerState +//----------------------------------------------------------------------------- +CPlayerState *CServerGameClients::GetPlayerState( edict_t *player ) +{ + // Is the client spawned yet? + if ( !player || !player->GetUnknown() ) + return NULL; + + CBasePlayer *pBasePlayer = ( CBasePlayer * )CBaseEntity::Instance( player ); + if ( !pBasePlayer ) + return NULL; + + return &pBasePlayer->pl; +} + +//----------------------------------------------------------------------------- +// Purpose: Anything this game .dll wants to add to the bug reporter text (e.g., the entity/model under the picker crosshair) +// can be added here +// Input : *buf - +// buflen - +//----------------------------------------------------------------------------- +void CServerGameClients::GetBugReportInfo( char *buf, int buflen ) +{ + recentNPCSpeech_t speech[ SPEECH_LIST_MAX_SOUNDS ]; + int num; + int i; + + buf[ 0 ] = 0; + + if ( gpGlobals->maxClients == 1 ) + { + CBaseEntity *ent = FindPickerEntity( UTIL_PlayerByIndex(1) ); + if ( ent ) + { + Q_snprintf( buf, buflen, "Picker %i/%s - ent %s model %s\n", + ent->entindex(), + ent->GetClassname(), + STRING( ent->GetEntityName() ), + STRING( ent->GetModelName() ) ); + } + + // get any sounds that were spoken by NPCs recently + num = GetRecentNPCSpeech( speech ); + if ( num > 0 ) + { + Q_snprintf( buf, buflen, "%sRecent NPC speech:\n", buf ); + for( i = 0; i < num; i++ ) + { + Q_snprintf( buf, buflen, "%s time: %6.3f sound name: %s scene: %s\n", buf, speech[ i ].time, speech[ i ].name, speech[ i ].sceneName ); + } + Q_snprintf( buf, buflen, "%sCurrent time: %6.3f\n", buf, gpGlobals->curtime ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: A user has had their network id setup and validated +//----------------------------------------------------------------------------- +void CServerGameClients::NetworkIDValidated( const char *pszUserName, const char *pszNetworkID ) +{ +} + +// The client has submitted a keyvalues command +void CServerGameClients::ClientCommandKeyValues( edict_t *pEntity, KeyValues *pKeyValues ) +{ + if ( !pKeyValues ) + return; + + if ( g_pGameRules ) + { + g_pGameRules->ClientCommandKeyValues( pEntity, pKeyValues ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static bf_write *g_pMsgBuffer = NULL; + +void EntityMessageBegin( CBaseEntity * entity, bool reliable /*= false*/ ) +{ + Assert( !g_pMsgBuffer ); + + Assert ( entity ); + + g_pMsgBuffer = engine->EntityMessageBegin( entity->entindex(), entity->GetServerClass(), reliable ); +} + +void UserMessageBegin( IRecipientFilter& filter, const char *messagename ) +{ + Assert( !g_pMsgBuffer ); + + Assert( messagename ); + + int msg_type = usermessages->LookupUserMessage( messagename ); + + if ( msg_type == -1 ) + { + Error( "UserMessageBegin: Unregistered message '%s'\n", messagename ); + } + + g_pMsgBuffer = engine->UserMessageBegin( &filter, msg_type ); +} + +void MessageEnd( void ) +{ + Assert( g_pMsgBuffer ); + + engine->MessageEnd(); + + g_pMsgBuffer = NULL; +} + +void MessageWriteByte( int iValue) +{ + if (!g_pMsgBuffer) + Error( "WRITE_BYTE called with no active message\n" ); + + g_pMsgBuffer->WriteByte( iValue ); +} + +void MessageWriteChar( int iValue) +{ + if (!g_pMsgBuffer) + Error( "WRITE_CHAR called with no active message\n" ); + + g_pMsgBuffer->WriteChar( iValue ); +} + +void MessageWriteShort( int iValue) +{ + if (!g_pMsgBuffer) + Error( "WRITE_SHORT called with no active message\n" ); + + g_pMsgBuffer->WriteShort( iValue ); +} + +void MessageWriteWord( int iValue ) +{ + if (!g_pMsgBuffer) + Error( "WRITE_WORD called with no active message\n" ); + + g_pMsgBuffer->WriteWord( iValue ); +} + +void MessageWriteLong( int iValue) +{ + if (!g_pMsgBuffer) + Error( "WriteLong called with no active message\n" ); + + g_pMsgBuffer->WriteLong( iValue ); +} + +void MessageWriteFloat( float flValue) +{ + if (!g_pMsgBuffer) + Error( "WriteFloat called with no active message\n" ); + + g_pMsgBuffer->WriteFloat( flValue ); +} + +void MessageWriteAngle( float flValue) +{ + if (!g_pMsgBuffer) + Error( "WriteAngle called with no active message\n" ); + + g_pMsgBuffer->WriteBitAngle( flValue, 8 ); +} + +void MessageWriteCoord( float flValue) +{ + if (!g_pMsgBuffer) + Error( "WriteCoord called with no active message\n" ); + + g_pMsgBuffer->WriteBitCoord( flValue ); +} + +void MessageWriteVec3Coord( const Vector& rgflValue) +{ + if (!g_pMsgBuffer) + Error( "WriteVec3Coord called with no active message\n" ); + + g_pMsgBuffer->WriteBitVec3Coord( rgflValue ); +} + +void MessageWriteVec3Normal( const Vector& rgflValue) +{ + if (!g_pMsgBuffer) + Error( "WriteVec3Normal called with no active message\n" ); + + g_pMsgBuffer->WriteBitVec3Normal( rgflValue ); +} + +void MessageWriteAngles( const QAngle& rgflValue) +{ + if (!g_pMsgBuffer) + Error( "WriteVec3Normal called with no active message\n" ); + + g_pMsgBuffer->WriteBitAngles( rgflValue ); +} + +void MessageWriteString( const char *sz ) +{ + if (!g_pMsgBuffer) + Error( "WriteString called with no active message\n" ); + + g_pMsgBuffer->WriteString( sz ); +} + +void MessageWriteEntity( int iValue) +{ + if (!g_pMsgBuffer) + Error( "WriteEntity called with no active message\n" ); + + g_pMsgBuffer->WriteShort( iValue ); +} + +void MessageWriteEHandle( CBaseEntity *pEntity ) +{ + if (!g_pMsgBuffer) + Error( "WriteEHandle called with no active message\n" ); + + long iEncodedEHandle; + + if( pEntity ) + { + EHANDLE hEnt = pEntity; + + int iSerialNum = hEnt.GetSerialNumber() & (1 << NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS) - 1; + iEncodedEHandle = hEnt.GetEntryIndex() | (iSerialNum << MAX_EDICT_BITS); + } + else + { + iEncodedEHandle = INVALID_NETWORKED_EHANDLE_VALUE; + } + + g_pMsgBuffer->WriteLong( iEncodedEHandle ); +} + +// bitwise +void MessageWriteBool( bool bValue ) +{ + if (!g_pMsgBuffer) + Error( "WriteBool called with no active message\n" ); + + g_pMsgBuffer->WriteOneBit( bValue ? 1 : 0 ); +} + +void MessageWriteUBitLong( unsigned int data, int numbits ) +{ + if (!g_pMsgBuffer) + Error( "WriteUBitLong called with no active message\n" ); + + g_pMsgBuffer->WriteUBitLong( data, numbits ); +} + +void MessageWriteSBitLong( int data, int numbits ) +{ + if (!g_pMsgBuffer) + Error( "WriteSBitLong called with no active message\n" ); + + g_pMsgBuffer->WriteSBitLong( data, numbits ); +} + +void MessageWriteBits( const void *pIn, int nBits ) +{ + if (!g_pMsgBuffer) + Error( "WriteBits called with no active message\n" ); + + g_pMsgBuffer->WriteBits( pIn, nBits ); +} + +class CServerDLLSharedAppSystems : public IServerDLLSharedAppSystems +{ +public: + CServerDLLSharedAppSystems() + { + AddAppSystem( "soundemittersystem" DLL_EXT_STRING, SOUNDEMITTERSYSTEM_INTERFACE_VERSION ); + AddAppSystem( "scenefilecache" DLL_EXT_STRING, SCENE_FILE_CACHE_INTERFACE_VERSION ); + } + + virtual int Count() + { + return m_Systems.Count(); + } + virtual char const *GetDllName( int idx ) + { + return m_Systems[ idx ].m_pModuleName; + } + virtual char const *GetInterfaceName( int idx ) + { + return m_Systems[ idx ].m_pInterfaceName; + } +private: + void AddAppSystem( char const *moduleName, char const *interfaceName ) + { + AppSystemInfo_t sys; + sys.m_pModuleName = moduleName; + sys.m_pInterfaceName = interfaceName; + m_Systems.AddToTail( sys ); + } + + CUtlVector< AppSystemInfo_t > m_Systems; +}; + +EXPOSE_SINGLE_INTERFACE( CServerDLLSharedAppSystems, IServerDLLSharedAppSystems, SERVER_DLL_SHARED_APPSYSTEMS ); + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CServerGameTags::GetTaggedConVarList( KeyValues *pCvarTagList ) +{ + if ( pCvarTagList && g_pGameRules ) + { + g_pGameRules->GetTaggedConVarList( pCvarTagList ); + } +} + + + +#ifndef NO_STEAM + +CSteamID GetSteamIDForPlayerIndex( int iPlayerIndex ) +{ + const CSteamID *pResult = engine->GetClientSteamIDByPlayerIndex( iPlayerIndex ); + if ( pResult ) + return *pResult; + + // Return a bogus steam ID + return CSteamID(); +} + +#endif diff --git a/sp/src/game/server/gameinterface.h b/sp/src/game/server/gameinterface.h new file mode 100644 index 00000000..67805ff6 --- /dev/null +++ b/sp/src/game/server/gameinterface.h @@ -0,0 +1,230 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Expose things from GameInterface.cpp. Mostly the engine interfaces. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef GAMEINTERFACE_H +#define GAMEINTERFACE_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "mapentities.h" + +class IReplayFactory; + +extern INetworkStringTable *g_pStringTableInfoPanel; +extern INetworkStringTable *g_pStringTableServerMapCycle; + +#ifdef TF_DLL +extern INetworkStringTable *g_pStringTableServerPopFiles; +#endif + +// Player / Client related functions +// Most of this is implemented in gameinterface.cpp, but some of it is per-mod in files like cs_gameinterface.cpp, etc. +class CServerGameClients : public IServerGameClients +{ +public: + virtual bool ClientConnect( edict_t *pEntity, char const* pszName, char const* pszAddress, char *reject, int maxrejectlen ); + virtual void ClientActive( edict_t *pEntity, bool bLoadGame ); + virtual void ClientDisconnect( edict_t *pEntity ); + virtual void ClientPutInServer( edict_t *pEntity, const char *playername ); + virtual void ClientCommand( edict_t *pEntity, const CCommand &args ); + virtual void ClientSettingsChanged( edict_t *pEntity ); + virtual void ClientSetupVisibility( edict_t *pViewEntity, edict_t *pClient, unsigned char *pvs, int pvssize ); + virtual float ProcessUsercmds( edict_t *player, bf_read *buf, int numcmds, int totalcmds, + int dropped_packets, bool ignore, bool paused ); + // Player is running a command + virtual void PostClientMessagesSent_DEPRECIATED( void ); + virtual void SetCommandClient( int index ); + virtual CPlayerState *GetPlayerState( edict_t *player ); + virtual void ClientEarPosition( edict_t *pEntity, Vector *pEarOrigin ); + + virtual void GetPlayerLimits( int& minplayers, int& maxplayers, int &defaultMaxPlayers ) const; + + // returns number of delay ticks if player is in Replay mode (0 = no delay) + virtual int GetReplayDelay( edict_t *player, int& entity ); + // Anything this game .dll wants to add to the bug reporter text (e.g., the entity/model under the picker crosshair) + // can be added here + virtual void GetBugReportInfo( char *buf, int buflen ); + virtual void NetworkIDValidated( const char *pszUserName, const char *pszNetworkID ); + + // The client has submitted a keyvalues command + virtual void ClientCommandKeyValues( edict_t *pEntity, KeyValues *pKeyValues ); + + // Notify that the player is spawned + virtual void ClientSpawned( edict_t *pPlayer ); +}; + + +class CServerGameDLL : public IServerGameDLL +{ +public: + virtual bool DLLInit(CreateInterfaceFn engineFactory, CreateInterfaceFn physicsFactory, + CreateInterfaceFn fileSystemFactory, CGlobalVars *pGlobals); + virtual void DLLShutdown( void ); + // Get the simulation interval (must be compiled with identical values into both client and game .dll for MOD!!!) + virtual bool ReplayInit( CreateInterfaceFn fnReplayFactory ); + virtual float GetTickInterval( void ) const; + virtual bool GameInit( void ); + virtual void GameShutdown( void ); + virtual bool LevelInit( const char *pMapName, char const *pMapEntities, char const *pOldLevel, char const *pLandmarkName, bool loadGame, bool background ); + virtual void ServerActivate( edict_t *pEdictList, int edictCount, int clientMax ); + virtual void LevelShutdown( void ); + virtual void GameFrame( bool simulating ); // could be called multiple times before sending data to clients + virtual void PreClientUpdate( bool simulating ); // called after all GameFrame() calls, before sending data to clients + + virtual ServerClass* GetAllServerClasses( void ); + virtual const char *GetGameDescription( void ); + virtual void CreateNetworkStringTables( void ); + + // Save/restore system hooks + virtual CSaveRestoreData *SaveInit( int size ); + virtual void SaveWriteFields( CSaveRestoreData *, char const* , void *, datamap_t *, typedescription_t *, int ); + virtual void SaveReadFields( CSaveRestoreData *, char const* , void *, datamap_t *, typedescription_t *, int ); + virtual void SaveGlobalState( CSaveRestoreData * ); + virtual void RestoreGlobalState( CSaveRestoreData * ); + virtual int CreateEntityTransitionList( CSaveRestoreData *, int ); + virtual void BuildAdjacentMapList( void ); + + virtual void PreSave( CSaveRestoreData * ); + virtual void Save( CSaveRestoreData * ); + virtual void GetSaveComment( char *comment, int maxlength, float flMinutes, float flSeconds, bool bNoTime = false ); +#ifdef _XBOX + virtual void GetTitleName( const char *pMapName, char* pTitleBuff, int titleBuffSize ); +#endif + virtual void WriteSaveHeaders( CSaveRestoreData * ); + + virtual void ReadRestoreHeaders( CSaveRestoreData * ); + virtual void Restore( CSaveRestoreData *, bool ); + virtual bool IsRestoring(); + + // Retrieve info needed for parsing the specified user message + virtual bool GetUserMessageInfo( int msg_type, char *name, int maxnamelength, int& size ); + + virtual CStandardSendProxies* GetStandardSendProxies(); + + virtual void PostInit(); + virtual void Think( bool finalTick ); + + virtual void OnQueryCvarValueFinished( QueryCvarCookie_t iCookie, edict_t *pPlayerEntity, EQueryCvarValueStatus eStatus, const char *pCvarName, const char *pCvarValue ); + + virtual void PreSaveGameLoaded( char const *pSaveName, bool bInGame ); + + // Returns true if the game DLL wants the server not to be made public. + // Used by commentary system to hide multiplayer commentary servers from the master. + virtual bool ShouldHideServer( void ); + + virtual void InvalidateMdlCache(); + + virtual void SetServerHibernation( bool bHibernating ); + + float m_fAutoSaveDangerousTime; + float m_fAutoSaveDangerousMinHealthToCommit; + bool m_bIsHibernating; + + // Called after the steam API has been activated post-level startup + virtual void GameServerSteamAPIActivated( void ); + + // Called after the steam API has been shutdown post-level startup + virtual void GameServerSteamAPIShutdown( void ); + + // interface to the new GC based lobby system + virtual IServerGCLobby *GetServerGCLobby() OVERRIDE; + + virtual const char *GetServerBrowserMapOverride() OVERRIDE; + virtual const char *GetServerBrowserGameData() OVERRIDE; + +private: + + // This can just be a wrapper on MapEntity_ParseAllEntities, but CS does some tricks in here + // with the entity list. + void LevelInit_ParseAllEntities( const char *pMapEntities ); + void LoadMessageOfTheDay(); + void LoadSpecificMOTDMsg( const ConVar &convar, const char *pszStringName ); +}; + + +// Normally, when the engine calls ClientPutInServer, it calls a global function in the game DLL +// by the same name. Use this to override the function that it calls. This is used for bots. +typedef CBasePlayer* (*ClientPutInServerOverrideFn)( edict_t *pEdict, const char *playername ); + +void ClientPutInServerOverride( ClientPutInServerOverrideFn fn ); + +// -------------------------------------------------------------------------------------------- // +// Entity list management stuff. +// -------------------------------------------------------------------------------------------- // +// These are created for map entities in order as the map entities are spawned. +class CMapEntityRef +{ +public: + int m_iEdict; // Which edict slot this entity got. -1 if CreateEntityByName failed. + int m_iSerialNumber; // The edict serial number. TODO used anywhere ? +}; + +extern CUtlLinkedList g_MapEntityRefs; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CMapLoadEntityFilter : public IMapEntityFilter +{ +public: + virtual bool ShouldCreateEntity( const char *pClassname ) + { + // During map load, create all the entities. + return true; + } + + virtual CBaseEntity* CreateNextEntity( const char *pClassname ) + { + CBaseEntity *pRet = CreateEntityByName( pClassname ); + + CMapEntityRef ref; + ref.m_iEdict = -1; + ref.m_iSerialNumber = -1; + + if ( pRet ) + { + ref.m_iEdict = pRet->entindex(); + if ( pRet->edict() ) + ref.m_iSerialNumber = pRet->edict()->m_NetworkSerialNumber; + } + + g_MapEntityRefs.AddToTail( ref ); + return pRet; + } +}; + +bool IsEngineThreaded(); + +class CServerGameTags : public IServerGameTags +{ +public: + virtual void GetTaggedConVarList( KeyValues *pCvarTagList ); + +}; +EXPOSE_SINGLE_INTERFACE( CServerGameTags, IServerGameTags, INTERFACEVERSION_SERVERGAMETAGS ); + +#ifdef MAPBASE +// +// Dynamic mod-based mod title comments +// +typedef struct +{ + char pBSPName[64]; + char pTitleName[64]; +} MODTITLECOMMENT; + +typedef struct +{ + int iChapter; + char pChapterName[64]; +} MODCHAPTER; +#endif + +#endif // GAMEINTERFACE_H + diff --git a/sp/src/game/server/gametrace_dll.cpp b/sp/src/game/server/gametrace_dll.cpp new file mode 100644 index 00000000..c95c3716 --- /dev/null +++ b/sp/src/game/server/gametrace_dll.cpp @@ -0,0 +1,33 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "gametrace.h" +#include "world.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +bool CGameTrace::DidHitWorld() const +{ + return m_pEnt == GetWorldEntity(); +} + + +bool CGameTrace::DidHitNonWorldEntity() const +{ + return m_pEnt != NULL && !DidHitWorld(); +} + + +int CGameTrace::GetEntityIndex() const +{ + if ( m_pEnt ) + return m_pEnt->entindex(); + else + return -1; +} + diff --git a/sp/src/game/server/gameweaponmanager.cpp b/sp/src/game/server/gameweaponmanager.cpp new file mode 100644 index 00000000..68bced8a --- /dev/null +++ b/sp/src/game/server/gameweaponmanager.cpp @@ -0,0 +1,290 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" + +#include "gameweaponmanager.h" +#include "saverestore_utlvector.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//========================================================= +//========================================================= +class CGameWeaponManager; +static CUtlVector g_Managers; + + +//========================================================= +//========================================================= +class CGameWeaponManager : public CBaseEntity +{ + DECLARE_CLASS( CGameWeaponManager, CBaseEntity ); + DECLARE_DATADESC(); + +public: + void Spawn(); + CGameWeaponManager() + { + m_flAmmoMod = 1.0f; + m_bExpectingWeapon = false; + g_Managers.AddToTail( this ); + } + + ~CGameWeaponManager() + { + g_Managers.FindAndRemove( this ); + } + + void Think(); + void InputSetMaxPieces( inputdata_t &inputdata ); + void InputSetAmmoModifier( inputdata_t &inputdata ); + + string_t m_iszWeaponName; + int m_iMaxPieces; + float m_flAmmoMod; + bool m_bExpectingWeapon; + + CUtlVector m_ManagedNonWeapons; + +}; + +BEGIN_DATADESC( CGameWeaponManager ) + +//fields + DEFINE_KEYFIELD( m_iszWeaponName, FIELD_STRING, "weaponname" ), + DEFINE_KEYFIELD( m_iMaxPieces, FIELD_INTEGER, "maxpieces" ), + DEFINE_KEYFIELD( m_flAmmoMod, FIELD_FLOAT, "ammomod" ), + DEFINE_FIELD( m_bExpectingWeapon, FIELD_BOOLEAN ), +// funcs + DEFINE_FUNCTION( Think ), +// inputs + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxPieces", InputSetMaxPieces ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetAmmoModifier", InputSetAmmoModifier ), + + DEFINE_UTLVECTOR( m_ManagedNonWeapons, FIELD_EHANDLE ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( game_weapon_manager, CGameWeaponManager ); + +void CreateWeaponManager( const char *pWeaponName, int iMaxPieces ) +{ + CGameWeaponManager *pManager = (CGameWeaponManager *)CreateEntityByName( "game_weapon_manager"); + + if( pManager ) + { + pManager->m_iszWeaponName = MAKE_STRING( pWeaponName ); + pManager->m_iMaxPieces = iMaxPieces; + DispatchSpawn( pManager ); + } +} + +void WeaponManager_AmmoMod( CBaseCombatWeapon *pWeapon ) +{ + for ( int i = 0; i < g_Managers.Count(); i++ ) + { + if ( g_Managers[i]->m_iszWeaponName == pWeapon->m_iClassname ) + { + int iNewClip = (int)(pWeapon->m_iClip1 * g_Managers[i]->m_flAmmoMod); + int iNewRandomClip = iNewClip + RandomInt( -2, 2 ); + + if ( iNewRandomClip > pWeapon->GetMaxClip1() ) + { + iNewRandomClip = pWeapon->GetMaxClip1(); + } + else if ( iNewRandomClip <= 0 ) + { + //Drop at least one bullet. + iNewRandomClip = 1; + } + + pWeapon->m_iClip1 = iNewRandomClip; + } + } +} + +void WeaponManager_AddManaged( CBaseEntity *pWeapon ) +{ + for ( int i = 0; i < g_Managers.Count(); i++ ) + { + if ( g_Managers[i]->m_iszWeaponName == pWeapon->m_iClassname ) + { + Assert( g_Managers[i]->m_ManagedNonWeapons.Find( pWeapon ) == g_Managers[i]->m_ManagedNonWeapons.InvalidIndex() ); + g_Managers[i]->m_ManagedNonWeapons.AddToTail( pWeapon ); + break; + } + } +} + +void WeaponManager_RemoveManaged( CBaseEntity *pWeapon ) +{ + for ( int i = 0; i < g_Managers.Count(); i++ ) + { + if ( g_Managers[i]->m_iszWeaponName == pWeapon->m_iClassname ) + { + int j = g_Managers[i]->m_ManagedNonWeapons.Find( pWeapon ); + if ( j != g_Managers[i]->m_ManagedNonWeapons.InvalidIndex() ) + { + g_Managers[i]->m_ManagedNonWeapons.FastRemove( j ); + } + } + } +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CGameWeaponManager::Spawn() +{ + SetThink( &CGameWeaponManager::Think ); + SetNextThink( gpGlobals->curtime ); + CBaseEntity *pEntity = CreateEntityByName( STRING(m_iszWeaponName) ); + if ( !pEntity ) + { + DevMsg("%s removed itself!\n", GetDebugName() ); + UTIL_Remove(this); + } + else + { + m_bExpectingWeapon = ( dynamic_cast(pEntity) != NULL ); + UTIL_Remove(pEntity); + } +} + +//--------------------------------------------------------- +// Count of all the weapons in the world of my type and +// see if we have a surplus. If there is a surplus, try +// to find suitable candidates for removal. +// +// Right now we just remove the first weapons we find that +// are behind the player, or are out of the player's PVS. +// Later, we may want to score the results so that we +// removed the farthest gun that's not in the player's +// viewcone, etc. +// +// Some notes and thoughts: +// +// This code is designed NOT to remove weapons that are +// hand-placed by level designers. It should only clean +// up weapons dropped by dead NPCs, which is useful in +// situations where enemies are spawned in for a sustained +// period of time. +// +// Right now we PREFER to remove weapons that are not in the +// player's PVS, but this could be opposite of what we +// really want. We may only want to conduct the cleanup on +// weapons that are IN the player's PVS. +//--------------------------------------------------------- +void CGameWeaponManager::Think() +{ + int i; + + // Don't have to think all that often. + SetNextThink( gpGlobals->curtime + 2.0 ); + + const char *pszWeaponName = STRING( m_iszWeaponName ); + + CUtlVector candidates( 0, 64 ); + + if ( m_bExpectingWeapon ) + { + CBaseCombatWeapon *pWeapon = NULL; + // Firstly, count the total number of weapons of this type in the world. + // Also count how many of those can potentially be removed. + pWeapon = assert_cast(gEntList.FindEntityByClassname( pWeapon, pszWeaponName )); + + while( pWeapon ) + { + if( !pWeapon->IsEffectActive( EF_NODRAW ) && pWeapon->IsRemoveable() ) + { + candidates.AddToTail( pWeapon ); + } + + pWeapon = assert_cast(gEntList.FindEntityByClassname( pWeapon, pszWeaponName )); + } + } + else + { + for ( i = 0; i < m_ManagedNonWeapons.Count(); i++) + { + CBaseEntity *pEntity = m_ManagedNonWeapons[i]; + if ( pEntity ) + { + Assert( pEntity->m_iClassname == m_iszWeaponName ); + if ( !pEntity->IsEffectActive( EF_NODRAW ) ) + { + candidates.AddToTail( pEntity ); + } + } + else + { + m_ManagedNonWeapons.FastRemove( i-- ); + } + } + } + + // Calculate the surplus. + int surplus = candidates.Count() - m_iMaxPieces; + + // Based on what the player can see, try to clean up the world by removing weapons that + // the player cannot see right at the moment. + CBaseEntity *pCandidate; + for ( i = 0; i < candidates.Count() && surplus > 0; i++ ) + { + bool fRemovedOne = false; + + pCandidate = candidates[i]; + Assert( !pCandidate->IsEffectActive( EF_NODRAW ) ); + + if ( gpGlobals->maxClients == 1 ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + // Nodraw serves as a flag that this weapon is already being removed since + // all we're really doing inside this loop is marking them for removal by + // the entity system. We don't want to count the same weapon as removed + // more than once. + if( !UTIL_FindClientInPVS( pCandidate->edict() ) ) + { + fRemovedOne = true; + } + else if( !pPlayer->FInViewCone( pCandidate ) ) + { + fRemovedOne = true; + } + else if ( UTIL_DistApprox( pPlayer->GetAbsOrigin(), pCandidate->GetAbsOrigin() ) > (30*12) ) + { + fRemovedOne = true; + } + } + else + { + fRemovedOne = true; + } + + if( fRemovedOne ) + { + pCandidate->AddEffects( EF_NODRAW ); + UTIL_Remove( pCandidate ); + + DevMsg( 2, "Surplus %s removed\n", pszWeaponName); + surplus--; + } + } +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CGameWeaponManager::InputSetMaxPieces( inputdata_t &inputdata ) +{ + m_iMaxPieces = inputdata.value.Int(); +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CGameWeaponManager::InputSetAmmoModifier( inputdata_t &inputdata ) +{ + m_flAmmoMod = inputdata.value.Float(); +} diff --git a/sp/src/game/server/gameweaponmanager.h b/sp/src/game/server/gameweaponmanager.h new file mode 100644 index 00000000..3179914c --- /dev/null +++ b/sp/src/game/server/gameweaponmanager.h @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef GAMEWEAPONMANAGER_H +#define GAMEWEAPONMANAGER_H + +#if defined( _WIN32 ) +#pragma once +#endif + +void CreateWeaponManager( const char *pWeaponName, int iMaxPieces ); + +class CBaseCombatWeapon; + +void WeaponManager_AmmoMod( CBaseCombatWeapon *pWeapon ); + +void WeaponManager_AddManaged( CBaseEntity *pWeapon ); +void WeaponManager_RemoveManaged( CBaseEntity *pWeapon ); + +#endif // GAMEWEAPONMANAGER_H diff --git a/sp/src/game/server/genericactor.cpp b/sp/src/game/server/genericactor.cpp new file mode 100644 index 00000000..bb0dfe0b --- /dev/null +++ b/sp/src/game/server/genericactor.cpp @@ -0,0 +1,634 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +//========================================================= +// Generic NPC - purely for scripted sequence work. +//========================================================= +#include "cbase.h" +#include "shareddefs.h" +#include "npcevent.h" +#include "ai_basenpc.h" +#include "ai_hull.h" +#include "ai_baseactor.h" +#include "tier1/strtools.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#ifdef MAPBASE +#include "ai_speech.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar flex_looktime( "flex_looktime", "5" ); + +//--------------------------------------------------------- +// Sounds +//--------------------------------------------------------- + + +//========================================================= +// NPC's Anim Events Go Here +//========================================================= + +class CGenericActor : public CAI_BaseActor +{ +public: + DECLARE_CLASS( CGenericActor, CAI_BaseActor ); + + void Spawn( void ); + void Precache( void ); + float MaxYawSpeed( void ); + Class_T Classify ( void ); + void HandleAnimEvent( animevent_t *pEvent ); + int GetSoundInterests ( void ); + + + void TempGunEffect( void ); + + string_t m_strHullName; +#ifdef MAPBASE + Class_T m_iClassify = CLASS_NONE; +#endif + + DECLARE_DATADESC(); +}; +LINK_ENTITY_TO_CLASS( generic_actor, CGenericActor ); + +BEGIN_DATADESC( CGenericActor ) + + DEFINE_KEYFIELD(m_strHullName, FIELD_STRING, "hull_name" ), +#ifdef MAPBASE + DEFINE_INPUT(m_iClassify, FIELD_INTEGER, "SetClassify" ), +#endif + +END_DATADESC() + + +//========================================================= +// Classify - indicates this NPC's place in the +// relationship table. +//========================================================= +Class_T CGenericActor::Classify ( void ) +{ +#ifdef MAPBASE + return m_iClassify; +#else + return CLASS_NONE; +#endif +} + +//========================================================= +// MaxYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CGenericActor::MaxYawSpeed ( void ) +{ + return 90; +} + +//========================================================= +// HandleAnimEvent - catches the NPC-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CGenericActor::HandleAnimEvent( animevent_t *pEvent ) +{ + BaseClass::HandleAnimEvent( pEvent ); +} + +//========================================================= +// GetSoundInterests - generic NPC can't hear. +//========================================================= +int CGenericActor::GetSoundInterests ( void ) +{ + return NULL; +} + +//========================================================= +// Spawn +//========================================================= +void CGenericActor::Spawn() +{ + Precache(); + + SetModel( STRING( GetModelName() ) ); + +/* + if ( FStrEq( STRING( GetModelName() ), "models/player.mdl" ) ) + UTIL_SetSize(this, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX); + else + UTIL_SetSize(this, VEC_HULL_MIN, VEC_HULL_MAX); +*/ + + if ( FStrEq( STRING( GetModelName() ), "models/player.mdl" ) || + FStrEq( STRING( GetModelName() ), "models/holo.mdl" ) || + FStrEq( STRING( GetModelName() ), "models/blackout.mdl" ) ) + { + UTIL_SetSize(this, VEC_HULL_MIN, VEC_HULL_MAX); + } + else + { + UTIL_SetSize(this, NAI_Hull::Mins(HULL_HUMAN), NAI_Hull::Maxs(HULL_HUMAN)); + } + + if ( !FStrEq( STRING( GetModelName() ), "models/blackout.mdl" ) ) + { + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + } + else + { + SetSolid( SOLID_NONE ); + } + + SetMoveType( MOVETYPE_STEP ); + SetBloodColor( BLOOD_COLOR_RED ); + m_iHealth = 8; + m_flFieldOfView = 0.5;// indicates the width of this NPC's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + +#ifdef MAPBASE + CapabilitiesAdd( bits_CAP_SQUAD ); + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_DOORS_GROUP ); +#else + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_OPEN_DOORS ); +#endif + + // remove head turn if no eyes or forward attachment + if (LookupAttachment( "eyes" ) > 0 && LookupAttachment( "forward" ) > 0) + { + CapabilitiesAdd( bits_CAP_TURN_HEAD | bits_CAP_ANIMATEDFACE ); + } + + if (m_strHullName != NULL_STRING) + { + SetHullType( NAI_Hull::LookupId( STRING( m_strHullName ) ) ); + } + else + { + SetHullType( HULL_HUMAN ); + } + SetHullSizeNormal( ); + + NPCInit(); +} + +//========================================================= +// Precache - precaches all resources this NPC needs +//========================================================= +void CGenericActor::Precache() +{ + PrecacheModel( STRING( GetModelName() ) ); +} + +//========================================================= +// AI Schedules Specific to this NPC +//========================================================= + + + +#ifdef MAPBASE +//========================================================= +#define TLK_ACTOR_PAIN "TLK_WOUND" +#define TLK_ACTOR_DEATH "TLK_DEATH" +#define TLK_ACTOR_ALERT "TLK_STARTCOMBAT" +#define TLK_ACTOR_IDLE "TLK_IDLE" +#define TLK_ACTOR_FEAR "TLK_FEAR" +#define TLK_ACTOR_LOSTENEMY "TLK_LOSTENEMY" +#define TLK_ACTOR_FOUNDENEMY "TLK_REFINDENEMY" +//========================================================= +// Enhanced generic actor with built-in response system usage, weapon capabilities, and more. +//========================================================= +class CGenericActorCustom : public CGenericActor +{ +private: + DECLARE_CLASS( CGenericActorCustom, CGenericActor ); +public: + //DECLARE_DATADESC(); + + CGenericActorCustom() { } + void Spawn( void ); + void Precache( void ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + + void SpeakIfAllowed( const char *concept, AI_CriteriaSet *modifiers = NULL ); + void ModifyOrAppendCriteria( AI_CriteriaSet& set ); + + void PainSound( const CTakeDamageInfo &info ); + void DeathSound( const CTakeDamageInfo &info ); + void AlertSound( void ); + void IdleSound( void ); + void FearSound( void ); + void LostEnemySound( void ); + void FoundEnemySound( void ); +}; + +LINK_ENTITY_TO_CLASS( generic_actor_custom, CGenericActorCustom ); + +//BEGIN_DATADESC( CGenericActorCustom ) +//END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::Spawn() +{ + BaseClass::Spawn(); + + CapabilitiesAdd( bits_CAP_USE_WEAPONS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::Precache() +{ + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Cache user entity field values until spawn is called. +// Input : szKeyName - Key to handle. +// szValue - Value for key. +// Output : Returns true if the key was handled, false if not. +//----------------------------------------------------------------------------- +bool CGenericActorCustom::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "UseShotRegulator")) + { + if (atoi(szValue) > 0) + CapabilitiesAdd( bits_CAP_USE_SHOT_REGULATOR ); + else + CapabilitiesRemove( bits_CAP_USE_SHOT_REGULATOR ); + + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Speak concept +//----------------------------------------------------------------------------- +void CGenericActorCustom::SpeakIfAllowed( const char *concept, AI_CriteriaSet *modifiers ) +{ + Speak( concept, modifiers ? *modifiers : AI_CriteriaSet() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::ModifyOrAppendCriteria( AI_CriteriaSet& set ) +{ + BaseClass::ModifyOrAppendCriteria( set ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::PainSound( const CTakeDamageInfo &info ) +{ + AI_CriteriaSet modifiers; + ModifyOrAppendDamageCriteria( modifiers, info ); + SpeakIfAllowed( TLK_ACTOR_PAIN, &modifiers ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::DeathSound( const CTakeDamageInfo &info ) +{ + AI_CriteriaSet modifiers; + ModifyOrAppendDamageCriteria( modifiers, info ); + SpeakIfAllowed( TLK_ACTOR_DEATH, &modifiers ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::AlertSound( void ) +{ + SpeakIfAllowed( TLK_ACTOR_ALERT ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::IdleSound( void ) +{ + SpeakIfAllowed( TLK_ACTOR_IDLE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::FearSound( void ) +{ + SpeakIfAllowed( TLK_ACTOR_FEAR ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::LostEnemySound( void ) +{ + SpeakIfAllowed( TLK_ACTOR_LOSTENEMY ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGenericActorCustom::FoundEnemySound( void ) +{ + SpeakIfAllowed( TLK_ACTOR_FOUNDENEMY ); +} +#endif + + + +// ----------------------------------------------------------------------- + + +// FIXME: delete this code + +class CFlextalkActor : public CGenericActor +{ +private: + DECLARE_CLASS( CFlextalkActor, CGenericActor ); +public: + DECLARE_DATADESC(); + + CFlextalkActor() { m_iszSentence = NULL_STRING; m_sentence = 0; } + //void GenericCyclerSpawn(char *szModel, Vector vecMin, Vector vecMax); + //virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() | FCAP_IMPULSE_USE); } + //int OnTakeDamage( CBaseEntity *pInflictor, CBaseEntity *pAttacker, float flDamage, int bitsDamageType ); + //void Spawn( void ); + //void Precache( void ); + //void Think( void ); + + virtual void ProcessSceneEvents( void ); + + // Don't treat as a live target + //virtual bool IsAlive( void ) { return FALSE; } + + float m_flextime; + LocalFlexController_t m_flexnum; + float m_flextarget[64]; + float m_blinktime; + float m_looktime; + Vector m_lookTarget; + float m_speaktime; + int m_istalking; + int m_phoneme; + + string_t m_iszSentence; + int m_sentence; + + void SetFlexTarget( LocalFlexController_t flexnum, float value ); + LocalFlexController_t LookupFlex( const char *szTarget ); +}; + +BEGIN_DATADESC( CFlextalkActor ) + + DEFINE_FIELD( m_flextime, FIELD_TIME ), + DEFINE_FIELD( m_flexnum, FIELD_INTEGER ), + DEFINE_ARRAY( m_flextarget, FIELD_FLOAT, 64 ), + DEFINE_FIELD( m_blinktime, FIELD_TIME ), + DEFINE_FIELD( m_looktime, FIELD_TIME ), + DEFINE_FIELD( m_lookTarget, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_speaktime, FIELD_TIME ), + DEFINE_FIELD( m_istalking, FIELD_INTEGER ), + DEFINE_FIELD( m_phoneme, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_iszSentence, FIELD_STRING, "Sentence" ), + DEFINE_FIELD( m_sentence, FIELD_INTEGER ), + +END_DATADESC() + + + +LINK_ENTITY_TO_CLASS( cycler_actor, CFlextalkActor ); + +extern ConVar flex_expression; +extern ConVar flex_talk; + +// Cycler member functions + + +extern const char *predef_flexcontroller_names[]; +extern float predef_flexcontroller_values[7][30]; + +void CFlextalkActor::SetFlexTarget( LocalFlexController_t flexnum, float value ) +{ + m_flextarget[flexnum] = value; + + const char *pszType = GetFlexControllerType( flexnum ); + + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + if (i != flexnum) + { + const char *pszOtherType = GetFlexControllerType( i ); + if (stricmp( pszType, pszOtherType ) == 0) + { + m_flextarget[i] = 0; + } + } + } + + float value2 = value; + if (1 || random->RandomFloat( 0.0, 1.0 ) < 0.2) + { + value2 = random->RandomFloat( value - 0.2, value + 0.2 ); + value2 = clamp( value2, 0.0f, 1.0f ); + } + + + // HACK, for now, consider then linked is named "right_" or "left_" + if (strncmp( "right_", GetFlexControllerName( flexnum ), 6 ) == 0) + { + m_flextarget[flexnum+1] = value2; + } + else if (strncmp( "left_", GetFlexControllerName( flexnum ), 5 ) == 0) + { + m_flextarget[flexnum-1] = value2; + } +} + + +LocalFlexController_t CFlextalkActor::LookupFlex( const char *szTarget ) +{ + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + const char *pszFlex = GetFlexControllerName( i ); + if (stricmp( szTarget, pszFlex ) == 0) + { + return i; + } + } + return LocalFlexController_t(-1); +} + + +void CFlextalkActor::ProcessSceneEvents( void ) +{ + if ( HasSceneEvents() ) + { + BaseClass::ProcessSceneEvents( ); + return; + } + + // only do this if they have more than eyelid movement + if (GetNumFlexControllers() > 2) + { + const char *pszExpression = flex_expression.GetString(); + + if (pszExpression && pszExpression[0] == '+' && pszExpression[1] != '\0') + { + int i; + int j = atoi( &pszExpression[1] ); + for (i = 0; i < GetNumFlexControllers(); i++) + { + m_flextarget[m_flexnum] = 0; + } + + for (i = 0; i < 35 && predef_flexcontroller_names[i]; i++) + { + m_flexnum = LookupFlex( predef_flexcontroller_names[i] ); + m_flextarget[m_flexnum] = predef_flexcontroller_values[j][i]; + // Msg( "%s %.3f\n", predef_flexcontroller_names[i], predef_flexcontroller_values[j][i] ); + } + } + else if (pszExpression && pszExpression[0] != '\0' && strcmp(pszExpression, "+") != 0) + { + char szExpression[128]; + char szTemp[32]; + + Q_strncpy( szExpression, pszExpression ,sizeof(szExpression)); + char *pszExpression = szExpression; + + while (*pszExpression != '\0') + { + if (*pszExpression == '+') + *pszExpression = ' '; + + pszExpression++; + } + + pszExpression = szExpression; + + while (*pszExpression) + { + if (*pszExpression != ' ') + { + if (*pszExpression == '-') + { + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + m_flextarget[i] = 0; + } + } + else if (*pszExpression == '?') + { + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + Msg( "\"%s\" ", GetFlexControllerName( i ) ); + } + Msg( "\n" ); + flex_expression.SetValue( "" ); + } + else + { + if (sscanf( pszExpression, "%31s", szTemp ) == 1) + { + m_flexnum = LookupFlex( szTemp ); + + if (m_flexnum != -1 && m_flextarget[m_flexnum] != 1) + { + m_flextarget[m_flexnum] = 1.0; + // SetFlexTarget( m_flexnum ); + } + pszExpression += strlen( szTemp ) - 1; + } + } + } + pszExpression++; + } + } + else if (m_flextime < gpGlobals->curtime) + { + m_flextime = gpGlobals->curtime + random->RandomFloat( 0.3, 0.5 ) * (30.0 / GetNumFlexControllers()); + m_flexnum = (LocalFlexController_t)random->RandomInt( 0, GetNumFlexControllers() - 1 ); + + if (m_flextarget[m_flexnum] == 1) + { + m_flextarget[m_flexnum] = 0; + } + else if (stricmp( GetFlexControllerType( m_flexnum ), "phoneme" ) != 0) + { + if (strstr( GetFlexControllerName( m_flexnum ), "upper_raiser" ) == NULL) + { + Msg( "%s:%s\n", GetFlexControllerType( m_flexnum ), GetFlexControllerName( m_flexnum ) ); + SetFlexTarget( m_flexnum, random->RandomFloat( 0.5, 1.0 ) ); + } + } + } + + // slide it up. + for (LocalFlexController_t i = LocalFlexController_t(0); i < GetNumFlexControllers(); i++) + { + float weight = GetFlexWeight( i ); + + if (weight != m_flextarget[i]) + { + weight = weight + (m_flextarget[i] - weight) / random->RandomFloat( 2.0, 4.0 ); + } + weight = clamp( weight, 0.0f, 1.0f ); + SetFlexWeight( i, weight ); + } + + if (flex_talk.GetInt() == -1) + { + m_istalking = 1; + + char pszSentence[256]; + Q_snprintf( pszSentence,sizeof(pszSentence), "%s%d", STRING(m_iszSentence), m_sentence++ ); + int sentenceIndex = engine->SentenceIndexFromName( pszSentence ); + if (sentenceIndex >= 0) + { + Msg( "%d : %s\n", sentenceIndex, pszSentence ); + CPASAttenuationFilter filter( this ); + CBaseEntity::EmitSentenceByIndex( filter, entindex(), CHAN_VOICE, sentenceIndex, 1, SNDLVL_TALKING, 0, PITCH_NORM ); + } + else + { + m_sentence = 0; + } + + flex_talk.SetValue( "0" ); + } + else if (flex_talk.GetInt() == -2) + { + m_flNextEyeLookTime = gpGlobals->curtime + 1000.0; + } + else if (flex_talk.GetInt() == -3) + { + m_flNextEyeLookTime = gpGlobals->curtime; + flex_talk.SetValue( "0" ); + } + else if (flex_talk.GetInt() == -4) + { + AddLookTarget( UTIL_PlayerByIndex( 1 ), 0.5, flex_looktime.GetFloat() ); + flex_talk.SetValue( "0" ); + } + else if (flex_talk.GetInt() == -5) + { + PickLookTarget( true ); + flex_talk.SetValue( "0" ); + } + } +} diff --git a/sp/src/game/server/genericmonster.cpp b/sp/src/game/server/genericmonster.cpp new file mode 100644 index 00000000..71fd197f --- /dev/null +++ b/sp/src/game/server/genericmonster.cpp @@ -0,0 +1,572 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +//========================================================= +// Generic NPC - purely for scripted sequence work. +//========================================================= +#include "cbase.h" +#include "npcevent.h" +#include "ai_basenpc.h" +#include "ai_hull.h" +#include "KeyValues.h" +#include "engine/IEngineSound.h" +#include "physics_bone_follower.h" +#include "ai_baseactor.h" +#include "ai_senses.h" +#ifdef MAPBASE +/* +#include "ai_basenpc_flyer.h" +#include "player_pickup.h" +*/ +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// For holograms, make them not solid so the player can walk through them +#define SF_GENERICNPC_NOTSOLID (1 << 16) + +//========================================================= +// NPC's Anim Events Go Here +//========================================================= + +class CGenericNPC : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CGenericNPC, CAI_BaseNPC ); + + void Spawn( void ); + void Precache( void ); + float MaxYawSpeed( void ); + Class_T Classify ( void ); + void HandleAnimEvent( animevent_t *pEvent ); + int GetSoundInterests ( void ); + + void TempGunEffect( void ); +}; + +LINK_ENTITY_TO_CLASS( monster_generic, CGenericNPC ); + +//========================================================= +// Classify - indicates this NPC's place in the +// relationship table. +//========================================================= +Class_T CGenericNPC::Classify ( void ) +{ + return CLASS_NONE; +} + + +//========================================================= +// MaxYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CGenericNPC::MaxYawSpeed ( void ) +{ + return 90; +} + +//--------------------------------------------------------- +// !!!TEMP +// !!!TEMP +// !!!TEMP +// !!!TEMP +// +// (sjb) +//--------------------------------------------------------- +void CGenericNPC::TempGunEffect( void ) +{ + QAngle vecAngle; + Vector vecDir, vecShot; + Vector vecMuzzle, vecButt; + + GetAttachment( 2, vecMuzzle, vecAngle ); + GetAttachment( 3, vecButt, vecAngle ); + + vecDir = vecMuzzle - vecButt; + VectorNormalize( vecDir ); + + // CPVSFilter filter( GetAbsOrigin() ); + //te->ShowLine( filter, 0.0, vecSpot, vecSpot + vecForward ); + //UTIL_Sparks( vecMuzzle ); + + bool fSound = false; + + if( random->RandomInt( 0, 3 ) == 0 ) + { + fSound = true; + } + + Vector start = vecMuzzle + vecDir * 64; + Vector end = vecMuzzle + vecDir * 4096; + UTIL_Tracer( start, end, 0, TRACER_DONT_USE_ATTACHMENT, 5500, fSound ); + CPASAttenuationFilter filter2( this, "GenericNPC.GunSound" ); + EmitSound( filter2, entindex(), "GenericNPC.GunSound" ); +} + + +//========================================================= +// HandleAnimEvent - catches the NPC-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CGenericNPC::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case 1: + // TEMPORARLY. Makes the May 2001 sniper demo work (sjb) + TempGunEffect(); + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//========================================================= +// GetSoundInterests - generic NPC can't hear. +//========================================================= +int CGenericNPC::GetSoundInterests ( void ) +{ + return NULL; +} + +//========================================================= +// Spawn +//========================================================= +void CGenericNPC::Spawn() +{ + Precache(); + + SetModel( STRING( GetModelName() ) ); + +/* + if ( FStrEq( STRING( GetModelName() ), "models/player.mdl" ) ) + UTIL_SetSize(this, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX); + else + UTIL_SetSize(this, VEC_HULL_MIN, VEC_HULL_MAX); +*/ + + if ( FStrEq( STRING( GetModelName() ), "models/player.mdl" ) || FStrEq( STRING( GetModelName() ), "models/holo.mdl" ) ) + UTIL_SetSize(this, VEC_HULL_MIN, VEC_HULL_MAX); + else + UTIL_SetSize(this, NAI_Hull::Mins(HULL_HUMAN), NAI_Hull::Maxs(HULL_HUMAN)); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_RED; + m_iHealth = 8; + m_flFieldOfView = 0.5;// indicates the width of this NPC's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_OPEN_DOORS ); + + NPCInit(); + if ( !HasSpawnFlags(SF_GENERICNPC_NOTSOLID) ) + { + trace_t tr; + UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin(), MASK_SOLID, &tr ); + if ( tr.startsolid ) + { + Msg("Placed npc_generic in solid!!! (%s)\n", STRING(GetModelName()) ); + m_spawnflags |= SF_GENERICNPC_NOTSOLID; + } + } + + if ( HasSpawnFlags(SF_GENERICNPC_NOTSOLID) ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + m_takedamage = DAMAGE_NO; + VPhysicsDestroyObject(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: precaches all resources this NPC needs +//----------------------------------------------------------------------------- +void CGenericNPC::Precache() +{ + BaseClass::Precache(); + + PrecacheModel( STRING( GetModelName() ) ); + + PrecacheScriptSound( "GenericNPC.GunSound" ); +} + +// a really large health is set to make sure these never die. +const int TOO_MUCH_HEALTH_TO_DIE = 1000; +//======================================================================================= +// Furniture: A dumb "NPC" that is uses in scripted sequences +// where an NPC needs to be frame locked with a prop. +//======================================================================================= +class CNPC_Furniture : public CAI_BaseActor +{ + DECLARE_CLASS( CNPC_Furniture, CAI_BaseActor ); + DECLARE_DATADESC(); +public: + void Spawn( void ); + void Precache( void ); + void Die( void ); + void UpdateEfficiency( bool bInPVS ) { SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); SetMoveEfficiency( AIME_NORMAL ); } + Class_T Classify ( void ); + float MaxYawSpeed( void ){ return 0; } + virtual int ObjectCaps( void ); + bool CreateVPhysics( void ); + void NPCThink( void ); + void UpdateOnRemove( void ); + int SelectSchedule( void ); + void OnRestore( void ); + int OnTakeDamage( const CTakeDamageInfo &info ) + { + if ( m_iHealth <= info.GetDamage() ) + m_iHealth = info.GetDamage() + TOO_MUCH_HEALTH_TO_DIE; + return BaseClass::OnTakeDamage(info); + } + + void DrawDebugGeometryOverlays(void); + + void SetPlayerAvoidState( void ); + void InputDisablePlayerCollision( inputdata_t &inputdata ); + void InputEnablePlayerCollision( inputdata_t &inputdata ); + void UpdateBoneFollowerState( void ); + +private: + // Contained Bone Follower manager + CBoneFollowerManager m_BoneFollowerManager; +}; + +LINK_ENTITY_TO_CLASS( monster_furniture, CNPC_Furniture ); +LINK_ENTITY_TO_CLASS( npc_furniture, CNPC_Furniture ); + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- + +BEGIN_DATADESC( CNPC_Furniture ) + DEFINE_EMBEDDED( m_BoneFollowerManager ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisablePlayerCollision", InputDisablePlayerCollision ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnablePlayerCollision", InputEnablePlayerCollision ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: This used to have something to do with bees flying, but +// now it only initializes moving furniture in scripted sequences +//----------------------------------------------------------------------------- +void CNPC_Furniture::Spawn( ) +{ + Precache(); + + SetModel( STRING(GetModelName()) ); + + SetMoveType( MOVETYPE_STEP ); + SetSolid( SOLID_BBOX ); + + // Our collision, if needed, will be done through bone followers + AddSolidFlags( FSOLID_NOT_SOLID ); + + SetBloodColor( DONT_BLEED ); + m_iHealth = TOO_MUCH_HEALTH_TO_DIE; //wow + m_takedamage = DAMAGE_AIM; + SetSequence( 0 ); + SetCycle( 0 ); + SetNavType( NAV_FLY ); + AddFlag( FL_FLY ); + + CapabilitiesAdd( bits_CAP_MOVE_FLY | bits_CAP_TURN_HEAD | bits_CAP_ANIMATEDFACE ); + + AddEFlags( EFL_NO_MEGAPHYSCANNON_RAGDOLL ); + +// pev->nextthink += 1.0; +// SetThink (WalkMonsterDelay); + + ResetSequenceInfo( ); + SetCycle( 0 ); + NPCInit(); + + // Furniture needs to block LOS + SetBlocksLOS( true ); + + // Furniture just wastes CPU doing sensing code, since all they do is idle and play scripts + GetSenses()->AddSensingFlags( SENSING_FLAGS_DONT_LOOK | SENSING_FLAGS_DONT_LISTEN ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Furniture::Precache( void ) +{ + PrecacheModel( STRING( GetModelName() ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CNPC_Furniture::ObjectCaps( void ) +{ + // HL2 furniture transitions +#ifdef HL2_DLL + return CAI_BaseNPC::ObjectCaps(); +#else + return (CAI_BaseNPC::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Furniture is killed +//----------------------------------------------------------------------------- +void CNPC_Furniture::Die( void ) +{ + SetThink ( &CNPC_Furniture::SUB_Remove ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: ID's Furniture as neutral (noone will attack it) +//----------------------------------------------------------------------------- +Class_T CNPC_Furniture::Classify ( void ) +{ + return CLASS_NONE; +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +bool CNPC_Furniture::CreateVPhysics( void ) +{ +#ifndef HL2_DLL + return false; +#endif + + if ( !m_BoneFollowerManager.GetNumBoneFollowers() ) + { + KeyValues *modelKeyValues = new KeyValues(""); + if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) + { + // Do we have a bone follower section? + KeyValues *pkvBoneFollowers = modelKeyValues->FindKey("bone_followers"); + if ( pkvBoneFollowers ) + { + // Loop through the list and create the bone followers + KeyValues *pBone = pkvBoneFollowers->GetFirstSubKey(); + while ( pBone ) + { + // Add it to the list + const char *pBoneName = pBone->GetString(); + m_BoneFollowerManager.AddBoneFollower( this, pBoneName ); + + pBone = pBone->GetNextKey(); + } + } + } + modelKeyValues->deleteThis(); + } + + return true; +} + +void CNPC_Furniture::InputDisablePlayerCollision( inputdata_t &inputdata ) +{ + SetCollisionGroup( COLLISION_GROUP_NPC_ACTOR ); + UpdateBoneFollowerState(); +} + +void CNPC_Furniture::InputEnablePlayerCollision( inputdata_t &inputdata ) +{ + SetCollisionGroup( COLLISION_GROUP_NPC ); + UpdateBoneFollowerState(); +} + +void CNPC_Furniture::UpdateBoneFollowerState( void ) +{ + if ( m_BoneFollowerManager.GetNumBoneFollowers() ) + { + physfollower_t* pBone = m_BoneFollowerManager.GetBoneFollower( 0 ); + + if ( pBone && pBone->hFollower && pBone->hFollower->GetCollisionGroup() != GetCollisionGroup() ) + { + for ( int i = 0; i < m_BoneFollowerManager.GetNumBoneFollowers(); i++ ) + { + pBone = m_BoneFollowerManager.GetBoneFollower( i ); + + if ( pBone && pBone->hFollower ) + { + pBone->hFollower->SetCollisionGroup( GetCollisionGroup() ); + } + } + } + } +} + +void CNPC_Furniture::SetPlayerAvoidState( void ) +{ + +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Furniture::NPCThink( void ) +{ + BaseClass::NPCThink(); + + // Update follower bones + m_BoneFollowerManager.UpdateBoneFollowers(this); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Furniture::UpdateOnRemove( void ) +{ + m_BoneFollowerManager.DestroyBoneFollowers(); + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : int +//----------------------------------------------------------------------------- +int CNPC_Furniture::SelectSchedule( void ) +{ + switch( m_NPCState ) + { + case NPC_STATE_NONE: + case NPC_STATE_PRONE: + case NPC_STATE_IDLE: + case NPC_STATE_ALERT: + case NPC_STATE_COMBAT: + case NPC_STATE_DEAD: + return SCHED_WAIT_FOR_SCRIPT; + + case NPC_STATE_SCRIPT: + return BaseClass::SelectSchedule(); + + default: + DevWarning( 2, "Invalid State for SelectSchedule!\n" ); + break; + } + + return SCHED_FAIL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Furniture::OnRestore( void ) +{ + // Recreate any bone followers we have + CreateVPhysics(); + + BaseClass::OnRestore(); +} +void CNPC_Furniture::DrawDebugGeometryOverlays( void ) +{ + //ugh + if ( m_debugOverlays & OVERLAY_NPC_ZAP_BIT ) + { + m_debugOverlays &= ~OVERLAY_NPC_ZAP_BIT; + } + + BaseClass::DrawDebugGeometryOverlays(); +} + +#ifdef MAPBASE +/* +//========================================================= +// Generic flying monster +//========================================================= + +class CGenericFlyingMonster : public CAI_BaseFlyingBot +{ +public: + DECLARE_CLASS( CGenericFlyingMonster, CAI_BaseFlyingBot ); + + CGenericFlyingMonster(); + + void Spawn( void ); + void Precache( void ); + int GetSoundInterests ( void ); +}; + +LINK_ENTITY_TO_CLASS( monster_flying_generic, CGenericFlyingMonster ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CGenericFlyingMonster::CGenericFlyingMonster() +{ +} + +//========================================================= +// GetSoundInterests - generic NPC can't hear. +//========================================================= +int CGenericFlyingMonster::GetSoundInterests ( void ) +{ + return NULL; +} + +//========================================================= +// Spawn +//========================================================= +void CGenericFlyingMonster::Spawn() +{ + Precache(); + + SetModel( STRING( GetModelName() ) ); + + if ( FStrEq( STRING( GetModelName() ), "models/player.mdl" ) || FStrEq( STRING( GetModelName() ), "models/holo.mdl" ) ) + UTIL_SetSize(this, VEC_HULL_MIN, VEC_HULL_MAX); + else + UTIL_SetSize(this, NAI_Hull::Mins(HULL_HUMAN), NAI_Hull::Maxs(HULL_HUMAN)); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_FLY ); + m_bloodColor = BLOOD_COLOR_RED; + m_flFieldOfView = 0.5;// indicates the width of this NPC's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + CapabilitiesAdd( bits_CAP_MOVE_FLY ); + + SetNavType( NAV_FLY ); + + AddFlag( FL_FLY ); + + NPCInit(); + if ( !HasSpawnFlags(SF_GENERICNPC_NOTSOLID) ) + { + trace_t tr; + UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin(), MASK_SOLID, &tr ); + if ( tr.startsolid ) + { + Msg("Placed npc_generic in solid!!! (%s)\n", STRING(GetModelName()) ); + m_spawnflags |= SF_GENERICNPC_NOTSOLID; + } + } + + if ( HasSpawnFlags(SF_GENERICNPC_NOTSOLID) ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + m_takedamage = DAMAGE_NO; + VPhysicsDestroyObject(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: precaches all resources this NPC needs +//----------------------------------------------------------------------------- +void CGenericFlyingMonster::Precache() +{ + BaseClass::Precache(); + + PrecacheModel( STRING( GetModelName() ) ); +} +*/ +#endif diff --git a/sp/src/game/server/gib.cpp b/sp/src/game/server/gib.cpp new file mode 100644 index 00000000..a5dec6da --- /dev/null +++ b/sp/src/game/server/gib.cpp @@ -0,0 +1,679 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A gib is a chunk of a body, or a piece of wood/metal/rocks/etc. +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "gib.h" +#include "soundent.h" +#include "func_break.h" // For materials +#include "player.h" +#include "vstdlib/random.h" +#include "ai_utils.h" +#include "EntityFlame.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern Vector g_vecAttackDir; // In globals.cpp + +BEGIN_DATADESC( CGib ) + + // gibs are not saved/restored +// DEFINE_FIELD( m_bloodColor, FIELD_INTEGER ), +// DEFINE_FIELD( m_hSprite, FIELD_EHANDLE ), +// DEFINE_FIELD( m_cBloodDecals, FIELD_INTEGER ), +// DEFINE_FIELD( m_material, FIELD_INTEGER ), +// DEFINE_FIELD( m_lifeTime, FIELD_TIME ), +// DEFINE_FIELD( m_pSprite, CSprite ), +// DEFINE_FIELD( m_hFlame, FIELD_EHANDLE ), + +// DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), +// DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), + +// DEFINE_FIELD( m_bForceRemove, FIELD_BOOLEAN ), + + // Function pointers + DEFINE_ENTITYFUNC( BounceGibTouch ), + DEFINE_ENTITYFUNC( StickyGibTouch ), + DEFINE_THINKFUNC( WaitTillLand ), + DEFINE_THINKFUNC( DieThink ), + +END_DATADESC() + + +// HACKHACK -- The gib velocity equations don't work +void CGib::LimitVelocity( void ) +{ + Vector vecNewVelocity = GetAbsVelocity(); + float length = VectorNormalize( vecNewVelocity ); + + // ceiling at 1500. The gib velocity equation is not bounded properly. Rather than tune it + // in 3 separate places again, I'll just limit it here. + if ( length > 1500.0 ) + { + vecNewVelocity *= 1500; // This should really be sv_maxvelocity * 0.75 or something + SetAbsVelocity( vecNewVelocity ); + } +} + + +void CGib::SpawnStickyGibs( CBaseEntity *pVictim, Vector vecOrigin, int cGibs ) +{ + int i; + + if ( g_Language.GetInt() == LANGUAGE_GERMAN ) + { + // no sticky gibs in germany right now! + return; + } + + for ( i = 0 ; i < cGibs ; i++ ) + { + CGib *pGib = (CGib *)CreateEntityByName( "gib" ); + + pGib->Spawn( "models/stickygib.mdl" ); + pGib->m_nBody = random->RandomInt(0,2); + + if ( pVictim ) + { + pGib->SetLocalOrigin( + Vector( vecOrigin.x + random->RandomFloat( -3, 3 ), + vecOrigin.y + random->RandomFloat( -3, 3 ), + vecOrigin.z + random->RandomFloat( -3, 3 ) ) ); + + // make the gib fly away from the attack vector + Vector vecNewVelocity = g_vecAttackDir * -1; + + // mix in some noise + vecNewVelocity.x += random->RandomFloat ( -0.15, 0.15 ); + vecNewVelocity.y += random->RandomFloat ( -0.15, 0.15 ); + vecNewVelocity.z += random->RandomFloat ( -0.15, 0.15 ); + + vecNewVelocity *= 900; + + QAngle vecAngVelocity( random->RandomFloat ( 250, 400 ), random->RandomFloat ( 250, 400 ), 0 ); + pGib->SetLocalAngularVelocity( vecAngVelocity ); + + // copy owner's blood color + pGib->SetBloodColor( pVictim->BloodColor() ); + + pGib->AdjustVelocityBasedOnHealth( pVictim->m_iHealth, vecNewVelocity ); + pGib->SetAbsVelocity( vecNewVelocity ); + + pGib->SetMoveType( MOVETYPE_FLYGRAVITY ); + pGib->RemoveSolidFlags( FSOLID_NOT_SOLID ); + pGib->SetCollisionBounds( vec3_origin, vec3_origin ); + pGib->SetTouch ( &CGib::StickyGibTouch ); + pGib->SetThink (NULL); + } + pGib->LimitVelocity(); + } +} + +void CGib::SpawnHeadGib( CBaseEntity *pVictim ) +{ + CGib *pGib = CREATE_ENTITY( CGib, "gib" ); + + if ( g_Language.GetInt() == LANGUAGE_GERMAN ) + { + pGib->Spawn( "models/germangibs.mdl" );// throw one head + pGib->m_nBody = 0; + } + else + { + pGib->Spawn( "models/gibs/hgibs.mdl" );// throw one head + pGib->m_nBody = 0; + } + + if ( pVictim ) + { + Vector vecNewVelocity = pGib->GetAbsVelocity(); + + pGib->SetLocalOrigin( pVictim->EyePosition() ); + + edict_t *pentPlayer = UTIL_FindClientInPVS( pGib->edict() ); + + if ( random->RandomInt ( 0, 100 ) <= 5 && pentPlayer ) + { + // 5% chance head will be thrown at player's face. + CBasePlayer *player = (CBasePlayer *)CBaseEntity::Instance( pentPlayer ); + if ( player ) + { + vecNewVelocity = ( player->EyePosition() ) - pGib->GetAbsOrigin(); + VectorNormalize(vecNewVelocity); + vecNewVelocity *= 300; + vecNewVelocity.z += 100; + } + } + else + { + vecNewVelocity = Vector (random->RandomFloat(-100,100), random->RandomFloat(-100,100), random->RandomFloat(200,300)); + } + + QAngle vecNewAngularVelocity = pGib->GetLocalAngularVelocity(); + vecNewAngularVelocity.x = random->RandomFloat ( 100, 200 ); + vecNewAngularVelocity.y = random->RandomFloat ( 100, 300 ); + pGib->SetLocalAngularVelocity( vecNewAngularVelocity ); + + // copy owner's blood color + pGib->SetBloodColor( pVictim->BloodColor() ); + pGib->AdjustVelocityBasedOnHealth( pVictim->m_iHealth, vecNewVelocity ); + pGib->SetAbsVelocity( vecNewVelocity ); + } + pGib->LimitVelocity(); +} + + +//----------------------------------------------------------------------------- +// Blood color (see BLOOD_COLOR_* macros in baseentity.h) +//----------------------------------------------------------------------------- +void CGib::SetBloodColor( int nBloodColor ) +{ + m_bloodColor = nBloodColor; +} + + +//------------------------------------------------------------------------------ +// A little piece of duplicated code +//------------------------------------------------------------------------------ +void CGib::AdjustVelocityBasedOnHealth( int nHealth, Vector &vecVelocity ) +{ + if ( nHealth > -50) + { + vecVelocity *= 0.7; + } + else if ( nHealth > -200) + { + vecVelocity *= 2; + } + else + { + vecVelocity *= 4; + } +} + + +//------------------------------------------------------------------------------ +// Purpose : Initialize a gibs position and velocity +// Input : +// Output : +//------------------------------------------------------------------------------ +void CGib::InitGib( CBaseEntity *pVictim, float fMinVelocity, float fMaxVelocity ) +{ + // ------------------------------------------------------------------------ + // If have a pVictim spawn the gib somewhere in the pVictim's bounding volume + // ------------------------------------------------------------------------ + if ( pVictim ) + { + // Find a random position within the bounding box (add 1 to Z to get it out of the ground) + Vector vecOrigin; + pVictim->CollisionProp()->RandomPointInBounds( vec3_origin, Vector( 1, 1, 1 ), &vecOrigin ); + vecOrigin.z += 1.0f; + SetAbsOrigin( vecOrigin ); + + // make the gib fly away from the attack vector + Vector vecNewVelocity = g_vecAttackDir * -1; + + // mix in some noise + vecNewVelocity.x += random->RandomFloat ( -0.25, 0.25 ); + vecNewVelocity.y += random->RandomFloat ( -0.25, 0.25 ); + vecNewVelocity.z += random->RandomFloat ( -0.25, 0.25 ); + + vecNewVelocity *= random->RandomFloat ( fMaxVelocity, fMinVelocity ); + + QAngle vecNewAngularVelocity = GetLocalAngularVelocity(); + vecNewAngularVelocity.x = random->RandomFloat ( 100, 200 ); + vecNewAngularVelocity.y = random->RandomFloat ( 100, 300 ); + SetLocalAngularVelocity( vecNewAngularVelocity ); + + // copy owner's blood color + SetBloodColor( pVictim->BloodColor() ); + + AdjustVelocityBasedOnHealth( pVictim->m_iHealth, vecNewVelocity ); + + // Attempt to be physical if we can + if ( VPhysicsInitNormal( SOLID_BBOX, 0, false ) ) + { + IPhysicsObject *pObj = VPhysicsGetObject(); + + if ( pObj != NULL ) + { + AngularImpulse angImpulse = RandomAngularImpulse( -500, 500 ); + pObj->AddVelocity( &vecNewVelocity, &angImpulse ); + } + } + else + { + SetSolid( SOLID_BBOX ); + SetCollisionBounds( vec3_origin, vec3_origin ); + SetAbsVelocity( vecNewVelocity ); + } + + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } + + LimitVelocity(); +} + +//------------------------------------------------------------------------------ +// Purpose : Given an .mdl file with gibs and the number of gibs in the file +// spawns them in pVictim's bounding box +// Input : +// Output : +//------------------------------------------------------------------------------ +void CGib::SpawnSpecificGibs( CBaseEntity* pVictim, + int nNumGibs, + float vMinVelocity, + float vMaxVelocity, + const char* cModelName, + float flLifetime) +{ + for (int i=0;iSpawn( cModelName ); + pGib->m_nBody = i; + pGib->InitGib( pVictim, vMinVelocity, vMaxVelocity ); + pGib->m_lifeTime = flLifetime; + + if ( pVictim != NULL ) + { + pGib->SetOwnerEntity( pVictim ); + } + } +} + +//------------------------------------------------------------------------------ +// Purpose : Spawn random gibs of the given gib type +// Input : +// Output : +//------------------------------------------------------------------------------ +void CGib::SpawnRandomGibs( CBaseEntity *pVictim, int cGibs, GibType_e eGibType ) +{ + int cSplat; + + for ( cSplat = 0 ; cSplat < cGibs ; cSplat++ ) + { + CGib *pGib = CREATE_ENTITY( CGib, "gib" ); + + if ( g_Language.GetInt() == LANGUAGE_GERMAN ) + { + pGib->Spawn( "models/germangibs.mdl" ); + pGib->m_nBody = random->RandomInt(0,GERMAN_GIB_COUNT-1); + } + else + { + switch (eGibType) + { + case GIB_HUMAN: + // human pieces + pGib->Spawn( "models/gibs/hgibs.mdl" ); + pGib->m_nBody = random->RandomInt(1,HUMAN_GIB_COUNT-1);// start at one to avoid throwing random amounts of skulls (0th gib) + break; + case GIB_ALIEN: + // alien pieces + pGib->Spawn( "models/gibs/agibs.mdl" ); + pGib->m_nBody = random->RandomInt(0,ALIEN_GIB_COUNT-1); + break; + } + } + pGib->InitGib( pVictim, 300, 400); + } +} + +//========================================================= +// WaitTillLand - in order to emit their meaty scent from +// the proper location, gibs should wait until they stop +// bouncing to emit their scent. That's what this function +// does. +//========================================================= +void CGib::WaitTillLand ( void ) +{ + if (!IsInWorld()) + { + UTIL_Remove( this ); + return; + } + + if ( GetAbsVelocity() == vec3_origin ) + { + SetRenderColorA( 255 ); + m_nRenderMode = kRenderTransTexture; + if ( GetMoveType() != MOVETYPE_VPHYSICS ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + SetLocalAngularVelocity( vec3_angle ); + + SetNextThink( gpGlobals->curtime + m_lifeTime ); + SetThink ( &CGib::SUB_FadeOut ); + + if ( GetSprite() ) + { + CSprite *pSprite = dynamic_cast( GetSprite() ); + + if ( pSprite ) + { + //Adrian - Why am I doing this? Check InitPointGib for the answer! + if ( m_lifeTime == 0 ) + m_lifeTime = random->RandomFloat( 1, 3 ); + + pSprite->FadeAndDie( m_lifeTime ); + } + } + + if ( GetFlame() ) + { + CEntityFlame *pFlame = dynamic_cast< CEntityFlame*>( GetFlame() ); + + if ( pFlame ) + { + pFlame->SetLifetime( 1.0f ); + } + } + + // If you bleed, you stink! + if ( m_bloodColor != DONT_BLEED ) + { + // ok, start stinkin! + // FIXME: It's too easy to fill up the sound queue with all these meat sounds + // CSoundEnt::InsertSound ( SOUND_MEAT, GetAbsOrigin(), 384, 25 ); + } + } + else + { + // wait and check again in another half second. + SetNextThink( gpGlobals->curtime + 0.5f ); + } +} + +bool CGib::SUB_AllowedToFade( void ) +{ + if( VPhysicsGetObject() ) + { + if( VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD || GetEFlags() & EFL_IS_BEING_LIFTED_BY_BARNACLE ) + return false; + } + + CBasePlayer *pPlayer = ( AI_IsSinglePlayer() ) ? UTIL_GetLocalPlayer() : NULL; + + if ( pPlayer && pPlayer->FInViewCone( this ) && m_bForceRemove == false ) + { + return false; + } + + return true; +} + + +void CGib::DieThink ( void ) +{ + if ( GetSprite() ) + { + CSprite *pSprite = dynamic_cast( GetSprite() ); + + if ( pSprite ) + { + pSprite->FadeAndDie( 0.0 ); + } + } + + if ( GetFlame() ) + { + CEntityFlame *pFlame = dynamic_cast< CEntityFlame*>( GetFlame() ); + + if ( pFlame ) + { + pFlame->SetLifetime( 1.0f ); + } + } + + if ( g_pGameRules->IsMultiplayer() ) + { + UTIL_Remove( this ); + } + else + { + SetThink ( &CGib::SUB_FadeOut ); + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGib::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + + if ( pPlayer ) + { + pPlayer->PickupObject( this ); + } +} + +//----------------------------------------------------------------------------- +// Physics Attacker +//----------------------------------------------------------------------------- +void CGib::SetPhysicsAttacker( CBasePlayer *pEntity, float flTime ) +{ + m_hPhysicsAttacker = pEntity; + m_flLastPhysicsInfluenceTime = flTime; +} + + +//----------------------------------------------------------------------------- +// Purpose: Keep track of physgun influence +//----------------------------------------------------------------------------- +void CGib::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + SetPhysicsAttacker( pPhysGunUser, gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGib::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + SetPhysicsAttacker( pPhysGunUser, gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CBasePlayer *CGib::HasPhysicsAttacker( float dt ) +{ + if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) + { + return m_hPhysicsAttacker; + } + return NULL; +} + + +// +// Gib bounces on the ground or wall, sponges some blood down, too! +// +void CGib::BounceGibTouch ( CBaseEntity *pOther ) +{ + Vector vecSpot; + trace_t tr; + + IPhysicsObject *pPhysics = VPhysicsGetObject(); + + if ( pPhysics ) + return; + + //if ( random->RandomInt(0,1) ) + // return;// don't bleed everytime + if (GetFlags() & FL_ONGROUND) + { + SetAbsVelocity( GetAbsVelocity() * 0.9 ); + QAngle angles = GetLocalAngles(); + angles.x = 0; + angles.z = 0; + SetLocalAngles( angles ); + + QAngle angVel = GetLocalAngularVelocity(); + angVel.x = 0; + angVel.z = 0; + SetLocalAngularVelocity( vec3_angle ); + } + else + { + if ( g_Language.GetInt() != LANGUAGE_GERMAN && m_cBloodDecals > 0 && m_bloodColor != DONT_BLEED ) + { + vecSpot = GetAbsOrigin() + Vector ( 0 , 0 , 8 );//move up a bit, and trace down. + UTIL_TraceLine ( vecSpot, vecSpot + Vector ( 0, 0, -24 ), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + UTIL_BloodDecalTrace( &tr, m_bloodColor ); + + m_cBloodDecals--; + } + + if ( m_material != matNone && random->RandomInt(0,2) == 0 ) + { + float volume; + float zvel = fabs(GetAbsVelocity().z); + + volume = 0.8f * MIN(1.0, ((float)zvel) / 450.0f); + + CBreakable::MaterialSoundRandom( entindex(), (Materials)m_material, volume ); + } + } +} + +// +// Sticky gib puts blood on the wall and stays put. +// +void CGib::StickyGibTouch ( CBaseEntity *pOther ) +{ + Vector vecSpot; + trace_t tr; + + SetThink ( &CGib::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 10 ); + + if ( !FClassnameIs( pOther, "worldspawn" ) ) + { + SetNextThink( gpGlobals->curtime ); + return; + } + + UTIL_TraceLine ( GetAbsOrigin(), GetAbsOrigin() + GetAbsVelocity() * 32, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + UTIL_BloodDecalTrace( &tr, m_bloodColor ); + + Vector vecForward = tr.plane.normal * -1; + QAngle angles; + VectorAngles( vecForward, angles ); + SetLocalAngles( angles ); + SetAbsVelocity( vec3_origin ); + SetLocalAngularVelocity( vec3_angle ); + SetMoveType( MOVETYPE_NONE ); +} + +// +// Throw a chunk +// +void CGib::Spawn( const char *szGibModel ) +{ + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + SetFriction(0.55); // deading the bounce a bit + + // sometimes an entity inherits the edict from a former piece of glass, + // and will spawn using the same render FX or m_nRenderMode! bad! + SetRenderColorA( 255 ); + m_nRenderMode = kRenderNormal; + m_nRenderFX = kRenderFxNone; + + // hopefully this will fix the VELOCITY TOO LOW crap + m_takedamage = DAMAGE_EVENTS_ONLY; + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + + SetModel( szGibModel ); + +#ifdef HL1_DLL + SetElasticity( 1.0 ); + UTIL_SetSize( this, vec3_origin, vec3_origin ); +#endif//HL1_DLL + + SetNextThink( gpGlobals->curtime + 4 ); + m_lifeTime = 25; + SetTouch ( &CGib::BounceGibTouch ); + + m_bForceRemove = false; + + m_material = matNone; + m_cBloodDecals = 5;// how many blood decals this gib can place (1 per bounce until none remain). + +} + + +//----------------------------------------------------------------------------- +// Spawn a gib with a finite lifetime, after which it will fade out. +//----------------------------------------------------------------------------- +void CGib::Spawn( const char *szGibModel, float flLifetime ) +{ + Spawn( szGibModel ); + m_lifeTime = flLifetime; + SetThink ( &CGib::SUB_FadeOut ); + SetNextThink( gpGlobals->curtime + m_lifeTime ); +} + + +LINK_ENTITY_TO_CLASS( gib, CGib ); + +CBaseEntity *CreateRagGib( const char *szModel, const Vector &vecOrigin, const QAngle &vecAngles, const Vector &vecForce, float flFadeTime, bool bShouldIgnite ) +{ + CRagGib *pGib; + + pGib = (CRagGib*)CreateEntityByName( "raggib" ); + + pGib->SetLocalAngles( vecAngles ); + + if ( !pGib ) + { + Msg( "**Can't create ragdoll gib!\n" ); + return NULL; + } + + if ( bShouldIgnite ) + { + CBaseAnimating *pAnimating = pGib->GetBaseAnimating(); + if (pAnimating != NULL ) + { + pAnimating->Ignite( random->RandomFloat( 8.0, 12.0 ), false ); + } + } + + pGib->Spawn( szModel, vecOrigin, vecForce, flFadeTime ); + + return pGib; +} + +void CRagGib::Spawn( const char *szModel, const Vector &vecOrigin, const Vector &vecForce, float flFadeTime = 0.0 ) +{ + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetModel( szModel ); + UTIL_SetSize(this, vec3_origin, vec3_origin); + UTIL_SetOrigin( this, vecOrigin ); + if ( !BecomeRagdollOnClient( vecForce ) ) + { + AddSolidFlags( FSOLID_NOT_STANDABLE ); + RemoveSolidFlags( FSOLID_NOT_SOLID ); + if( flFadeTime > 0.0 ) + { + SUB_StartFadeOut( flFadeTime ); + } + } +} + +LINK_ENTITY_TO_CLASS( raggib, CRagGib ); diff --git a/sp/src/game/server/gib.h b/sp/src/game/server/gib.h new file mode 100644 index 00000000..57d5630f --- /dev/null +++ b/sp/src/game/server/gib.h @@ -0,0 +1,115 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A gib is a chunk of a body, or a piece of wood/metal/rocks/etc. +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef GIB_H +#define GIB_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "baseanimating.h" +#include "player_pickup.h" +#include "Sprite.h" + +extern CBaseEntity *CreateRagGib( const char *szModel, const Vector &vecOrigin, const QAngle &vecAngles, const Vector &vecForce, float flFadeTime = 0.0, bool bShouldIgnite = false ); + +#define GERMAN_GIB_COUNT 4 +#define HUMAN_GIB_COUNT 6 +#define ALIEN_GIB_COUNT 4 + +enum GibType_e +{ + GIB_HUMAN, + GIB_ALIEN, +}; + +class CGib : public CBaseAnimating, + public CDefaultPlayerPickupVPhysics +{ +public: + DECLARE_CLASS( CGib, CBaseAnimating ); + + void Spawn( const char *szGibModel ); + void Spawn( const char *szGibModel, float flLifetime ); + + void InitGib( CBaseEntity *pVictim, float fMaxVelocity, float fMinVelocity ); + void BounceGibTouch ( CBaseEntity *pOther ); + void StickyGibTouch ( CBaseEntity *pOther ); + void WaitTillLand( void ); + void DieThink( void ); + void LimitVelocity( void ); + virtual bool SUB_AllowedToFade( void ); + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION) | FCAP_DONT_SAVE | FCAP_IMPULSE_USE; } + static void SpawnHeadGib( CBaseEntity *pVictim ); + static void SpawnRandomGibs( CBaseEntity *pVictim, int cGibs, GibType_e eGibType ); + static void SpawnStickyGibs( CBaseEntity *pVictim, Vector vecOrigin, int cGibs ); + static void SpawnSpecificGibs( CBaseEntity *pVictim, int nNumGibs, float fMaxVelocity, float fMinVelocity, const char* cModelName, float flLifetime = 25); + + void SetPhysicsAttacker( CBasePlayer *pEntity, float flTime ); + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ); + virtual CBasePlayer *HasPhysicsAttacker( float dt ); + + void SetSprite( CBaseEntity *pSprite ) + { + m_hSprite = pSprite; + } + + CBaseEntity *GetSprite( void ) + { + return m_hSprite.Get(); + } + + void SetFlame( CBaseEntity *pFlame ) + { + m_hFlame = pFlame; + } + + CBaseEntity *GetFlame( void ) + { + return m_hFlame.Get(); + } + + DECLARE_DATADESC(); + + +public: + void SetBloodColor( int nBloodColor ); + + int m_cBloodDecals; + int m_material; + float m_lifeTime; + bool m_bForceRemove; + + CHandle m_hPhysicsAttacker; + float m_flLastPhysicsInfluenceTime; + +private: + // A little piece of duplicated code + void AdjustVelocityBasedOnHealth( int nHealth, Vector &vecVelocity ); + int m_bloodColor; + + EHANDLE m_hSprite; + EHANDLE m_hFlame; +}; + +class CRagGib : public CBaseAnimating +{ +public: + DECLARE_CLASS( CRagGib, CBaseAnimating ); + + void Spawn( const char *szModel, const Vector &vecOrigin, const Vector &vecForce, float flFadeTime ); +}; + + +#endif //GIB_H diff --git a/sp/src/game/server/globals.cpp b/sp/src/game/server/globals.cpp new file mode 100644 index 00000000..f568ed58 --- /dev/null +++ b/sp/src/game/server/globals.cpp @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* + +===== globals.cpp ======================================================== + + DLL-wide global variable definitions. + They're all defined here, for convenient centralization. + Source files that need them should "extern ..." declare each + variable, to better document what globals they care about. + +*/ + +#include "cbase.h" +#include "soundent.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +Vector g_vecAttackDir; +int g_iSkillLevel; +bool g_fGameOver; diff --git a/sp/src/game/server/globals.h b/sp/src/game/server/globals.h new file mode 100644 index 00000000..03076e37 --- /dev/null +++ b/sp/src/game/server/globals.h @@ -0,0 +1,20 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef GLOBALS_H +#define GLOBALS_H +#ifdef _WIN32 +#pragma once +#endif + + +extern Vector g_vecAttackDir; +extern int g_iSkillLevel; +extern bool g_fGameOver; +extern ConVar g_Language; + + +#endif // GLOBALS_H diff --git a/sp/src/game/server/globalstate.cpp b/sp/src/game/server/globalstate.cpp new file mode 100644 index 00000000..7ade7762 --- /dev/null +++ b/sp/src/game/server/globalstate.cpp @@ -0,0 +1,359 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "basetypes.h" +#include "saverestore.h" +#include "saverestore_utlvector.h" +#include "saverestore_utlsymbol.h" +#include "globalstate.h" +#include "igamesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +struct globalentity_t +{ + DECLARE_SIMPLE_DATADESC(); + + CUtlSymbol name; + CUtlSymbol levelName; + GLOBALESTATE state; + int counter; +}; + + +class CGlobalState : public CAutoGameSystem +{ +public: + CGlobalState( char const *name ) : CAutoGameSystem( name ), m_disableStateUpdates(false) + { + } + + // IGameSystem + virtual void LevelShutdownPreEntity() + { + // don't allow state updates during shutdowns + Assert( !m_disableStateUpdates ); + m_disableStateUpdates = true; + } + + virtual void LevelShutdownPostEntity() + { + Assert( m_disableStateUpdates ); + m_disableStateUpdates = false; + } + + void EnableStateUpdates( bool bEnable ) + { + m_disableStateUpdates = !bEnable; + } + + void SetState( int globalIndex, GLOBALESTATE state ) + { + if ( m_disableStateUpdates || !m_list.IsValidIndex(globalIndex) ) + return; + m_list[globalIndex].state = state; + } + + GLOBALESTATE GetState( int globalIndex ) + { + if ( !m_list.IsValidIndex(globalIndex) ) + return GLOBAL_OFF; + return m_list[globalIndex].state; + } + + void SetCounter( int globalIndex, int counter ) + { + if ( m_disableStateUpdates || !m_list.IsValidIndex(globalIndex) ) + return; + m_list[globalIndex].counter = counter; + } + + int AddToCounter( int globalIndex, int delta ) + { + if ( m_disableStateUpdates || !m_list.IsValidIndex(globalIndex) ) + return 0; + return ( m_list[globalIndex].counter += delta ); + } + + int GetCounter( int globalIndex ) + { + if ( !m_list.IsValidIndex(globalIndex) ) + return 0; + return m_list[globalIndex].counter; + } + + void SetMap( int globalIndex, string_t mapname ) + { + if ( !m_list.IsValidIndex(globalIndex) ) + return; + m_list[globalIndex].levelName = m_nameList.AddString( STRING(mapname) ); + } + + const char *GetMap( int globalIndex ) + { + if ( !m_list.IsValidIndex(globalIndex) ) + return NULL; + return m_nameList.String( m_list[globalIndex].levelName ); + } + + const char *GetName( int globalIndex ) + { + if ( !m_list.IsValidIndex(globalIndex) ) + return NULL; + return m_nameList.String( m_list[globalIndex].name ); + } + + int GetIndex( const char *pGlobalname ) + { + CUtlSymbol symName = m_nameList.Find( pGlobalname ); + + if ( symName.IsValid() ) + { + for ( int i = m_list.Count() - 1; i >= 0; --i ) + { + if ( m_list[i].name == symName ) + return i; + } + } + + return -1; + } + + int AddEntity( const char *pGlobalname, const char *pMapName, GLOBALESTATE state ) + { + globalentity_t entity; + entity.name = m_nameList.AddString( pGlobalname ); + entity.levelName = m_nameList.AddString( pMapName ); + entity.state = state; + + int index = GetIndex( m_nameList.String( entity.name ) ); + if ( index >= 0 ) + return index; + return m_list.AddToTail( entity ); + } + + int GetNumGlobals( void ) + { + return m_list.Count(); + } + +#ifdef MAPBASE_VSCRIPT + virtual void RegisterVScript() + { + g_pScriptVM->RegisterInstance( this, "Globals" ); + } + + int ScriptAddEntity( const char *pGlobalname, const char *pMapName, int state ) + { + return AddEntity( pGlobalname, pMapName, (GLOBALESTATE)state ); + } + + void ScriptSetState( int globalIndex, int state ) + { + SetState( globalIndex, (GLOBALESTATE)state ); + } + + int ScriptGetState( int globalIndex ) + { + return (int)GetState( globalIndex ); + } +#endif + + void Reset( void ); + int Save( ISave &save ); + int Restore( IRestore &restore ); + DECLARE_SIMPLE_DATADESC(); + +//#ifdef _DEBUG + void DumpGlobals( void ); +//#endif + +public: + CUtlSymbolTable m_nameList; +private: + bool m_disableStateUpdates; + CUtlVector m_list; +}; + +static CGlobalState gGlobalState( "CGlobalState" ); + +static CUtlSymbolDataOps g_GlobalSymbolDataOps( gGlobalState.m_nameList ); + + +void GlobalEntity_SetState( int globalIndex, GLOBALESTATE state ) +{ + gGlobalState.SetState( globalIndex, state ); +} + +void GlobalEntity_SetCounter( int globalIndex, int counter ) +{ + gGlobalState.SetCounter( globalIndex, counter ); +} + +int GlobalEntity_AddToCounter( int globalIndex, int delta ) +{ + return gGlobalState.AddToCounter( globalIndex, delta ); +} + +void GlobalEntity_EnableStateUpdates( bool bEnable ) +{ + gGlobalState.EnableStateUpdates( bEnable ); +} + + +void GlobalEntity_SetMap( int globalIndex, string_t mapname ) +{ + gGlobalState.SetMap( globalIndex, mapname ); +} + +int GlobalEntity_Add( const char *pGlobalname, const char *pMapName, GLOBALESTATE state ) +{ + return gGlobalState.AddEntity( pGlobalname, pMapName, state ); +} + +int GlobalEntity_GetIndex( const char *pGlobalname ) +{ + return gGlobalState.GetIndex( pGlobalname ); +} + +GLOBALESTATE GlobalEntity_GetState( int globalIndex ) +{ + return gGlobalState.GetState( globalIndex ); +} + +int GlobalEntity_GetCounter( int globalIndex ) +{ + return gGlobalState.GetCounter( globalIndex ); +} + +const char *GlobalEntity_GetMap( int globalIndex ) +{ + return gGlobalState.GetMap( globalIndex ); +} + +const char *GlobalEntity_GetName( int globalIndex ) +{ + return gGlobalState.GetName( globalIndex ); +} + +int GlobalEntity_GetNumGlobals( void ) +{ + return gGlobalState.GetNumGlobals(); +} + +CON_COMMAND(dump_globals, "Dump all global entities/states") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + gGlobalState.DumpGlobals(); +} + +// This is available all the time now on impulse 104, remove later +//#ifdef _DEBUG +void CGlobalState::DumpGlobals( void ) +{ + static const char *estates[] = { "Off", "On", "Dead" }; + + Msg( "-- Globals --\n" ); + for ( int i = 0; i < m_list.Count(); i++ ) + { + Msg( "%s: %s (%s) = %d\n", m_nameList.String( m_list[i].name ), m_nameList.String( m_list[i].levelName ), estates[m_list[i].state], m_list[i].counter ); + } +} +//#endif + + +// Global state Savedata +BEGIN_SIMPLE_DATADESC( CGlobalState ) + DEFINE_UTLVECTOR( m_list, FIELD_EMBEDDED ), + // DEFINE_FIELD( m_nameList, CUtlSymbolTable ), + // DEFINE_FIELD( m_disableStateUpdates, FIELD_BOOLEAN ), +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( globalentity_t ) + DEFINE_CUSTOM_FIELD( name, &g_GlobalSymbolDataOps ), + DEFINE_CUSTOM_FIELD( levelName, &g_GlobalSymbolDataOps ), + DEFINE_FIELD( state, FIELD_INTEGER ), + DEFINE_FIELD( counter, FIELD_INTEGER ), +END_DATADESC() + + +int CGlobalState::Save( ISave &save ) +{ + if ( !save.WriteFields( "GLOBAL", this, NULL, m_DataMap.dataDesc, m_DataMap.dataNumFields ) ) + return 0; + + return 1; +} + +int CGlobalState::Restore( IRestore &restore ) +{ + Reset(); + if ( !restore.ReadFields( "GLOBAL", this, NULL, m_DataMap.dataDesc, m_DataMap.dataNumFields ) ) + return 0; + + return 1; +} + +void CGlobalState::Reset( void ) +{ + m_list.Purge(); + m_nameList.RemoveAll(); +} + + +void SaveGlobalState( CSaveRestoreData *pSaveData ) +{ + CSave saveHelper( pSaveData ); + gGlobalState.Save( saveHelper ); +} + + +void RestoreGlobalState( CSaveRestoreData *pSaveData ) +{ + CRestore restoreHelper( pSaveData ); + gGlobalState.Restore( restoreHelper ); +} + + +//----------------------------------------------------------------------------- +// Purpose: This gets called when a level is shut down +//----------------------------------------------------------------------------- + +void ResetGlobalState( void ) +{ + gGlobalState.Reset(); +} + + +void ShowServerGameTime() +{ + Msg( "Server game time: %f\n", gpGlobals->curtime ); +} + +CON_COMMAND(server_game_time, "Gives the game time in seconds (server's curtime)") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + ShowServerGameTime(); +} + +#ifdef MAPBASE_VSCRIPT +BEGIN_SCRIPTDESC_ROOT( CGlobalState, SCRIPT_SINGLETON "Global state system." ) + DEFINE_SCRIPTFUNC( GetIndex, "Gets the index of the specified global name. Returns -1 if it does not exist." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptAddEntity, "AddGlobal", "Adds a new global with a specific map name and state. Returns its index." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetState, "GetState", "Gets the state of the specified global." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetState, "SetState", "Sets the state of the specified global." ) + DEFINE_SCRIPTFUNC( GetCounter, "Gets the counter of the specified global." ) + DEFINE_SCRIPTFUNC( SetCounter, "Sets the counter of the specified global." ) + DEFINE_SCRIPTFUNC( AddToCounter, "Adds to the counter of the specified global." ) +END_SCRIPTDESC(); +#endif diff --git a/sp/src/game/server/globalstate.h b/sp/src/game/server/globalstate.h new file mode 100644 index 00000000..ff9d4b31 --- /dev/null +++ b/sp/src/game/server/globalstate.h @@ -0,0 +1,109 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef GLOBALSTATE_H +#define GLOBALSTATE_H +#ifdef _WIN32 +#pragma once +#endif + +typedef enum { GLOBAL_OFF = 0, GLOBAL_ON = 1, GLOBAL_DEAD = 2 } GLOBALESTATE; + +void GlobalEntity_SetState( int globalIndex, GLOBALESTATE state ); +void GlobalEntity_SetMap( int globalIndex, string_t mapname ); +int GlobalEntity_Add( const char *pGlobalname, const char *pMapName, GLOBALESTATE state ); + +int GlobalEntity_GetIndex( const char *pGlobalname ); +GLOBALESTATE GlobalEntity_GetState( int globalIndex ); +const char *GlobalEntity_GetMap( int globalIndex ); +const char *GlobalEntity_GetName( int globalIndex ); + +int GlobalEntity_GetCounter( int globalIndex ); +void GlobalEntity_SetCounter( int globalIndex, int counter ); +int GlobalEntity_AddToCounter( int globalIndex, int delta ); + +int GlobalEntity_GetNumGlobals( void ); +void GlobalEntity_EnableStateUpdates( bool bEnable ); + +inline int GlobalEntity_Add( string_t globalname, string_t mapName, GLOBALESTATE state ) +{ + return GlobalEntity_Add( STRING(globalname), STRING(mapName), state ); +} + +inline int GlobalEntity_GetIndex( string_t globalname ) +{ + return GlobalEntity_GetIndex( STRING(globalname) ); +} + +inline int GlobalEntity_IsInTable( string_t globalname ) +{ + return GlobalEntity_GetIndex( STRING(globalname) ) >= 0 ? true : false; +} + +inline int GlobalEntity_IsInTable( const char *pGlobalname ) +{ + return GlobalEntity_GetIndex( pGlobalname ) >= 0 ? true : false; +} + +inline void GlobalEntity_SetState( string_t globalname, GLOBALESTATE state ) +{ + GlobalEntity_SetState( GlobalEntity_GetIndex( globalname ), state ); +} + +inline void GlobalEntity_SetMap( string_t globalname, string_t mapname ) +{ + GlobalEntity_SetMap( GlobalEntity_GetIndex( globalname ), mapname ); +} + +inline GLOBALESTATE GlobalEntity_GetState( string_t globalname ) +{ + return GlobalEntity_GetState( GlobalEntity_GetIndex( globalname ) ); +} + +inline GLOBALESTATE GlobalEntity_GetState( const char *pGlobalName ) +{ + return GlobalEntity_GetState( GlobalEntity_GetIndex( pGlobalName ) ); +} + +inline int GlobalEntity_GetCounter( string_t globalname ) +{ + return GlobalEntity_GetCounter( GlobalEntity_GetIndex( globalname ) ); +} + +inline int GlobalEntity_GetCounter( const char *pGlobalName ) +{ + return GlobalEntity_GetCounter( GlobalEntity_GetIndex( pGlobalName ) ); +} + +inline void GlobalEntity_SetCounter( string_t globalname, int counter ) +{ + GlobalEntity_SetCounter( GlobalEntity_GetIndex( globalname ), counter ); +} + +inline void GlobalEntity_SetCounter( const char *pGlobalName, int counter ) +{ + GlobalEntity_SetCounter( GlobalEntity_GetIndex( pGlobalName ), counter ); +} + +inline int GlobalEntity_AddToCounter( string_t globalname, int delta ) +{ + return GlobalEntity_AddToCounter( GlobalEntity_GetIndex( globalname ), delta ); +} + +inline int GlobalEntity_AddToCounter( const char *pGlobalName, int delta ) +{ + return GlobalEntity_AddToCounter( GlobalEntity_GetIndex( pGlobalName ), delta ); +} + +inline GLOBALESTATE GlobalEntity_GetStateByIndex( int iIndex ) +{ + return GlobalEntity_GetState( iIndex ); +} + +void ResetGlobalState( void ); + +#endif // GLOBALSTATE_H diff --git a/sp/src/game/server/globalstate_private.h b/sp/src/game/server/globalstate_private.h new file mode 100644 index 00000000..fb3f8f9e --- /dev/null +++ b/sp/src/game/server/globalstate_private.h @@ -0,0 +1,15 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef GLOBALSTATE_PRIVATE_H +#define GLOBALSTATE_PRIVATE_H +#ifdef _WIN32 +#pragma once +#endif + + +#endif // GLOBALSTATE_PRIVATE_H diff --git a/sp/src/game/server/grenadethrown.cpp b/sp/src/game/server/grenadethrown.cpp new file mode 100644 index 00000000..ed688027 --- /dev/null +++ b/sp/src/game/server/grenadethrown.cpp @@ -0,0 +1,81 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* + +===== grenade_base.cpp ======================================================== + + Base Handling for all the player's grenades + +*/ +#include "cbase.h" +#include "grenadethrown.h" +#include "ammodef.h" +#include "vstdlib/random.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Precaches a grenade and ensures clients know of it's "ammo" +void UTIL_PrecacheOtherGrenade( const char *szClassname ) +{ + CBaseEntity *pEntity = CreateEntityByName( szClassname ); + if ( !pEntity ) + { + Msg( "NULL Ent in UTIL_PrecacheOtherGrenade\n" ); + return; + } + + CThrownGrenade *pGrenade = dynamic_cast( pEntity ); + + if (pGrenade) + { + pGrenade->Precache( ); + } + + UTIL_Remove( pEntity ); +} + +//----------------------------------------------------------------------------- +// Purpose: Setup basic values for Thrown grens +//----------------------------------------------------------------------------- +void CThrownGrenade::Spawn( void ) +{ + // point sized, solid, bouncing + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE ); + SetSolid( SOLID_BBOX ); + UTIL_SetSize(this, vec3_origin, vec3_origin); + + // Movement + SetGravity( UTIL_ScaleForGravity( 648 ) ); + SetFriction(0.6); + QAngle angles; + VectorAngles( GetAbsVelocity(), angles ); + SetLocalAngles( angles ); + QAngle vecAngVel( random->RandomFloat ( -100, -500 ), 0, 0 ); + SetLocalAngularVelocity( vecAngVel ); + + SetTouch( &CThrownGrenade::BounceTouch ); +} + +//----------------------------------------------------------------------------- +// Purpose: Throw the grenade. +// Input : vecOrigin - Starting position +// vecVelocity - Starting velocity +// flExplodeTime - Time at which to detonate +//----------------------------------------------------------------------------- +void CThrownGrenade::Thrown( Vector vecOrigin, Vector vecVelocity, float flExplodeTime ) +{ + // Throw + SetLocalOrigin( vecOrigin ); + SetAbsVelocity( vecVelocity ); + + // Explode in 3 seconds + SetThink( &CThrownGrenade::Detonate ); + SetNextThink( gpGlobals->curtime + flExplodeTime ); +} + diff --git a/sp/src/game/server/grenadethrown.h b/sp/src/game/server/grenadethrown.h new file mode 100644 index 00000000..361f13f1 --- /dev/null +++ b/sp/src/game/server/grenadethrown.h @@ -0,0 +1,38 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Header file for player-thrown grenades. +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef GRENADE_BASE_H +#define GRENADE_BASE_H +#pragma once + +#include "basegrenade_shared.h" + +class CSprite; + +#define GRENADE_TIMER 5 // Try 5 seconds instead of 3? + +//----------------------------------------------------------------------------- +// Purpose: Base Thrown-Grenade class +//----------------------------------------------------------------------------- +class CThrownGrenade : public CBaseGrenade +{ +public: + DECLARE_CLASS( CThrownGrenade, CBaseGrenade ); + + void Spawn( void ); + void Thrown( Vector vecOrigin, Vector vecVelocity, float flExplodeTime ); +}; + + + +#endif // GRENADE_BASE_H diff --git a/sp/src/game/server/guntarget.cpp b/sp/src/game/server/guntarget.cpp new file mode 100644 index 00000000..cbae0f83 --- /dev/null +++ b/sp/src/game/server/guntarget.cpp @@ -0,0 +1,255 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements a moving target that moves along a path of path_tracks +// and can be shot and killed. When the target it killed it fires an +// OnDeath output. +// +// m_flSpeed is the travel speed +// m_iHealth is current health +// m_iMaxHealth is the amount to reset to each time it starts +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "entityoutput.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define FGUNTARGET_START_ON 0x0001 + + +class CGunTarget : public CBaseToggle +{ + DECLARE_CLASS( CGunTarget, CBaseToggle ); + +public: + + virtual void Spawn( void ); + virtual void Activate( void ); + bool CreateVPhysics( void ); + + virtual int BloodColor( void ) { return DONT_BLEED; } + +#if defined( HL2_DLL ) + virtual Class_T Classify( void ) { return CLASS_MILITARY; } +#elif defined( HL1_DLL ) + virtual Class_T Classify( void ) { return CLASS_MACHINE; } +#else + virtual Class_T Classify( void ) { return CLASS_NONE; } +#endif + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual Vector BodyTarget( const Vector &posSrc, bool bNoisy = true ) { return GetAbsOrigin(); } + + // Input handlers + void InputStart( inputdata_t &inputdata ); + void InputStop( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +protected: + + void Next( void ); + void Start( void ); + void Wait( void ); + void Stop( void ); + +private: + + bool m_on; + EHANDLE m_hTargetEnt; + + // Outputs + COutputEvent m_OnDeath; +}; + + +LINK_ENTITY_TO_CLASS( func_guntarget, CGunTarget ); + +BEGIN_DATADESC( CGunTarget ) + + DEFINE_FIELD( m_on, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hTargetEnt, FIELD_EHANDLE ), + + // Function Pointers + DEFINE_FUNCTION( Next ), + DEFINE_FUNCTION( Start ), + DEFINE_FUNCTION( Wait ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Start", InputStart ), + DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + + // Outputs + DEFINE_OUTPUT(m_OnDeath, "OnDeath"), + +END_DATADESC() + + + +void CGunTarget::Spawn( void ) +{ + SetSolid( SOLID_BSP ); + SetMoveType( MOVETYPE_PUSH ); + + SetModel( STRING( GetModelName() ) ); + + if ( m_flSpeed == 0 ) + m_flSpeed = 100; + + // Don't take damage until "on" + m_takedamage = DAMAGE_NO; + AddFlag( FL_NPC ); + + m_on = false; + m_iMaxHealth = m_iHealth; + + if ( HasSpawnFlags(FGUNTARGET_START_ON) ) + { + SetMoveDone( &CGunTarget::Start ); + SetMoveDoneTime( 0.3 ); + } + CreateVPhysics(); +} + + +bool CGunTarget::CreateVPhysics( void ) +{ + VPhysicsInitShadow( false, false ); + return true; +} + +void CGunTarget::Activate( void ) +{ + BaseClass::Activate(); + + CBaseEntity *pTarg; + // now find our next target + pTarg = GetNextTarget(); + if ( pTarg ) + { + m_hTargetEnt = pTarg; + Vector nextPos = pTarg->GetAbsOrigin(); + Teleport( &nextPos, NULL, NULL ); + } +} + + +void CGunTarget::Start( void ) +{ + m_takedamage = DAMAGE_YES; + AddFlag( FL_AIMTARGET ); + m_hTargetEnt = GetNextTarget(); + if ( m_hTargetEnt == NULL ) + return; + m_iHealth = m_iMaxHealth; + Next(); +} + + +void CGunTarget::Next( void ) +{ + SetThink( NULL ); + + m_hTargetEnt = GetNextTarget(); + CBaseEntity *pTarget = m_hTargetEnt; + + if ( !pTarget ) + { + Stop(); + return; + } + + SetMoveDone( &CGunTarget::Wait ); + LinearMove( pTarget->GetLocalOrigin(), m_flSpeed ); +} + + +void CGunTarget::Wait( void ) +{ + CBaseEntity *pTarget = m_hTargetEnt; + + if ( !pTarget ) + { + Stop(); + return; + } + + variant_t emptyVariant; + pTarget->AcceptInput( "InPass", this, this, emptyVariant, 0 ); + + m_flWait = pTarget->GetDelay(); + + m_target = pTarget->m_target; + SetMoveDone( &CGunTarget::Next ); + if (m_flWait != 0) + {// -1 wait will wait forever! + SetMoveDoneTime( m_flWait ); + } + else + { + Next();// do it RIGHT now! + } +} + + +void CGunTarget::Stop( void ) +{ + SetAbsVelocity( vec3_origin ); + SetMoveDoneTime( -1 ); + m_takedamage = DAMAGE_NO; +} + + +int CGunTarget::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( m_iHealth > 0 ) + { + m_iHealth -= info.GetDamage(); + if ( m_iHealth <= 0 ) + { + m_iHealth = 0; + Stop(); + + m_OnDeath.FireOutput( info.GetInflictor(), this ); + } + } + return 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that starts the target moving. +//----------------------------------------------------------------------------- +void CGunTarget::InputStart( inputdata_t &inputdata ) +{ + Start(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that stops the target from moving. +//----------------------------------------------------------------------------- +void CGunTarget::InputStop( inputdata_t &inputdata ) +{ + Stop(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that toggles the start/stop state of the target. +//----------------------------------------------------------------------------- +void CGunTarget::InputToggle( inputdata_t &inputdata ) +{ + if ( m_on ) + { + Stop(); + } + else + { + Start(); + } +} diff --git a/sp/src/game/server/h_ai.cpp b/sp/src/game/server/h_ai.cpp new file mode 100644 index 00000000..52dffc8e --- /dev/null +++ b/sp/src/game/server/h_ai.cpp @@ -0,0 +1,266 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Utility functions used by AI code. +// +//=============================================================================// + +#include "cbase.h" +#include "game.h" +#include "vstdlib/random.h" +#include "movevars_shared.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define NUM_LATERAL_CHECKS 13 // how many checks are made on each side of a NPC looking for lateral cover +#define NUM_LATERAL_LOS_CHECKS 6 // how many checks are made on each side of a NPC looking for lateral cover + +#define TOSS_HEIGHT_MAX 300 // altitude of initial trace done to see how high something can be tossed + +//float flRandom = random->RandomFloat(0,1); + +bool g_fDrawLines = FALSE; + + +//========================================================= +// FBoxVisible - a more accurate ( and slower ) version +// of FVisible. +// +// !!!UNDONE - make this CAI_BaseNPC? +//========================================================= +bool FBoxVisible( CBaseEntity *pLooker, CBaseEntity *pTarget, Vector &vecTargetOrigin, float flSize ) +{ + // don't look through water + if ((pLooker->GetWaterLevel() != 3 && pTarget->GetWaterLevel() == 3) + || (pLooker->GetWaterLevel() == 3 && pTarget->GetWaterLevel() == 0)) + return FALSE; + + trace_t tr; + Vector vecLookerOrigin = pLooker->EyePosition();//look through the NPC's 'eyes' + for (int i = 0; i < 5; i++) + { + Vector vecTarget = pTarget->GetAbsOrigin(); + vecTarget.x += random->RandomFloat( pTarget->WorldAlignMins().x + flSize, pTarget->WorldAlignMaxs().x - flSize); + vecTarget.y += random->RandomFloat( pTarget->WorldAlignMins().y + flSize, pTarget->WorldAlignMaxs().y - flSize); + vecTarget.z += random->RandomFloat( pTarget->WorldAlignMins().z + flSize, pTarget->WorldAlignMaxs().z - flSize); + + UTIL_TraceLine(vecLookerOrigin, vecTarget, MASK_BLOCKLOS, pLooker, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction == 1.0) + { + vecTargetOrigin = vecTarget; + return TRUE;// line of sight is valid. + } + } + return FALSE;// Line of sight is not established +} + + + +//----------------------------------------------------------------------------- +// Purpose: Returns the correct toss velocity to throw a given object at a point. +// Like the other version of VecCheckToss, but allows you to filter for any +// number of entities to ignore. +// Input : pEntity - The object doing the throwing. Is *NOT* automatically included in the +// filter below. +// pFilter - A trace filter of entities to ignore in the object's collision sweeps. +// It is recommended to include at least the thrower. +// vecSpot1 - The point from which the object is being thrown. +// vecSpot2 - The point TO which the object is being thrown. +// flHeightMaxRatio - A scale factor indicating the maximum ratio of height +// to total throw distance, measured from the higher of the two endpoints to +// the apex. -1 indicates that there is no maximum. +// flGravityAdj - Scale factor for gravity - should match the gravity scale +// that the object will use in midair. +// bRandomize - when true, introduces a little fudge to the throw +// Output : Velocity to throw the object with. +//----------------------------------------------------------------------------- +Vector VecCheckToss( CBaseEntity *pEntity, ITraceFilter *pFilter, Vector vecSpot1, Vector vecSpot2, float flHeightMaxRatio, float flGravityAdj, bool bRandomize, Vector *vecMins, Vector *vecMaxs ) +{ + trace_t tr; + Vector vecMidPoint;// halfway point between Spot1 and Spot2 + Vector vecApex;// highest point + Vector vecScale; + Vector vecTossVel; + Vector vecTemp; + float flGravity = GetCurrentGravity() * flGravityAdj; + + if (vecSpot2.z - vecSpot1.z > 500) + { + // to high, fail + return vec3_origin; + } + + Vector forward, right; + AngleVectors( pEntity->GetLocalAngles(), &forward, &right, NULL ); + + if (bRandomize) + { + // toss a little bit to the left or right, not right down on the enemy's bean (head). + vecSpot2 += right * ( random->RandomFloat(-8,8) + random->RandomFloat(-16,16) ); + vecSpot2 += forward * ( random->RandomFloat(-8,8) + random->RandomFloat(-16,16) ); + } + + // calculate the midpoint and apex of the 'triangle' + // UNDONE: normalize any Z position differences between spot1 and spot2 so that triangle is always RIGHT + // get a rough idea of how high it can be thrown + vecMidPoint = vecSpot1 + (vecSpot2 - vecSpot1) * 0.5; + UTIL_TraceLine(vecMidPoint, vecMidPoint + Vector(0,0,TOSS_HEIGHT_MAX), MASK_SOLID_BRUSHONLY, pFilter, &tr); + vecMidPoint = tr.endpos; + + if( tr.fraction != 1.0 ) + { + // (subtract 15 so the object doesn't hit the ceiling) + vecMidPoint.z -= 15; + } + + if (flHeightMaxRatio != -1) + { + // But don't throw so high that it looks silly. Maximize the height of the + // throw above the highest of the two endpoints to a ratio of the throw length. + float flHeightMax = flHeightMaxRatio * (vecSpot2 - vecSpot1).Length(); + float flHighestEndZ = MAX(vecSpot1.z, vecSpot2.z); + if ((vecMidPoint.z - flHighestEndZ) > flHeightMax) + { + vecMidPoint.z = flHighestEndZ + flHeightMax; + } + } + + if (vecMidPoint.z < vecSpot1.z || vecMidPoint.z < vecSpot2.z) + { + // Not enough space, fail + return vec3_origin; + } + + // How high should the object travel to reach the apex + float distance1 = (vecMidPoint.z - vecSpot1.z); + float distance2 = (vecMidPoint.z - vecSpot2.z); + + // How long will it take for the object to travel this distance + float time1 = sqrt( distance1 / (0.5 * flGravity) ); + float time2 = sqrt( distance2 / (0.5 * flGravity) ); + + if (time1 < 0.1) + { + // too close + return vec3_origin; + } + + // how hard to throw sideways to get there in time. + vecTossVel = (vecSpot2 - vecSpot1) / (time1 + time2); + + // how hard upwards to reach the apex at the right time. + vecTossVel.z = flGravity * time1; + + // find the apex + vecApex = vecSpot1 + vecTossVel * time1; + vecApex.z = vecMidPoint.z; + + // JAY: Repro behavior from HL1 -- toss check went through gratings + UTIL_TraceLine(vecSpot1, vecApex, (MASK_SOLID&(~CONTENTS_GRATE)), pFilter, &tr); + if (tr.fraction != 1.0) + { + // fail! + return vec3_origin; + } + + // UNDONE: either ignore NPCs or change it to not care if we hit our enemy + UTIL_TraceLine(vecSpot2, vecApex, (MASK_SOLID_BRUSHONLY&(~CONTENTS_GRATE)), pFilter, &tr); + if (tr.fraction != 1.0) + { + // fail! + return vec3_origin; + } + + if ( vecMins && vecMaxs ) + { + // Check to ensure the entity's hull can travel the first half of the grenade throw + UTIL_TraceHull( vecSpot1, vecApex, *vecMins, *vecMaxs, (MASK_SOLID&(~CONTENTS_GRATE)), pFilter, &tr); + if ( tr.fraction < 1.0 ) + return vec3_origin; + } + + return vecTossVel; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Returns the correct toss velocity to throw a given object at a point. +// Input : pEntity - The entity that is throwing the object. +// vecSpot1 - The point from which the object is being thrown. +// vecSpot2 - The point TO which the object is being thrown. +// flHeightMaxRatio - A scale factor indicating the maximum ratio of height +// to total throw distance, measured from the higher of the two endpoints to +// the apex. -1 indicates that there is no maximum. +// flGravityAdj - Scale factor for gravity - should match the gravity scale +// that the object will use in midair. +// bRandomize - when true, introduces a little fudge to the throw +// Output : Velocity to throw the object with. +//----------------------------------------------------------------------------- +Vector VecCheckToss( CBaseEntity *pEntity, Vector vecSpot1, Vector vecSpot2, float flHeightMaxRatio, float flGravityAdj, bool bRandomize, Vector *vecMins, Vector *vecMaxs ) +{ + // construct a filter and call through to the other version of this function. + CTraceFilterSimple traceFilter( pEntity, COLLISION_GROUP_NONE ); + return VecCheckToss( pEntity, &traceFilter, vecSpot1, vecSpot2, + flHeightMaxRatio, flGravityAdj, bRandomize, + vecMins, vecMaxs ); +} + +// +// VecCheckThrow - returns the velocity vector at which an object should be thrown from vecspot1 to hit vecspot2. +// returns vec3_origin if throw is not feasible. +// +Vector VecCheckThrow ( CBaseEntity *pEdict, const Vector &vecSpot1, Vector vecSpot2, float flSpeed, float flGravityAdj, Vector *vecMins, Vector *vecMaxs ) +{ + float flGravity = GetCurrentGravity() * flGravityAdj; + + Vector vecGrenadeVel = (vecSpot2 - vecSpot1); + + // throw at a constant time + float time = vecGrenadeVel.Length( ) / flSpeed; + vecGrenadeVel = vecGrenadeVel * (1.0 / time); + + // adjust upward toss to compensate for gravity loss + vecGrenadeVel.z += flGravity * time * 0.5; + + Vector vecApex = vecSpot1 + (vecSpot2 - vecSpot1) * 0.5; + vecApex.z += 0.5 * flGravity * (time * 0.5) * (time * 0.5); + + + trace_t tr; + UTIL_TraceLine(vecSpot1, vecApex, MASK_SOLID, pEdict, COLLISION_GROUP_NONE, &tr); + if (tr.fraction != 1.0) + { + // fail! + //NDebugOverlay::Line( vecSpot1, vecApex, 255, 0, 0, true, 5.0 ); + return vec3_origin; + } + + //NDebugOverlay::Line( vecSpot1, vecApex, 0, 255, 0, true, 5.0 ); + + UTIL_TraceLine(vecSpot2, vecApex, MASK_SOLID_BRUSHONLY, pEdict, COLLISION_GROUP_NONE, &tr); + if (tr.fraction != 1.0) + { + // fail! + //NDebugOverlay::Line( vecApex, vecSpot2, 255, 0, 0, true, 5.0 ); + return vec3_origin; + } + + //NDebugOverlay::Line( vecApex, vecSpot2, 0, 255, 0, true, 5.0 ); + + if ( vecMins && vecMaxs ) + { + // Check to ensure the entity's hull can travel the first half of the grenade throw + UTIL_TraceHull( vecSpot1, vecApex, *vecMins, *vecMaxs, MASK_SOLID, pEdict, COLLISION_GROUP_NONE, &tr); + if ( tr.fraction < 1.0 ) + { + //NDebugOverlay::SweptBox( vecSpot1, tr.endpos, *vecMins, *vecMaxs, vec3_angle, 255, 0, 0, 64, 5.0 ); + return vec3_origin; + } + } + + //NDebugOverlay::SweptBox( vecSpot1, vecApex, *vecMins, *vecMaxs, vec3_angle, 0, 255, 0, 64, 5.0 ); + + return vecGrenadeVel; +} diff --git a/sp/src/game/server/h_cycler.cpp b/sp/src/game/server/h_cycler.cpp new file mode 100644 index 00000000..973deec2 --- /dev/null +++ b/sp/src/game/server/h_cycler.cpp @@ -0,0 +1,518 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: The Halflife Cycler NPCs +// +//=============================================================================// + +#include "cbase.h" +#include "ai_basenpc.h" +#include "ai_motor.h" +#include "basecombatweapon.h" +#include "animation.h" +#include "vstdlib/random.h" +#include "h_cycler.h" +#include "Sprite.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define FCYCLER_NOTSOLID 0x0001 + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +BEGIN_DATADESC( CCycler ) + + // Fields + DEFINE_FIELD( m_animate, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetSequence", InputSetSequence ), + +END_DATADESC() + +// +// we should get rid of all the other cyclers and replace them with this. +// +class CGenericCycler : public CCycler +{ +public: + DECLARE_CLASS( CGenericCycler, CCycler ); + + void Spawn() + { + GenericCyclerSpawn( (char *)STRING( GetModelName() ), Vector(-16, -16, 0), Vector(16, 16, 72) ); + } +}; +LINK_ENTITY_TO_CLASS( cycler, CGenericCycler ); +LINK_ENTITY_TO_CLASS( model_studio, CGenericCycler ); // For now model_studios build as cyclers. + + +// Cycler member functions + +void CCycler::GenericCyclerSpawn(char *szModel, Vector vecMin, Vector vecMax) +{ + if (!szModel || !*szModel) + { + Warning( "cycler at %.0f %.0f %0.f missing modelname\n", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + UTIL_Remove( this ); + return; + } + + Precache(); + + SetModel( szModel ); + + m_bloodColor = DONT_BLEED; + + CCycler::Spawn( ); + + UTIL_SetSize(this, vecMin, vecMax); +} + + +void CCycler::Precache() +{ + PrecacheModel( (const char *)STRING( GetModelName() ) ); +} + + +void CCycler::Spawn( ) +{ + InitBoneControllers(); + + SetSolid( SOLID_BBOX ); + if ( m_spawnflags & FCYCLER_NOTSOLID ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + else + { + AddSolidFlags( FSOLID_NOT_STANDABLE ); + } + + SetMoveType( MOVETYPE_NONE ); + m_takedamage = DAMAGE_YES; + m_iHealth = 80000;// no cycler should die + GetMotor()->SetIdealYaw( GetLocalAngles().y ); + GetMotor()->SnapYaw(); + + m_flPlaybackRate = 1.0; + m_flGroundSpeed = 0; + + SetNextThink( gpGlobals->curtime + 1.0f ); + + ResetSequenceInfo( ); + + if (GetSequence() != 0 || m_flCycle != 0) + { +#ifdef TF2_DLL + m_animate = 1; +#else + m_animate = 0; + m_flPlaybackRate = 0; +#endif + } + else + { + m_animate = 1; + } +} + + + + +// +// cycler think +// +void CCycler::Think( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + if (m_animate) + { + StudioFrameAdvance ( ); + DispatchAnimEvents( this ); + } + if (IsSequenceFinished() && !SequenceLoops()) + { + // ResetSequenceInfo(); + // hack to avoid reloading model every frame + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 1.0; + m_bSequenceFinished = false; + m_flLastEventCheck = 0; + m_flCycle = 0; + if (!m_animate) + m_flPlaybackRate = 0.0; // FIX: don't reset framerate + } +} + +// +// CyclerUse - starts a rotation trend +// +void CCycler::Use ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + m_animate = !m_animate; + if (m_animate) + m_flPlaybackRate = 1.0; + else + m_flPlaybackRate = 0.0; +} + +//----------------------------------------------------------------------------- +// Purpose: Changes sequences when hurt. +//----------------------------------------------------------------------------- +int CCycler::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if (m_animate) + { + int nSequence = GetSequence() + 1; + if ( !IsValidSequence(nSequence) ) + { + nSequence = 0; + } + + ResetSequence( nSequence ); + m_flCycle = 0; + } + else + { + m_flPlaybackRate = 1.0; + StudioFrameAdvance (); + m_flPlaybackRate = 0; + Msg( "sequence: %d, frame %.0f\n", GetSequence(), m_flCycle.Get() ); + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Input that sets the sequence of the cycler +//----------------------------------------------------------------------------- +void CCycler::InputSetSequence( inputdata_t &inputdata ) +{ + if (m_animate) + { + // Legacy support: Try it as a number, and support '0' + const char *sChar = inputdata.value.String(); + int iSeqNum = atoi( sChar ); + if ( !iSeqNum && sChar[0] != '0' ) + { + // Treat it as a sequence name + ResetSequence( LookupSequence( sChar ) ); + } + else + { + ResetSequence( iSeqNum ); + } + + if (m_flPlaybackRate == 0.0) + { + ResetSequence( 0 ); + } + + m_flCycle = 0; + } +} + +// FIXME: this doesn't work anymore, and hasn't for a while now. + +class CWeaponCycler : public CBaseCombatWeapon +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( CWeaponCycler, CBaseCombatWeapon ); + + DECLARE_SERVERCLASS(); + + void Spawn( void ); + + void PrimaryAttack( void ); + void SecondaryAttack( void ); + bool Deploy( void ); + bool Holster( CBaseCombatWeapon *pSwitchingTo = NULL ); + string_t m_iszModel; + int m_iModel; +}; + +IMPLEMENT_SERVERCLASS_ST(CWeaponCycler, DT_WeaponCycler) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( cycler_weapon, CWeaponCycler ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CWeaponCycler ) + + DEFINE_FIELD( m_iszModel, FIELD_STRING ), + DEFINE_FIELD( m_iModel, FIELD_INTEGER ), + +END_DATADESC() + +void CWeaponCycler::Spawn( ) +{ + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_NONE ); + + PrecacheModel( STRING( GetModelName() ) ); + SetModel( STRING( GetModelName() ) ); + m_iszModel = GetModelName(); + m_iModel = GetModelIndex(); + + UTIL_SetSize(this, Vector(-16, -16, 0), Vector(16, 16, 16)); + SetTouch( &CWeaponCycler::DefaultTouch ); +} + + + +bool CWeaponCycler::Deploy( ) +{ + CBaseCombatCharacter *pOwner = GetOwner(); + + if (pOwner) + { + pOwner->m_flNextAttack = gpGlobals->curtime + 1.0; + SendWeaponAnim( 0 ); + m_iClip1 = 0; + m_iClip2 = 0; + return true; + } + return false; +} + + +bool CWeaponCycler::Holster( CBaseCombatWeapon *pSwitchingTo ) +{ + CBaseCombatCharacter *pOwner = GetOwner(); + if (pOwner) + { + pOwner->m_flNextAttack = gpGlobals->curtime + 0.5; + } + + return true; +} + + +void CWeaponCycler::PrimaryAttack() +{ + SendWeaponAnim( GetSequence() ); + m_flNextPrimaryAttack = gpGlobals->curtime + 0.3; +} + + +void CWeaponCycler::SecondaryAttack( void ) +{ + float flFrameRate; + + int nSequence = (GetSequence() + 1) % 8; + + // BUG: Why do we set this here and then set to zero right after? + SetModelIndex( m_iModel ); + flFrameRate = 0.0; + + SetModelIndex( 0 ); + + if (flFrameRate == 0.0) + { + nSequence = 0; + } + + SetSequence( nSequence ); + SendWeaponAnim( nSequence ); + + m_flNextSecondaryAttack = gpGlobals->curtime + 0.3; +} + + + +// Flaming Wreakage +class CWreckage : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CWreckage, CAI_BaseNPC ); + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + void Think( void ); + + float m_flStartTime; + float m_flDieTime; +}; + +BEGIN_DATADESC( CWreckage ) + + DEFINE_FIELD( m_flStartTime, FIELD_TIME ), + DEFINE_FIELD( m_flDieTime, FIELD_TIME ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( cycler_wreckage, CWreckage ); + +void CWreckage::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + m_takedamage = 0; + + SetCycle( 0 ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + if (GetModelName() != NULL_STRING) + { + PrecacheModel( STRING( GetModelName() ) ); + SetModel( STRING( GetModelName() ) ); + } + + m_flStartTime = gpGlobals->curtime; +} + +void CWreckage::Precache( ) +{ + if ( GetModelName() != NULL_STRING ) + PrecacheModel( STRING( GetModelName() ) ); +} + +void CWreckage::Think( void ) +{ + StudioFrameAdvance( ); + SetNextThink( gpGlobals->curtime + 0.2 ); + + if (m_flDieTime) + { + if (m_flDieTime < gpGlobals->curtime) + { + UTIL_Remove( this ); + return; + } + else if (random->RandomFloat( 0, m_flDieTime - m_flStartTime ) > m_flDieTime - gpGlobals->curtime) + { + return; + } + } + + Vector vecSrc; + CollisionProp()->RandomPointInBounds( vec3_origin, Vector(1, 1, 1), &vecSrc ); + CPVSFilter filter( vecSrc ); + te->Smoke( filter, 0.0, + &vecSrc, g_sModelIndexSmoke, + random->RandomFloat(0,4.9) + 5.0, + random->RandomInt(0, 3) + 8 ); +} + +// BlendingCycler +// Used to demonstrate animation blending +class CBlendingCycler : public CCycler +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( CBlendingCycler, CCycler ); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void Think( void ); + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() | FCAP_DONT_SAVE | FCAP_IMPULSE_USE); } + + int m_iLowerBound; + int m_iUpperBound; + int m_iCurrent; + int m_iBlendspeed; + string_t m_iszSequence; +}; +LINK_ENTITY_TO_CLASS( cycler_blender, CBlendingCycler ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CBlendingCycler ) + + DEFINE_FIELD( m_iLowerBound, FIELD_INTEGER ), + DEFINE_FIELD( m_iUpperBound, FIELD_INTEGER ), + DEFINE_FIELD( m_iCurrent, FIELD_INTEGER ), + DEFINE_FIELD( m_iBlendspeed, FIELD_INTEGER ), + DEFINE_FIELD( m_iszSequence, FIELD_STRING ), + +END_DATADESC() + +void CBlendingCycler::Spawn( void ) +{ + // Remove if it's not blending + if (m_iLowerBound == 0 && m_iUpperBound == 0) + { + UTIL_Remove( this ); + return; + } + + GenericCyclerSpawn( (char *)STRING( GetModelName() ), Vector(-16,-16,-16), Vector(16,16,16)); + if (!m_iBlendspeed) + m_iBlendspeed = 5; + + // Initialise Sequence + if (m_iszSequence != NULL_STRING) + { + SetSequence( LookupSequence( STRING(m_iszSequence) ) ); + } + + m_iCurrent = m_iLowerBound; +} + +bool CBlendingCycler::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "lowboundary")) + { + m_iLowerBound = atoi(szValue); + } + else if (FStrEq(szKeyName, "highboundary")) + { + m_iUpperBound = atoi(szValue); + } + else if (FStrEq(szKeyName, "blendspeed")) + { + m_iBlendspeed = atoi(szValue); + } + else if (FStrEq(szKeyName, "blendsequence")) + { + m_iszSequence = AllocPooledString(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +// Blending Cycler think +void CBlendingCycler::Think( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + // Move + m_iCurrent += m_iBlendspeed; + if ( (m_iCurrent > m_iUpperBound) || (m_iCurrent < m_iLowerBound) ) + m_iBlendspeed = m_iBlendspeed * -1; + + // Set blend + SetPoseParameter( 0, m_iCurrent ); + + Msg( "Current Blend: %d\n", m_iCurrent ); + + if (IsSequenceFinished() && !SequenceLoops()) + { + // ResetSequenceInfo(); + // hack to avoid reloading model every frame + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 1.0; + m_bSequenceFinished = false; + m_flLastEventCheck = 0; + m_flCycle = 0; + if (!m_animate) + { + m_flPlaybackRate = 0.0; // FIX: don't reset framerate + } + } +} + + diff --git a/sp/src/game/server/h_cycler.h b/sp/src/game/server/h_cycler.h new file mode 100644 index 00000000..2bc7a2a1 --- /dev/null +++ b/sp/src/game/server/h_cycler.h @@ -0,0 +1,41 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef H_CYCLER_H +#define H_CYCLER_H +#ifdef _WIN32 +#pragma once +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CCycler : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CCycler, CAI_BaseNPC ); + + void GenericCyclerSpawn(char *szModel, Vector vecMin, Vector vecMax); + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() | FCAP_IMPULSE_USE); } + int OnTakeDamage( const CTakeDamageInfo &info ); + void Spawn( void ); + void Precache( void ); + void Think( void ); + //void Pain( float flDamage ); + void Use ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + // Don't treat as a live target + virtual bool IsAlive( void ) { return false; } + + // Inputs + void InputSetSequence( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + int m_animate; +}; + +#endif // H_CYCLER_H diff --git a/sp/src/game/server/h_export.cpp b/sp/src/game/server/h_export.cpp new file mode 100644 index 00000000..fbd7c508 --- /dev/null +++ b/sp/src/game/server/h_export.cpp @@ -0,0 +1,48 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* + +===== h_export.cpp ======================================================== + + Entity classes exported by Halflife. + +*/ + +#if defined(_WIN32) && !defined(_XBOX) + +#include "winlite.h" +#include "datamap.h" +#include "tier0/dbg.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +HMODULE win32DLLHandle; + +// Required DLL entry point +BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved ) +{ + // ensure data sizes are stable + if ( sizeof(inputfunc_t) != sizeof(int) ) + { + Assert( sizeof(inputfunc_t) == sizeof(int) ); + return FALSE; + } + + if ( fdwReason == DLL_PROCESS_ATTACH ) + { + win32DLLHandle = hinstDLL; + } + else if ( fdwReason == DLL_PROCESS_DETACH ) + { + } + return TRUE; +} + +#endif + diff --git a/sp/src/game/server/hierarchy.cpp b/sp/src/game/server/hierarchy.cpp new file mode 100644 index 00000000..ae708703 --- /dev/null +++ b/sp/src/game/server/hierarchy.cpp @@ -0,0 +1,169 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Contains the set of functions for manipulating entity hierarchies. +// +// $NoKeywords: $ +//=============================================================================// + +// UNDONE: Reconcile this with SetParent() + +#include "cbase.h" +#include "hierarchy.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Does the linked list work of removing a child object from the hierarchy. +// Input : pParent - +// pChild - +//----------------------------------------------------------------------------- +void UnlinkChild( CBaseEntity *pParent, CBaseEntity *pChild ) +{ + CBaseEntity *pList; + EHANDLE *pPrev; + + pList = pParent->m_hMoveChild; + pPrev = &pParent->m_hMoveChild; + while ( pList ) + { + CBaseEntity *pNext = pList->m_hMovePeer; + if ( pList == pChild ) + { + // patch up the list + pPrev->Set( pNext ); + + // Clear hierarchy bits for this guy + pList->m_hMoveParent.Set( NULL ); + pList->m_hMovePeer.Set( NULL ); + pList->NetworkProp()->SetNetworkParent( CBaseHandle() ); + pList->DispatchUpdateTransmitState(); + pList->OnEntityEvent( ENTITY_EVENT_PARENT_CHANGED, NULL ); + + pParent->RecalcHasPlayerChildBit(); + return; + } + else + { + pPrev = &pList->m_hMovePeer; + pList = pNext; + } + } + + // This only happens if the child wasn't found in the parent's child list + Assert(0); +} + +void LinkChild( CBaseEntity *pParent, CBaseEntity *pChild ) +{ + EHANDLE hParent; + hParent.Set( pParent ); + pChild->m_hMovePeer.Set( pParent->FirstMoveChild() ); + pParent->m_hMoveChild.Set( pChild ); + pChild->m_hMoveParent = hParent; + pChild->NetworkProp()->SetNetworkParent( hParent ); + pChild->DispatchUpdateTransmitState(); + pChild->OnEntityEvent( ENTITY_EVENT_PARENT_CHANGED, NULL ); + pParent->RecalcHasPlayerChildBit(); +} + +void TransferChildren( CBaseEntity *pOldParent, CBaseEntity *pNewParent ) +{ + CBaseEntity *pChild = pOldParent->FirstMoveChild(); + while ( pChild ) + { + // NOTE: Have to do this before the unlink to ensure local coords are valid + Vector vecAbsOrigin = pChild->GetAbsOrigin(); + QAngle angAbsRotation = pChild->GetAbsAngles(); + Vector vecAbsVelocity = pChild->GetAbsVelocity(); +// QAngle vecAbsAngVelocity = pChild->GetAbsAngularVelocity(); + + UnlinkChild( pOldParent, pChild ); + LinkChild( pNewParent, pChild ); + + // FIXME: This is a hack to guarantee update of the local origin, angles, etc. + pChild->m_vecAbsOrigin.Init( FLT_MAX, FLT_MAX, FLT_MAX ); + pChild->m_angAbsRotation.Init( FLT_MAX, FLT_MAX, FLT_MAX ); + pChild->m_vecAbsVelocity.Init( FLT_MAX, FLT_MAX, FLT_MAX ); + + pChild->SetAbsOrigin(vecAbsOrigin); + pChild->SetAbsAngles(angAbsRotation); + pChild->SetAbsVelocity(vecAbsVelocity); +// pChild->SetAbsAngularVelocity(vecAbsAngVelocity); + + pChild = pOldParent->FirstMoveChild(); + } +} + +void UnlinkFromParent( CBaseEntity *pRemove ) +{ + if ( pRemove->GetMoveParent() ) + { + // NOTE: Have to do this before the unlink to ensure local coords are valid + Vector vecAbsOrigin = pRemove->GetAbsOrigin(); + QAngle angAbsRotation = pRemove->GetAbsAngles(); + Vector vecAbsVelocity = pRemove->GetAbsVelocity(); +// QAngle vecAbsAngVelocity = pRemove->GetAbsAngularVelocity(); + + UnlinkChild( pRemove->GetMoveParent(), pRemove ); + + pRemove->SetLocalOrigin(vecAbsOrigin); + pRemove->SetLocalAngles(angAbsRotation); + pRemove->SetLocalVelocity(vecAbsVelocity); +// pRemove->SetLocalAngularVelocity(vecAbsAngVelocity); + pRemove->UpdateWaterState(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Clears the parent of all the children of the given object. +//----------------------------------------------------------------------------- +void UnlinkAllChildren( CBaseEntity *pParent ) +{ + CBaseEntity *pChild = pParent->FirstMoveChild(); + while ( pChild ) + { + CBaseEntity *pNext = pChild->NextMovePeer(); + UnlinkFromParent( pChild ); + pChild = pNext; + } +} + +bool EntityIsParentOf( CBaseEntity *pParent, CBaseEntity *pEntity ) +{ + while ( pEntity->GetMoveParent() ) + { + pEntity = pEntity->GetMoveParent(); + if ( pParent == pEntity ) + return true; + } + + return false; +} + +static void GetAllChildren_r( CBaseEntity *pEntity, CUtlVector &list ) +{ + for ( ; pEntity != NULL; pEntity = pEntity->NextMovePeer() ) + { + list.AddToTail( pEntity ); + GetAllChildren_r( pEntity->FirstMoveChild(), list ); + } +} + +int GetAllChildren( CBaseEntity *pParent, CUtlVector &list ) +{ + if ( !pParent ) + return 0; + + GetAllChildren_r( pParent->FirstMoveChild(), list ); + return list.Count(); +} + +int GetAllInHierarchy( CBaseEntity *pParent, CUtlVector &list ) +{ + if (!pParent) + return 0; + list.AddToTail( pParent ); + return GetAllChildren( pParent, list ) + 1; +} diff --git a/sp/src/game/server/hierarchy.h b/sp/src/game/server/hierarchy.h new file mode 100644 index 00000000..874f5ada --- /dev/null +++ b/sp/src/game/server/hierarchy.h @@ -0,0 +1,26 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Contains the set of functions for manipulating entity hierarchies. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef HIERARCHY_H +#define HIERARCHY_H +#ifdef _WIN32 +#pragma once +#endif + +#include "utlvector.h" + +class CBaseEntity; + +void UnlinkFromParent( CBaseEntity *pRemove ); +void TransferChildren( CBaseEntity *pOldParent, CBaseEntity *pNewParent ); +void LinkChild( CBaseEntity *pParent, CBaseEntity *pChild ); +void UnlinkAllChildren( CBaseEntity *pParent ); +int GetAllChildren( CBaseEntity *pParent, CUtlVector &list ); +bool EntityIsParentOf( CBaseEntity *pParent, CBaseEntity *pEntity ); +int GetAllInHierarchy( CBaseEntity *pParent, CUtlVector &list ); + +#endif // HIERARCHY_H diff --git a/sp/src/game/server/hl1_CBaseHelicopter.h b/sp/src/game/server/hl1_CBaseHelicopter.h new file mode 100644 index 00000000..01586126 --- /dev/null +++ b/sp/src/game/server/hl1_CBaseHelicopter.h @@ -0,0 +1,136 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +//--------------------------------------------------------- +// Helicopter flags +//--------------------------------------------------------- +enum HelicopterFlags_t +{ + BITS_HELICOPTER_GUN_ON = 0x00000001, // Gun is on and aiming + BITS_HELICOPTER_MISSILE_ON = 0x00000002, // Missile turrets are on and aiming +}; + + +//--------------------------------------------------------- +//--------------------------------------------------------- + +#define SF_NOWRECKAGE 0x08 +#define SF_NOROTORWASH 0x20 +#define SF_AWAITINPUT 0x40 + + +//--------------------------------------------------------- +//--------------------------------------------------------- +#define BASECHOPPER_MAX_SPEED 400.0f +#define BASECHOPPER_MAX_FIRING_SPEED 250.0f +#define BASECHOPPER_MIN_ROCKET_DIST 1000.0f +#define BASECHOPPER_MAX_GUN_DIST 2000.0f + + +//========================================================= +//========================================================= +class CBaseHelicopter : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CBaseHelicopter, CAI_BaseNPC ); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + void Spawn( void ); + void Precache( void ); + + void Event_Killed( const CTakeDamageInfo &info ); + void StopLoopingSounds(); + + int BloodColor( void ) { return DONT_BLEED; } + void GibMonster( void ); + + Class_T Classify ( void ) { return CLASS_HUMAN_MILITARY; } + + void CallDyingThink( void ) { DyingThink(); } + + bool HasEnemy( void ) { return GetEnemy() != NULL; } + void CheckEnemy( CBaseEntity *pEnemy ); + virtual bool ChooseEnemy( void ); + virtual void HelicopterThink( void ); + virtual void HelicopterPostThink( void ) { }; + virtual void FlyTouch( CBaseEntity *pOther ); + virtual void CrashTouch( CBaseEntity *pOther ); + virtual void DyingThink( void ); + virtual void Startup( void ); + virtual void NullThink( void ); + + virtual void Flight( void ); + + virtual void ShowDamage( void ) {}; + + virtual void FlyPathCorners( void ); + void UpdatePlayerDopplerShift( void ); + + virtual void Hunt( void ); + + virtual bool IsCrashing( void ) { return m_lifeState != LIFE_ALIVE; } + virtual float GetAcceleration( void ) { return 5; } + virtual bool HasReachedTarget( void ); + virtual void OnReachedTarget( CBaseEntity *pTarget ) {}; + + virtual void ApplySidewaysDrag( const Vector &vecRight ); + virtual void ApplyGeneralDrag( void ); + + + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + virtual bool FireGun( void ); + + virtual float GetRotorVolume( void ) { return 1.0; } + virtual void InitializeRotorSound( void ); + virtual void UpdateRotorSoundPitch( int iPitch ); + + virtual void AimRocketGun(void) {}; + virtual void FireRocket( Vector vLaunchPos, Vector vLaunchDir ) {}; + + void DrawDebugGeometryOverlays(void); + + CSoundPatch *m_pRotorSound; + + float m_flForce; + int m_fHelicopterFlags; + + Vector m_vecDesiredFaceDir; + Vector m_vecDesiredPosition; + + Vector m_vecGoalOrientation; // orientation of the goal entity. + + float m_flLastSeen; + float m_flPrevSeen; + + int m_iSoundState; // don't save this + + Vector m_vecTarget; + Vector m_vecTargetPosition; + + float m_flMaxSpeed; // Maximum speed of the helicopter. + float m_flMaxSpeedFiring; // Maximum speed of the helicopter whilst firing guns. + + float m_flGoalSpeed; // Goal speed + float m_flInitialSpeed; + float m_angleVelocity; + + void ChangePathCorner( const char *pszName ); + + // Inputs + void InputChangePathCorner( inputdata_t &inputdata ); + void InputActivate( inputdata_t &inputdata ); + + // Outputs + COutputEvent m_AtTarget; // Fired when pathcorner has been reached + COutputEvent m_LeaveTarget; // Fired when pathcorner is left + + float m_flNextCrashExplosion; +}; \ No newline at end of file diff --git a/sp/src/game/server/hltvdirector.cpp b/sp/src/game/server/hltvdirector.cpp new file mode 100644 index 00000000..34d69736 --- /dev/null +++ b/sp/src/game/server/hltvdirector.cpp @@ -0,0 +1,1184 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// hltvdirector.cpp: implementation of the CHLTVDirector class. +// +////////////////////////////////////////////////////////////////////// + +#include "cbase.h" +#include "hltvdirector.h" +#include "KeyValues.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + + +static ConVar tv_delay( "tv_delay", "30", 0, "SourceTV broadcast delay in seconds", true, 0, true, HLTV_MAX_DELAY ); +static ConVar tv_allow_static_shots( "tv_allow_static_shots", "1", 0, "Auto director uses fixed level cameras for shots" ); +static ConVar tv_allow_camera_man( "tv_allow_camera_man", "1", 0, "Auto director allows spectators to become camera man" ); + +static bool GameEventLessFunc( CHLTVGameEvent const &e1, CHLTVGameEvent const &e2 ) +{ + return e1.m_Tick < e2.m_Tick; +} + +#define RANDOM_MAX_ELEMENTS 256 +static int s_RndOrder[RANDOM_MAX_ELEMENTS]; +static void InitRandomOrder(int nFields) +{ + if ( nFields > RANDOM_MAX_ELEMENTS ) + { + Assert( nFields > RANDOM_MAX_ELEMENTS ); + nFields = RANDOM_MAX_ELEMENTS; + } + + for ( int i=0; i= 0 ); + + return a*a; // vectors are facing opposite direction +} + +#if !defined( CSTRIKE_DLL ) && !defined( DOD_DLL ) && !defined( TF_DLL )// add your mod here if you use your own director + +static CHLTVDirector s_HLTVDirector; // singleton + +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CHLTVDirector, IHLTVDirector, INTERFACEVERSION_HLTVDIRECTOR, s_HLTVDirector ); + +CHLTVDirector* HLTVDirector() +{ + return &s_HLTVDirector; +} + +IGameSystem* HLTVDirectorSystem() +{ + return &s_HLTVDirector; +} + +#endif // MODs + + + +CHLTVDirector::CHLTVDirector() +{ + m_iPVSEntity = 0; + m_fDelay = 30.0; + m_iLastPlayer = 1; + m_pHLTVServer = NULL; + m_pHLTVClient = NULL; + m_iCameraMan = 0; + m_nNumFixedCameras = 0; + m_EventHistory.SetLessFunc( GameEventLessFunc ); + m_nNextAnalyzeTick = 0; + m_iCameraManIndex = 0; +} + +CHLTVDirector::~CHLTVDirector() +{ + +} + +bool CHLTVDirector::Init() +{ + return gameeventmanager->LoadEventsFromFile( "resource/hltvevents.res" ) > 0; +} + +void CHLTVDirector::Shutdown() +{ + RemoveEventsFromHistory(-1); // all +} + +void CHLTVDirector::FireGameEvent( IGameEvent * event ) +{ + if ( !m_pHLTVServer ) + return; // don't do anything + + CHLTVGameEvent gameevent; + + gameevent.m_Event = gameeventmanager->DuplicateEvent( event ); + gameevent.m_Priority = event->GetInt( "priority", -1 ); // priorities are leveled between 0..10, -1 means ignore + gameevent.m_Tick = gpGlobals->tickcount; + + m_EventHistory.Insert( gameevent ); +} + +IHLTVServer* CHLTVDirector::GetHLTVServer( void ) +{ + return m_pHLTVServer; +} + +void CHLTVDirector::SetHLTVServer( IHLTVServer *hltv ) +{ + RemoveEventsFromHistory(-1); // all + + if ( hltv ) + { + m_pHLTVClient = UTIL_PlayerByIndex( hltv->GetHLTVSlot() + 1 ); + + if ( m_pHLTVClient && m_pHLTVClient->IsHLTV() ) + { + m_pHLTVServer = hltv; + } + else + { + m_pHLTVServer = NULL; + Error( "Couldn't find HLTV client player." ); + } + + // register for events the director needs to know + ListenForGameEvent( "player_hurt" ); + ListenForGameEvent( "player_death" ); + ListenForGameEvent( "round_end" ); + ListenForGameEvent( "round_start" ); + ListenForGameEvent( "hltv_cameraman" ); + ListenForGameEvent( "hltv_rank_entity" ); + ListenForGameEvent( "hltv_rank_camera" ); + } + else + { + // deactivate HLTV director + m_pHLTVServer = NULL; + } +} + +bool CHLTVDirector::IsActive( void ) +{ + return (m_pHLTVServer != NULL ); +} + +float CHLTVDirector::GetDelay( void ) +{ + return m_fDelay; +} + +int CHLTVDirector::GetDirectorTick( void ) +{ + // just simple delay it + return m_nBroadcastTick; +} + +int CHLTVDirector::GetPVSEntity( void ) +{ + return m_iPVSEntity; +} + +Vector CHLTVDirector::GetPVSOrigin( void ) +{ + return m_vPVSOrigin; +} + +void CHLTVDirector::UpdateSettings() +{ + // set delay + m_fDelay = tv_delay.GetFloat(); + + int newBroadcastTick = gpGlobals->tickcount; + + if ( m_fDelay < HLTV_MIN_DIRECTOR_DELAY ) + { + // instant broadcast, no delay + m_fDelay = 0.0; + } + else + { + // broadcast time is current time - delay time + newBroadcastTick -= TIME_TO_TICKS( m_fDelay ); + } + + if( (m_nBroadcastTick == 0) && (newBroadcastTick > 0) ) + { + // we start broadcasting right now, reset NextShotTimer + m_nNextShotTick = 0; + } + + // check if camera man is still valid + if ( m_iCameraManIndex > 0 ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( m_iCameraManIndex ); + if ( !pPlayer || pPlayer->GetTeamNumber() != TEAM_SPECTATOR ) + { + SetCameraMan( 0 ); + } + } + + m_nBroadcastTick = MAX( 0, newBroadcastTick ); +} + +const char** CHLTVDirector::GetModEvents() +{ + static const char *s_modevents[] = + { + "hltv_status", + "hltv_chat", + "player_connect", + "player_disconnect", + "player_team", + "player_info", + "server_cvar", + "player_death", + "player_chat", + "round_start", + "round_end", + NULL + }; + + return s_modevents; +} + + +void CHLTVDirector::BuildCameraList( void ) +{ + m_nNumFixedCameras = 0; + memset( m_pFixedCameras, 0, sizeof ( m_pFixedCameras ) ); + + CBaseEntity *pCamera = gEntList.FindEntityByClassname( NULL, GetFixedCameraEntityName() ); + + while ( pCamera && m_nNumFixedCameras < MAX_NUM_CAMERAS) + { + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, STRING(pCamera->m_target) ); + + if ( pTarget ) + { + // look at target if any given + QAngle angles; + VectorAngles( pTarget->GetAbsOrigin() - pCamera->GetAbsOrigin(), angles ); + pCamera->SetAbsAngles( angles ); + } + + m_pFixedCameras[m_nNumFixedCameras] = pCamera; + + m_nNumFixedCameras++; + pCamera = gEntList.FindEntityByClassname( pCamera, GetFixedCameraEntityName() ); + } +} + +// this is called with every new map +void CHLTVDirector::LevelInitPostEntity( void ) +{ + BuildCameraList(); + + m_vPVSOrigin.Init(); + m_iPVSEntity = 0; + m_nNextShotTick = 0; + m_nNextAnalyzeTick = 0; + m_iCameraManIndex = 0; + + RemoveEventsFromHistory(-1); // all + + // DevMsg("HLTV Director: found %i fixed cameras.\n", m_nNumFixedCameras ); +} + +void CHLTVDirector::FrameUpdatePostEntityThink( void ) +{ + if ( !m_pHLTVServer ) + return; // don't do anything + + // This function is called each tick + UpdateSettings(); // update settings from cvars + + if ( (m_nNextAnalyzeTick < gpGlobals->tickcount) && + (m_fDelay >= HLTV_MIN_DIRECTOR_DELAY) ) + { + m_nNextAnalyzeTick = gpGlobals->tickcount + TIME_TO_TICKS( 0.5f ); + + AnalyzePlayers(); + + AnalyzeCameras(); + } + + if ( m_nBroadcastTick <= 0 ) + { + // game start is still in delay loop + StartDelayMessage(); + } + else if ( m_nNextShotTick <= m_nBroadcastTick ) + { + // game is being broadcasted, generate camera shots + StartNewShot(); + } +} + +void CHLTVDirector::StartDelayMessage() +{ + if ( m_nNextShotTick > gpGlobals->tickcount ) + return; + + // check the next 8 seconds for interrupts/important events + m_nNextShotTick = gpGlobals->tickcount + TIME_TO_TICKS( DEF_SHOT_LENGTH ); + + // game hasn't started yet, we are still in the broadcast delay hole + IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_message", true ); + + if ( msg ) + { + msg->SetString("text", "Please wait for broadcast to start ..." ); + + // send spectators the HLTV director command as a game event + m_pHLTVServer->BroadcastEvent( msg ); + gameeventmanager->FreeEvent( msg ); + } + + StartBestFixedCameraShot( true ); +} + +void CHLTVDirector::StartBestPlayerCameraShot() +{ + float flPlayerRanking[MAX_PLAYERS]; + + memset( flPlayerRanking, 0, sizeof(flPlayerRanking) ); + + int firstIndex = FindFirstEvent( m_nBroadcastTick ); + + int index = firstIndex; + + float flBestRank = -1.0f; + int iBestCamera = -1; + int iBestTarget = -1; + + // sum all ranking values for the cameras + + while( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &dc = m_EventHistory[index]; + + if ( dc.m_Tick >= m_nNextShotTick ) + break; + + // search for camera ranking events + if ( Q_strcmp( dc.m_Event->GetName(), "hltv_rank_entity") == 0 ) + { + int index = dc.m_Event->GetInt("index"); + + if ( index < MAX_PLAYERS ) + { + flPlayerRanking[index] += dc.m_Event->GetFloat("rank" ); + + // find best camera + if ( flPlayerRanking[index] > flBestRank ) + { + iBestCamera = index; + flBestRank = flPlayerRanking[index]; + iBestTarget = dc.m_Event->GetInt("target"); + } + } + } + + index = m_EventHistory.NextInorder( index ); + } + + if ( iBestCamera != -1 ) + { + // view over shoulder, randomly left or right + StartChaseCameraShot( iBestCamera, iBestTarget, 112.0f, 20, (RandomFloat()>0.5)?20:-20, false ); + } + else + { + StartBestFixedCameraShot( true ); + } +} + +void CHLTVDirector::StartFixedCameraShot(int iCamera, int iTarget) +{ + CBaseEntity *pCamera = m_pFixedCameras[iCamera]; + + + Vector vCamPos = pCamera->GetAbsOrigin(); + QAngle aViewAngle = pCamera->GetAbsAngles(); + + m_iPVSEntity = 0; // don't use camera entity, since it may not been transmitted + m_vPVSOrigin = vCamPos; + + IGameEvent *shot = gameeventmanager->CreateEvent( "hltv_fixed", true ); + + if ( shot ) + { + shot->SetInt("posx", vCamPos.x ); + shot->SetInt("posy", vCamPos.y ); + shot->SetInt("posz", vCamPos.z ); + shot->SetInt("theta", aViewAngle.x ); + shot->SetInt("phi", aViewAngle.y ); + shot->SetInt("target", iTarget ); + shot->SetFloat("fov", RandomFloat(50,110) ); + + // send spectators the HLTV director command as a game event + m_pHLTVServer->BroadcastEvent( shot ); + gameeventmanager->FreeEvent( shot ); + } +} + +void CHLTVDirector::StartChaseCameraShot(int iTarget1, int iTarget2, int distance, int phi, int theta, bool bInEye) +{ + IGameEvent *shot = gameeventmanager->CreateEvent( "hltv_chase", true ); + + if ( !shot ) + return; + + shot->SetInt("target1", iTarget1 ); + shot->SetInt("target2", iTarget2 ); + shot->SetInt("distance", distance ); + shot->SetInt("phi", phi ); // hi/low + shot->SetInt( "theta", theta ); // left/right + shot->SetInt( "ineye", bInEye?1:0 ); + + m_iPVSEntity = iTarget1; + + // send spectators the HLTV director command as a game event + m_pHLTVServer->BroadcastEvent( shot ); + gameeventmanager->FreeEvent( shot ); +} + +void CHLTVDirector::StartBestFixedCameraShot( bool bForce ) +{ + float flCameraRanking[MAX_NUM_CAMERAS]; + + if ( m_nNumFixedCameras <= 0 ) + return; + + memset( flCameraRanking, 0, sizeof(flCameraRanking) ); + + int firstIndex = FindFirstEvent( m_nBroadcastTick ); + + int index = firstIndex; + + float flBestRank = -1.0f; + int iBestCamera = -1; + int iBestTarget = -1; + + // sum all ranking values for the cameras + + while( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &dc = m_EventHistory[index]; + + if ( dc.m_Tick >= m_nNextShotTick ) + break; + + // search for camera ranking events + if ( Q_strcmp( dc.m_Event->GetName(), "hltv_rank_camera") == 0 ) + { + int index = dc.m_Event->GetInt("index"); + flCameraRanking[index] += dc.m_Event->GetFloat("rank" ); + + // find best camera + if ( flCameraRanking[index] > flBestRank ) + { + iBestCamera = index; + flBestRank = flCameraRanking[index]; + iBestTarget = dc.m_Event->GetInt("target"); + } + } + + index = m_EventHistory.NextInorder( index ); + } + + if ( !bForce && flBestRank == 0 ) + { + // if we are not forcing a fixed camera shot, switch to player chase came + // if no camera shows any players + StartBestPlayerCameraShot(); + } + else if ( iBestCamera != -1 ) + { + StartFixedCameraShot( iBestCamera, iBestTarget ); + } +} + +void CHLTVDirector::StartRandomShot() +{ + int toTick = m_nBroadcastTick + TIME_TO_TICKS ( DEF_SHOT_LENGTH ); + m_nNextShotTick = MIN( m_nNextShotTick, toTick ); + + if ( RandomFloat(0,1) < 0.25 && tv_allow_static_shots.GetBool() ) + { + // create a static shot from a level camera + StartBestFixedCameraShot( false ); + } + else + { + // follow a player + StartBestPlayerCameraShot(); + } +} + +void CHLTVDirector::CreateShotFromEvent( CHLTVGameEvent *event ) +{ + // show event at least for 2 more seconds after it occured + const char *name = event->m_Event->GetName(); + + bool bPlayerHurt = Q_strcmp( "player_hurt", name ) == 0; + bool bPlayerKilled = Q_strcmp( "player_death", name ) == 0; + bool bRoundStart = Q_strcmp( "round_start", name ) == 0; + bool bRoundEnd = Q_strcmp( "round_end", name ) == 0; + + if ( bPlayerHurt || bPlayerKilled ) + { + CBaseEntity *victim = UTIL_PlayerByUserId( event->m_Event->GetInt("userid") ); + CBaseEntity *attacker = UTIL_PlayerByUserId( event->m_Event->GetInt("attacker") ); + + if ( !victim ) + return; + + if ( attacker == victim || attacker == NULL ) + { + // player killed self or by WORLD + StartChaseCameraShot( victim->entindex(), 0, 96, 20, 0, false ); + } + else // attacker != NULL + { + // check if we would show it from ineye view + bool bInEye = (bPlayerKilled && RandomFloat(0,1) > 0.33) || (bPlayerHurt && RandomFloat(0,1) > 0.66); + + // if we show ineye view, show it more likely from killer + if ( RandomFloat(0,1) > (bInEye?0.3f:0.7f) ) + { + ::V_swap( attacker, victim ); + } + + // hurting a victim is shown as chase more often + // view from behind over head + // lower view point, dramatic + // view over shoulder, randomly left or right + StartChaseCameraShot( victim->entindex(), attacker->entindex(), 96, -20, (RandomFloat()>0.5)?30:-30, bInEye ); + } + + // shot 2 seconds after death/hurt + m_nNextShotTick = MIN( m_nNextShotTick, (event->m_Tick+TIME_TO_TICKS(2.0)) ); + } + else if ( bRoundStart || bRoundEnd ) + { + StartBestFixedCameraShot( false ); + } + else + { + DevMsg( "No known TV shot for event %s\n", name ); + } +} + +void CHLTVDirector::CheckHistory() +{ + int index = m_EventHistory.FirstInorder(); + int lastTick = -1; + + while ( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &dc = m_EventHistory[index]; + + Assert( lastTick <= dc.m_Tick ); + lastTick = dc.m_Tick; + + index = m_EventHistory.NextInorder( index ); + } +} + +void CHLTVDirector::RemoveEventsFromHistory(int tick) +{ + int index = m_EventHistory.FirstInorder(); + + while ( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &dc = m_EventHistory[index]; + + if ( (dc.m_Tick < tick) || (tick == -1) ) + { + gameeventmanager->FreeEvent( dc.m_Event ); + dc.m_Event = NULL; + m_EventHistory.RemoveAt( index ); + index = m_EventHistory.FirstInorder(); // start again + } + else + { + index = m_EventHistory.NextInorder( index ); + } + } + +#ifdef _DEBUG + CheckHistory(); +#endif +} + +int CHLTVDirector::FindFirstEvent( int tick ) +{ + // TODO cache last queried ticks + + int index = m_EventHistory.FirstInorder(); + + if ( index == m_EventHistory.InvalidIndex() ) + return index; // no commands in list + + CHLTVGameEvent *event = &m_EventHistory[index]; + + while ( event->m_Tick < tick ) + { + index = m_EventHistory.NextInorder( index ); + + if ( index == m_EventHistory.InvalidIndex() ) + break; + + event = &m_EventHistory[index]; + } + + return index; +} + +bool CHLTVDirector::SetCameraMan( int iPlayerIndex ) +{ + if ( !tv_allow_camera_man.GetBool() ) + return false; + + if ( m_iCameraManIndex == iPlayerIndex ) + return true; + + // check if somebody else is already the camera man + if ( m_iCameraManIndex != 0 && iPlayerIndex != 0 ) + return false; + + CBasePlayer *pPlayer = NULL; + + if ( iPlayerIndex > 0 ) + { + pPlayer = UTIL_PlayerByIndex( iPlayerIndex ); + if ( !pPlayer || pPlayer->GetTeamNumber() != TEAM_SPECTATOR ) + return false; + } + + m_iCameraManIndex = iPlayerIndex; + + // create event for director event history + IGameEvent *event = gameeventmanager->CreateEvent( "hltv_cameraman" ); + if ( event ) + { + event->SetInt("index", iPlayerIndex ); + gameeventmanager->FireEvent( event ); + } + + CRecipientFilter filter; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( pPlayer && pPlayer->GetTeamNumber() == TEAM_SPECTATOR && !pPlayer->IsFakeClient() ) + { + filter.AddRecipient( pPlayer ); + } + } + + filter.MakeReliable(); + + if ( iPlayerIndex > 0 ) + { + // tell all spectators that the camera is in use. + char szText[200]; + Q_snprintf( szText, sizeof(szText), "SourceTV camera is now controlled by %s.", pPlayer->GetPlayerName() ); + UTIL_ClientPrintFilter( filter, HUD_PRINTTALK, szText ); + } + else + { + // tell all spectators that the camera is available again. + UTIL_ClientPrintFilter( filter, HUD_PRINTTALK, "SourceTV camera switched to auto-director mode." ); + } + + return true; +} + +void CHLTVDirector::FinishCameraManShot() +{ + Assert( m_iCameraMan == m_iPVSEntity ); + + int index = FindFirstEvent( m_nBroadcastTick ); + + if ( index == m_EventHistory.InvalidIndex() ) + { + // check next frame again if event history is empty + m_nNextShotTick = m_nBroadcastTick+1; + return; + } + + m_nNextShotTick = m_nBroadcastTick + TIME_TO_TICKS( MIN_SHOT_LENGTH ); + + //check if camera turns camera off within broadcast time and game time + while( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &dc = m_EventHistory[index]; + + if ( dc.m_Tick >= m_nNextShotTick ) + break; + + if ( Q_strcmp( dc.m_Event->GetName(), "hltv_cameraman") == 0 ) + { + int iNewCameraMan = dc.m_Event->GetInt("index"); + + if ( iNewCameraMan == 0 ) + { + // camera man switched camera off + m_nNextShotTick = dc.m_Tick+1; + m_iCameraMan = 0; + return; + } + } + + index = m_EventHistory.NextInorder( index ); + } + + // camera man is still recording and live, resend camera man message + IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_cameraman", true ); + if ( msg ) + { + msg->SetInt("index", m_iCameraMan ); + m_pHLTVServer->BroadcastEvent( msg ); + gameeventmanager->FreeEvent( msg ); + } + +} + +bool CHLTVDirector::StartCameraManShot() +{ + Assert( m_nNextShotTick <= m_nBroadcastTick ); + + int index = FindFirstEvent( m_nNextShotTick ); + + // check for cameraman mode + while( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &dc = m_EventHistory[index]; + + // only check if this is the current tick + if ( dc.m_Tick > m_nBroadcastTick ) + break; + + if ( Q_strcmp( dc.m_Event->GetName(), "hltv_cameraman") == 0 ) + { + if ( dc.m_Event->GetInt("index") > 0 ) + { + // ok, this guy is now the active camera man + m_iCameraMan = dc.m_Event->GetInt("index"); + + m_iPVSEntity = m_iCameraMan; + m_nNextShotTick = m_nBroadcastTick+1; // check setting right on next frame + + // send camera man command to client + m_pHLTVServer->BroadcastEvent( dc.m_Event ); + return true; + } + } + + index = m_EventHistory.NextInorder( index ); + } + + return false; // no camera man found +} + +void CHLTVDirector::StartInstantBroadcastShot() +{ + m_nNextShotTick = m_nBroadcastTick + TIME_TO_TICKS( MAX_SHOT_LENGTH ); + + if ( m_iCameraManIndex > 0 ) + { + // camera man is still recording and live, resend camera man message + IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_cameraman", true ); + if ( msg ) + { + msg->SetInt("index", m_iCameraManIndex ); + m_pHLTVServer->BroadcastEvent( msg ); + gameeventmanager->FreeEvent( msg ); + + m_iPVSEntity = m_iCameraManIndex; + m_nNextShotTick = m_nBroadcastTick+TIME_TO_TICKS( MIN_SHOT_LENGTH ); + } + } + else + { + RemoveEventsFromHistory(-1); // all + + AnalyzePlayers(); + + AnalyzeCameras(); + + StartRandomShot(); + } +} + +void CHLTVDirector::StartNewShot() +{ + // we can remove all events the + int smallestTick = MAX(0, gpGlobals->tickcount - TIME_TO_TICKS(HLTV_MAX_DELAY) ); + RemoveEventsFromHistory( smallestTick ); + + // if the delay time is to short for autodirector, just show next best thing + if ( m_fDelay < HLTV_MIN_DIRECTOR_DELAY ) + { + StartInstantBroadcastShot(); + return; + } + + if ( m_iCameraMan > 0 ) + { + // we already have an active camera man, + // wait until he releases the "record" lock + FinishCameraManShot(); + return; + } + + if ( StartCameraManShot() ) + { + // now we have an active camera man + return; + } + + // ok, no camera man active, now check how much time + // we have for the next shot, if the time diff to the next + // important event we have to switch to is too short (<2sec) + // just extent the current shot and don't start a new one + + // check the next 8 seconds for interrupts/important events + m_nNextShotTick = m_nBroadcastTick + TIME_TO_TICKS( MAX_SHOT_LENGTH ); + + if ( m_nBroadcastTick <= 0 ) + { + // game hasn't started yet, we are still in the broadcast delay hole + IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_message", true ); + + if ( msg ) + { + msg->SetString("text", "Please wait for broadcast to start ..." ); + + // send spectators the HLTV director command as a game event + m_pHLTVServer->BroadcastEvent( msg ); + gameeventmanager->FreeEvent( msg ); + } + + StartBestFixedCameraShot( true ); + return; + } + + int index = FindFirstEvent( m_nBroadcastTick ); + + while( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &dc = m_EventHistory[index]; + + if ( dc.m_Tick >= m_nNextShotTick ) + break; // we have searched enough + + // a camera man is always interrupting auto director + if ( Q_strcmp( dc.m_Event->GetName(), "hltv_cameraman") == 0 ) + { + if ( dc.m_Event->GetInt("index") > 0 ) + { + // stop the next cut when this cameraman starts recording + m_nNextShotTick = dc.m_Tick; + break; + } + } + + index = m_EventHistory.NextInorder( index ); + } + + float flDuration = TICKS_TO_TIME(m_nNextShotTick - m_nBroadcastTick); + + if ( flDuration < MIN_SHOT_LENGTH ) + return; // not enough time for a new shot + + // find the most interesting game event for next shot + CHLTVGameEvent *dc = FindBestGameEvent(); + + if ( dc ) + { + // show the game event + CreateShotFromEvent( dc ); + } + else + { + // no interesting events found, start random shot + StartRandomShot(); + } +} + +CHLTVGameEvent *CHLTVDirector::FindBestGameEvent() +{ + int bestEvent[4]; + int bestEventPrio[4]; + + Q_memset( bestEvent, 0, sizeof(bestEvent) ); + Q_memset( bestEventPrio, 0, sizeof(bestEventPrio) ); + + int index = FindFirstEvent( m_nBroadcastTick ); + + // search for next 4 best events within next 8 seconds + for (int i = 0; i<4; i ++) + { + bestEventPrio[i] = 0; + bestEvent[i] = 0; + + int tillTick = m_nBroadcastTick + TIME_TO_TICKS( 2.0f*(1.0f+i) ); + + if ( tillTick > m_nNextShotTick ) + break; + + // sum all action for the next time + while ( index != m_EventHistory.InvalidIndex() ) + { + CHLTVGameEvent &event = m_EventHistory[index]; + + if ( event.m_Tick > tillTick ) + break; + + int priority = event.m_Priority; + + if ( priority > bestEventPrio[i] ) + { + bestEvent[i] = index; + bestEventPrio[i] = priority; + } + + index = m_EventHistory.NextInorder( index ); + } + } + + if ( !( bestEventPrio[0] || bestEventPrio[1] || bestEventPrio[2] ) ) + return NULL; // no event found at all, give generic algorithm a chance + + // camera cut rules : + + if ( bestEventPrio[1] >= bestEventPrio[0] && + bestEventPrio[1] >= bestEventPrio[2] && + bestEventPrio[1] >= bestEventPrio[3] ) + { + return &m_EventHistory[ bestEvent[1] ]; // best case + } + else if ( bestEventPrio[0] > bestEventPrio[1] && + bestEventPrio[0] > bestEventPrio[2] ) + { + return &m_EventHistory[ bestEvent[0] ]; // event 0 is very important + } + else if ( bestEventPrio[2] > bestEventPrio[3] ) + { + return &m_EventHistory[ bestEvent[2] ]; + } + else + { + // event 4 is the best but too far away, so show event 1 + if ( bestEvent[0] ) + return &m_EventHistory[ bestEvent[0] ]; + else + return NULL; + } +} + +void CHLTVDirector::AnalyzeCameras() +{ + InitRandomOrder( m_nNumFixedCameras ); + + for ( int i = 0; iGetAbsOrigin(); + + for ( int j=0; jGetAbsOrigin(); + + float dist = VectorLength( vPlayerPos - vCamPos ); + + if ( dist > 1024.0f || dist < 4.0f ) + continue; // too colse or far away + + // check visibility + trace_t tr; + UTIL_TraceLine( vCamPos, pPlayer->GetAbsOrigin(), MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction < 1.0 ) + continue; // not visible for camera + + nCount++; + + // remember closest player + if ( dist < flClosestPlayerDist ) + { + iClosestPlayer = pPlayer->entindex(); + flClosestPlayerDist = dist; + } + + Vector v1; AngleVectors( pPlayer->EyeAngles(), &v1 ); + + // check players orientation towards camera + Vector v2 = vCamPos - vPlayerPos; + VectorNormalize( v2 ); + + // player/camera cost function: + flRank += ( 1.0f/sqrt(dist) ) * WeightedAngle( v1, v2 ); + + vDistribution += v2; + } + + if ( nCount > 0 ) + { + // normalize distribution + flRank *= VectorLength( vDistribution ) / nCount; + } + + IGameEvent *event = gameeventmanager->CreateEvent("hltv_rank_camera"); + + if ( event ) + { + event->SetFloat("rank", flRank ); + event->SetInt("index", iCameraIndex ); // index in m_pFixedCameras + event->SetInt("target", iClosestPlayer ); // ent index + gameeventmanager->FireEvent( event ); + } + } +} + +void CHLTVDirector::BuildActivePlayerList() +{ + // first build list of all active players + + m_nNumActivePlayers = 0; + + for ( int i =1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( !pPlayer ) + continue; + + if ( !pPlayer->IsAlive() ) + continue; + + if ( pPlayer->IsObserver() ) + continue; + + if ( pPlayer->GetTeamNumber() <= TEAM_SPECTATOR ) + continue; + + m_pActivePlayers[m_nNumActivePlayers] = pPlayer; + m_nNumActivePlayers++; + } +} + +void CHLTVDirector::AnalyzePlayers() +{ + // build list of current active players + BuildActivePlayerList(); + + // analyzes every active player + + InitRandomOrder( m_nNumActivePlayers ); + + for ( int i = 0; iGetAbsOrigin(); + + Vector v1; AngleVectors( pPlayer->EyeAngles(), &v1 ); + + v1 *= -1; // inverted + + for ( int j=0; jGetAbsOrigin(); + + float dist = VectorLength( vPlayerPos - vCamPos ); + + if ( dist > 1024.0f || dist < 4.0f ) + continue; // too close or far away + + // check visibility + trace_t tr; + UTIL_TraceLine( vCamPos, pOtherPlayer->GetAbsOrigin(), MASK_SOLID, pOtherPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction < 1.0 ) + continue; // not visible for camera + + nCount++; + + // check players orientation towards camera + Vector v2; AngleVectors( pOtherPlayer->EyeAngles(), &v2 ); + + float facing = WeightedAngle( v1, v2 ); + + // remember closest player + if ( facing > flBestFacingPlayer ) + { + iBestFacingPlayer = pOtherPlayer->entindex(); + flBestFacingPlayer = facing; + } + + // player/camera cost function: + flRank += ( 1.0f/sqrt(dist) ) * facing; + + vDistribution += v2; + } + + if ( nCount > 0 ) + { + float flDistribution = VectorLength( vDistribution ) / nCount; // normalize distribution + flRank *= flDistribution; + } + + IGameEvent *event = gameeventmanager->CreateEvent("hltv_rank_entity"); + if ( event ) + { + event->SetInt("index", pPlayer->entindex() ); + event->SetFloat("rank", flRank ); + event->SetInt("target", iBestFacingPlayer ); // ent index + gameeventmanager->FireEvent( event ); + } + } +} + diff --git a/sp/src/game/server/hltvdirector.h b/sp/src/game/server/hltvdirector.h new file mode 100644 index 00000000..653bf1dd --- /dev/null +++ b/sp/src/game/server/hltvdirector.h @@ -0,0 +1,124 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef HLTVDIRECTOR_H +#define HLTVDIRECTOR_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "GameEventListener.h" +#include +#include +#include +#include + +#define HLTV_MIN_DIRECTOR_DELAY 10 // minimum delay if director is enabled +#define HLTV_MAX_DELAY 120 // maximum delay + + +#define MAX_NUM_CAMERAS 64 // support up to 64 fixed cameras per level + +#define MIN_SHOT_LENGTH 4.0f // minimum time of a cut (seconds) +#define MAX_SHOT_LENGTH 8.0f // maximum time of a cut (seconds) +#define DEF_SHOT_LENGTH 6.0f // average time of a cut (seconds) + +class CHLTVGameEvent +{ +public: + int m_Tick; // tick of this command + int m_Priority; // game event priority + IGameEvent *m_Event; // IGameEvent +}; + +class CHLTVDirector : public CGameEventListener, public CBaseGameSystemPerFrame, public IHLTVDirector +{ +public: + DECLARE_CLASS_NOBASE( CHLTVDirector ); + + virtual char const *Name() { return "CHLTVDirector"; } + + CHLTVDirector(); + virtual ~CHLTVDirector(); + + virtual void SetHLTVServer( IHLTVServer *hltv ); // give the director an HLTV interface + IHLTVServer* GetHLTVServer( void ); + int GetDirectorTick( void ); // get current broadcast tick from director + int GetPVSEntity( void ); // get current view entity (PVS) + Vector GetPVSOrigin( void ); // get current PVS origin, if PVS entity is 0 + float GetDelay( void ); // returns current delay in seconds + bool IsActive( void ); + + virtual const char** GetModEvents(); // returns list of event names forwarded to HLTV clients + + void BuildCameraList( void ); + + +public: // IGameEventListener Interface + virtual void FireGameEvent( IGameEvent * event ); + +public: // CBaseGameSystem overrides + + virtual bool Init(); + virtual void Shutdown(); + virtual void FrameUpdatePostEntityThink(); + virtual void LevelInitPostEntity(); + virtual char *GetFixedCameraEntityName( void ) { return "point_viewcontrol"; } + + bool SetCameraMan( int iPlayerIndex ); + int GetCameraMan() { return m_iCameraManIndex; } + + +protected: + + virtual void StartNewShot(); + virtual void StartRandomShot(); + virtual void StartDelayMessage(); + virtual void StartBestFixedCameraShot(bool bForce); + virtual void StartBestPlayerCameraShot(); + virtual void StartFixedCameraShot(int iCamera, int iTarget); + virtual void StartChaseCameraShot(int iTarget1, int iTarget2, int distance, int phi, int theta, bool bInEye); + virtual void UpdateSettings(); + virtual void AnalyzePlayers(); + virtual void AnalyzeCameras(); + virtual bool StartCameraManShot(); + virtual void StartInstantBroadcastShot(); + virtual void FinishCameraManShot(); + virtual void BuildActivePlayerList(); + virtual CHLTVGameEvent *FindBestGameEvent(); + virtual void CreateShotFromEvent( CHLTVGameEvent *ge ); + + int FindFirstEvent( int tick ); // finds first event >= tick + void CheckHistory(); + void RemoveEventsFromHistory(int tick); // removes all commands < tick, or all if tick -1 + + IHLTVServer *m_pHLTVServer; // interface to servers HLTV object + float m_fDelay; // hltv delay in seconds + int m_nBroadcastTick; // world time that is currently "on the air" + int m_iPVSEntity; // entity for PVS center + Vector m_vPVSOrigin; // PVS origin if PVS entity is 0 + int m_iCameraMan; // >0 if current view entity is a cameraman + CBasePlayer *m_pHLTVClient; // the HLTV fake client + int m_nNextShotTick; // time for the next scene cut + int m_iLastPlayer; // last player in random rotation + + int m_nNextAnalyzeTick; + + int m_nNumFixedCameras; //number of cameras in current map + CBaseEntity *m_pFixedCameras[MAX_NUM_CAMERAS]; // fixed cameras (point_viewcontrol) + + int m_nNumActivePlayers; //number of cameras in current map + CBasePlayer *m_pActivePlayers[MAX_PLAYERS]; // fixed cameras (point_viewcontrol) + int m_iCameraManIndex; // entity index of current camera man or 0 + + CUtlRBTree m_EventHistory; +}; + +extern IGameSystem* HLTVDirectorSystem(); +extern CHLTVDirector* HLTVDirector(); + +#endif // HLTVDIRECTOR_H diff --git a/sp/src/game/server/ilagcompensationmanager.h b/sp/src/game/server/ilagcompensationmanager.h new file mode 100644 index 00000000..507d29bf --- /dev/null +++ b/sp/src/game/server/ilagcompensationmanager.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ILAGCOMPENSATIONMANAGER_H +#define ILAGCOMPENSATIONMANAGER_H +#ifdef _WIN32 +#pragma once +#endif + +class CBasePlayer; +class CUserCmd; + +//----------------------------------------------------------------------------- +// Purpose: This is also an IServerSystem +//----------------------------------------------------------------------------- +abstract_class ILagCompensationManager +{ +public: + // Called during player movement to set up/restore after lag compensation + virtual void StartLagCompensation( CBasePlayer *player, CUserCmd *cmd ) = 0; + virtual void FinishLagCompensation( CBasePlayer *player ) = 0; +}; + +extern ILagCompensationManager *lagcompensation; + +#endif // ILAGCOMPENSATIONMANAGER_H diff --git a/sp/src/game/server/info_camera_link.cpp b/sp/src/game/server/info_camera_link.cpp new file mode 100644 index 00000000..bdc8a8c8 --- /dev/null +++ b/sp/src/game/server/info_camera_link.cpp @@ -0,0 +1,183 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" + +#include "info_camera_link.h" +#include "point_camera.h" +#include "utllinkedlist.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//----------------------------------------------------------------------------- +// Link between entities and cameras +//----------------------------------------------------------------------------- +class CInfoCameraLink : public CLogicalEntity +{ + DECLARE_CLASS( CInfoCameraLink, CLogicalEntity ); + DECLARE_DATADESC(); + +public: + CInfoCameraLink(); + ~CInfoCameraLink(); + + virtual void Activate(); + +private: + void InputSetCamera(inputdata_t &inputdata); + void InputSetTargetEntity(inputdata_t &inputdata); + void SetCameraByName(const char *szName); + + CHandle m_hCamera; + EHANDLE m_hTargetEntity; + string_t m_strPointCamera; + + friend CBaseEntity *CreateInfoCameraLink( CBaseEntity *pTarget, CPointCamera *pCamera ); + friend void PointCameraSetupVisibility( CBaseEntity *pPlayer, int area, unsigned char *pvs, int pvssize ); +}; + + +//----------------------------------------------------------------------------- +// List of all info camera links +//----------------------------------------------------------------------------- +CUtlFixedLinkedList g_InfoCameraLinkList; + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CInfoCameraLink ) + + DEFINE_KEYFIELD( m_strPointCamera, FIELD_STRING, "PointCamera" ), + + DEFINE_FIELD( m_hCamera, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ), + + // Outputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetCamera", InputSetCamera ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( info_camera_link, CInfoCameraLink ); + + +//----------------------------------------------------------------------------- +// Constructor, destructor +//----------------------------------------------------------------------------- +CInfoCameraLink::CInfoCameraLink() +{ + g_InfoCameraLinkList.AddToTail( this ); +} + +CInfoCameraLink::~CInfoCameraLink() +{ + g_InfoCameraLinkList.FindAndRemove( this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called after all entities have spawned and after a load game. +//----------------------------------------------------------------------------- +void CInfoCameraLink::Activate() +{ + BaseClass::Activate(); + + // Checks necessary to prevent interference with CreateInfoCameraLink + if ( !m_hCamera ) + { + SetCameraByName( STRING(m_strPointCamera) ); + } + + if ( !m_hTargetEntity ) + { + m_hTargetEntity = gEntList.FindEntityByName( NULL, STRING(m_target) ); + } +} + +void CInfoCameraLink::SetCameraByName(const char *szName) +{ + CBaseEntity *pBaseEnt = gEntList.FindEntityByName( NULL, szName ); + if( pBaseEnt ) + { + m_hCamera = dynamic_cast( pBaseEnt ); + if ( m_hCamera ) + { + // Keep the camera name consistent for save/load + m_strPointCamera = MAKE_STRING( szName ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInfoCameraLink::InputSetCamera(inputdata_t &inputdata) +{ + SetCameraByName( inputdata.value.String() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CreateInfoCameraLink( CBaseEntity *pTarget, CPointCamera *pCamera ) +{ + CInfoCameraLink *pInfoCameraLink = (CInfoCameraLink*)CreateEntityByName( "info_camera_link" ); + if ( !pInfoCameraLink ) + return NULL; + + pInfoCameraLink->m_hCamera = pCamera; + pInfoCameraLink->m_hTargetEntity = pTarget; + pInfoCameraLink->Spawn(); + return pInfoCameraLink; +} + + +//----------------------------------------------------------------------------- +// Sets up visibility +//----------------------------------------------------------------------------- +void PointCameraSetupVisibility( CBaseEntity *pPlayer, int area, unsigned char *pvs, int pvssize ) +{ + for ( CPointCamera *pCameraEnt = GetPointCameraList(); pCameraEnt != NULL; pCameraEnt = pCameraEnt->m_pNext ) + { + pCameraEnt->SetActive( false ); + } + + int nNext; + for ( int i = g_InfoCameraLinkList.Head(); i != g_InfoCameraLinkList.InvalidIndex(); i = nNext ) + { + nNext = g_InfoCameraLinkList.Next( i ); + + CBaseEntity *pTargetEnt = g_InfoCameraLinkList[i]->m_hTargetEntity; + if ( !pTargetEnt ) + { + UTIL_Remove( g_InfoCameraLinkList[i] ); + continue; + } + + // Don't bother if it's not visible + if ( pTargetEnt->IsEffectActive( EF_NODRAW ) ) + continue; + + if ( !pTargetEnt->NetworkProp()->IsInPVS( pPlayer->edict(), pvs, pvssize ) ) + continue; + + if ( engine->CheckAreasConnected( area, pTargetEnt->NetworkProp()->AreaNum() ) ) + { + CPointCamera *pCameraEnt = g_InfoCameraLinkList[i]->m_hCamera; + if ( pCameraEnt ) + { + engine->AddOriginToPVS( pCameraEnt->GetAbsOrigin() ); + pCameraEnt->SetActive( true ); + } + } + } +} + diff --git a/sp/src/game/server/info_camera_link.h b/sp/src/game/server/info_camera_link.h new file mode 100644 index 00000000..592e4f5f --- /dev/null +++ b/sp/src/game/server/info_camera_link.h @@ -0,0 +1,32 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef INFO_CAMERA_LINK_H +#define INFO_CAMERA_LINK_H + +#include "baseentity.h" + +//----------------------------------------------------------------------------- +// Forward declarations +//----------------------------------------------------------------------------- +class CPointCamera; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CreateInfoCameraLink( CBaseEntity *pTarget, CPointCamera *pCamera ); + + +//----------------------------------------------------------------------------- +// Sets up visibility +//----------------------------------------------------------------------------- +void PointCameraSetupVisibility( CBaseEntity *pPlayer, int area, unsigned char *pvs, int pvssize ); + + + +#endif // INFO_CAMERA_LINK_H + diff --git a/sp/src/game/server/info_overlay_accessor.cpp b/sp/src/game/server/info_overlay_accessor.cpp new file mode 100644 index 00000000..ad81f91a --- /dev/null +++ b/sp/src/game/server/info_overlay_accessor.cpp @@ -0,0 +1,50 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// -------------------------------------------------------------------------------- // +// An entity used to access overlays (and change their texture) +// -------------------------------------------------------------------------------- // + +class CInfoOverlayAccessor : public CPointEntity +{ +public: + + DECLARE_CLASS( CInfoOverlayAccessor, CPointEntity ); + + int UpdateTransmitState(); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + + CNetworkVar( int, m_iOverlayID ); +}; + + +// This table encodes the CBaseEntity data. +IMPLEMENT_SERVERCLASS_ST_NOBASE(CInfoOverlayAccessor, DT_InfoOverlayAccessor) + SendPropInt ( SENDINFO(m_iTextureFrameIndex), 8, SPROP_UNSIGNED ), + SendPropInt ( SENDINFO(m_iOverlayID), 32, SPROP_UNSIGNED ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( info_overlay_accessor, CInfoOverlayAccessor ); + +BEGIN_DATADESC( CInfoOverlayAccessor ) + DEFINE_KEYFIELD( m_iOverlayID, FIELD_INTEGER, "OverlayID" ), +END_DATADESC() + + +int CInfoOverlayAccessor::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} diff --git a/sp/src/game/server/init_factory.cpp b/sp/src/game/server/init_factory.cpp new file mode 100644 index 00000000..a3778939 --- /dev/null +++ b/sp/src/game/server/init_factory.cpp @@ -0,0 +1,25 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "init_factory.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static factorylist_t s_factories; + +// Store off the factories +void FactoryList_Store( const factorylist_t &sourceData ) +{ + s_factories = sourceData; +} + +// retrieve the stored factories +void FactoryList_Retrieve( factorylist_t &destData ) +{ + destData = s_factories; +} diff --git a/sp/src/game/server/init_factory.h b/sp/src/game/server/init_factory.h new file mode 100644 index 00000000..1cbc95c3 --- /dev/null +++ b/sp/src/game/server/init_factory.h @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef INIT_FACTORY_H +#define INIT_FACTORY_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "interface.h" + +struct factorylist_t +{ + CreateInterfaceFn engineFactory; + CreateInterfaceFn physicsFactory; + CreateInterfaceFn fileSystemFactory; +}; + +// Store off the factories +void FactoryList_Store( const factorylist_t &sourceData ); + +// retrieve the stored factories +void FactoryList_Retrieve( factorylist_t &destData ); + +#endif // INIT_FACTORY_H diff --git a/sp/src/game/server/intermission.cpp b/sp/src/game/server/intermission.cpp new file mode 100644 index 00000000..76cc801d --- /dev/null +++ b/sp/src/game/server/intermission.cpp @@ -0,0 +1,52 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "entitylist.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//========================================================= +// Multiplayer intermission spots. +//========================================================= +class CInfoIntermission:public CPointEntity +{ +public: + DECLARE_CLASS( CInfoIntermission, CPointEntity ); + + void Spawn( void ); + void Think( void ); +}; + +void CInfoIntermission::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); + SetLocalAngles( vec3_angle ); + SetNextThink( gpGlobals->curtime + 2 );// let targets spawn ! +} + +void CInfoIntermission::Think ( void ) +{ + CBaseEntity *pTarget; + + // find my target + pTarget = gEntList.FindEntityByName( NULL, m_target ); + + if ( pTarget ) + { + Vector dir = pTarget->GetLocalOrigin() - GetLocalOrigin(); + VectorNormalize( dir ); + QAngle angles; + VectorAngles( dir, angles ); + SetLocalAngles( angles ); + } +} + +LINK_ENTITY_TO_CLASS( info_intermission, CInfoIntermission ); diff --git a/sp/src/game/server/iscorer.h b/sp/src/game/server/iscorer.h new file mode 100644 index 00000000..efaf2ef8 --- /dev/null +++ b/sp/src/game/server/iscorer.h @@ -0,0 +1,29 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Interface that entities can use to redirect scoring to other entities. +// i.e. A rocket redirects scoring to the player that fired it. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ISCORER_H +#define ISCORER_H +#ifdef _WIN32 +#pragma once +#endif + +//----------------------------------------------------------------------------- +// Purpose: Interface that entities can use to redirect scoring to other entities. +// i.e. A rocket redirects scoring to the player that fired it. +//----------------------------------------------------------------------------- +abstract_class IScorer +{ +public: + // Return the entity that should receive the score + virtual CBasePlayer *GetScorer( void ) = 0; + // Return the entity that should get assistance credit + virtual CBasePlayer *GetAssistant( void ) = 0; +}; + + +#endif // ISCORER_H diff --git a/sp/src/game/server/iservervehicle.h b/sp/src/game/server/iservervehicle.h new file mode 100644 index 00000000..60be0083 --- /dev/null +++ b/sp/src/game/server/iservervehicle.h @@ -0,0 +1,139 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ISERVERVEHICLE_H +#define ISERVERVEHICLE_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "IVehicle.h" +#include "vphysics/vehicles.h" + +class CBaseEntity; +class CBasePlayer; +class CBaseCombatCharacter; +class CNPC_VehicleDriver; +enum VehicleSeatQuery_e; + +// This is used by the player to access vehicles. It's an interface so the +// vehicles are not restricted in what they can derive from. +abstract_class IServerVehicle : public IVehicle +{ +public: + // Get the entity associated with the vehicle. + virtual CBaseEntity* GetVehicleEnt() = 0; + + // Get and set the current driver. Use PassengerRole_t enum in shareddefs.h for adding passengers + virtual void SetPassenger( int nRole, CBaseCombatCharacter *pPassenger ) = 0; + + // Is the player visible while in the vehicle? (this is a constant the vehicle) + virtual bool IsPassengerVisible( int nRole = VEHICLE_ROLE_DRIVER ) = 0; + + // Can a given passenger take damage? + virtual bool IsPassengerDamagable( int nRole = VEHICLE_ROLE_DRIVER ) = 0; + virtual bool PassengerShouldReceiveDamage( CTakeDamageInfo &info ) = 0; + + // Is the vehicle upright? + virtual bool IsVehicleUpright( void ) = 0; + + // Whether or not we're in a transitional phase + virtual bool IsPassengerEntering( void ) = 0; + virtual bool IsPassengerExiting( void ) = 0; + + // Get a position in *world space* inside the vehicle for the player to start at + virtual void GetPassengerSeatPoint( int nRole, Vector *pPoint, QAngle *pAngles ) = 0; + + virtual void HandlePassengerEntry( CBaseCombatCharacter *pPassenger, bool bAllowEntryOutsideZone = false ) = 0; + virtual bool HandlePassengerExit( CBaseCombatCharacter *pPassenger ) = 0; + + // Get a point in *world space* to leave the vehicle from (may be in solid) + virtual bool GetPassengerExitPoint( int nRole, Vector *pPoint, QAngle *pAngles ) = 0; + virtual int GetEntryAnimForPoint( const Vector &vecPoint ) = 0; + virtual int GetExitAnimToUse( Vector &vecEyeExitEndpoint, bool &bAllPointsBlocked ) = 0; + virtual void HandleEntryExitFinish( bool bExitAnimOn, bool bResetAnim ) = 0; + + virtual Class_T ClassifyPassenger( CBaseCombatCharacter *pPassenger, Class_T defaultClassification ) = 0; + virtual float PassengerDamageModifier( const CTakeDamageInfo &info ) = 0; + + // Get me the parameters for this vehicle + virtual const vehicleparams_t *GetVehicleParams( void ) = 0; + // If I'm a physics vehicle, get the controller + virtual IPhysicsVehicleController *GetVehicleController() = 0; + + virtual int NPC_GetAvailableSeat( CBaseCombatCharacter *pPassenger, string_t strRoleName, VehicleSeatQuery_e nQueryType ) = 0; + virtual bool NPC_AddPassenger( CBaseCombatCharacter *pPassenger, string_t strRoleName, int nSeat ) = 0; + virtual bool NPC_RemovePassenger( CBaseCombatCharacter *pPassenger ) = 0; + virtual bool NPC_GetPassengerSeatPosition( CBaseCombatCharacter *pPassenger, Vector *vecResultPos, QAngle *vecResultAngle ) = 0; + virtual bool NPC_GetPassengerSeatPositionLocal( CBaseCombatCharacter *pPassenger, Vector *vecResultPos, QAngle *vecResultAngle ) = 0; + virtual int NPC_GetPassengerSeatAttachment( CBaseCombatCharacter *pPassenger ) = 0; + virtual bool NPC_HasAvailableSeat( string_t strRoleName ) = 0; + + virtual const PassengerSeatAnims_t *NPC_GetPassengerSeatAnims( CBaseCombatCharacter *pPassenger, PassengerSeatAnimType_t nType ) = 0; + virtual CBaseCombatCharacter *NPC_GetPassengerInSeat( int nRoleID, int nSeatID ) = 0; + + virtual void RestorePassengerInfo( void ) = 0; + + // NPC Driving + virtual bool NPC_CanDrive( void ) = 0; + virtual void NPC_SetDriver( CNPC_VehicleDriver *pDriver ) = 0; + virtual void NPC_DriveVehicle( void ) = 0; + virtual void NPC_ThrottleCenter( void ) = 0; + virtual void NPC_ThrottleReverse( void ) = 0; + virtual void NPC_ThrottleForward( void ) = 0; + virtual void NPC_Brake( void ) = 0; + virtual void NPC_TurnLeft( float flDegrees ) = 0; + virtual void NPC_TurnRight( float flDegrees ) = 0; + virtual void NPC_TurnCenter( void ) = 0; + virtual void NPC_PrimaryFire( void ) = 0; + virtual void NPC_SecondaryFire( void ) = 0; + virtual bool NPC_HasPrimaryWeapon( void ) = 0; + virtual bool NPC_HasSecondaryWeapon( void ) = 0; + virtual void NPC_AimPrimaryWeapon( Vector vecTarget ) = 0; + virtual void NPC_AimSecondaryWeapon( Vector vecTarget ) = 0; + + // Weapon handling + virtual void Weapon_PrimaryRanges( float *flMinRange, float *flMaxRange ) = 0; + virtual void Weapon_SecondaryRanges( float *flMinRange, float *flMaxRange ) = 0; + virtual float Weapon_PrimaryCanFireAt( void ) = 0; // Return the time at which this vehicle's primary weapon can fire again + virtual float Weapon_SecondaryCanFireAt( void ) = 0; // Return the time at which this vehicle's secondary weapon can fire again + + // debugging, script file flushed + virtual void ReloadScript() = 0; +}; + +// This is an interface to derive from if your class contains an IServerVehicle +// handler (i.e. something derived CBaseServerVehicle. +abstract_class IDrivableVehicle +{ +public: + virtual CBaseEntity *GetDriver( void ) = 0; + + // Process movement + virtual void ItemPostFrame( CBasePlayer *pPlayer ) = 0; + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) = 0; + virtual void ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ) = 0; + virtual void FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ) = 0; + + // Entering / Exiting + virtual bool CanEnterVehicle( CBaseEntity *pEntity ) = 0; + virtual bool CanExitVehicle( CBaseEntity *pEntity ) = 0; + virtual void SetVehicleEntryAnim( bool bOn ) = 0; + virtual void SetVehicleExitAnim( bool bOn, Vector vecEyeExitEndpoint ) = 0; + virtual void EnterVehicle( CBaseCombatCharacter *pPassenger ) = 0; + + virtual void PreExitVehicle( CBaseCombatCharacter *pPassenger, int nRole ) = 0; + virtual void ExitVehicle( int nRole ) = 0; + virtual bool AllowBlockedExit( CBaseCombatCharacter *pPassenger, int nRole ) = 0; + virtual bool AllowMidairExit( CBaseCombatCharacter *pPassenger, int nRole ) = 0; + virtual string_t GetVehicleScriptName() = 0; + + virtual bool PassengerShouldReceiveDamage( CTakeDamageInfo &info ) = 0; +}; + +#endif // IVEHICLE_H diff --git a/sp/src/game/server/item_world.cpp b/sp/src/game/server/item_world.cpp new file mode 100644 index 00000000..6f124280 --- /dev/null +++ b/sp/src/game/server/item_world.cpp @@ -0,0 +1,614 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Handling for the base world item. Most of this was moved from items.cpp. +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "player.h" +#include "items.h" +#include "gamerules.h" +#include "engine/IEngineSound.h" +#include "iservervehicle.h" +#include "physics_saverestore.h" +#include "world.h" + +#ifdef HL2MP +#include "hl2mp_gamerules.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define ITEM_PICKUP_BOX_BLOAT 24 + +class CWorldItem : public CBaseAnimating +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( CWorldItem, CBaseAnimating ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Spawn( void ); + + int m_iType; +}; + +LINK_ENTITY_TO_CLASS(world_items, CWorldItem); + +BEGIN_DATADESC( CWorldItem ) + +DEFINE_FIELD( m_iType, FIELD_INTEGER ), + +END_DATADESC() + + +bool CWorldItem::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "type")) + { + m_iType = atoi(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +void CWorldItem::Spawn( void ) +{ + CBaseEntity *pEntity = NULL; + + switch (m_iType) + { + case 44: // ITEM_BATTERY: + pEntity = CBaseEntity::Create( "item_battery", GetLocalOrigin(), GetLocalAngles() ); + break; + case 45: // ITEM_SUIT: + pEntity = CBaseEntity::Create( "item_suit", GetLocalOrigin(), GetLocalAngles() ); + break; + } + + if (!pEntity) + { + Warning("unable to create world_item %d\n", m_iType ); + } + else + { + pEntity->m_target = m_target; + pEntity->SetName( GetEntityName() ); + pEntity->ClearSpawnFlags(); + pEntity->AddSpawnFlags( m_spawnflags ); + } + + UTIL_RemoveImmediate( this ); +} + + +BEGIN_DATADESC( CItem ) + + DEFINE_FIELD( m_bActivateWhenAtRest, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vOriginalSpawnOrigin, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vOriginalSpawnAngles, FIELD_VECTOR ), + DEFINE_PHYSPTR( m_pConstraint ), + + // Function Pointers + DEFINE_ENTITYFUNC( ItemTouch ), + DEFINE_THINKFUNC( Materialize ), + DEFINE_THINKFUNC( ComeToRest ), + +#if defined( HL2MP ) || defined( TF_DLL ) + DEFINE_FIELD( m_flNextResetCheckTime, FIELD_TIME ), + DEFINE_THINKFUNC( FallThink ), +#endif + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "EnablePlayerPickup", InputEnablePlayerPickup ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisablePlayerPickup", InputDisablePlayerPickup ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableNPCPickup", InputEnableNPCPickup ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableNPCPickup", InputDisableNPCPickup ), + DEFINE_INPUTFUNC( FIELD_VOID, "BreakConstraint", InputBreakConstraint ), +#endif + + // Outputs + DEFINE_OUTPUT( m_OnPlayerTouch, "OnPlayerTouch" ), + DEFINE_OUTPUT( m_OnCacheInteraction, "OnCacheInteraction" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CItem::CItem() +{ + m_bActivateWhenAtRest = false; +} + +bool CItem::CreateItemVPhysicsObject( void ) +{ + // Create the object in the physics system + int nSolidFlags = GetSolidFlags() | FSOLID_NOT_STANDABLE; + if ( !m_bActivateWhenAtRest ) + { + nSolidFlags |= FSOLID_TRIGGER; + } + + if ( VPhysicsInitNormal( SOLID_VPHYSICS, nSolidFlags, false ) == NULL ) + { + SetSolid( SOLID_BBOX ); + AddSolidFlags( nSolidFlags ); + + // If it's not physical, drop it to the floor + if (UTIL_DropToFloor(this, MASK_SOLID) == 0) + { + Warning( "Item %s fell out of level at %f,%f,%f\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z); + UTIL_Remove( this ); + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CItem::Spawn( void ) +{ + if ( g_pGameRules->IsAllowedToSpawn( this ) == false ) + { + UTIL_Remove( this ); + return; + } + + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetSolid( SOLID_BBOX ); + SetBlocksLOS( false ); + AddEFlags( EFL_NO_ROTORWASH_PUSH ); + + if( IsX360() ) + { + AddEffects( EF_ITEM_BLINK ); + } + + // This will make them not collide with the player, but will collide + // against other items + weapons + SetCollisionGroup( COLLISION_GROUP_WEAPON ); + CollisionProp()->UseTriggerBounds( true, ITEM_PICKUP_BOX_BLOAT ); + SetTouch(&CItem::ItemTouch); + + if ( CreateItemVPhysicsObject() == false ) + return; + + m_takedamage = DAMAGE_EVENTS_ONLY; + +#if !defined( CLIENT_DLL ) + // Constrained start? + if ( HasSpawnFlags( SF_ITEM_START_CONSTRAINED ) ) + { + //Constrain the weapon in place + IPhysicsObject *pReferenceObject, *pAttachedObject; + + pReferenceObject = g_PhysWorldObject; + pAttachedObject = VPhysicsGetObject(); + + if ( pReferenceObject && pAttachedObject ) + { + constraint_fixedparams_t fixed; + fixed.Defaults(); + fixed.InitWithCurrentObjectState( pReferenceObject, pAttachedObject ); + + fixed.constraint.forceLimit = lbs2kg( 10000 ); + fixed.constraint.torqueLimit = lbs2kg( 10000 ); + + m_pConstraint = physenv->CreateFixedConstraint( pReferenceObject, pAttachedObject, NULL, fixed ); + + m_pConstraint->SetGameData( (void *) this ); + } + } +#endif //CLIENT_DLL + +#if defined( HL2MP ) || defined( TF_DLL ) + SetThink( &CItem::FallThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); +#endif +} + +unsigned int CItem::PhysicsSolidMaskForEntity( void ) const +{ + return BaseClass::PhysicsSolidMaskForEntity() | CONTENTS_PLAYERCLIP; +} + +void CItem::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + + if ( pPlayer ) + { + pPlayer->PickupObject( this ); + } +} + +extern int gEvilImpulse101; + + +//----------------------------------------------------------------------------- +// Activate when at rest, but don't allow pickup until then +//----------------------------------------------------------------------------- +void CItem::ActivateWhenAtRest( float flTime /* = 0.5f */ ) +{ + RemoveSolidFlags( FSOLID_TRIGGER ); + m_bActivateWhenAtRest = true; + SetThink( &CItem::ComeToRest ); + SetNextThink( gpGlobals->curtime + flTime ); +} + + +//----------------------------------------------------------------------------- +// Become touchable when we are at rest +//----------------------------------------------------------------------------- +void CItem::OnEntityEvent( EntityEvent_t event, void *pEventData ) +{ + BaseClass::OnEntityEvent( event, pEventData ); + + switch( event ) + { + case ENTITY_EVENT_WATER_TOUCH: + { + // Delay rest for a sec, to avoid changing collision + // properties inside a collision callback. + SetThink( &CItem::ComeToRest ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } + break; + } +} + + +//----------------------------------------------------------------------------- +// Become touchable when we are at rest +//----------------------------------------------------------------------------- +void CItem::ComeToRest( void ) +{ + if ( m_bActivateWhenAtRest ) + { + m_bActivateWhenAtRest = false; + AddSolidFlags( FSOLID_TRIGGER ); + SetThink( NULL ); + } +} + +#if defined( HL2MP ) || defined( TF_DLL ) + +//----------------------------------------------------------------------------- +// Purpose: Items that have just spawned run this think to catch them when +// they hit the ground. Once we're sure that the object is grounded, +// we change its solid type to trigger and set it in a large box that +// helps the player get it. +//----------------------------------------------------------------------------- +void CItem::FallThink ( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + +#if defined( HL2MP ) + bool shouldMaterialize = false; + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if ( pPhysics ) + { + shouldMaterialize = pPhysics->IsAsleep(); + } + else + { + shouldMaterialize = (GetFlags() & FL_ONGROUND) ? true : false; + } + + if ( shouldMaterialize ) + { + SetThink ( NULL ); + + m_vOriginalSpawnOrigin = GetAbsOrigin(); + m_vOriginalSpawnAngles = GetAbsAngles(); + + HL2MPRules()->AddLevelDesignerPlacedObject( this ); + } +#endif // HL2MP + +#if defined( TF_DLL ) + // We only come here if ActivateWhenAtRest() is never called, + // which is the case when creating currencypacks in MvM + if ( !( GetFlags() & FL_ONGROUND ) ) + { + if ( !GetAbsVelocity().Length() && GetMoveType() == MOVETYPE_FLYGRAVITY ) + { + // Mr. Game, meet Mr. Hammer. Mr. Hammer, meet the uncooperative Mr. Physics. + // Mr. Physics really doesn't want to give our friend the FL_ONGROUND flag. + // This means our wonderfully helpful radius currency collection code will be sad. + // So in the name of justice, we ask that this flag be delivered unto him. + + SetMoveType( MOVETYPE_NONE ); + SetGroundEntity( GetWorldEntity() ); + } + } + else + { + SetThink( &CItem::ComeToRest ); + } +#endif // TF +} + +#endif // HL2MP, TF + +//----------------------------------------------------------------------------- +// Purpose: Used to tell whether an item may be picked up by the player. This +// accounts for solid obstructions being in the way. +// Input : *pItem - item in question +// *pPlayer - player attempting the pickup +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool UTIL_ItemCanBeTouchedByPlayer( CBaseEntity *pItem, CBasePlayer *pPlayer ) +{ + if ( pItem == NULL || pPlayer == NULL ) + return false; + +#ifdef MAPBASE + // Weapons go through this, but this is identical to SF_WEAPON_NO_PLAYER_PICKUP and that would be a convenient coincidence, + // but OnCacheInteraction worked with "No player pickup" before and SF_WEAPON_NO_PLAYER_PICKUP is often checked after this, + // so we have to make sure we're not dealing with a weapon for this check after all. + if (pItem->HasSpawnFlags(SF_ITEM_NO_PLAYER_PICKUP) && !pItem->IsBaseCombatWeapon()) + return false; + + // Fortunately, unlike the above code, this flag is identical in between weapons and items + // and can safely be used without identifying the entity. + if (pItem->HasSpawnFlags(SF_ITEM_ALWAYS_TOUCHABLE)) + return true; +#endif + + // For now, always allow a vehicle riding player to pick up things they're driving over + if ( pPlayer->IsInAVehicle() ) + return true; + + // Get our test positions + Vector vecStartPos; + IPhysicsObject *pPhysObj = pItem->VPhysicsGetObject(); + if ( pPhysObj != NULL ) + { + // Use the physics hull's center + QAngle vecAngles; + pPhysObj->GetPosition( &vecStartPos, &vecAngles ); + } + else + { + // Use the generic bbox center + vecStartPos = pItem->CollisionProp()->WorldSpaceCenter(); + } + + Vector vecEndPos = pPlayer->EyePosition(); + + // FIXME: This is the simple first try solution towards the problem. We need to take edges and shape more into account + // for this to be fully robust. + + // Trace between to see if we're occluded + trace_t tr; + CTraceFilterSkipTwoEntities filter( pPlayer, pItem, COLLISION_GROUP_PLAYER_MOVEMENT ); + UTIL_TraceLine( vecStartPos, vecEndPos, MASK_SOLID, &filter, &tr ); + + // Occluded + // FIXME: For now, we exclude starting in solid because there are cases where this doesn't matter + if ( tr.fraction < 1.0f ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Whether or not the item can be touched and picked up by the player, taking +// into account obstructions and other hinderances +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CItem::ItemCanBeTouchedByPlayer( CBasePlayer *pPlayer ) +{ + return UTIL_ItemCanBeTouchedByPlayer( this, pPlayer ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pOther - +//----------------------------------------------------------------------------- +void CItem::ItemTouch( CBaseEntity *pOther ) +{ + // Vehicles can touch items + pick them up + if ( pOther->GetServerVehicle() ) + { + pOther = pOther->GetServerVehicle()->GetPassenger(); + if ( !pOther ) + return; + } + + // if it's not a player, ignore + if ( !pOther->IsPlayer() ) + return; + + CBasePlayer *pPlayer = (CBasePlayer *)pOther; + + // Must be a valid pickup scenario (no blocking). Though this is a more expensive + // check than some that follow, this has to be first Obecause it's the only one + // that inhibits firing the output OnCacheInteraction. + if ( ItemCanBeTouchedByPlayer( pPlayer ) == false ) + return; + + m_OnCacheInteraction.FireOutput(pOther, this); + + // Can I even pick stuff up? + if ( !pPlayer->IsAllowedToPickupWeapons() ) + return; + + // ok, a player is touching this item, but can he have it? + if ( !g_pGameRules->CanHaveItem( pPlayer, this ) ) + { + // no? Ignore the touch. + return; + } + + if ( MyTouch( pPlayer ) ) + { + m_OnPlayerTouch.FireOutput(pOther, this); + + SetTouch( NULL ); + SetThink( NULL ); + + // player grabbed the item. + g_pGameRules->PlayerGotItem( pPlayer, this ); + if ( g_pGameRules->ItemShouldRespawn( this ) == GR_ITEM_RESPAWN_YES ) + { + Respawn(); + } + else + { + UTIL_Remove( this ); + +#ifdef HL2MP + HL2MPRules()->RemoveLevelDesignerPlacedObject( this ); +#endif + } + } + else if (gEvilImpulse101) + { + UTIL_Remove( this ); + } +} + +CBaseEntity* CItem::Respawn( void ) +{ + SetTouch( NULL ); + AddEffects( EF_NODRAW ); + + VPhysicsDestroyObject(); + + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_TRIGGER ); + + UTIL_SetOrigin( this, g_pGameRules->VecItemRespawnSpot( this ) );// blip to whereever you should respawn. + SetAbsAngles( g_pGameRules->VecItemRespawnAngles( this ) );// set the angles. + +#if !defined( TF_DLL ) + UTIL_DropToFloor( this, MASK_SOLID ); +#endif + + RemoveAllDecals(); //remove any decals + + SetThink ( &CItem::Materialize ); + SetNextThink( gpGlobals->curtime + g_pGameRules->FlItemRespawnTime( this ) ); + return this; +} + +void CItem::Materialize( void ) +{ + CreateItemVPhysicsObject(); + + if ( IsEffectActive( EF_NODRAW ) ) + { + // changing from invisible state to visible. + +#ifdef HL2MP + EmitSound( "AlyxEmp.Charge" ); +#else + EmitSound( "Item.Materialize" ); +#endif + RemoveEffects( EF_NODRAW ); + DoMuzzleFlash(); + } + + SetTouch( &CItem::ItemTouch ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CItem::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "Item.Materialize" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPhysGunUser - +// PICKED_UP_BY_CANNON - +//----------------------------------------------------------------------------- +void CItem::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + m_OnCacheInteraction.FireOutput(pPhysGunUser, this); + + if ( reason == PICKED_UP_BY_CANNON ) + { + // Expand the pickup box + CollisionProp()->UseTriggerBounds( true, ITEM_PICKUP_BOX_BLOAT * 2 ); + + if( m_pConstraint != NULL ) + { + physenv->DestroyConstraint( m_pConstraint ); + m_pConstraint = NULL; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPhysGunUser - +// reason - +//----------------------------------------------------------------------------- +void CItem::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ) +{ + // Restore the pickup box to the original + CollisionProp()->UseTriggerBounds( true, ITEM_PICKUP_BOX_BLOAT ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CItem::InputEnablePlayerPickup( inputdata_t &inputdata ) +{ + RemoveSpawnFlags(SF_ITEM_NO_PLAYER_PICKUP); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CItem::InputDisablePlayerPickup( inputdata_t &inputdata ) +{ + AddSpawnFlags(SF_ITEM_NO_PLAYER_PICKUP); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CItem::InputEnableNPCPickup( inputdata_t &inputdata ) +{ + RemoveSpawnFlags(SF_ITEM_NO_NPC_PICKUP); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CItem::InputDisableNPCPickup( inputdata_t &inputdata ) +{ + AddSpawnFlags(SF_ITEM_NO_NPC_PICKUP); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CItem::InputBreakConstraint( inputdata_t &inputdata ) +{ + if ( m_pConstraint != NULL ) + { + physenv->DestroyConstraint( m_pConstraint ); + m_pConstraint = NULL; + } +} +#endif diff --git a/sp/src/game/server/items.h b/sp/src/game/server/items.h new file mode 100644 index 00000000..2089fa1f --- /dev/null +++ b/sp/src/game/server/items.h @@ -0,0 +1,118 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ITEMS_H +#define ITEMS_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "entityoutput.h" +#include "player_pickup.h" +#include "vphysics/constraints.h" + + +// Armor given by a battery +#define MAX_NORMAL_BATTERY 100 + +// Ammo counts given by ammo items +#define SIZE_AMMO_PISTOL 20 +#define SIZE_AMMO_PISTOL_LARGE 100 +#define SIZE_AMMO_SMG1 45 +#define SIZE_AMMO_SMG1_LARGE 225 +#define SIZE_AMMO_AR2 20 +#define SIZE_AMMO_AR2_LARGE 100 +#define SIZE_AMMO_RPG_ROUND 1 +#define SIZE_AMMO_SMG1_GRENADE 1 +#define SIZE_AMMO_BUCKSHOT 20 +#define SIZE_AMMO_357 6 +#define SIZE_AMMO_357_LARGE 20 +#define SIZE_AMMO_CROSSBOW 6 +#define SIZE_AMMO_AR2_ALTFIRE 1 + +#define SF_ITEM_START_CONSTRAINED 0x00000001 +#ifdef MAPBASE +// Copied from CBaseCombatWeapon's flags, including any additions we made to those. +// I really, REALLY hope no item uses their own spawnflags either. +#define SF_ITEM_NO_PLAYER_PICKUP (1<<1) +#define SF_ITEM_NO_PHYSCANNON_PUNT (1<<2) +#define SF_ITEM_NO_NPC_PICKUP (1<<3) + +#define SF_ITEM_ALWAYS_TOUCHABLE (1<<6) // This needs to stay synced with the weapon spawnflag +#endif + + +class CItem : public CBaseAnimating, public CDefaultPlayerPickupVPhysics +{ +public: + DECLARE_CLASS( CItem, CBaseAnimating ); + + CItem(); + + virtual void Spawn( void ); + virtual void Precache(); + + unsigned int PhysicsSolidMaskForEntity( void ) const; + + virtual CBaseEntity* Respawn( void ); + virtual void ItemTouch( CBaseEntity *pOther ); + virtual void Materialize( void ); + virtual bool MyTouch( CBasePlayer *pPlayer ) { return false; }; + + // Become touchable when we are at rest + virtual void OnEntityEvent( EntityEvent_t event, void *pEventData ); + + // Activate when at rest, but don't allow pickup until then + void ActivateWhenAtRest( float flTime = 0.5f ); + + // IPlayerPickupVPhysics + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason = PICKED_UP_BY_CANNON ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ); + + virtual int ObjectCaps() { return BaseClass::ObjectCaps() | FCAP_IMPULSE_USE | FCAP_WCEDIT_POSITION; }; + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + Vector GetOriginalSpawnOrigin( void ) { return m_vOriginalSpawnOrigin; } + QAngle GetOriginalSpawnAngles( void ) { return m_vOriginalSpawnAngles; } + void SetOriginalSpawnOrigin( const Vector& origin ) { m_vOriginalSpawnOrigin = origin; } + void SetOriginalSpawnAngles( const QAngle& angles ) { m_vOriginalSpawnAngles = angles; } + bool CreateItemVPhysicsObject( void ); + virtual bool ItemCanBeTouchedByPlayer( CBasePlayer *pPlayer ); + +#if defined( HL2MP ) || defined( TF_DLL ) + void FallThink( void ); + float m_flNextResetCheckTime; +#endif + +#ifdef MAPBASE + // This is in CBaseEntity, but I can't find a use for it anywhere. + // Must not have been fully implemented. Please remove this if it turns out to be something important. + virtual bool IsCombatItem() { return true; } + + void InputEnablePlayerPickup( inputdata_t &inputdata ); + void InputDisablePlayerPickup( inputdata_t &inputdata ); + void InputEnableNPCPickup( inputdata_t &inputdata ); + void InputDisableNPCPickup( inputdata_t &inputdata ); + void InputBreakConstraint( inputdata_t &inputdata ); +#endif + + DECLARE_DATADESC(); +protected: + virtual void ComeToRest( void ); + +private: + bool m_bActivateWhenAtRest; + COutputEvent m_OnPlayerTouch; + COutputEvent m_OnCacheInteraction; + + Vector m_vOriginalSpawnOrigin; + QAngle m_vOriginalSpawnAngles; + + IPhysicsConstraint *m_pConstraint; +}; + +#endif // ITEMS_H diff --git a/sp/src/game/server/lightglow.cpp b/sp/src/game/server/lightglow.cpp new file mode 100644 index 00000000..d941f033 --- /dev/null +++ b/sp/src/game/server/lightglow.cpp @@ -0,0 +1,152 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" +#include "sendproxy.h" +#include "sun_shared.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define LIGHTGLOW_MAXDIST_BITS 16 +#define LIGHTGLOW_MAXDIST_MAX_VALUE ((1 << LIGHTGLOW_MAXDIST_BITS)-1) + +#define LIGHTGLOW_OUTERMAXDIST_BITS 16 +#define LIGHTGLOW_OUTERMAXDIST_MAX_VALUE ((1 << LIGHTGLOW_OUTERMAXDIST_BITS)-1) + +class CLightGlow : public CBaseEntity +{ +public: + DECLARE_CLASS( CLightGlow, CBaseEntity ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CLightGlow(); + + virtual void Spawn( void ); + virtual void Activate( void ); + virtual int UpdateTransmitState( void ); + + void InputColor(inputdata_t &data); +#ifdef MAPBASE + void InputEnable( inputdata_t &data ) { m_bDisabled = false; } + void InputDisable( inputdata_t &data ) { m_bDisabled = true; } +#endif + +public: + CNetworkVar( int, m_nHorizontalSize ); + CNetworkVar( int, m_nVerticalSize ); + CNetworkVar( int, m_nMinDist ); + CNetworkVar( int, m_nMaxDist ); + CNetworkVar( int, m_nOuterMaxDist ); + + CNetworkVar( float, m_flGlowProxySize ); + CNetworkVar( float, m_flHDRColorScale ); + +#ifdef MAPBASE + CNetworkVar( bool, m_bDisabled ); +#endif +}; + +extern void SendProxy_Angles( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CLightGlow, DT_LightGlow ) + SendPropInt( SENDINFO(m_clrRender), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), + SendPropInt( SENDINFO(m_nHorizontalSize), 16, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nVerticalSize), 16, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nMinDist), 16, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nMaxDist), LIGHTGLOW_MAXDIST_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nOuterMaxDist), LIGHTGLOW_OUTERMAXDIST_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_spawnflags), 8, SPROP_UNSIGNED ), + SendPropVector(SENDINFO(m_vecOrigin), -1, SPROP_COORD ), + SendPropQAngles (SENDINFO(m_angRotation), 13, 0, SendProxy_Angles ), + SendPropEHandle (SENDINFO_NAME(m_hMoveParent, moveparent)), + SendPropFloat( SENDINFO(m_flGlowProxySize ), 6, SPROP_ROUNDUP, 0.0f, 64.0f ), + SendPropFloat( SENDINFO_NAME( m_flHDRColorScale, HDRColorScale ), 0, SPROP_NOSCALE, 0.0f, 100.0f ), +#ifdef MAPBASE + SendPropBool( SENDINFO( m_bDisabled ) ), +#endif +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_lightglow, CLightGlow ); + +BEGIN_DATADESC( CLightGlow ) + + DEFINE_KEYFIELD( m_nVerticalSize, FIELD_INTEGER, "VerticalGlowSize" ), + DEFINE_KEYFIELD( m_nHorizontalSize, FIELD_INTEGER, "HorizontalGlowSize" ), + DEFINE_KEYFIELD( m_nMinDist, FIELD_INTEGER, "MinDist" ), + DEFINE_KEYFIELD( m_nMaxDist, FIELD_INTEGER, "MaxDist" ), + DEFINE_KEYFIELD( m_nOuterMaxDist, FIELD_INTEGER, "OuterMaxDist" ), + DEFINE_KEYFIELD( m_flGlowProxySize, FIELD_FLOAT, "GlowProxySize" ), + DEFINE_KEYFIELD( m_flHDRColorScale, FIELD_FLOAT, "HDRColorScale" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +#endif + DEFINE_INPUTFUNC( FIELD_COLOR32, "Color", InputColor ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CLightGlow::CLightGlow( void ) +{ + m_nHorizontalSize = 0.0f; + m_nVerticalSize = 0.0f; + m_nMinDist = 0.0f; + m_nMaxDist = 0.0f; + + m_flGlowProxySize = 2.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLightGlow::Spawn( void ) +{ + BaseClass::Spawn(); + + // No model but we still need to force this! + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); +} + +//----------------------------------------------------------------------------- +// Purpose: Always transmit light glows to clients to avoid spikes as we enter +// or leave PVS. Done because we often have many glows in an area. +//----------------------------------------------------------------------------- +int CLightGlow::UpdateTransmitState( void ) +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLightGlow::Activate() +{ + BaseClass::Activate(); + + if ( m_nMaxDist > LIGHTGLOW_MAXDIST_MAX_VALUE ) + { + Warning( "env_lightglow maxdist too large (%d should be %d).\n", m_nMaxDist.Get(), LIGHTGLOW_MAXDIST_MAX_VALUE ); + m_nMaxDist = LIGHTGLOW_MAXDIST_MAX_VALUE; + } + + if ( m_nOuterMaxDist > LIGHTGLOW_OUTERMAXDIST_MAX_VALUE ) + { + Warning( "env_lightglow outermaxdist too large (%d should be %d).\n", m_nOuterMaxDist.Get(), LIGHTGLOW_OUTERMAXDIST_MAX_VALUE ); + m_nOuterMaxDist = LIGHTGLOW_OUTERMAXDIST_MAX_VALUE; + } +} + +void CLightGlow::InputColor(inputdata_t &inputdata) +{ + m_clrRender = inputdata.value.Color32(); +} diff --git a/sp/src/game/server/lights.cpp b/sp/src/game/server/lights.cpp new file mode 100644 index 00000000..5e6a0918 --- /dev/null +++ b/sp/src/game/server/lights.cpp @@ -0,0 +1,257 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: spawn and think functions for editor-placed lights +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "lights.h" +#include "world.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( light, CLight ); + +BEGIN_DATADESC( CLight ) + + DEFINE_FIELD( m_iCurrentFade, FIELD_CHARACTER), + DEFINE_FIELD( m_iTargetFade, FIELD_CHARACTER), + + DEFINE_KEYFIELD( m_iStyle, FIELD_INTEGER, "style" ), + DEFINE_KEYFIELD( m_iDefaultStyle, FIELD_INTEGER, "defaultstyle" ), + DEFINE_KEYFIELD( m_iszPattern, FIELD_STRING, "pattern" ), + + // Fuctions + DEFINE_FUNCTION( FadeThink ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetPattern", InputSetPattern ), + DEFINE_INPUTFUNC( FIELD_STRING, "FadeToPattern", InputFadeToPattern ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + +END_DATADESC() + + + +// +// Cache user-entity-field values until spawn is called. +// +bool CLight::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "pitch")) + { + QAngle angles = GetAbsAngles(); + angles.x = atof(szValue); + SetAbsAngles( angles ); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +// Light entity +// If targeted, it will toggle between on or off. +void CLight::Spawn( void ) +{ + if (!GetEntityName()) + { // inert light + UTIL_Remove( this ); + return; + } + + if (m_iStyle >= 32) + { + if ( m_iszPattern == NULL_STRING && m_iDefaultStyle > 0 ) + { + m_iszPattern = MAKE_STRING(GetDefaultLightstyleString(m_iDefaultStyle)); + } + + if (FBitSet(m_spawnflags, SF_LIGHT_START_OFF)) + engine->LightStyle(m_iStyle, "a"); + else if (m_iszPattern != NULL_STRING) + engine->LightStyle(m_iStyle, (char *)STRING( m_iszPattern )); + else + engine->LightStyle(m_iStyle, "m"); + } +} + + +void CLight::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if (m_iStyle >= 32) + { + if ( !ShouldToggle( useType, !FBitSet(m_spawnflags, SF_LIGHT_START_OFF) ) ) + return; + + Toggle(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Turn the light on +//----------------------------------------------------------------------------- +void CLight::TurnOn( void ) +{ + if ( m_iszPattern != NULL_STRING ) + { + engine->LightStyle( m_iStyle, (char *) STRING( m_iszPattern ) ); + } + else + { + engine->LightStyle( m_iStyle, "m" ); + } + + CLEARBITS( m_spawnflags, SF_LIGHT_START_OFF ); +} + +//----------------------------------------------------------------------------- +// Purpose: Turn the light off +//----------------------------------------------------------------------------- +void CLight::TurnOff( void ) +{ + engine->LightStyle( m_iStyle, "a" ); + SETBITS( m_spawnflags, SF_LIGHT_START_OFF ); +} + +//----------------------------------------------------------------------------- +// Purpose: Toggle the light on/off +//----------------------------------------------------------------------------- +void CLight::Toggle( void ) +{ + //Toggle it + if ( FBitSet( m_spawnflags, SF_LIGHT_START_OFF ) ) + { + TurnOn(); + } + else + { + TurnOff(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handle the "turnon" input handler +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CLight::InputTurnOn( inputdata_t &inputdata ) +{ + TurnOn(); +} + +//----------------------------------------------------------------------------- +// Purpose: Handle the "turnoff" input handler +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CLight::InputTurnOff( inputdata_t &inputdata ) +{ + TurnOff(); +} + +//----------------------------------------------------------------------------- +// Purpose: Handle the "toggle" input handler +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CLight::InputToggle( inputdata_t &inputdata ) +{ + Toggle(); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting a light pattern +//----------------------------------------------------------------------------- +void CLight::InputSetPattern( inputdata_t &inputdata ) +{ + m_iszPattern = inputdata.value.StringID(); + engine->LightStyle(m_iStyle, (char *)STRING( m_iszPattern )); + + // Light is on if pattern is set + CLEARBITS(m_spawnflags, SF_LIGHT_START_OFF); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for fading from first value in old pattern to +// first value in new pattern +//----------------------------------------------------------------------------- +void CLight::InputFadeToPattern( inputdata_t &inputdata ) +{ + m_iCurrentFade = (STRING(m_iszPattern))[0]; + m_iTargetFade = inputdata.value.String()[0]; + m_iszPattern = inputdata.value.StringID(); + SetThink(&CLight::FadeThink); + SetNextThink( gpGlobals->curtime ); + + // Light is on if pattern is set + CLEARBITS(m_spawnflags, SF_LIGHT_START_OFF); +} + + +//------------------------------------------------------------------------------ +// Purpose : Fade light to new starting pattern value then stop thinking +//------------------------------------------------------------------------------ +void CLight::FadeThink(void) +{ + if (m_iCurrentFade < m_iTargetFade) + { + m_iCurrentFade++; + } + else if (m_iCurrentFade > m_iTargetFade) + { + m_iCurrentFade--; + } + + // If we're done fading instantiate our light pattern and stop thinking + if (m_iCurrentFade == m_iTargetFade) + { + engine->LightStyle(m_iStyle, (char *)STRING( m_iszPattern )); + SetNextThink( TICK_NEVER_THINK ); + } + // Otherwise instantiate our current fade value and keep thinking + else + { + char sCurString[2]; + sCurString[0] = m_iCurrentFade; + sCurString[1] = 0; + engine->LightStyle(m_iStyle, sCurString); + + // UNDONE: Consider making this settable war to control fade speed + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + +// +// shut up spawn functions for new spotlights +// +LINK_ENTITY_TO_CLASS( light_spot, CLight ); +LINK_ENTITY_TO_CLASS( light_glspot, CLight ); + + + +LINK_ENTITY_TO_CLASS( light_environment, CEnvLight ); + +bool CEnvLight::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "_light")) + { + // nothing + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +void CEnvLight::Spawn( void ) +{ + BaseClass::Spawn( ); +} diff --git a/sp/src/game/server/lights.h b/sp/src/game/server/lights.h new file mode 100644 index 00000000..b457da9e --- /dev/null +++ b/sp/src/game/server/lights.h @@ -0,0 +1,57 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef LIGHTS_H +#define LIGHTS_H +#ifdef _WIN32 +#pragma once +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CLight : public CPointEntity +{ +public: + DECLARE_CLASS( CLight, CPointEntity ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Spawn( void ); + void FadeThink( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void TurnOn( void ); + void TurnOff( void ); + void Toggle( void ); + + // Input handlers + void InputSetPattern( inputdata_t &inputdata ); + void InputFadeToPattern( inputdata_t &inputdata ); + + void InputToggle( inputdata_t &inputdata ); + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + int m_iStyle; + int m_iDefaultStyle; + string_t m_iszPattern; + char m_iCurrentFade; + char m_iTargetFade; +}; + +class CEnvLight : public CLight +{ +public: + DECLARE_CLASS(CEnvLight, CLight); + + bool KeyValue(const char* szKeyName, const char* szValue); + void Spawn(void); +}; + +#endif // LIGHTS_H diff --git a/sp/src/game/server/locksounds.h b/sp/src/game/server/locksounds.h new file mode 100644 index 00000000..c70b4c52 --- /dev/null +++ b/sp/src/game/server/locksounds.h @@ -0,0 +1,37 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Defines a structure common to buttons and doors for playing locked +// and unlocked sounds. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef LOCKSOUNDS_H +#define LOCKSOUNDS_H +#ifdef _WIN32 +#pragma once +#endif + +#include "datamap.h" + + +struct locksound_t // sounds that doors and buttons make when locked/unlocked +{ + DECLARE_DATADESC(); + + string_t sLockedSound; // sound a door makes when it's locked + string_t sLockedSentence; // sentence group played when door is locked + string_t sUnlockedSound; // sound a door makes when it's unlocked + string_t sUnlockedSentence; // sentence group played when door is unlocked + + int iLockedSentence; // which sentence in sentence group to play next + int iUnlockedSentence; // which sentence in sentence group to play next + + float flwaitSound; // time delay between playing consecutive 'locked/unlocked' sounds + float flwaitSentence; // time delay between playing consecutive sentences + byte bEOFLocked; // true if hit end of list of locked sentences + byte bEOFUnlocked; // true if hit end of list of unlocked sentences +}; + + +#endif // LOCKSOUNDS_H diff --git a/sp/src/game/server/logic_achievement.cpp b/sp/src/game/server/logic_achievement.cpp new file mode 100644 index 00000000..5dcea15a --- /dev/null +++ b/sp/src/game/server/logic_achievement.cpp @@ -0,0 +1,182 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Defines a logical entity which passes achievement related events to the gamerules system. + +#include "cbase.h" +#include "gamerules.h" +#include "entityinput.h" +#include "entityoutput.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +/* + These are the string choices in the FGD: + + ACHIEVEMENT_EVENT_HL2_HIT_CANCOP_WITHCAN + ACHIEVEMENT_EVENT_HL2_ESCAPE_APARTMENTRAID + ACHIEVEMENT_EVENT_HL2_FIND_ONEGMAN + ACHIEVEMENT_EVENT_HL2_BREAK_MINITELEPORTER + ACHIEVEMENT_EVENT_HL2_GET_PISTOL + ACHIEVEMENT_EVENT_HL2_GET_AIRBOAT + ACHIEVEMENT_EVENT_HL2_GET_AIRBOATGUN + ACHIEVEMENT_EVENT_HL2_FIND_VORTIGAUNTCAVE + ACHIEVEMENT_EVENT_HL2_KILL_CHOPPER + ACHIEVEMENT_EVENT_HL2_FIND_HEVFACEPLATE + ACHIEVEMENT_EVENT_HL2_GET_GRAVITYGUN + ACHIEVEMENT_EVENT_HL2_MAKEABASKET + ACHIEVEMENT_EVENT_HL2_BEAT_RAVENHOLM_NOWEAPONS_START + ACHIEVEMENT_EVENT_HL2_BEAT_RAVENHOLM_NOWEAPONS_END + ACHIEVEMENT_EVENT_HL2_BEAT_CEMETERY + ACHIEVEMENT_EVENT_HL2_KILL_ENEMIES_WITHCRANE + ACHIEVEMENT_EVENT_HL2_PIN_SOLDIER_TOBILLBOARD + ACHIEVEMENT_EVENT_HL2_KILL_ODESSAGUNSHIP + ACHIEVEMENT_EVENT_HL2_BEAT_DONTTOUCHSAND + ACHIEVEMENT_EVENT_HL2_ENTER_NOVAPROSPEKT, + ACHIEVEMENT_EVENT_HL2_BEAT_TURRETSTANDOFF2 + ACHIEVEMENT_EVENT_HL2_BEAT_NOVAPROSPEKT + ACHIEVEMENT_EVENT_HL2_BEAT_TOXICTUNNEL + ACHIEVEMENT_EVENT_HL2_BEAT_PLAZASTANDOFF + ACHIEVEMENT_EVENT_HL2_KILL_ALLC17SNIPERS + ACHIEVEMENT_EVENT_HL2_BEAT_SUPRESSIONDEVICE + ACHIEVEMENT_EVENT_HL2_BEAT_C17STRIDERSTANDOFF + ACHIEVEMENT_EVENT_HL2_REACH_BREENSOFFICE + ACHIEVEMENT_EVENT_HL2_FIND_LAMDACACHE + + // EP1 + ACHIEVEMENT_EVENT_EP1_BEAT_MAINELEVATOR + ACHIEVEMENT_EVENT_EP1_BEAT_CITADELCORE + ACHIEVEMENT_EVENT_EP1_BEAT_CITADELCORE_NOSTALKERKILLS + ACHIEVEMENT_EVENT_EP1_BEAT_GARAGEELEVATORSTANDOFF + ACHIEVEMENT_EVENT_EP1_KILL_ENEMIES_WITHSNIPERALYX + ACHIEVEMENT_EVENT_EP1_BEAT_HOSPITALATTICGUNSHIP + ACHIEVEMENT_EVENT_EP1_BEAT_CITIZENESCORT_NOCITIZENDEATHS + + // EP2 + ACHIEVEMENT_EVENT_EP2_BREAK_ALLWEBS + ACHIEVEMENT_EVENT_EP2_BEAT_ANTLIONINVASION + ACHIEVEMENT_EVENT_EP2_BEAT_ANTLIONGUARDS + ACHIEVEMENT_EVENT_EP2_BEAT_HUNTERAMBUSH + ACHIEVEMENT_EVENT_EP2_KILL_COMBINECANNON + ACHIEVEMENT_EVENT_EP2_FIND_RADAR_CACHE + ACHIEVEMENT_EVENT_EP2_BEAT_RACEWITHDOG + ACHIEVEMENT_EVENT_EP2_BEAT_ROCKETCACHEPUZZLE + ACHIEVEMENT_EVENT_EP2_BEAT_WHITEFORESTINN + ACHIEVEMENT_EVENT_EP2_PUT_ITEMINROCKET + ACHIEVEMENT_EVENT_EP2_BEAT_MISSILESILO2 + ACHIEVEMENT_EVENT_EP2_BEAT_OUTLAND12_NOBUILDINGSDESTROYED + + // PORTAL + ACHIEVEMENT_EVENT_PORTAL_GET_PORTALGUNS + ACHIEVEMENT_EVENT_PORTAL_KILL_COMPANIONCUBE + ACHIEVEMENT_EVENT_PORTAL_ESCAPE_TESTCHAMBERS + ACHIEVEMENT_EVENT_PORTAL_BEAT_GAME +*/ + + +// Allows map logic to send achievement related events to the achievement system. +class CLogicAchievement : public CLogicalEntity +{ +public: + DECLARE_CLASS( CLogicAchievement, CLogicalEntity ); + + CLogicAchievement(); + +protected: + + // Inputs + void InputFireEvent( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + bool m_bDisabled; + string_t m_iszAchievementEventID; // Which achievement event this entity marks + + COutputEvent m_OnFired; + + DECLARE_DATADESC(); +}; + + +LINK_ENTITY_TO_CLASS( logic_achievement, CLogicAchievement ); + + +BEGIN_DATADESC( CLogicAchievement ) + + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_KEYFIELD( m_iszAchievementEventID, FIELD_STRING, "AchievementEvent" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "FireEvent", InputFireEvent ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + + // Outputs + DEFINE_OUTPUT( m_OnFired, "OnFired" ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Constructor. +//----------------------------------------------------------------------------- +CLogicAchievement::CLogicAchievement(void) +{ + m_iszAchievementEventID = NULL_STRING; +} + +#define ACHIEVEMENT_PREFIX "ACHIEVEMENT_EVENT_" + +//----------------------------------------------------------------------------- +// Purpose: Sends the achievement event to the achievement marking system. +//----------------------------------------------------------------------------- +void CLogicAchievement::InputFireEvent( inputdata_t &inputdata ) +{ + // If we're active, and our string matched a valid achievement ID + if ( !m_bDisabled && m_iszAchievementEventID != NULL_STRING) + { + m_OnFired.FireOutput( inputdata.pActivator, this ); + + char const *pchName = STRING( m_iszAchievementEventID ); + + int nPrefixLen = Q_strlen( ACHIEVEMENT_PREFIX ); + if ( !Q_strnicmp( pchName, ACHIEVEMENT_PREFIX, nPrefixLen ) ) + { + // Skip the prefix + pchName += nPrefixLen; + if ( pchName && *pchName ) + { + CBroadcastRecipientFilter filter; + g_pGameRules->MarkAchievement( filter, pchName ); + } + } + } +} + +//------------------------------------------------------------------------------ +// Purpose: Turns on the relay, allowing it to fire outputs. +//------------------------------------------------------------------------------ +void CLogicAchievement::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//------------------------------------------------------------------------------ +// Purpose: Turns off the relay, preventing it from firing outputs. +//------------------------------------------------------------------------------ +void CLogicAchievement::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + + +//------------------------------------------------------------------------------ +// Purpose: Toggles the enabled/disabled state of the relay. +//------------------------------------------------------------------------------ +void CLogicAchievement::InputToggle( inputdata_t &inputdata ) +{ + m_bDisabled = !m_bDisabled; +} \ No newline at end of file diff --git a/sp/src/game/server/logic_measure_movement.cpp b/sp/src/game/server/logic_measure_movement.cpp new file mode 100644 index 00000000..06c3da45 --- /dev/null +++ b/sp/src/game/server/logic_measure_movement.cpp @@ -0,0 +1,901 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: This will measure the movement of a target entity and move +// another entity to match the movement of the first. +// +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" +#ifdef MAPBASE +#include "filters.h" +#include "ai_basenpc.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef MAPBASE +// These spawnflags were originally only on logic_measure_direction. +#define SF_LOGIC_MEASURE_MOVEMENT_IGNORE_X ( 1 << 0 ) +#define SF_LOGIC_MEASURE_MOVEMENT_IGNORE_Y ( 1 << 1 ) +#define SF_LOGIC_MEASURE_MOVEMENT_IGNORE_Z ( 1 << 2 ) + +// Uses the "Ignore X/Y/Z" flags for the origin instead of the angles. +// logic_measure_direction uses this flag to control trace direction. +#define SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN ( 1 << 3 ) + +// Uses "Teleport" instead of "SetAbsOrigin" for smoother movement +#define SF_LOGIC_MEASURE_MOVEMENT_TELEPORT ( 1 << 4 ) + +// Specifically refuse to set the target's angles, rather than just turning them to 0 +#define SF_LOGIC_MEASURE_MOVEMENT_DONT_SET_ANGLES ( 1 << 5 ) +#endif + +//----------------------------------------------------------------------------- +// This will measure the movement of a target entity and move +// another entity to match the movement of the first. +//----------------------------------------------------------------------------- +class CLogicMeasureMovement : public CLogicalEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CLogicMeasureMovement, CLogicalEntity ); + +public: + virtual void Activate(); + +#ifdef MAPBASE +public: +#else +private: +#endif + void SetMeasureTarget( const char *pName ); + void SetMeasureReference( const char *pName ); + void SetTarget( const char *pName ); + void SetTargetReference( const char *pName ); + + void InputSetMeasureTarget( inputdata_t &inputdata ); + void InputSetMeasureReference( inputdata_t &inputdata ); + void InputSetTarget( inputdata_t &inputdata ); + void InputSetTargetReference( inputdata_t &inputdata ); + void InputSetTargetScale( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +#ifdef MAPBASE + // Allows for derived class trickery + void MeasureThink(); //{ DoMeasure(); } + + // Allows for InputGetPosition(), etc. + virtual void DoMeasure(Vector &vecOrigin, QAngle &angAngles); + void HandleIgnoreFlags( float *vec ); + + void InputSetMeasureAttachment( inputdata_t &inputdata ); + void InputSetMeasureType( inputdata_t &inputdata ) { m_nMeasureType = inputdata.value.Int(); } + void InputGetPosition( inputdata_t &inputdata ); +#else + void MeasureThink(); + +private: +#endif + enum + { + MEASURE_POSITION = 0, + MEASURE_EYE_POSITION, +#ifdef MAPBASE + MEASURE_ATTACHMENT, + //MEASURE_BARREL_POSITION, +#endif + }; + + string_t m_strMeasureTarget; + string_t m_strMeasureReference; + string_t m_strTargetReference; + + EHANDLE m_hMeasureTarget; + EHANDLE m_hMeasureReference; + EHANDLE m_hTarget; + EHANDLE m_hTargetReference; + +#ifdef MAPBASE + string_t m_strAttachment; + int m_iAttachment; + + bool m_bOutputPosition; + + COutputVector m_OutPosition; + COutputVector m_OutAngles; +#endif + + float m_flScale; + int m_nMeasureType; +}; + + +LINK_ENTITY_TO_CLASS( logic_measure_movement, CLogicMeasureMovement ); + + +BEGIN_DATADESC( CLogicMeasureMovement ) + + DEFINE_KEYFIELD( m_strMeasureTarget, FIELD_STRING, "MeasureTarget" ), + DEFINE_KEYFIELD( m_strMeasureReference, FIELD_STRING, "MeasureReference" ), + DEFINE_KEYFIELD( m_strTargetReference, FIELD_STRING, "TargetReference" ), + DEFINE_KEYFIELD( m_flScale, FIELD_FLOAT, "TargetScale" ), + DEFINE_KEYFIELD( m_nMeasureType, FIELD_INTEGER, "MeasureType" ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMeasureType", InputSetMeasureType ), + + DEFINE_KEYFIELD( m_strAttachment, FIELD_STRING, "MeasureAttachment" ), + DEFINE_FIELD( m_iAttachment, FIELD_EHANDLE ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetMeasureAttachment", InputSetMeasureAttachment ), + + DEFINE_INPUT( m_bOutputPosition, FIELD_BOOLEAN, "ShouldOutputPosition" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "GetPosition", InputGetPosition ), +#endif + + DEFINE_FIELD( m_hMeasureTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_hMeasureReference, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTargetReference, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetMeasureTarget", InputSetMeasureTarget ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetMeasureReference", InputSetMeasureReference ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget", InputSetTarget ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "Target", InputSetTarget ), // For legacy support...even though that name was broken before. +#endif + DEFINE_INPUTFUNC( FIELD_STRING, "SetTargetReference", InputSetTargetReference ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetTargetScale", InputSetTargetScale ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +#ifdef MAPBASE + DEFINE_OUTPUT( m_OutPosition, "OutPosition" ), + DEFINE_OUTPUT( m_OutAngles, "OutAngles" ), +#endif + + DEFINE_THINKFUNC( MeasureThink ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Methods to change various targets +//----------------------------------------------------------------------------- +void CLogicMeasureMovement::Activate() +{ + BaseClass::Activate(); + + SetMeasureTarget( STRING(m_strMeasureTarget) ); + SetMeasureReference( STRING(m_strMeasureReference) ); + SetTarget( STRING(m_target) ); + SetTargetReference( STRING(m_strTargetReference) ); + + SetThink( &CLogicMeasureMovement::MeasureThink ); + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + + +//----------------------------------------------------------------------------- +// Sets the name +//----------------------------------------------------------------------------- +void CLogicMeasureMovement::SetMeasureTarget( const char *pName ) +{ +#ifdef MAPBASE + m_hMeasureTarget = gEntList.FindEntityByName( NULL, pName, this ); +#else + m_hMeasureTarget = gEntList.FindEntityByName( NULL, pName ); +#endif + if ( !m_hMeasureTarget ) + { + if ( Q_strnicmp( STRING(m_strMeasureTarget), "!player", 8 ) ) + { +#ifdef MAPBASE + Warning( "%s: Unable to find measure target entity %s\n", GetDebugName(), pName ); +#else + Warning("logic_measure_movement: Unable to find measure target entity %s\n", pName ); +#endif + } + } +#ifdef MAPBASE + m_iAttachment = 0; +#endif +} + +void CLogicMeasureMovement::SetMeasureReference( const char *pName ) +{ +#ifdef MAPBASE + m_hMeasureReference = gEntList.FindEntityByName( NULL, pName, this ); +#else + m_hMeasureReference = gEntList.FindEntityByName( NULL, pName ); +#endif + if ( !m_hMeasureReference ) + { +#ifdef MAPBASE + Warning( "%s: Unable to find measure reference entity %s\n", GetDebugName(), pName ); +#else + Warning("logic_measure_movement: Unable to find measure reference entity %s\n", pName ); +#endif + } +} + +void CLogicMeasureMovement::SetTarget( const char *pName ) +{ +#ifdef MAPBASE + m_hTarget = gEntList.FindEntityByName( NULL, pName, this ); +#else + m_hTarget = gEntList.FindEntityByName( NULL, pName ); +#endif + if ( !m_hTarget ) + { +#ifdef MAPBASE + Warning( "%s: Unable to find movement target entity %s\n", GetDebugName(), pName ); +#else + Warning("logic_measure_movement: Unable to find movement target entity %s\n", pName ); +#endif + } +} + +void CLogicMeasureMovement::SetTargetReference( const char *pName ) +{ +#ifdef MAPBASE + m_hTargetReference = gEntList.FindEntityByName( NULL, pName, this ); +#else + m_hTargetReference = gEntList.FindEntityByName( NULL, pName ); +#endif + if ( !m_hTargetReference ) + { +#ifdef MAPBASE + Warning( "%s: Unable to find movement reference entity %s\n", GetDebugName(), pName ); +#else + Warning("logic_measure_movement: Unable to find movement reference entity %s\n", pName ); +#endif + } +} + + +//----------------------------------------------------------------------------- +// Apply movement +//----------------------------------------------------------------------------- +void CLogicMeasureMovement::MeasureThink( ) +{ + // FIXME: This is a hack to make measuring !player simpler. The player isn't + // created at Activate time, so m_hMeasureTarget may be NULL because of that. + if ( !m_hMeasureTarget.Get() && !Q_strnicmp( STRING(m_strMeasureTarget), "!player", 8 ) ) + { + SetMeasureTarget( STRING(m_strMeasureTarget) ); + } + + // Make sure all entities are valid + if ( m_hMeasureTarget.Get() && m_hMeasureReference.Get() && m_hTarget.Get() && m_hTargetReference.Get() ) + { +#ifdef MAPBASE + Vector vecNewOrigin; + QAngle vecNewAngles; + DoMeasure(vecNewOrigin, vecNewAngles); + + if (m_bOutputPosition) + { + m_OutPosition.Set(vecNewOrigin, m_hTarget.Get(), this); + m_OutAngles.Set(vecNewAngles, m_hTarget.Get(), this); + } + + if (HasSpawnFlags( SF_LOGIC_MEASURE_MOVEMENT_TELEPORT )) + { + m_hTarget->Teleport( &vecNewOrigin, !HasSpawnFlags(SF_LOGIC_MEASURE_MOVEMENT_DONT_SET_ANGLES) ? &vecNewAngles : NULL, NULL ); + } + else + { + m_hTarget->SetAbsOrigin( vecNewOrigin ); + + if (!HasSpawnFlags(SF_LOGIC_MEASURE_MOVEMENT_DONT_SET_ANGLES)) + m_hTarget->SetAbsAngles( vecNewAngles ); + } +#else + matrix3x4_t matRefToMeasure, matWorldToMeasure; + switch( m_nMeasureType ) + { + case MEASURE_POSITION: + MatrixInvert( m_hMeasureTarget->EntityToWorldTransform(), matWorldToMeasure ); + break; + + case MEASURE_EYE_POSITION: + AngleIMatrix( m_hMeasureTarget->EyeAngles(), m_hMeasureTarget->EyePosition(), matWorldToMeasure ); + break; + // FIXME: Could add attachment point measurement here easily + } + + ConcatTransforms( matWorldToMeasure, m_hMeasureReference->EntityToWorldTransform(), matRefToMeasure ); + + // Apply the scale factor + if ( ( m_flScale != 0.0f ) && ( m_flScale != 1.0f ) ) + { + Vector vecTranslation; + MatrixGetColumn( matRefToMeasure, 3, vecTranslation ); + vecTranslation /= m_flScale; + MatrixSetColumn( vecTranslation, 3, matRefToMeasure ); + } + + // Now apply the new matrix to the new reference point + matrix3x4_t matMeasureToRef, matNewTargetToWorld; + MatrixInvert( matRefToMeasure, matMeasureToRef ); + + ConcatTransforms( m_hTargetReference->EntityToWorldTransform(), matMeasureToRef, matNewTargetToWorld ); + + Vector vecNewOrigin; + QAngle vecNewAngles; + MatrixAngles( matNewTargetToWorld, vecNewAngles, vecNewOrigin ); + m_hTarget->SetAbsOrigin( vecNewOrigin ); + m_hTarget->SetAbsAngles( vecNewAngles ); +#endif + } + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Moves logic_measure_movement's movement measurements to its own function, +// primarily to allow for the GetPosition input without any hacks. +// Also helps with derivative entities that would otherwise have to find a way to re-define the think function. +// Warning: Doesn't account for whether these handles are null! +//----------------------------------------------------------------------------- +void CLogicMeasureMovement::DoMeasure( Vector &vecOrigin, QAngle &angAngles ) +{ + matrix3x4_t matRefToMeasure, matWorldToMeasure; + switch( m_nMeasureType ) + { + case MEASURE_POSITION: + MatrixInvert( m_hMeasureTarget->EntityToWorldTransform(), matWorldToMeasure ); + break; + + case MEASURE_EYE_POSITION: + AngleIMatrix( m_hMeasureTarget->EyeAngles(), m_hMeasureTarget->EyePosition(), matWorldToMeasure ); + break; + + case MEASURE_ATTACHMENT: + if (CBaseAnimating *pAnimating = m_hMeasureTarget->GetBaseAnimating()) + { + if (m_iAttachment <= 0) + m_iAttachment = m_hMeasureTarget->GetBaseAnimating()->LookupAttachment(STRING(m_strAttachment)); + + if (m_iAttachment == -1) + Warning("WARNING: %s requesting invalid attachment %s on %s!\n", GetDebugName(), STRING(m_strAttachment), m_hMeasureTarget->GetDebugName()); + else + pAnimating->GetAttachment(m_iAttachment, matWorldToMeasure); + } + else + { + Warning("WARNING: %s requesting attachment point on non-animating entity %s!\n", GetDebugName(), m_hMeasureTarget->GetDebugName()); + } + break; + } + + ConcatTransforms( matWorldToMeasure, m_hMeasureReference->EntityToWorldTransform(), matRefToMeasure ); + + // Apply the scale factor + if ( ( m_flScale != 0.0f ) && ( m_flScale != 1.0f ) ) + { + Vector vecTranslation; + MatrixGetColumn( matRefToMeasure, 3, vecTranslation ); + vecTranslation /= m_flScale; + MatrixSetColumn( vecTranslation, 3, matRefToMeasure ); + } + + // Now apply the new matrix to the new reference point + matrix3x4_t matMeasureToRef, matNewTargetToWorld; + MatrixInvert( matRefToMeasure, matMeasureToRef ); + + // Handle origin ignorance + if (HasSpawnFlags( SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN )) + { + // Get the position from the matrix's column directly and re-assign it + Vector vecPosition; + MatrixGetColumn( matMeasureToRef, 3, vecPosition ); + + HandleIgnoreFlags( vecPosition.Base() ); + + MatrixSetColumn( vecPosition, 3, matMeasureToRef ); + } + + ConcatTransforms( m_hTargetReference->EntityToWorldTransform(), matMeasureToRef, matNewTargetToWorld ); + + MatrixAngles( matNewTargetToWorld, angAngles, vecOrigin ); + + // If our spawnflags are greater than 0 (and don't just contain our default "TELEPORT" flag), we might need to ignore one of our angles. + if (GetSpawnFlags() && GetSpawnFlags() != SF_LOGIC_MEASURE_MOVEMENT_TELEPORT && !HasSpawnFlags(SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN)) + { + HandleIgnoreFlags( angAngles.Base() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Handles logic_measure_movement's ignore flags on the specified Vector/QAngle +//----------------------------------------------------------------------------- +FORCEINLINE void CLogicMeasureMovement::HandleIgnoreFlags( float *vec ) +{ + if (HasSpawnFlags( SF_LOGIC_MEASURE_MOVEMENT_IGNORE_X )) + vec[0] = 0.0f; + if (HasSpawnFlags( SF_LOGIC_MEASURE_MOVEMENT_IGNORE_Y )) + vec[1] = 0.0f; + if (HasSpawnFlags( SF_LOGIC_MEASURE_MOVEMENT_IGNORE_Z )) + vec[2] = 0.0f; +} +#endif + + +//----------------------------------------------------------------------------- +// Enable, disable +//----------------------------------------------------------------------------- +void CLogicMeasureMovement::InputEnable( inputdata_t &inputdata ) +{ + SetThink( &CLogicMeasureMovement::MeasureThink ); + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + +void CLogicMeasureMovement::InputDisable( inputdata_t &inputdata ) +{ + SetThink( NULL ); +} + + +//----------------------------------------------------------------------------- +// Methods to change various targets +//----------------------------------------------------------------------------- +#ifdef MAPBASE + +// +// Inputs work differently now so they could take !activator, etc. +// + +void CLogicMeasureMovement::InputSetMeasureTarget( inputdata_t &inputdata ) +{ + m_strMeasureTarget = inputdata.value.StringID(); + m_hMeasureTarget = gEntList.FindEntityByName( NULL, STRING(m_strMeasureTarget), this, inputdata.pActivator, inputdata.pCaller ); + if ( !m_hMeasureTarget ) + { + if ( Q_strnicmp( STRING(m_strMeasureTarget), "!player", 8 ) ) + { + Warning( "%s: Unable to find measure target entity %s\n", GetDebugName(), STRING(m_strMeasureTarget) ); + } + } + + m_iAttachment = 0; + + if (!m_hTarget) + SetTarget( STRING(m_target) ); + if (!m_hTargetReference) + SetTargetReference( STRING(m_strTargetReference) ); +} + +void CLogicMeasureMovement::InputSetMeasureReference( inputdata_t &inputdata ) +{ + m_strMeasureReference = inputdata.value.StringID(); + m_hMeasureReference = gEntList.FindEntityByName( NULL, STRING(m_strMeasureReference), this, inputdata.pActivator, inputdata.pCaller ); + if ( !m_hMeasureReference ) + { + Warning( "%s: Unable to find measure reference entity %s\n", GetDebugName(), STRING(m_strMeasureReference) ); + } +} + +void CLogicMeasureMovement::InputSetTarget( inputdata_t &inputdata ) +{ + m_target = inputdata.value.StringID(); + m_hTarget = gEntList.FindEntityByName( NULL, STRING(m_target), this, inputdata.pActivator, inputdata.pCaller ); + if ( !m_hTarget ) + { + Warning( "%s: Unable to find movement target entity %s\n", GetDebugName(), STRING(m_target) ); + } +} + +void CLogicMeasureMovement::InputSetTargetReference( inputdata_t &inputdata ) +{ + m_strTargetReference = inputdata.value.StringID(); + m_hTargetReference = gEntList.FindEntityByName( NULL, STRING(m_strTargetReference), this, inputdata.pActivator, inputdata.pCaller ); + if ( !m_hTargetReference ) + { + Warning( "%s: Unable to find movement reference entity %s\n", GetDebugName(), STRING(m_strTargetReference) ); + } +} + +void CLogicMeasureMovement::InputSetMeasureAttachment( inputdata_t &inputdata ) +{ + m_strAttachment = inputdata.value.StringID(); + m_iAttachment = 0; +} + +// Just gets the position once and fires outputs without moving anything. +// We don't even need a target for this. +void CLogicMeasureMovement::InputGetPosition( inputdata_t &inputdata ) +{ + if ( !m_hMeasureTarget.Get() || !m_hMeasureReference.Get() || !m_hTargetReference.Get() ) + return; + + Vector vecNewOrigin; + QAngle vecNewAngles; + DoMeasure(vecNewOrigin, vecNewAngles); + + // m_bOutputPosition has been repurposed here to toggle between using the target or the input activator as the activator. + m_OutPosition.Set(vecNewOrigin, m_bOutputPosition ? m_hTarget.Get() : inputdata.pActivator, this); + m_OutAngles.Set(Vector(vecNewAngles.x, vecNewAngles.y, vecNewAngles.z), m_bOutputPosition ? m_hTarget.Get() : inputdata.pActivator, this); +} +#else +void CLogicMeasureMovement::InputSetMeasureTarget( inputdata_t &inputdata ) +{ + m_strMeasureTarget = MAKE_STRING( inputdata.value.String() ); + SetMeasureTarget( inputdata.value.String() ); + SetTarget( STRING(m_target) ); + SetTargetReference( STRING(m_strTargetReference) ); +} + +void CLogicMeasureMovement::InputSetMeasureReference( inputdata_t &inputdata ) +{ + m_strMeasureReference = MAKE_STRING( inputdata.value.String() ); + SetMeasureReference( inputdata.value.String() ); +} + +void CLogicMeasureMovement::InputSetTarget( inputdata_t &inputdata ) +{ + m_target = MAKE_STRING( inputdata.value.String() ); + SetTarget( inputdata.value.String() ); +} + +void CLogicMeasureMovement::InputSetTargetReference( inputdata_t &inputdata ) +{ + m_strTargetReference = MAKE_STRING( inputdata.value.String() ); + SetTargetReference( inputdata.value.String() ); +} +#endif + +void CLogicMeasureMovement::InputSetTargetScale( inputdata_t &inputdata ) +{ + m_flScale = inputdata.value.Float(); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// This will measure the direction of a target entity and move +// another entity to where the target entity is facing. +// +// m_hMeasureTarget; // Whose direction is measured +// m_hMeasureReference; // Position where direction is measured +// m_hTarget; // Target whose origin is applied +// m_hTargetReference; // From where the target's origin is applied +//----------------------------------------------------------------------------- +class CLogicMeasureDirection : public CLogicMeasureMovement +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CLogicMeasureDirection, CLogicMeasureMovement ); + +public: + + virtual void DoMeasure(Vector &vecOrigin, QAngle &angAngles); + + CBaseFilter *GetTraceFilter(); + //void InputSetTraceFilter( inputdata_t &inputdata ) { InputSetDamageFilter(inputdata); } + +private: + + float m_flTraceDistance; + int m_iMask; + int m_iCollisionGroup; + bool m_bHitIfPassed; + //string_t m_iszTraceFilter; + //CHandle m_hTraceFilter; + + bool m_bTraceTargetReference; + +}; + + +LINK_ENTITY_TO_CLASS( logic_measure_direction, CLogicMeasureDirection ); + + +BEGIN_DATADESC( CLogicMeasureDirection ) + + DEFINE_KEYFIELD( m_flTraceDistance, FIELD_FLOAT, "TraceDistance" ), + DEFINE_KEYFIELD( m_iMask, FIELD_INTEGER, "Mask" ), + DEFINE_KEYFIELD( m_iCollisionGroup, FIELD_INTEGER, "CollisionGroup" ), + DEFINE_KEYFIELD( m_bHitIfPassed, FIELD_BOOLEAN, "HitIfPassed" ), + DEFINE_KEYFIELD( m_bTraceTargetReference, FIELD_BOOLEAN, "TraceTargetReference" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetTraceFilter", InputSetDamageFilter ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Gets our "trace filter". +//----------------------------------------------------------------------------- +inline CBaseFilter *CLogicMeasureDirection::GetTraceFilter() +{ + return static_cast(m_hDamageFilter.Get()); // pranked +} + +//----------------------------------------------------------------------------- +// Purpose: Does measure. +//----------------------------------------------------------------------------- +void CLogicMeasureDirection::DoMeasure( Vector &vecOrigin, QAngle &angAngles ) +{ + trace_t tr; + Vector vecStart, vecDir; + QAngle angStart; + switch( m_nMeasureType ) + { + case MEASURE_POSITION: + vecStart = m_hMeasureReference->GetAbsOrigin(); + angStart = m_hMeasureTarget->GetAbsAngles(); + break; + + case MEASURE_EYE_POSITION: + vecStart = m_hMeasureReference->EyePosition(); + angStart = m_hMeasureTarget->EyeAngles(); + break; + + case MEASURE_ATTACHMENT: + CBaseAnimating *pAnimating = m_hMeasureTarget->GetBaseAnimating(); + if (pAnimating) + { + if (m_iAttachment <= 0) + m_iAttachment = m_hMeasureTarget->GetBaseAnimating()->LookupAttachment(STRING(m_strAttachment)); + + if (m_iAttachment == -1) + Warning("WARNING: %s requesting invalid attachment %s on %s!\n", GetDebugName(), STRING(m_strAttachment), m_hMeasureTarget->GetDebugName()); + else + { + pAnimating->GetAttachment(m_iAttachment, vecStart, angStart); + } + } + else + { + Warning("WARNING: %s requesting attachment point on non-animating entity %s!\n", GetDebugName(), m_hMeasureTarget->GetDebugName()); + } + break; + } + + // If we have spawn flags, we might be supposed to ignore something + if (GetSpawnFlags() > 0) + { + if (!HasSpawnFlags(SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN)) + AngleVectors(angStart, &vecDir); + + HandleIgnoreFlags( angStart.Base() ); + + if (HasSpawnFlags(SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN)) + AngleVectors(angStart, &vecDir); + } + else + { + AngleVectors(angStart, &vecDir); + } + + CTraceFilterEntityFilter traceFilter(m_hMeasureReference, m_iCollisionGroup); + traceFilter.m_pFilter = GetTraceFilter(); + traceFilter.m_bHitIfPassed = m_bHitIfPassed; + UTIL_TraceLine( vecStart, vecStart + vecDir * (m_flTraceDistance != 0 ? m_flTraceDistance : MAX_TRACE_LENGTH), m_iMask, &traceFilter, &tr ); //MASK_BLOCKLOS_AND_NPCS + + Vector vecEnd = tr.endpos; + + // Apply the scale factor + float flScale = m_flScale; + if ( ( flScale != 0.0f ) && ( flScale != 1.0f ) ) + { + vecEnd = (vecStart + ((vecEnd - vecStart) / flScale)); + } + + Vector refPos = m_hTargetReference->GetAbsOrigin(); + Vector vecPos = refPos + (vecEnd - vecStart); + + if (m_bTraceTargetReference) + { + // Make sure we can go the whole distance there + UTIL_TraceLine( refPos, vecPos, m_iMask, &traceFilter, &tr ); + vecPos = tr.endpos; + } + + vecOrigin = vecPos; + angAngles = angStart; +} + + + +//----------------------------------------------------------------------------- +// The unused, "forgotten" entity brought back to life. +// Mirrors an entity's movement across a reference. +// It derives from logic_measure_movement now so it could use its features. +// This is unfinished and I'm still figuring out how it works. +// +// m_hMeasureTarget; // Whose position is mirrored (m_hRemoteTarget) +// m_hMeasureReference; // Position where position is mirrored (m_hMirrorRelative) +// m_hTarget; // Target whose origin is mirrored (m_hMovementTarget) +// m_hTargetReference; // From where the target's origin is mirrored (m_hMirrorTarget) +//----------------------------------------------------------------------------- +class CLogicMirrorMovement : public CLogicMeasureMovement +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CLogicMirrorMovement, CLogicMeasureMovement ); + +public: + virtual void DoMeasure(Vector &vecOrigin, QAngle &angAngles); +}; + + +LINK_ENTITY_TO_CLASS( logic_mirror_movement, CLogicMirrorMovement ); + +BEGIN_DATADESC( CLogicMirrorMovement ) +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Does measure. +//----------------------------------------------------------------------------- +void CLogicMirrorMovement::DoMeasure( Vector &vecOrigin, QAngle &angAngles ) +{ + + matrix3x4_t matRefToMeasure, matWorldToMeasure; + switch( m_nMeasureType ) + { + case MEASURE_POSITION: + MatrixInvert( m_hMeasureTarget->EntityToWorldTransform(), matWorldToMeasure ); + break; + + case MEASURE_EYE_POSITION: + AngleIMatrix( m_hMeasureTarget->EyeAngles(), m_hMeasureTarget->EyePosition(), matWorldToMeasure ); + break; + + case MEASURE_ATTACHMENT: + if (CBaseAnimating *pAnimating = m_hMeasureTarget->GetBaseAnimating()) + { + if (m_iAttachment <= 0) + m_iAttachment = m_hMeasureTarget->GetBaseAnimating()->LookupAttachment(STRING(m_strAttachment)); + + if (m_iAttachment == -1) + Warning("WARNING: %s requesting invalid attachment %s on %s!\n", GetDebugName(), STRING(m_strAttachment), m_hMeasureTarget->GetDebugName()); + else + pAnimating->GetAttachment(m_iAttachment, matWorldToMeasure); + } + else + { + Warning("WARNING: %s requesting attachment point on non-animating entity %s!\n", GetDebugName(), m_hMeasureTarget->GetDebugName()); + } + break; + } + + ConcatTransforms( matWorldToMeasure, m_hMeasureReference->EntityToWorldTransform(), matRefToMeasure ); + + // Apply the scale factor + if ( ( m_flScale != 0.0f ) && ( m_flScale != 1.0f ) ) + { + Vector vecTranslation; + MatrixGetColumn( matRefToMeasure, 3, vecTranslation ); + vecTranslation /= m_flScale; + MatrixSetColumn( vecTranslation, 3, matRefToMeasure ); + } + + MatrixScaleBy( -1.0f, matRefToMeasure ); + + QAngle angRot; + Vector vecPos; + MatrixAngles( matRefToMeasure, angRot ); + MatrixPosition( matRefToMeasure, vecPos ); + angRot.z *= -1.0f; + vecPos.z *= -1.0f; + AngleMatrix( angRot, vecPos, matWorldToMeasure ); + + // Now apply the new matrix to the new reference point + matrix3x4_t matMeasureToRef, matNewTargetToWorld; + MatrixInvert( matRefToMeasure, matMeasureToRef ); + + // Handle origin ignorance + if (HasSpawnFlags( SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN )) + { + // Get the position from the matrix's column directly and re-assign it + Vector vecPosition; + MatrixGetColumn( matRefToMeasure, 3, vecPosition ); + + HandleIgnoreFlags( vecPosition.Base() ); + + MatrixSetColumn( vecPosition, 3, matRefToMeasure ); + } + + ConcatTransforms( m_hTargetReference->EntityToWorldTransform(), matMeasureToRef, matNewTargetToWorld ); + + MatrixAngles( matNewTargetToWorld, angAngles, vecOrigin ); + + // If our spawnflags are greater than 0 (and don't just contain our default "TELEPORT" flag), we might need to ignore one of our angles. + if (GetSpawnFlags() && GetSpawnFlags() != SF_LOGIC_MEASURE_MOVEMENT_TELEPORT && !HasSpawnFlags(SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN)) + { + HandleIgnoreFlags( angAngles.Base() ); + } + + /* + VMatrix matPortal1ToWorldInv, matPortal2ToWorld; + MatrixInverseGeneral( m_hMeasureReference->EntityToWorldTransform(), matPortal1ToWorldInv ); + switch( m_nMeasureType ) + { + case MEASURE_POSITION: + matPortal2ToWorld = m_hMeasureTarget->EntityToWorldTransform(); + break; + + case MEASURE_EYE_POSITION: + matPortal2ToWorld.SetupMatrixOrgAngles( m_hMeasureTarget->EyePosition(), m_hMeasureTarget->EyeAngles() ); + break; + + case MEASURE_ATTACHMENT: + CBaseAnimating *pAnimating = m_hMeasureTarget->GetBaseAnimating(); + if (pAnimating) + { + if (m_iAttachment <= 0) + m_iAttachment = m_hMeasureTarget->GetBaseAnimating()->LookupAttachment(STRING(m_strAttachment)); + + if (m_iAttachment == -1) + Warning("WARNING: %s requesting invalid attachment %s on %s!\n", GetDebugName(), STRING(m_strAttachment), m_hMeasureTarget->GetDebugName()); + else + { + pAnimating->GetAttachment( m_iAttachment, matPortal2ToWorld.As3x4() ); + } + } + else + { + Warning("WARNING: %s requesting attachment point on non-animating entity %s!\n", GetDebugName(), m_hMeasureTarget->GetDebugName()); + } + break; + } + + // If we have spawn flags, we might be supposed to ignore something + if (GetSpawnFlags() > 0) + { + if (HasSpawnFlags( SF_LOGIC_MEASURE_MOVEMENT_USE_IGNORE_FLAGS_FOR_ORIGIN )) + { + // Get the position from the matrix's column directly and re-assign it + Vector vecPosition; + MatrixGetColumn( matPortal2ToWorld, 3, &vecPosition ); + + HandleIgnoreFlags( vecPosition.Base() ); + + MatrixSetColumn( matPortal2ToWorld, 3, vecPosition ); + } + else + { + // Get the angles from the matrix and re-assign it + QAngle angAngles; + MatrixToAngles( matPortal2ToWorld, angAngles ); + + HandleIgnoreFlags( angAngles.Base() ); + + matPortal2ToWorld.SetupMatrixAngles( angAngles ); + } + } + + // Apply the scale factor + if ( ( m_flScale != 0.0f ) && ( m_flScale != 1.0f ) ) + { + Vector vecTranslation; + MatrixGetColumn( matPortal2ToWorld.As3x4(), 3, vecTranslation ); + vecTranslation /= m_flScale; + MatrixSetColumn( vecTranslation, 3, matPortal2ToWorld.As3x4() ); + } + + // Get our scene camera's current orientation + Vector ptCameraPosition, vCameraLook, vCameraRight, vCameraUp; + ptCameraPosition = m_hTargetReference->EyePosition(); + m_hTargetReference->GetVectors( &vCameraLook, &vCameraRight, &vCameraUp ); + + // map this position and orientation to the remote portal, mirrored (invert the result) + Vector ptNewPosition, vNewLook; + ptNewPosition = matPortal1ToWorldInv * ptCameraPosition; + ptNewPosition = matPortal2ToWorld*(Vector( -ptNewPosition.x, -ptNewPosition.y, ptNewPosition.z )); + + vNewLook = matPortal1ToWorldInv.ApplyRotation( vCameraLook ); + vNewLook = matPortal2ToWorld.ApplyRotation( Vector( -vNewLook.x, -vNewLook.y, vNewLook.z ) ); + + // Set the point camera to the new location/orientation + QAngle qNewAngles; + VectorAngles( vNewLook, qNewAngles ); + + vecOrigin = ptNewPosition; + angAngles = qNewAngles; + */ +} +#endif diff --git a/sp/src/game/server/logic_mirror_movement.cpp b/sp/src/game/server/logic_mirror_movement.cpp new file mode 100644 index 00000000..d2be00c7 --- /dev/null +++ b/sp/src/game/server/logic_mirror_movement.cpp @@ -0,0 +1,198 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Mirrors the movement of a camera about a given point. +// +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" +#include "modelentities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +////////////////////////////////////////////////////////////////////////// +// CLogicMirrorMovement +// This will record the vector offset of an entity's center from a given reference point +// (most likely the center of a mirror or portal) and place an entity (most likely a point camera) +// at a the same offset, mirrored about the reference point and orientation. +////////////////////////////////////////////////////////////////////////// +class CLogicMirrorMovement : public CLogicalEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CLogicMirrorMovement, CLogicalEntity ); + +private: + void SetMirrorTarget( const char *pName ); // Set entity to watch and mirror (ex. the player) + void SetTarget( const char *pName ); // Set entity to move based on the Mirror Target entity (ex. a point_camera) + void SetMirrorRelative( const char* pName ); // Set the point about which to measure an offset to orient based upon (ex. portal 1) + void SetRemoteTarget ( const char *pName ); // Entity's orientation/location from which to offset the movement target (ex. portal 2) + void SetDrawingSurface ( const char *pName ); + + void InputSetMirrorTarget( inputdata_t &inputdata ); + void InputSetTarget( inputdata_t &inputdata ); + void InputSetRemoteTarget ( inputdata_t &inputdata ); + void InputSetMirrorRelative ( inputdata_t &inputdata ); + + void Think(); + virtual void Activate(); + + string_t m_strMirrorTarget; + string_t m_strRemoteTarget; + string_t m_strMirrorRelative; + + EHANDLE m_hRemoteTarget; + EHANDLE m_hMirrorTarget; + EHANDLE m_hMovementTarget; + EHANDLE m_hMirrorRelative; + +}; + +LINK_ENTITY_TO_CLASS( logic_mirror_movement, CLogicMirrorMovement ); + + +BEGIN_DATADESC( CLogicMirrorMovement ) + +DEFINE_KEYFIELD( m_strMirrorTarget, FIELD_STRING, "MirrorTarget" ), +DEFINE_KEYFIELD( m_strRemoteTarget, FIELD_STRING, "RemoteTarget" ), +DEFINE_KEYFIELD( m_strMirrorRelative, FIELD_STRING, "MirrorRelative" ), + +DEFINE_FIELD( m_hMirrorTarget, FIELD_EHANDLE ), +DEFINE_FIELD( m_hMovementTarget, FIELD_EHANDLE ), +DEFINE_FIELD( m_hRemoteTarget, FIELD_EHANDLE ), +DEFINE_FIELD( m_hMirrorRelative, FIELD_EHANDLE ), + +DEFINE_INPUTFUNC( FIELD_STRING, "SetMirrorTarget", InputSetMirrorTarget ), +DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget", InputSetTarget ), +DEFINE_INPUTFUNC( FIELD_STRING, "SetRemoteTarget", InputSetRemoteTarget ), +DEFINE_INPUTFUNC( FIELD_STRING, "SetMirrorRelative", InputSetMirrorRelative ), + +DEFINE_THINKFUNC( Think ), + +END_DATADESC() + + +void CLogicMirrorMovement::Activate() +{ + BaseClass::Activate(); + + SetMirrorTarget( STRING(m_strMirrorTarget) ); + SetTarget( STRING(m_target) ); + SetRemoteTarget( STRING(m_strRemoteTarget ) ); + SetMirrorRelative( STRING( m_strMirrorRelative) ); + + SetThink( &CLogicMirrorMovement::Think ); + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + + +void CLogicMirrorMovement::SetMirrorTarget( const char *pName ) +{ + m_hMirrorTarget = gEntList.FindEntityByName( NULL, pName ); + if ( !m_hMirrorTarget ) + { + if ( Q_strnicmp( STRING(m_strMirrorTarget), "!player", 8 ) ) + { + Warning("logic_mirror_movement: Unable to find mirror target entity %s\n", pName ); + } + } +} + +void CLogicMirrorMovement::SetTarget( const char *pName ) +{ + m_hMovementTarget = gEntList.FindEntityByName( NULL, pName ); + if ( !m_hMovementTarget ) + { + Warning("logic_mirror_movement: Unable to find movement target entity %s\n", pName ); + } +} + +void CLogicMirrorMovement::SetRemoteTarget(const char *pName ) +{ + m_hRemoteTarget = gEntList.FindEntityByName( NULL, pName ); + if ( !m_hRemoteTarget ) + { + Warning("logic_mirror_movement: Unable to find remote target entity %s\n", pName ); + } +} + +void CLogicMirrorMovement::SetMirrorRelative(const char* pName ) +{ + m_hMirrorRelative = gEntList.FindEntityByName( NULL, pName ); + if ( !m_hMirrorRelative ) + { + Warning("logic_mirror_movement: Unable to find mirror relative entity %s\n", pName ); + } +} + +void CLogicMirrorMovement::InputSetMirrorTarget( inputdata_t &inputdata ) +{ + m_strMirrorTarget = AllocPooledString( inputdata.value.String() ); + SetMirrorTarget( inputdata.value.String() ); +} + +void CLogicMirrorMovement::InputSetTarget( inputdata_t &inputdata ) +{ + m_target = AllocPooledString( inputdata.value.String() ); + SetTarget( inputdata.value.String() ); +} + +void CLogicMirrorMovement::InputSetRemoteTarget(inputdata_t &inputdata ) +{ + m_strRemoteTarget = AllocPooledString( inputdata.value.String() ); + SetRemoteTarget( inputdata.value.String() ); +} + +void CLogicMirrorMovement::InputSetMirrorRelative(inputdata_t &inputdata ) +{ + m_strMirrorRelative = AllocPooledString ( inputdata.value.String() ); + SetMirrorRelative( inputdata.value.String() ); +} + + +void CLogicMirrorMovement::Think() +{ + // Attempt to get the player's handle because it didn't exist at Activate time + if ( !m_hMirrorTarget.Get() ) + { + // If we will never find a target, we don't have a use... shutdown + if ( m_strMirrorTarget == NULL_STRING ) + SetNextThink ( NULL ); + + //BUGBUG: If m_strSetMirrorTarget doesn't exist in ent list, we get per-think searches with no results ever... + SetMirrorTarget ( STRING(m_strMirrorTarget) ); + } + + // Make sure all entities are valid + if ( m_hMirrorTarget.Get() && m_hMovementTarget.Get() && m_hRemoteTarget.Get() && m_hMirrorRelative.Get() ) + { + // Get our two portal's world transforms transforms + VMatrix matPortal1ToWorldInv, matPortal2ToWorld; + MatrixInverseGeneral( m_hMirrorRelative->EntityToWorldTransform(), matPortal1ToWorldInv ); + matPortal2ToWorld = m_hRemoteTarget->EntityToWorldTransform(); + + VMatrix matTransformToRemotePortal = matPortal1ToWorldInv * matPortal2ToWorld; + + // Get our scene camera's current orientation + Vector ptCameraPosition, vCameraLook, vCameraRight, vCameraUp; + ptCameraPosition = m_hMirrorTarget->EyePosition(); + m_hMirrorTarget->GetVectors ( &vCameraLook, &vCameraRight, &vCameraUp ); + + // map this position and orientation to the remote portal, mirrored (invert the result) + Vector ptNewPosition, vNewLook; + ptNewPosition = matPortal1ToWorldInv * ptCameraPosition; + ptNewPosition = matPortal2ToWorld*( Vector( -ptNewPosition.x, -ptNewPosition.y, ptNewPosition.z ) ); + + vNewLook = matPortal1ToWorldInv.ApplyRotation( vCameraLook ); + vNewLook = matPortal2ToWorld.ApplyRotation( Vector( -vNewLook.x, -vNewLook.y, vNewLook.z) ); + + // Set the point camera to the new location/orientation + QAngle qNewAngles; + VectorAngles( vNewLook, qNewAngles ); + m_hMovementTarget->Teleport( &ptNewPosition, &qNewAngles, NULL ); + } + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + + diff --git a/sp/src/game/server/logic_navigation.cpp b/sp/src/game/server/logic_navigation.cpp new file mode 100644 index 00000000..601425b5 --- /dev/null +++ b/sp/src/game/server/logic_navigation.cpp @@ -0,0 +1,183 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_NAV_START_ON 0x0001 + +enum navproperties_t +{ + NAV_IGNORE = 1<<0, +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CLogicNavigation : public CLogicalEntity, + public IEntityListener +{ + DECLARE_CLASS( CLogicNavigation, CLogicalEntity ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Activate( void ); + +private: + void UpdateOnRemove(); + + void OnEntitySpawned( CBaseEntity *pEntity ); + + // Inputs + void InputTurnOn( inputdata_t &inputdata ) { TurnOn(); } + void InputTurnOff( inputdata_t &inputdata ) { TurnOff(); } + void InputToggle( inputdata_t &inputdata ) + { + if ( m_isOn ) + TurnOff(); + else + TurnOn(); + } + + void TurnOn(); + void TurnOff(); + void UpdateProperty(); + + DECLARE_DATADESC(); + + bool m_isOn; + navproperties_t m_navProperty; +}; + +LINK_ENTITY_TO_CLASS(logic_navigation, CLogicNavigation); + + +BEGIN_DATADESC( CLogicNavigation ) + + DEFINE_FIELD( m_isOn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_navProperty, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "TurnOn", InputTurnOn), + DEFINE_INPUTFUNC(FIELD_VOID, "TurnOff", InputTurnOff), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *szKeyName - +// *szValue - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CLogicNavigation::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq(szKeyName, "navprop") ) + { + if ( FStrEq( szValue, "Ignore" ) ) + { + m_navProperty = NAV_IGNORE; + } + else + { + DevMsg( 1, "Unknown nav property %s\n", szValue ); + } + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicNavigation::Activate() +{ + BaseClass::Activate(); + + if ( HasSpawnFlags( SF_NAV_START_ON ) ) + { + TurnOn(); + RemoveSpawnFlags( SF_NAV_START_ON ); + } + else if ( m_isOn ) + { + gEntList.AddListenerEntity( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicNavigation::UpdateOnRemove() +{ + if ( m_isOn ) + { + gEntList.RemoveListenerEntity( this ); + } + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicNavigation::OnEntitySpawned( CBaseEntity *pEntity ) +{ + if ( m_isOn && ( m_navProperty & NAV_IGNORE ) && pEntity->NameMatches( m_target ) ) + { + pEntity->SetNavIgnore(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicNavigation::TurnOn() +{ + if ( m_isOn ) + return; + m_isOn = true; + gEntList.AddListenerEntity( this ); + UpdateProperty(); +} + +void CLogicNavigation::UpdateProperty() +{ + CBaseEntity *pEntity = NULL; + while ( ( pEntity = gEntList.FindEntityByName( pEntity, STRING(m_target) ) ) != NULL ) + { + if ( m_isOn ) + { + if ( m_navProperty & NAV_IGNORE ) + pEntity->SetNavIgnore(); + } + else + { + if ( m_navProperty & NAV_IGNORE ) + pEntity->ClearNavIgnore(); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicNavigation::TurnOff() +{ + if ( !m_isOn ) + return; + + m_isOn = false; + gEntList.RemoveListenerEntity( this ); + UpdateProperty(); +} + diff --git a/sp/src/game/server/logic_playmovie.cpp b/sp/src/game/server/logic_playmovie.cpp new file mode 100644 index 00000000..4e62f738 --- /dev/null +++ b/sp/src/game/server/logic_playmovie.cpp @@ -0,0 +1,136 @@ +//===== Copyright © 1996-2009, Valve Corporation, All rights reserved. ======// +// +// Purpose: Plays a movie and reports on finish +// +//===========================================================================// + +#include "cbase.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CLogicPlayMovie : public CLogicalEntity +{ +public: + DECLARE_CLASS( CLogicPlayMovie, CLogicalEntity ); + DECLARE_DATADESC(); + + CLogicPlayMovie( void ) { } + ~CLogicPlayMovie( void ) { } + + virtual void Precache( void ); + virtual void Spawn( void ); + +private: + + void InputPlayMovie( inputdata_t &data ); +#ifdef MAPBASE + void InputStopMovie( inputdata_t &data ); +#endif + void InputMovieFinished( inputdata_t &data ); + + string_t m_strMovieFilename; + bool m_bAllowUserSkip; +#ifdef MAPBASE + bool m_bLooping; + bool m_bMuted; + + bool m_bPlayingVideo; +#endif + + COutputEvent m_OnPlaybackFinished; +}; + +LINK_ENTITY_TO_CLASS( logic_playmovie, CLogicPlayMovie ); + +BEGIN_DATADESC( CLogicPlayMovie ) + + DEFINE_KEYFIELD( m_strMovieFilename, FIELD_STRING, "MovieFilename" ), + DEFINE_KEYFIELD( m_bAllowUserSkip, FIELD_BOOLEAN, "allowskip" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bLooping, FIELD_BOOLEAN, "loopvideo" ), + DEFINE_KEYFIELD( m_bMuted, FIELD_BOOLEAN, "mute" ), + + DEFINE_FIELD( m_bPlayingVideo, FIELD_BOOLEAN ), +#endif + + DEFINE_INPUTFUNC( FIELD_VOID, "PlayMovie", InputPlayMovie ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "StopMovie", InputStopMovie ), +#endif + DEFINE_INPUTFUNC( FIELD_VOID, "__MovieFinished", InputMovieFinished ), + + DEFINE_OUTPUT( m_OnPlaybackFinished, "OnPlaybackFinished" ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicPlayMovie::Precache( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicPlayMovie::Spawn( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicPlayMovie::InputPlayMovie( inputdata_t &data ) +{ + // Build the hacked string + + char szClientCmd[256]; + Q_snprintf( szClientCmd, sizeof(szClientCmd), + "playvideo_complex %s \"ent_fire %s __MovieFinished\" %d %d %d\n", + STRING(m_strMovieFilename), + GetEntityNameAsCStr(), + m_bAllowUserSkip, +#ifdef MAPBASE + m_bLooping, + m_bMuted +#else + 0, + 0 +#endif + ); + + // Send it on + engine->ServerCommand( szClientCmd ); + +#ifdef MAPBASE + m_bPlayingVideo = true; +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicPlayMovie::InputStopMovie( inputdata_t &data ) +{ + if (m_bPlayingVideo) + { + // Send it on + engine->ServerCommand( "stopvideos\n" ); + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicPlayMovie::InputMovieFinished( inputdata_t &data ) +{ + // Simply fire our output + m_OnPlaybackFinished.FireOutput( this, this ); + +#ifdef MAPBASE + m_bPlayingVideo = false; +#endif +} diff --git a/sp/src/game/server/logic_random_outputs.cpp b/sp/src/game/server/logic_random_outputs.cpp new file mode 100644 index 00000000..b5c9f64b --- /dev/null +++ b/sp/src/game/server/logic_random_outputs.cpp @@ -0,0 +1,221 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ==== +// +// When triggered, will attempt to fire off each of its outputs. Each output +// has its own chance of firing. +// +//============================================================================= + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "soundent.h" +#include "logic_random_outputs.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +const int SF_REMOVE_ON_FIRE = 0x001; // Relay will remove itself after being triggered. +const int SF_ALLOW_FAST_RETRIGGER = 0x002; // Unless set, entity will disable itself until the last output is sent. + +LINK_ENTITY_TO_CLASS(logic_random_outputs, CLogicRandomOutputs); + + +BEGIN_DATADESC( CLogicRandomOutputs ) + + DEFINE_FIELD(m_bWaitForRefire, FIELD_BOOLEAN), + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled"), + + DEFINE_AUTO_ARRAY( m_flOnTriggerChance, FIELD_FLOAT ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable), + DEFINE_INPUTFUNC(FIELD_VOID, "EnableRefire", InputEnableRefire), + DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + DEFINE_INPUTFUNC(FIELD_VOID, "Trigger", InputTrigger), + DEFINE_INPUTFUNC(FIELD_VOID, "CancelPending", InputCancelPending), + + // Outputs + DEFINE_OUTPUT(m_OnSpawn, "OnSpawn"), + DEFINE_OUTPUT(m_Output[0], "OnTrigger1"), + DEFINE_OUTPUT(m_Output[1], "OnTrigger2"), + DEFINE_OUTPUT(m_Output[2], "OnTrigger3"), + DEFINE_OUTPUT(m_Output[3], "OnTrigger4"), + DEFINE_OUTPUT(m_Output[4], "OnTrigger5"), + DEFINE_OUTPUT(m_Output[5], "OnTrigger6"), + DEFINE_OUTPUT(m_Output[6], "OnTrigger7"), + DEFINE_OUTPUT(m_Output[7], "OnTrigger8"), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Constructor. +//----------------------------------------------------------------------------- +CLogicRandomOutputs::CLogicRandomOutputs(void) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Read in the chance of firing each output +//----------------------------------------------------------------------------- +bool CLogicRandomOutputs::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( szValue && szValue[0] ) + { + for ( int i=0; i < NUM_RANDOM_OUTPUTS; i++ ) + { + if ( FStrEq( szKeyName, UTIL_VarArgs( "OnTriggerChance%d", i ) ) ) + { + m_flOnTriggerChance[i] = atof( szValue ); + return true; + } + } + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Give out the chance of firing each output +//----------------------------------------------------------------------------- +bool CLogicRandomOutputs::GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ) +{ + if ( !Q_strnicmp(szKeyName, "OnTriggerChance", 15) ) + { + for ( int i=0; i < NUM_RANDOM_OUTPUTS; i++ ) + { + if ( FStrEq( szKeyName, UTIL_VarArgs( "OnTriggerChance%d", i ) ) ) + { + Q_snprintf( szValue, iMaxLen, "%f", m_flOnTriggerChance[i] ); + return true; + } + } + } + + return BaseClass::GetKeyValue( szKeyName, szValue, iMaxLen ); +} +#endif + +//------------------------------------------------------------------------------ +// Kickstarts a think if we have OnSpawn connections. +//------------------------------------------------------------------------------ +void CLogicRandomOutputs::Activate() +{ + BaseClass::Activate(); + + if ( m_OnSpawn.NumberOfElements() > 0) + { + SetNextThink( gpGlobals->curtime + 0.01 ); + } +} + + +//----------------------------------------------------------------------------- +// If we have OnSpawn connections, this is called shortly after spawning to +// fire the OnSpawn output. +//----------------------------------------------------------------------------- +void CLogicRandomOutputs::Think() +{ + // Fire an output when we spawn. This is used for self-starting an entity + // template -- since the logic_random_outputs is inside the template, it gets all the + // name and I/O connection fixup, so can target other entities in the template. + m_OnSpawn.FireOutput( this, this ); + + // We only get here if we had OnSpawn connections, so this is safe. + if ( m_spawnflags & SF_REMOVE_ON_FIRE ) + { + UTIL_Remove(this); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns on the entity, allowing it to fire outputs. +//------------------------------------------------------------------------------ +void CLogicRandomOutputs::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//------------------------------------------------------------------------------ +// Purpose: Enables us to fire again. This input is only posted from our Trigger +// function to prevent rapid refire. +//------------------------------------------------------------------------------ +void CLogicRandomOutputs::InputEnableRefire( inputdata_t &inputdata ) +{ + Msg(" now enabling refire\n" ); + m_bWaitForRefire = false; +} + + +//------------------------------------------------------------------------------ +// Purpose: Cancels any I/O events in the queue that were fired by us. +//------------------------------------------------------------------------------ +void CLogicRandomOutputs::InputCancelPending( inputdata_t &inputdata ) +{ + g_EventQueue.CancelEvents( this ); + + // Stop waiting; allow another Trigger. + m_bWaitForRefire = false; +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns off the entity, preventing it from firing outputs. +//------------------------------------------------------------------------------ +void CLogicRandomOutputs::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + + +//------------------------------------------------------------------------------ +// Purpose: Toggles the enabled/disabled state of the entity. +//------------------------------------------------------------------------------ +void CLogicRandomOutputs::InputToggle( inputdata_t &inputdata ) +{ + m_bDisabled = !m_bDisabled; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that triggers the logic_random_outputs. +//----------------------------------------------------------------------------- +void CLogicRandomOutputs::InputTrigger( inputdata_t &inputdata ) +{ + if ((!m_bDisabled) && (!m_bWaitForRefire)) + { + for ( int i=0 ; i < NUM_RANDOM_OUTPUTS ; i++ ) + { + if ( RandomFloat() <= m_flOnTriggerChance[i] ) + { + m_Output[i].FireOutput( inputdata.pActivator, this ); + } + } + + if (m_spawnflags & SF_REMOVE_ON_FIRE) + { + UTIL_Remove(this); + } + else if (!(m_spawnflags & SF_ALLOW_FAST_RETRIGGER)) + { + // find the max delay from all our outputs + float fMaxDelay = 0; + for ( int i=0 ; i < NUM_RANDOM_OUTPUTS ; i++ ) + { + fMaxDelay = MAX( fMaxDelay, m_Output[i].GetMaxDelay() ); + } + if ( fMaxDelay > 0 ) + { + // Disable the relay so that it cannot be refired until after the last output + // has been fired and post an input to re-enable ourselves. + m_bWaitForRefire = true; + g_EventQueue.AddEvent(this, "EnableRefire", fMaxDelay + 0.001, this, this); + } + } + } +} diff --git a/sp/src/game/server/logic_random_outputs.h b/sp/src/game/server/logic_random_outputs.h new file mode 100644 index 00000000..aeb60bff --- /dev/null +++ b/sp/src/game/server/logic_random_outputs.h @@ -0,0 +1,54 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef LOGICRANDOMOUTPUTS_H +#define LOGICRANDOMOUTPUTS_H + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" + +#define NUM_RANDOM_OUTPUTS 8 + +class CLogicRandomOutputs : public CLogicalEntity +{ +public: + DECLARE_CLASS( CLogicRandomOutputs, CLogicalEntity ); + + CLogicRandomOutputs(); + + void Activate(); + void Think(); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); +#ifdef MAPBASE + virtual bool GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ); +#endif + + // Input handlers + void InputEnable( inputdata_t &inputdata ); + void InputEnableRefire( inputdata_t &inputdata ); // Private input handler, not in FGD + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputTrigger( inputdata_t &inputdata ); + void InputCancelPending( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + // Outputs + COutputEvent m_Output[ NUM_RANDOM_OUTPUTS ]; + COutputEvent m_OnSpawn; + + float m_flOnTriggerChance[ NUM_RANDOM_OUTPUTS ]; + +private: + + bool m_bDisabled; + bool m_bWaitForRefire; // Set to disallow a refire while we are waiting for our outputs to finish firing. +}; + +#endif //LOGICRANDOMOUTPUTS_H diff --git a/sp/src/game/server/logicauto.cpp b/sp/src/game/server/logicauto.cpp new file mode 100644 index 00000000..af711dbe --- /dev/null +++ b/sp/src/game/server/logicauto.cpp @@ -0,0 +1,127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Fires an output when the map spawns (or respawns if not set to +// only fire once). It can be set to check a global state before firing. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "mathlib/mathlib.h" +#include "globalstate.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +const int SF_AUTO_FIREONCE = 0x01; +const int SF_AUTO_FIREONRELOAD = 0x02; + + +class CLogicAuto : public CBaseEntity +{ +public: + DECLARE_CLASS( CLogicAuto, CBaseEntity ); + + void Activate(void); + void Think(void); + + int ObjectCaps(void) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_DATADESC(); + +private: + + // fired no matter why the map loaded + COutputEvent m_OnMapSpawn; + + // fired for specified types of map loads + COutputEvent m_OnNewGame; + COutputEvent m_OnLoadGame; + COutputEvent m_OnMapTransition; + COutputEvent m_OnBackgroundMap; + COutputEvent m_OnMultiNewMap; + COutputEvent m_OnMultiNewRound; + + string_t m_globalstate; +}; + +LINK_ENTITY_TO_CLASS(logic_auto, CLogicAuto); + + +BEGIN_DATADESC( CLogicAuto ) + + DEFINE_KEYFIELD(m_globalstate, FIELD_STRING, "globalstate"), + + // Outputs + DEFINE_OUTPUT(m_OnMapSpawn, "OnMapSpawn"), + DEFINE_OUTPUT(m_OnNewGame, "OnNewGame"), + DEFINE_OUTPUT(m_OnLoadGame, "OnLoadGame"), + DEFINE_OUTPUT(m_OnMapTransition, "OnMapTransition"), + DEFINE_OUTPUT(m_OnBackgroundMap, "OnBackgroundMap"), + DEFINE_OUTPUT(m_OnMultiNewMap, "OnMultiNewMap" ), + DEFINE_OUTPUT(m_OnMultiNewRound, "OnMultiNewRound" ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose : Fire my outputs here if I fire on map reload +//------------------------------------------------------------------------------ +void CLogicAuto::Activate(void) +{ + BaseClass::Activate(); + SetNextThink( gpGlobals->curtime + 0.2 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called shortly after level spawn. Checks the global state and fires +// targets if the global state is set or if there is not global state +// to check. +//----------------------------------------------------------------------------- +void CLogicAuto::Think(void) +{ + if (!m_globalstate || GlobalEntity_GetState(m_globalstate) == GLOBAL_ON) + { + if (gpGlobals->eLoadType == MapLoad_Transition) + { + m_OnMapTransition.FireOutput(NULL, this); + } + else if (gpGlobals->eLoadType == MapLoad_NewGame) + { + m_OnNewGame.FireOutput(NULL, this); + } + else if (gpGlobals->eLoadType == MapLoad_LoadGame) + { + m_OnLoadGame.FireOutput(NULL, this); + } + else if (gpGlobals->eLoadType == MapLoad_Background) + { + m_OnBackgroundMap.FireOutput(NULL, this); + } + + m_OnMapSpawn.FireOutput(NULL, this); + + if ( g_pGameRules->IsMultiplayer() ) + { + // In multiplayer, fire the new map / round events. + if ( g_pGameRules->InRoundRestart() ) + { + m_OnMultiNewRound.FireOutput(NULL, this); + } + else + { + m_OnMultiNewMap.FireOutput(NULL, this); + } + } + + if (m_spawnflags & SF_AUTO_FIREONCE) + { + UTIL_Remove(this); + } + } +} + diff --git a/sp/src/game/server/logicentities.cpp b/sp/src/game/server/logicentities.cpp new file mode 100644 index 00000000..2197baff --- /dev/null +++ b/sp/src/game/server/logicentities.cpp @@ -0,0 +1,7178 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements many of the entities that control logic flow within a map. +// +//============================================================================= + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "mathlib/mathlib.h" +#include "globalstate.h" +#include "ndebugoverlay.h" +#include "saverestore_utlvector.h" +#include "vstdlib/random.h" +#include "gameinterface.h" +#ifdef MAPBASE +#include "mapbase/variant_tools.h" +#include "mapbase/matchers.h" +#include "mapbase/datadesc_mod.h" +#include "activitylist.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +extern CServerGameDLL g_ServerGameDLL; + +//----------------------------------------------------------------------------- +// Purpose: An entity that acts as a container for game scripts. +//----------------------------------------------------------------------------- + +#define MAX_SCRIPT_GROUP 16 + +class CLogicScript : public CPointEntity +{ +public: + DECLARE_CLASS( CLogicScript, CPointEntity ); + DECLARE_DATADESC(); + + void RunVScripts() + { + /* + EntityGroup <- []; + function __AppendToScriptGroup( name ) + { + if ( name.len() == 0 ) + { + EntityGroup.append( null ); + } + else + { + local ent = Entities.FindByName( null, name ); + EntityGroup.append( ent ); + if ( ent != null ) + { + ent.ValidateScriptScope(); + ent.GetScriptScope().EntityGroup <- EntityGroup; + } + } + } + */ + + static const char szAddCode[] = + { + 0x45,0x6e,0x74,0x69,0x74,0x79,0x47,0x72,0x6f,0x75,0x70,0x20,0x3c,0x2d,0x20,0x5b,0x5d,0x3b,0x0d,0x0a, + 0x66,0x75,0x6e,0x63,0x74,0x69,0x6f,0x6e,0x20,0x5f,0x5f,0x41,0x70,0x70,0x65,0x6e,0x64,0x54,0x6f,0x53, + 0x63,0x72,0x69,0x70,0x74,0x47,0x72,0x6f,0x75,0x70,0x28,0x20,0x6e,0x61,0x6d,0x65,0x20,0x29,0x20,0x0d, + 0x0a,0x7b,0x0d,0x0a,0x09,0x69,0x66,0x20,0x28,0x20,0x6e,0x61,0x6d,0x65,0x2e,0x6c,0x65,0x6e,0x28,0x29, + 0x20,0x3d,0x3d,0x20,0x30,0x20,0x29,0x20,0x0d,0x0a,0x09,0x7b,0x20,0x0d,0x0a,0x09,0x09,0x45,0x6e,0x74, + 0x69,0x74,0x79,0x47,0x72,0x6f,0x75,0x70,0x2e,0x61,0x70,0x70,0x65,0x6e,0x64,0x28,0x20,0x6e,0x75,0x6c, + 0x6c,0x20,0x29,0x3b,0x20,0x0d,0x0a,0x09,0x7d,0x20,0x0d,0x0a,0x09,0x65,0x6c,0x73,0x65,0x0d,0x0a,0x09, + 0x7b,0x20,0x0d,0x0a,0x09,0x09,0x6c,0x6f,0x63,0x61,0x6c,0x20,0x65,0x6e,0x74,0x20,0x3d,0x20,0x45,0x6e, + 0x74,0x69,0x74,0x69,0x65,0x73,0x2e,0x46,0x69,0x6e,0x64,0x42,0x79,0x4e,0x61,0x6d,0x65,0x28,0x20,0x6e, + 0x75,0x6c,0x6c,0x2c,0x20,0x6e,0x61,0x6d,0x65,0x20,0x29,0x3b,0x0d,0x0a,0x09,0x09,0x45,0x6e,0x74,0x69, + 0x74,0x79,0x47,0x72,0x6f,0x75,0x70,0x2e,0x61,0x70,0x70,0x65,0x6e,0x64,0x28,0x20,0x65,0x6e,0x74,0x20, + 0x29,0x3b,0x0d,0x0a,0x09,0x09,0x69,0x66,0x20,0x28,0x20,0x65,0x6e,0x74,0x20,0x21,0x3d,0x20,0x6e,0x75, + 0x6c,0x6c,0x20,0x29,0x0d,0x0a,0x09,0x09,0x7b,0x0d,0x0a,0x09,0x09,0x09,0x65,0x6e,0x74,0x2e,0x56,0x61, + 0x6c,0x69,0x64,0x61,0x74,0x65,0x53,0x63,0x72,0x69,0x70,0x74,0x53,0x63,0x6f,0x70,0x65,0x28,0x29,0x3b, + 0x0d,0x0a,0x09,0x09,0x09,0x65,0x6e,0x74,0x2e,0x47,0x65,0x74,0x53,0x63,0x72,0x69,0x70,0x74,0x53,0x63, + 0x6f,0x70,0x65,0x28,0x29,0x2e,0x45,0x6e,0x74,0x69,0x74,0x79,0x47,0x72,0x6f,0x75,0x70,0x20,0x3c,0x2d, + 0x20,0x45,0x6e,0x74,0x69,0x74,0x79,0x47,0x72,0x6f,0x75,0x70,0x3b,0x0d,0x0a,0x09,0x09,0x7d,0x0d,0x0a, + 0x09,0x7d,0x0d,0x0a,0x7d,0x0d,0x0a,0x00 + }; + + int iLastMember; + for ( iLastMember = MAX_SCRIPT_GROUP - 1; iLastMember >= 0; iLastMember-- ) + { + if ( m_iszGroupMembers[iLastMember] != NULL_STRING ) + { + break; + } + } + + if ( iLastMember >= 0 ) + { + HSCRIPT hAddScript = g_pScriptVM->CompileScript( szAddCode ); + if ( hAddScript ) + { + ValidateScriptScope(); + m_ScriptScope.Run( hAddScript ); + HSCRIPT hAddFunc = m_ScriptScope.LookupFunction( "__AppendToScriptGroup" ); + if ( hAddFunc ) + { + for ( int i = 0; i <= iLastMember; i++ ) + { + m_ScriptScope.Call( hAddFunc, NULL, STRING(m_iszGroupMembers[i]) ); + } + g_pScriptVM->ReleaseFunction( hAddFunc ); + m_ScriptScope.ClearValue( "__AppendToScriptGroup" ); + } + + g_pScriptVM->ReleaseScript( hAddScript ); + } + } + BaseClass::RunVScripts(); + } + + string_t m_iszGroupMembers[MAX_SCRIPT_GROUP]; + +}; + +LINK_ENTITY_TO_CLASS( logic_script, CLogicScript ); + +BEGIN_DATADESC( CLogicScript ) + // Silence, Classcheck! + // DEFINE_ARRAY( m_iszGroupMembers, FIELD_STRING, MAX_NUM_TEMPLATES ), + + DEFINE_KEYFIELD( m_iszGroupMembers[0], FIELD_STRING, "Group00"), + DEFINE_KEYFIELD( m_iszGroupMembers[1], FIELD_STRING, "Group01"), + DEFINE_KEYFIELD( m_iszGroupMembers[2], FIELD_STRING, "Group02"), + DEFINE_KEYFIELD( m_iszGroupMembers[3], FIELD_STRING, "Group03"), + DEFINE_KEYFIELD( m_iszGroupMembers[4], FIELD_STRING, "Group04"), + DEFINE_KEYFIELD( m_iszGroupMembers[5], FIELD_STRING, "Group05"), + DEFINE_KEYFIELD( m_iszGroupMembers[6], FIELD_STRING, "Group06"), + DEFINE_KEYFIELD( m_iszGroupMembers[7], FIELD_STRING, "Group07"), + DEFINE_KEYFIELD( m_iszGroupMembers[8], FIELD_STRING, "Group08"), + DEFINE_KEYFIELD( m_iszGroupMembers[9], FIELD_STRING, "Group09"), + DEFINE_KEYFIELD( m_iszGroupMembers[10], FIELD_STRING, "Group10"), + DEFINE_KEYFIELD( m_iszGroupMembers[11], FIELD_STRING, "Group11"), + DEFINE_KEYFIELD( m_iszGroupMembers[12], FIELD_STRING, "Group12"), + DEFINE_KEYFIELD( m_iszGroupMembers[13], FIELD_STRING, "Group13"), + DEFINE_KEYFIELD( m_iszGroupMembers[14], FIELD_STRING, "Group14"), + DEFINE_KEYFIELD( m_iszGroupMembers[15], FIELD_STRING, "Group15"), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Compares a set of integer inputs to the one main input +// Outputs true if they are all equivalant, false otherwise +//----------------------------------------------------------------------------- +class CLogicCompareInteger : public CLogicalEntity +{ +public: + DECLARE_CLASS( CLogicCompareInteger, CLogicalEntity ); + + // outputs +#ifdef MAPBASE + COutputVariant m_OnEqual; + COutputVariant m_OnNotEqual; +#else + COutputEvent m_OnEqual; + COutputEvent m_OnNotEqual; +#endif + + // data +#ifdef MAPBASE + variant_t m_iValue; + bool m_iShouldCompareToValue; + bool m_bStrLenAllowed = true; + int DrawDebugTextOverlays(void); +#else + int m_iIntegerValue; + int m_iShouldCompareToValue; +#endif + + DECLARE_DATADESC(); + + CMultiInputVar m_AllIntCompares; + + // Input handlers + void InputValue( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputValueNoFire( inputdata_t &inputdata ); + void InputSetIntegerValue( inputdata_t &inputdata ); +#endif + void InputCompareValues( inputdata_t &inputdata ); +}; + + +LINK_ENTITY_TO_CLASS( logic_multicompare, CLogicCompareInteger ); + + +BEGIN_DATADESC( CLogicCompareInteger ) + + DEFINE_OUTPUT( m_OnEqual, "OnEqual" ), + DEFINE_OUTPUT( m_OnNotEqual, "OnNotEqual" ), + +#ifdef MAPBASE + DEFINE_KEYVARIANT( m_iValue, "IntegerValue" ), + DEFINE_KEYFIELD( m_iShouldCompareToValue, FIELD_BOOLEAN, "ShouldComparetoValue" ), + DEFINE_KEYFIELD( m_bStrLenAllowed, FIELD_BOOLEAN, "StrLenAllowed" ), +#else + DEFINE_KEYFIELD( m_iIntegerValue, FIELD_INTEGER, "IntegerValue" ), + DEFINE_KEYFIELD( m_iShouldCompareToValue, FIELD_INTEGER, "ShouldComparetoValue" ), +#endif + + DEFINE_FIELD( m_AllIntCompares, FIELD_INPUT ), + + DEFINE_INPUTFUNC( FIELD_INPUT, "InputValue", InputValue ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_INPUT, "InputValueNoFire", InputValueNoFire ), + DEFINE_INPUTFUNC( FIELD_INPUT, "SetReferenceValue", InputSetIntegerValue ), +#endif + DEFINE_INPUTFUNC( FIELD_INPUT, "CompareValues", InputCompareValues ), + +END_DATADESC() + + + + +//----------------------------------------------------------------------------- +// Purpose: Adds to the list of compared values +//----------------------------------------------------------------------------- +void CLogicCompareInteger::InputValue( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + // Parse the input value, regardless of field type + inputdata.value = Variant_ParseInput(inputdata); +#else + // make sure it's an int, if it can't be converted just throw it away + if ( !inputdata.value.Convert(FIELD_INTEGER) ) + return; +#endif + + // update the value list with the new value + m_AllIntCompares.AddValue( inputdata.value, inputdata.nOutputID ); + + // if we haven't already this frame, send a message to ourself to update and fire + if ( !m_AllIntCompares.m_bUpdatedThisFrame ) + { +#ifdef MAPBASE + // Need to wait for all inputs to arrive + g_EventQueue.AddEvent( this, "CompareValues", 0.01, inputdata.pActivator, this, inputdata.nOutputID ); +#else + // TODO: need to add this event with a lower priority, so it gets called after all inputs have arrived + g_EventQueue.AddEvent( this, "CompareValues", 0, inputdata.pActivator, this, inputdata.nOutputID ); +#endif + m_AllIntCompares.m_bUpdatedThisFrame = TRUE; + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Adds to the list of compared values without firing +//----------------------------------------------------------------------------- +void CLogicCompareInteger::InputValueNoFire( inputdata_t &inputdata ) +{ + // Parse the input value, regardless of field type + inputdata.value = Variant_ParseInput(inputdata); + + // update the value list with the new value + m_AllIntCompares.AddValue( inputdata.value, inputdata.nOutputID ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets our reference value +//----------------------------------------------------------------------------- +void CLogicCompareInteger::InputSetIntegerValue( inputdata_t &inputdata ) +{ + m_iValue = Variant_ParseInput(inputdata); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Forces a recompare +//----------------------------------------------------------------------------- +void CLogicCompareInteger::InputCompareValues( inputdata_t &inputdata ) +{ + m_AllIntCompares.m_bUpdatedThisFrame = FALSE; + + // loop through all the values comparing them +#ifdef MAPBASE + variant_t value = m_iValue; + CMultiInputVar::inputitem_t *input = m_AllIntCompares.m_InputList; + + if ( !m_iShouldCompareToValue && input ) + { + value = input->value; + } + + while ( input ) + { + if ( !Variant_Equal(value, input->value, m_bStrLenAllowed) ) + { + // false + m_OnNotEqual.Set( input->value, inputdata.pActivator, this ); + return; + } + + input = input->next; + } + + // true! all values equal + m_OnEqual.Set( value, inputdata.pActivator, this ); +#else + int value = m_iIntegerValue; + CMultiInputVar::inputitem_t *input = m_AllIntCompares.m_InputList; + + if ( !m_iShouldCompareToValue && input ) + { + value = input->value.Int(); + } + + while ( input ) + { + if ( input->value.Int() != value ) + { + // false + m_OnNotEqual.FireOutput( inputdata.pActivator, this ); + return; + } + + input = input->next; + } + + // true! all values equal + m_OnEqual.FireOutput( inputdata.pActivator, this ); +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CLogicCompareInteger::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + Q_snprintf(tempstr, sizeof(tempstr), " Reference Value: %s", m_iValue.GetDebug()); + EntityText(text_offset, tempstr, 0); + text_offset++; + + int count = 1; + CMultiInputVar::inputitem_t *input = m_AllIntCompares.m_InputList; + while ( input ) + { + Q_snprintf(tempstr, sizeof(tempstr), " Value %i: %s", count, input->value.GetDebug()); + EntityText(text_offset, tempstr, 0); + text_offset++; + + count++; + input = input->next; + } + } + return text_offset; +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Timer entity. Fires an output at regular or random intervals. +//----------------------------------------------------------------------------- +// +// Spawnflags and others constants. +// +const int SF_TIMER_UPDOWN = 1; +const float LOGIC_TIMER_MIN_INTERVAL = 0.01; + + +class CTimerEntity : public CLogicalEntity +{ +public: + DECLARE_CLASS( CTimerEntity, CLogicalEntity ); + + void Spawn( void ); + void Think( void ); + + void Toggle( void ); + void Enable( void ); + void Disable( void ); + void FireTimer( void ); + + int DrawDebugTextOverlays(void); + + // outputs + COutputEvent m_OnTimer; + COutputEvent m_OnTimerHigh; + COutputEvent m_OnTimerLow; + + // inputs + void InputToggle( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputFireTimer( inputdata_t &inputdata ); + void InputRefireTime( inputdata_t &inputdata ); + void InputResetTimer( inputdata_t &inputdata ); + void InputAddToTimer( inputdata_t &inputdata ); + void InputSubtractFromTimer( inputdata_t &inputdata ); + + int m_iDisabled; + float m_flRefireTime; + bool m_bUpDownState; + int m_iUseRandomTime; + float m_flLowerRandomBound; + float m_flUpperRandomBound; +#ifdef MAPBASE + bool m_bUseBoundsForTimerInputs; +#endif + + // methods + void ResetTimer( void ); + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( logic_timer, CTimerEntity ); + + +BEGIN_DATADESC( CTimerEntity ) + + // Keys + DEFINE_KEYFIELD( m_iDisabled, FIELD_INTEGER, "StartDisabled" ), + DEFINE_KEYFIELD( m_flRefireTime, FIELD_FLOAT, "RefireTime" ), + + DEFINE_FIELD( m_bUpDownState, FIELD_BOOLEAN ), + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bUseBoundsForTimerInputs, FIELD_BOOLEAN, "UseBoundsForTimerInputs" ), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "RefireTime", InputRefireTime ), + DEFINE_INPUTFUNC( FIELD_VOID, "FireTimer", InputFireTimer ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "AddToTimer", InputAddToTimer ), + DEFINE_INPUTFUNC( FIELD_VOID, "ResetTimer", InputResetTimer ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SubtractFromTimer", InputSubtractFromTimer ), + + DEFINE_INPUT( m_iUseRandomTime, FIELD_INTEGER, "UseRandomTime" ), + DEFINE_INPUT( m_flLowerRandomBound, FIELD_FLOAT, "LowerRandomBound" ), + DEFINE_INPUT( m_flUpperRandomBound, FIELD_FLOAT, "UpperRandomBound" ), + + + // Outputs + DEFINE_OUTPUT( m_OnTimer, "OnTimer" ), + DEFINE_OUTPUT( m_OnTimerHigh, "OnTimerHigh" ), + DEFINE_OUTPUT( m_OnTimerLow, "OnTimerLow" ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::Spawn( void ) +{ + if (!m_iUseRandomTime && (m_flRefireTime < LOGIC_TIMER_MIN_INTERVAL)) + { + m_flRefireTime = LOGIC_TIMER_MIN_INTERVAL; + } + + if ( !m_iDisabled && (m_flRefireTime > 0 || m_iUseRandomTime) ) + { + Enable(); + } + else + { + Disable(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::Think( void ) +{ + FireTimer(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the time the timerentity will next fire +//----------------------------------------------------------------------------- +void CTimerEntity::ResetTimer( void ) +{ + if ( m_iDisabled ) + return; + + if ( m_iUseRandomTime ) + { + m_flRefireTime = random->RandomFloat( m_flLowerRandomBound, m_flUpperRandomBound ); + } + + SetNextThink( gpGlobals->curtime + m_flRefireTime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::Enable( void ) +{ + m_iDisabled = FALSE; + ResetTimer(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::Disable( void ) +{ + m_iDisabled = TRUE; + SetNextThink( TICK_NEVER_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::Toggle( void ) +{ + if ( m_iDisabled ) + { + Enable(); + } + else + { + Disable(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::FireTimer( void ) +{ + if ( !m_iDisabled ) + { + // + // Up/down timers alternate between two outputs. + // + if (m_spawnflags & SF_TIMER_UPDOWN) + { + if (m_bUpDownState) + { + m_OnTimerHigh.FireOutput( this, this ); + } + else + { + m_OnTimerLow.FireOutput( this, this ); + } + + m_bUpDownState = !m_bUpDownState; + } + // + // Regular timers only fire a single output. + // + else + { + m_OnTimer.FireOutput( this, this ); + } + + ResetTimer(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::InputToggle( inputdata_t &inputdata ) +{ + Toggle(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTimerEntity::InputFireTimer( inputdata_t &inputdata ) +{ + FireTimer(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Changes the time interval between timer fires +// Resets the next firing to be time + newRefireTime +// Input : Float refire frequency in seconds. +//----------------------------------------------------------------------------- +void CTimerEntity::InputRefireTime( inputdata_t &inputdata ) +{ + float flRefireInterval = inputdata.value.Float(); + + if ( flRefireInterval < LOGIC_TIMER_MIN_INTERVAL) + { + flRefireInterval = LOGIC_TIMER_MIN_INTERVAL; + } + + if (m_flRefireTime != flRefireInterval ) + { + m_flRefireTime = flRefireInterval; + ResetTimer(); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CTimerEntity::InputResetTimer( inputdata_t &inputdata ) +{ + // don't reset the timer if it isn't enabled + if ( m_iDisabled ) + return; + + ResetTimer(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds to the time interval if the timer is enabled +// Input : Float time to add in seconds +//----------------------------------------------------------------------------- +void CTimerEntity::InputAddToTimer( inputdata_t &inputdata ) +{ + // don't add time if the timer isn't enabled + if ( m_iDisabled ) + return; + + // Add time to timer + float flNextThink = GetNextThink(); +#ifdef MAPBASE + if (m_bUseBoundsForTimerInputs) + { + // Make sure it's not above our min or max bounds + if (flNextThink - gpGlobals->curtime > m_flUpperRandomBound) + return; + + flNextThink += inputdata.value.Float(); + flNextThink = clamp( flNextThink - gpGlobals->curtime, m_flLowerRandomBound, m_flUpperRandomBound ); + SetNextThink( gpGlobals->curtime + flNextThink ); + } + else + { + SetNextThink( flNextThink + inputdata.value.Float() ); + } +#else + SetNextThink( flNextThink += inputdata.value.Float() ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Subtract from the time interval if the timer is enabled +// Input : Float time to subtract in seconds +//----------------------------------------------------------------------------- +void CTimerEntity::InputSubtractFromTimer( inputdata_t &inputdata ) +{ + // don't add time if the timer isn't enabled + if ( m_iDisabled ) + return; + + // Subtract time from the timer but don't let the timer go negative + float flNextThink = GetNextThink(); +#ifdef MAPBASE + if (m_bUseBoundsForTimerInputs) + { + // Make sure it's not above our min or max bounds + if (flNextThink - gpGlobals->curtime < m_flLowerRandomBound) + return; + + flNextThink -= inputdata.value.Float(); + flNextThink = clamp( flNextThink - gpGlobals->curtime, m_flLowerRandomBound, m_flUpperRandomBound ); + SetNextThink( gpGlobals->curtime + flNextThink ); + } + else + { + flNextThink -= inputdata.value.Float(); + SetNextThink( (flNextThink <= gpGlobals->curtime) ? gpGlobals->curtime : flNextThink ); + } +#else + if ( ( flNextThink - gpGlobals->curtime ) <= inputdata.value.Float() ) + { + SetNextThink( gpGlobals->curtime ); + } + else + { + SetNextThink( flNextThink -= inputdata.value.Float() ); + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CTimerEntity::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print refire time + Q_snprintf(tempstr,sizeof(tempstr),"refire interval: %.2f sec", m_flRefireTime); + EntityText(text_offset,tempstr,0); + text_offset++; + + // print seconds to next fire + if ( !m_iDisabled ) + { + float flNextThink = GetNextThink(); + Q_snprintf( tempstr, sizeof( tempstr ), " firing in: %.2f sec", flNextThink - gpGlobals->curtime ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + } + return text_offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Computes a line between two entities +//----------------------------------------------------------------------------- +class CLogicLineToEntity : public CLogicalEntity +{ +public: + DECLARE_CLASS( CLogicLineToEntity, CLogicalEntity ); + + void Activate(void); + void Spawn( void ); + void Think( void ); + + // outputs + COutputVector m_Line; + + DECLARE_DATADESC(); + +private: + string_t m_SourceName; + EHANDLE m_StartEntity; + EHANDLE m_EndEntity; +}; + +LINK_ENTITY_TO_CLASS( logic_lineto, CLogicLineToEntity ); + + +BEGIN_DATADESC( CLogicLineToEntity ) + + // Keys + // target is handled in the base class, stored in field m_target + DEFINE_KEYFIELD( m_SourceName, FIELD_STRING, "source" ), + DEFINE_FIELD( m_StartEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_EndEntity, FIELD_EHANDLE ), + + // Outputs + DEFINE_OUTPUT( m_Line, "Line" ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Find the entities +//----------------------------------------------------------------------------- +void CLogicLineToEntity::Activate(void) +{ + BaseClass::Activate(); + + if (m_target != NULL_STRING) + { + m_EndEntity = gEntList.FindEntityByName( NULL, m_target ); + + // + // If we were given a bad measure target, just measure sound where we are. + // + if ((m_EndEntity == NULL) || (m_EndEntity->edict() == NULL)) + { + Warning( "logic_lineto - Target not found or target with no origin!\n"); + m_EndEntity = this; + } + } + else + { + m_EndEntity = this; + } + + if (m_SourceName != NULL_STRING) + { + m_StartEntity = gEntList.FindEntityByName( NULL, m_SourceName ); + + // + // If we were given a bad measure target, just measure sound where we are. + // + if ((m_StartEntity == NULL) || (m_StartEntity->edict() == NULL)) + { + Warning( "logic_lineto - Source not found or source with no origin!\n"); + m_StartEntity = this; + } + } + else + { + m_StartEntity = this; + } +} + + +//----------------------------------------------------------------------------- +// Find the entities +//----------------------------------------------------------------------------- +void CLogicLineToEntity::Spawn(void) +{ + SetNextThink( gpGlobals->curtime + 0.01f ); +} + + +//----------------------------------------------------------------------------- +// Find the entities +//----------------------------------------------------------------------------- +void CLogicLineToEntity::Think(void) +{ + CBaseEntity* pDest = m_EndEntity.Get(); + CBaseEntity* pSrc = m_StartEntity.Get(); + if (!pDest || !pSrc || !pDest->edict() || !pSrc->edict()) + { + // Can sleep for a long time, no more lines. + m_Line.Set( vec3_origin, this, this ); + SetNextThink( gpGlobals->curtime + 10 ); + return; + } + + Vector delta; + VectorSubtract( pDest->GetAbsOrigin(), pSrc->GetAbsOrigin(), delta ); + m_Line.Set(delta, this, this); + + SetNextThink( gpGlobals->curtime + 0.01f ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Remaps a given input range to an output range. +//----------------------------------------------------------------------------- +const int SF_MATH_REMAP_IGNORE_OUT_OF_RANGE = 1; +const int SF_MATH_REMAP_CLAMP_OUTPUT_TO_RANGE = 2; + +class CMathRemap : public CLogicalEntity +{ +public: + + DECLARE_CLASS( CMathRemap, CLogicalEntity ); + + void Spawn(void); + + // Keys + float m_flInMin; + float m_flInMax; + float m_flOut1; // Output value when input is m_fInMin + float m_flOut2; // Output value when input is m_fInMax + + bool m_bEnabled; + + // Inputs + void InputValue( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + // Outputs + COutputFloat m_OutValue; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_remap, CMathRemap); + + +BEGIN_DATADESC( CMathRemap ) + + DEFINE_INPUTFUNC(FIELD_FLOAT, "InValue", InputValue ), + + DEFINE_OUTPUT(m_OutValue, "OutValue"), + + DEFINE_KEYFIELD(m_flInMin, FIELD_FLOAT, "in1"), + DEFINE_KEYFIELD(m_flInMax, FIELD_FLOAT, "in2"), + DEFINE_KEYFIELD(m_flOut1, FIELD_FLOAT, "out1"), + DEFINE_KEYFIELD(m_flOut2, FIELD_FLOAT, "out2"), + + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +END_DATADESC() + + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathRemap::Spawn(void) +{ + // + // Avoid a divide by zero in ValueChanged. + // + if (m_flInMin == m_flInMax) + { + m_flInMin = 0; + m_flInMax = 1; + } + + // + // Make sure min and max are set properly relative to one another. + // + if (m_flInMin > m_flInMax) + { + float flTemp = m_flInMin; + m_flInMin = m_flInMax; + m_flInMax = flTemp; + } + + m_bEnabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathRemap::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathRemap::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler that is called when the input value changes. +//----------------------------------------------------------------------------- +void CMathRemap::InputValue( inputdata_t &inputdata ) +{ + float flValue = inputdata.value.Float(); + + // + // Disallow out-of-range input values to avoid out-of-range output values. + // + float flClampValue = clamp(flValue, m_flInMin, m_flInMax); + + if ((flClampValue == flValue) || !FBitSet(m_spawnflags, SF_MATH_REMAP_IGNORE_OUT_OF_RANGE)) + { + // + // Remap the input value to the desired output range and update the output. + // + float flRemappedValue = m_flOut1 + (((flValue - m_flInMin) * (m_flOut2 - m_flOut1)) / (m_flInMax - m_flInMin)); + + if ( FBitSet( m_spawnflags, SF_MATH_REMAP_CLAMP_OUTPUT_TO_RANGE ) ) + { + flRemappedValue = clamp( flRemappedValue, m_flOut1, m_flOut2 ); + } + + if ( m_bEnabled == true ) + { + m_OutValue.Set(flRemappedValue, inputdata.pActivator, this); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Remaps a given input range to an output range. +//----------------------------------------------------------------------------- +const int SF_COLOR_BLEND_IGNORE_OUT_OF_RANGE = 1; + +class CMathColorBlend : public CLogicalEntity +{ +public: + + DECLARE_CLASS( CMathColorBlend, CLogicalEntity ); + + void Spawn(void); + + // Keys + float m_flInMin; + float m_flInMax; + color32 m_OutColor1; // Output color when input is m_fInMin + color32 m_OutColor2; // Output color when input is m_fInMax + + // Inputs + void InputValue( inputdata_t &inputdata ); + + // Outputs + COutputColor32 m_OutValue; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_colorblend, CMathColorBlend); + + +BEGIN_DATADESC( CMathColorBlend ) + + DEFINE_INPUTFUNC(FIELD_FLOAT, "InValue", InputValue ), + + DEFINE_OUTPUT(m_OutValue, "OutColor"), + + DEFINE_KEYFIELD(m_flInMin, FIELD_FLOAT, "inmin"), + DEFINE_KEYFIELD(m_flInMax, FIELD_FLOAT, "inmax"), + DEFINE_KEYFIELD(m_OutColor1, FIELD_COLOR32, "colormin"), + DEFINE_KEYFIELD(m_OutColor2, FIELD_COLOR32, "colormax"), + +END_DATADESC() + + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathColorBlend::Spawn(void) +{ + // + // Avoid a divide by zero in ValueChanged. + // + if (m_flInMin == m_flInMax) + { + m_flInMin = 0; + m_flInMax = 1; + } + + // + // Make sure min and max are set properly relative to one another. + // + if (m_flInMin > m_flInMax) + { + float flTemp = m_flInMin; + m_flInMin = m_flInMax; + m_flInMax = flTemp; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that is called when the input value changes. +//----------------------------------------------------------------------------- +void CMathColorBlend::InputValue( inputdata_t &inputdata ) +{ + float flValue = inputdata.value.Float(); + + // + // Disallow out-of-range input values to avoid out-of-range output values. + // + float flClampValue = clamp(flValue, m_flInMin, m_flInMax); + if ((flClampValue == flValue) || !FBitSet(m_spawnflags, SF_COLOR_BLEND_IGNORE_OUT_OF_RANGE)) + { + // + // Remap the input value to the desired output color and update the output. + // + color32 Color; + Color.r = m_OutColor1.r + (((flClampValue - m_flInMin) * (m_OutColor2.r - m_OutColor1.r)) / (m_flInMax - m_flInMin)); + Color.g = m_OutColor1.g + (((flClampValue - m_flInMin) * (m_OutColor2.g - m_OutColor1.g)) / (m_flInMax - m_flInMin)); + Color.b = m_OutColor1.b + (((flClampValue - m_flInMin) * (m_OutColor2.b - m_OutColor1.b)) / (m_flInMax - m_flInMin)); + Color.a = m_OutColor1.a + (((flClampValue - m_flInMin) * (m_OutColor2.a - m_OutColor1.a)) / (m_flInMax - m_flInMin)); + + m_OutValue.Set(Color, inputdata.pActivator, this); + } +} + + +//----------------------------------------------------------------------------- +// Console command to set the state of a global +//----------------------------------------------------------------------------- +void CC_Global_Set( const CCommand &args ) +{ + const char *szGlobal = args[1]; + const char *szState = args[2]; + + if ( szGlobal == NULL || szState == NULL ) + { + Msg( "Usage: global_set : Sets the state of the given env_global (0 = OFF, 1 = ON, 2 = DEAD).\n" ); + return; + } + + int nState = atoi( szState ); + + int nIndex = GlobalEntity_GetIndex( szGlobal ); + + if ( nIndex >= 0 ) + { + GlobalEntity_SetState( nIndex, ( GLOBALESTATE )nState ); + } + else + { + GlobalEntity_Add( szGlobal, STRING( gpGlobals->mapname ), ( GLOBALESTATE )nState ); + } +} + +static ConCommand global_set( "global_set", CC_Global_Set, "global_set : Sets the state of the given env_global (0 = OFF, 1 = ON, 2 = DEAD).", FCVAR_CHEAT ); + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Console command to set the counter of a global +//----------------------------------------------------------------------------- +void CC_Global_Counter( const CCommand &args ) +{ + const char *szGlobal = args[1]; + const char *szCounter = args[2]; + + if ( szGlobal == NULL || szCounter == NULL ) + { + Msg( "Usage: global_counter : Sets the counter of the given env_global.\n" ); + return; + } + + int nCounter = atoi( szCounter ); + + int nIndex = GlobalEntity_GetIndex( szGlobal ); + + if ( nIndex >= 0 ) + { + GlobalEntity_SetCounter( nIndex, nCounter ); + } + else + { + nIndex = GlobalEntity_Add( szGlobal, STRING( gpGlobals->mapname ), GLOBAL_ON ); + GlobalEntity_SetCounter( nIndex, nCounter ); + } +} + +static ConCommand global_counter( "global_counter", CC_Global_Counter, "global_counter : Sets the counter of the given env_global.", FCVAR_CHEAT ); +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Holds a global state that can be queried by other entities to change +// their behavior, such as "predistaster". +//----------------------------------------------------------------------------- +const int SF_GLOBAL_SET = 1; // Set global state to initial state on spawn + +class CEnvGlobal : public CLogicalEntity +{ +public: + DECLARE_CLASS( CEnvGlobal, CLogicalEntity ); + + void Spawn( void ); + +#ifdef MAPBASE + bool KeyValue( const char *szKeyName, const char *szValue ); +#endif + + // Input handlers + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputRemove( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputSetCounter( inputdata_t &inputdata ); + void InputAddToCounter( inputdata_t &inputdata ); + void InputGetCounter( inputdata_t &inputdata ); + + int DrawDebugTextOverlays(void); + + DECLARE_DATADESC(); + + COutputInt m_outCounter; + + string_t m_globalstate; + int m_triggermode; + int m_initialstate; + int m_counter; // A counter value associated with this global. +}; + + +BEGIN_DATADESC( CEnvGlobal ) + + DEFINE_KEYFIELD( m_globalstate, FIELD_STRING, "globalstate" ), + DEFINE_FIELD( m_triggermode, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_initialstate, FIELD_INTEGER, "initialstate" ), + DEFINE_KEYFIELD( m_counter, FIELD_INTEGER, "counter" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Remove", InputRemove ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetCounter", InputSetCounter ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddToCounter", InputAddToCounter ), + DEFINE_INPUTFUNC( FIELD_VOID, "GetCounter", InputGetCounter ), + +#ifdef MAPBASE + DEFINE_OUTPUT( m_outCounter, "OutCounter" ), +#else + DEFINE_OUTPUT( m_outCounter, "Counter" ), +#endif + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( env_global, CEnvGlobal ); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvGlobal::Spawn( void ) +{ + if ( !m_globalstate ) + { + UTIL_Remove( this ); + return; + } + +#ifdef HL2_EPISODIC + // if we modify the state of the physics cannon, make sure we precache the ragdoll boogie zap sound + if ( ( m_globalstate != NULL_STRING ) && ( stricmp( STRING( m_globalstate ), "super_phys_gun" ) == 0 ) ) + { + PrecacheScriptSound( "RagdollBoogie.Zap" ); + } +#endif + + if ( FBitSet( m_spawnflags, SF_GLOBAL_SET ) ) + { + if ( !GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, (GLOBALESTATE)m_initialstate ); + } + + if ( m_counter != 0 ) + { + GlobalEntity_SetCounter( m_globalstate, m_counter ); + } + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Cache user entity field values until spawn is called. +// Input : szKeyName - Key to handle. +// szValue - Value for key. +// Output : Returns true if the key was handled, false if not. +//----------------------------------------------------------------------------- +bool CEnvGlobal::KeyValue( const char *szKeyName, const char *szValue ) +{ + // Any "Counter" outputs are changed to "OutCounter" before spawning. + if (FStrEq(szKeyName, "Counter") && strchr(szValue, ',')) + { + return BaseClass::KeyValue( "OutCounter", szValue ); + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} +#endif + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CEnvGlobal::InputTurnOn( inputdata_t &inputdata ) +{ + if ( GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_SetState( m_globalstate, GLOBAL_ON ); + } + else + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, GLOBAL_ON ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CEnvGlobal::InputTurnOff( inputdata_t &inputdata ) +{ + if ( GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_SetState( m_globalstate, GLOBAL_OFF ); + } + else + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, GLOBAL_OFF ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CEnvGlobal::InputRemove( inputdata_t &inputdata ) +{ + if ( GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_SetState( m_globalstate, GLOBAL_DEAD ); + } + else + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, GLOBAL_DEAD ); + } +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CEnvGlobal::InputSetCounter( inputdata_t &inputdata ) +{ + if ( !GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, GLOBAL_ON ); + } + + GlobalEntity_SetCounter( m_globalstate, inputdata.value.Int() ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CEnvGlobal::InputAddToCounter( inputdata_t &inputdata ) +{ + if ( !GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, GLOBAL_ON ); + } + + GlobalEntity_AddToCounter( m_globalstate, inputdata.value.Int() ); +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CEnvGlobal::InputGetCounter( inputdata_t &inputdata ) +{ + if ( !GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, GLOBAL_ON ); + } + + m_outCounter.Set( GlobalEntity_GetCounter( m_globalstate ), inputdata.pActivator, this ); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CEnvGlobal::InputToggle( inputdata_t &inputdata ) +{ + GLOBALESTATE oldState = GlobalEntity_GetState( m_globalstate ); + GLOBALESTATE newState; + + if ( oldState == GLOBAL_ON ) + { + newState = GLOBAL_OFF; + } + else if ( oldState == GLOBAL_OFF ) + { + newState = GLOBAL_ON; + } + else + { + return; + } + + if ( GlobalEntity_IsInTable( m_globalstate ) ) + { + GlobalEntity_SetState( m_globalstate, newState ); + } + else + { + GlobalEntity_Add( m_globalstate, gpGlobals->mapname, newState ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CEnvGlobal::DrawDebugTextOverlays(void) +{ + // Skip AIClass debug overlays + int text_offset = CBaseEntity::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf(tempstr,sizeof(tempstr),"State: %s",STRING(m_globalstate)); + EntityText(text_offset,tempstr,0); + text_offset++; + + GLOBALESTATE nState = GlobalEntity_GetState( m_globalstate ); + + switch( nState ) + { + case GLOBAL_OFF: + Q_strncpy(tempstr,"Value: OFF",sizeof(tempstr)); + break; + + case GLOBAL_ON: + Q_strncpy(tempstr,"Value: ON",sizeof(tempstr)); + break; + + case GLOBAL_DEAD: + Q_strncpy(tempstr,"Value: DEAD",sizeof(tempstr)); + break; + } + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +#define MS_MAX_TARGETS 32 + +const int SF_MULTI_INIT = 1; + +class CMultiSource : public CLogicalEntity +{ +public: + DECLARE_CLASS( CMultiSource, CLogicalEntity ); + + void Spawn( ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void Use( ::CBaseEntity *pActivator, ::CBaseEntity *pCaller, USE_TYPE useType, float value ); + int ObjectCaps( void ) { return(BaseClass::ObjectCaps() | FCAP_MASTER); } + bool IsTriggered( ::CBaseEntity *pActivator ); + void Register( void ); + + DECLARE_DATADESC(); + + EHANDLE m_rgEntities[MS_MAX_TARGETS]; + int m_rgTriggered[MS_MAX_TARGETS]; + + COutputEvent m_OnTrigger; // Fired when all connections are triggered. + + int m_iTotal; + string_t m_globalstate; +}; + +BEGIN_DATADESC( CMultiSource ) + + //!!!BUGBUG FIX + DEFINE_ARRAY( m_rgEntities, FIELD_EHANDLE, MS_MAX_TARGETS ), + DEFINE_ARRAY( m_rgTriggered, FIELD_INTEGER, MS_MAX_TARGETS ), + DEFINE_FIELD( m_iTotal, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_globalstate, FIELD_STRING, "globalstate" ), + + // Function pointers + DEFINE_FUNCTION( Register ), + + // Outputs + DEFINE_OUTPUT(m_OnTrigger, "OnTrigger"), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( multisource, CMultiSource ); + + +//----------------------------------------------------------------------------- +// Purpose: Cache user entity field values until spawn is called. +// Input : szKeyName - Key to handle. +// szValue - Value for key. +// Output : Returns true if the key was handled, false if not. +//----------------------------------------------------------------------------- +bool CMultiSource::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq(szKeyName, "style") || + FStrEq(szKeyName, "height") || + FStrEq(szKeyName, "killtarget") || + FStrEq(szKeyName, "value1") || + FStrEq(szKeyName, "value2") || + FStrEq(szKeyName, "value3")) + { + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMultiSource::Spawn() +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + m_spawnflags |= SF_MULTI_INIT; // Until it's initialized + SetThink(&CMultiSource::Register); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CMultiSource::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + int i = 0; + + // Find the entity in our list + while (i < m_iTotal) + if ( m_rgEntities[i++] == pCaller ) + break; + + // if we didn't find it, report error and leave + if (i > m_iTotal) + { + Warning("MultiSrc: Used by non member %s.\n", pCaller->edict() ? pCaller->GetClassname() : ""); + return; + } + + // CONSIDER: a Use input to the multisource always toggles. Could check useType for ON/OFF/TOGGLE + + m_rgTriggered[i-1] ^= 1; + + // + if ( IsTriggered( pActivator ) ) + { + DevMsg( 2, "Multisource %s enabled (%d inputs)\n", GetDebugName(), m_iTotal ); + USE_TYPE useType = USE_TOGGLE; + if ( m_globalstate != NULL_STRING ) + useType = USE_ON; + + m_OnTrigger.FireOutput(pActivator, this); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CMultiSource::IsTriggered( CBaseEntity * ) +{ + // Is everything triggered? + int i = 0; + + // Still initializing? + if ( m_spawnflags & SF_MULTI_INIT ) + return 0; + + while (i < m_iTotal) + { + if (m_rgTriggered[i] == 0) + break; + i++; + } + + if (i == m_iTotal) + { + if ( !m_globalstate || GlobalEntity_GetState( m_globalstate ) == GLOBAL_ON ) + return 1; + } + + return 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMultiSource::Register(void) +{ + CBaseEntity *pTarget = NULL; + + m_iTotal = 0; + memset( m_rgEntities, 0, MS_MAX_TARGETS * sizeof(EHANDLE) ); + + SetThink(&CMultiSource::SUB_DoNothing); + + // search for all entities which target this multisource (m_iName) + // dvsents2: port multisource to entity I/O! + + pTarget = gEntList.FindEntityByTarget( NULL, STRING(GetEntityName()) ); + + while ( pTarget && (m_iTotal < MS_MAX_TARGETS) ) + { + if ( pTarget ) + m_rgEntities[m_iTotal++] = pTarget; + + pTarget = gEntList.FindEntityByTarget( pTarget, STRING(GetEntityName()) ); + } + + pTarget = gEntList.FindEntityByClassname( NULL, "multi_manager" ); + while (pTarget && (m_iTotal < MS_MAX_TARGETS)) + { + if ( pTarget && pTarget->HasTarget(GetEntityName()) ) + m_rgEntities[m_iTotal++] = pTarget; + + pTarget = gEntList.FindEntityByClassname( pTarget, "multi_manager" ); + } + + m_spawnflags &= ~SF_MULTI_INIT; +} + + +//----------------------------------------------------------------------------- +// Purpose: Holds a value that can be added to and subtracted from. +//----------------------------------------------------------------------------- +class CMathCounter : public CLogicalEntity +{ + DECLARE_CLASS( CMathCounter, CLogicalEntity ); +#ifdef MAPBASE +protected: +#else +private: +#endif + float m_flMin; // Minimum clamp value. If min and max are BOTH zero, no clamping is done. + float m_flMax; // Maximum clamp value. + bool m_bHitMin; // Set when we reach or go below our minimum value, cleared if we go above it again. + bool m_bHitMax; // Set when we reach or exceed our maximum value, cleared if we fall below it again. + + bool m_bDisabled; + + bool KeyValue(const char *szKeyName, const char *szValue); + void Spawn(void); + + int DrawDebugTextOverlays(void); + +#ifdef MAPBASE + virtual +#endif + void UpdateOutValue(CBaseEntity *pActivator, float fNewValue); + + // Inputs + void InputAdd( inputdata_t &inputdata ); + void InputDivide( inputdata_t &inputdata ); + void InputMultiply( inputdata_t &inputdata ); + void InputSetValue( inputdata_t &inputdata ); + void InputSetValueNoFire( inputdata_t &inputdata ); + void InputSubtract( inputdata_t &inputdata ); + void InputSetHitMax( inputdata_t &inputdata ); + void InputSetHitMin( inputdata_t &inputdata ); + void InputGetValue( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetMinValueNoFire( inputdata_t &inputdata ); + void InputSetMaxValueNoFire( inputdata_t &inputdata ); +#endif + + // Outputs + COutputFloat m_OutValue; + COutputFloat m_OnGetValue; // Used for polling the counter value. + COutputEvent m_OnHitMin; + COutputEvent m_OnHitMax; +#ifdef MAPBASE + COutputEvent m_OnChangedFromMin; + COutputEvent m_OnChangedFromMax; +#endif + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_counter, CMathCounter); + + +BEGIN_DATADESC( CMathCounter ) + + DEFINE_FIELD(m_bHitMax, FIELD_BOOLEAN), + DEFINE_FIELD(m_bHitMin, FIELD_BOOLEAN), + + // Keys + DEFINE_KEYFIELD(m_flMin, FIELD_FLOAT, "min"), + DEFINE_KEYFIELD(m_flMax, FIELD_FLOAT, "max"), + + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_FLOAT, "Add", InputAdd), + DEFINE_INPUTFUNC(FIELD_FLOAT, "Divide", InputDivide), + DEFINE_INPUTFUNC(FIELD_FLOAT, "Multiply", InputMultiply), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetValue", InputSetValue), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetValueNoFire", InputSetValueNoFire), + DEFINE_INPUTFUNC(FIELD_FLOAT, "Subtract", InputSubtract), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetHitMax", InputSetHitMax), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetHitMin", InputSetHitMin), + DEFINE_INPUTFUNC(FIELD_VOID, "GetValue", InputGetValue), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMaxValueNoFire", InputSetMaxValueNoFire ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMinValueNoFire", InputSetMinValueNoFire ), +#endif + + // Outputs + DEFINE_OUTPUT(m_OutValue, "OutValue"), + DEFINE_OUTPUT(m_OnHitMin, "OnHitMin"), + DEFINE_OUTPUT(m_OnHitMax, "OnHitMax"), + DEFINE_OUTPUT(m_OnGetValue, "OnGetValue"), +#ifdef MAPBASE + DEFINE_OUTPUT( m_OnChangedFromMin, "OnChangedFromMin" ), + DEFINE_OUTPUT( m_OnChangedFromMax, "OnChangedFromMax" ), +#endif + +END_DATADESC() + + + + +//----------------------------------------------------------------------------- +// Purpose: Handles key values from the BSP before spawn is called. +//----------------------------------------------------------------------------- +bool CMathCounter::KeyValue(const char *szKeyName, const char *szValue) +{ + // + // Set the initial value of the counter. + // + if (!stricmp(szKeyName, "startvalue")) + { +#ifdef MAPBASE + m_OutValue.Init(atof(szValue)); +#else + m_OutValue.Init(atoi(szValue)); +#endif + return(true); + } + + return(BaseClass::KeyValue(szKeyName, szValue)); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after key values have been set. +//----------------------------------------------------------------------------- +void CMathCounter::Spawn( void ) +{ + // + // Make sure max and min are ordered properly or clamp won't work. + // + if (m_flMin > m_flMax) + { + float flTemp = m_flMax; + m_flMax = m_flMin; + m_flMin = flTemp; + } + + // + // Clamp initial value to within the valid range. + // + if ((m_flMin != 0) || (m_flMax != 0)) + { + float flStartValue = clamp(m_OutValue.Get(), m_flMin, m_flMax); + m_OutValue.Init(flStartValue); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CMathCounter::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + Q_snprintf(tempstr,sizeof(tempstr)," min value: %f", m_flMin); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr)," max value: %f", m_flMax); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"current value: %f", m_OutValue.Get()); + EntityText(text_offset,tempstr,0); + text_offset++; + + if( m_bDisabled ) + { + Q_snprintf(tempstr,sizeof(tempstr),"*DISABLED*"); + } + else + { + Q_snprintf(tempstr,sizeof(tempstr),"Enabled."); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Change min/max +//----------------------------------------------------------------------------- +void CMathCounter::InputSetHitMax( inputdata_t &inputdata ) +{ + m_flMax = inputdata.value.Float(); + if ( m_flMax < m_flMin ) + { + m_flMin = m_flMax; + } + UpdateOutValue( inputdata.pActivator, m_OutValue.Get() ); +} + +void CMathCounter::InputSetHitMin( inputdata_t &inputdata ) +{ + m_flMin = inputdata.value.Float(); + if ( m_flMax < m_flMin ) + { + m_flMax = m_flMin; + } + UpdateOutValue( inputdata.pActivator, m_OutValue.Get() ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Change min/max +//----------------------------------------------------------------------------- +void CMathCounter::InputSetMaxValueNoFire( inputdata_t &inputdata ) +{ + m_flMax = inputdata.value.Float(); + if ( m_flMax < m_flMin ) + { + m_flMin = m_flMax; + } +} + +void CMathCounter::InputSetMinValueNoFire( inputdata_t &inputdata ) +{ + m_flMin = inputdata.value.Float(); + if ( m_flMax < m_flMin ) + { + m_flMax = m_flMin; + } +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for adding to the accumulator value. +// Input : Float value to add. +//----------------------------------------------------------------------------- +void CMathCounter::InputAdd( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring ADD because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = m_OutValue.Get() + inputdata.value.Float(); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for multiplying the current value. +// Input : Float value to multiply the value by. +//----------------------------------------------------------------------------- +void CMathCounter::InputDivide( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring DIVIDE because it is disabled\n", GetDebugName() ); + return; + } + + if (inputdata.value.Float() != 0) + { + float fNewValue = m_OutValue.Get() / inputdata.value.Float(); + UpdateOutValue( inputdata.pActivator, fNewValue ); + } + else + { + DevMsg( 1, "LEVEL DESIGN ERROR: Divide by zero in math_value\n" ); + UpdateOutValue( inputdata.pActivator, m_OutValue.Get() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for multiplying the current value. +// Input : Float value to multiply the value by. +//----------------------------------------------------------------------------- +void CMathCounter::InputMultiply( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring MULTIPLY because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = m_OutValue.Get() * inputdata.value.Float(); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for updating the value. +// Input : Float value to set. +//----------------------------------------------------------------------------- +void CMathCounter::InputSetValue( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SETVALUE because it is disabled\n", GetDebugName() ); + return; + } + + UpdateOutValue( inputdata.pActivator, inputdata.value.Float() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for updating the value. +// Input : Float value to set. +//----------------------------------------------------------------------------- +void CMathCounter::InputSetValueNoFire( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SETVALUENOFIRE because it is disabled\n", GetDebugName() ); + return; + } + + float flNewValue = inputdata.value.Float(); + if (( m_flMin != 0 ) || (m_flMax != 0 )) + { + flNewValue = clamp(flNewValue, m_flMin, m_flMax); + } + + m_OutValue.Init( flNewValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for subtracting from the current value. +// Input : Float value to subtract. +//----------------------------------------------------------------------------- +void CMathCounter::InputSubtract( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SUBTRACT because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = m_OutValue.Get() - inputdata.value.Float(); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathCounter::InputGetValue( inputdata_t &inputdata ) +{ + float flOutValue = m_OutValue.Get(); + m_OnGetValue.Set( flOutValue, inputdata.pActivator, inputdata.pCaller ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathCounter::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathCounter::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the value to the new value, clamping and firing the output value. +// Input : fNewValue - Value to set. +//----------------------------------------------------------------------------- +void CMathCounter::UpdateOutValue(CBaseEntity *pActivator, float fNewValue) +{ + if ((m_flMin != 0) || (m_flMax != 0)) + { + // + // Fire an output any time we reach or exceed our maximum value. + // + if ( fNewValue >= m_flMax ) + { + if ( !m_bHitMax ) + { + m_bHitMax = true; + m_OnHitMax.FireOutput( pActivator, this ); + } + } + else + { +#ifdef MAPBASE + // Fire an output if we just changed from the maximum value + if ( m_OutValue.Get() == m_flMax ) + { + m_OnChangedFromMax.FireOutput( pActivator, this ); + } +#endif + + m_bHitMax = false; + } + + // + // Fire an output any time we reach or go below our minimum value. + // + if ( fNewValue <= m_flMin ) + { + if ( !m_bHitMin ) + { + m_bHitMin = true; + m_OnHitMin.FireOutput( pActivator, this ); + } + } + else + { +#ifdef MAPBASE + // Fire an output if we just changed from the maximum value + if ( m_OutValue.Get() == m_flMin ) + { + m_OnChangedFromMin.FireOutput( pActivator, this ); + } +#endif + m_bHitMin = false; + } + + fNewValue = clamp(fNewValue, m_flMin, m_flMax); + } + + m_OutValue.Set(fNewValue, pActivator, this); +} + + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Advanced math_counter with advanced calculation capabilities. +//----------------------------------------------------------------------------- +class CMathCounterAdvanced : public CMathCounter +{ + DECLARE_CLASS( CMathCounterAdvanced, CMathCounter ); +private: + + bool m_bPreserveValue; + bool m_bAlwaysOutputAsInt; + float m_flLerpPercent; + + void UpdateOutValue(CBaseEntity *pActivator, float fNewValue); + + void InputSetValueToPi( inputdata_t &inputdata ); + + void InputPower( inputdata_t &inputdata ); + void InputSquareRoot( inputdata_t &inputdata ); + + void InputRound( inputdata_t &inputdata ); + void InputFloor( inputdata_t &inputdata ); + void InputCeiling( inputdata_t &inputdata ); + void InputTrunc( inputdata_t &inputdata ); + + void InputSine( inputdata_t &inputdata ); + void InputCosine( inputdata_t &inputdata ); + void InputTangent( inputdata_t &inputdata ); + + void InputRandomInt( inputdata_t &inputdata ); + void InputRandomFloat( inputdata_t &inputdata ); + + void InputLerpTo( inputdata_t &inputdata ); + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_counter_advanced, CMathCounterAdvanced); + + +BEGIN_DATADESC( CMathCounterAdvanced ) + + // Keys + DEFINE_INPUT(m_bPreserveValue, FIELD_BOOLEAN, "PreserveValue"), + DEFINE_INPUT(m_bAlwaysOutputAsInt, FIELD_BOOLEAN, "AlwaysOutputAsInt"), + DEFINE_INPUT(m_flLerpPercent, FIELD_FLOAT, "SetLerpPercent"), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "SetValueToPi", InputSetValueToPi), + + DEFINE_INPUTFUNC(FIELD_VOID, "SquareRoot", InputSquareRoot), + DEFINE_INPUTFUNC(FIELD_INTEGER, "Power", InputPower), + + DEFINE_INPUTFUNC(FIELD_INTEGER, "Round", InputRound), + DEFINE_INPUTFUNC(FIELD_INTEGER, "Floor", InputFloor), + DEFINE_INPUTFUNC(FIELD_INTEGER, "Ceil", InputCeiling), + DEFINE_INPUTFUNC(FIELD_INTEGER, "Trunc", InputTrunc), + + DEFINE_INPUTFUNC(FIELD_VOID, "Sin", InputSine), + DEFINE_INPUTFUNC(FIELD_VOID, "Cos", InputCosine), + DEFINE_INPUTFUNC(FIELD_VOID, "Tan", InputTangent), + + DEFINE_INPUTFUNC(FIELD_STRING, "RandomInt", InputRandomInt), + DEFINE_INPUTFUNC(FIELD_STRING, "RandomFloat", InputRandomFloat), + + DEFINE_INPUTFUNC(FIELD_FLOAT, "LerpTo", InputLerpTo), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the current value to pi. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputSetValueToPi( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SET TO PI because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = M_PI; + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for calculating the square root of the current value. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputSquareRoot( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SQUARE ROOT because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = sqrt(m_OutValue.Get()); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for exponentiation of the current value. Use 2 to square it. +// Input : Integer value to raise the current value's power. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputPower( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring POWER!!! because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = pow(m_OutValue.Get(), inputdata.value.Int()); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +// +// For some reason, I had trouble finding the original math functions at first. +// Then I just randomly stumbled upon them, bright as day. +// Oh well. These might be faster anyway. +// +FORCEINLINE int RoundToNumber(int input, int number) +{ + (input < 0 && number > 0) ? number *= -1 : 0; + int result = (input + (number / 2)); + result -= (result % number); + return result; +} + +// Warning: Negative numbers should be ceiled +FORCEINLINE int FloorToNumber(int input, int number) +{ + return (input - (input % number)); +} + +FORCEINLINE int CeilToNumber(int input, int number) +{ + (input < 0 && number > 0) ? number *= -1 : 0; + int result = (input - (input % number)); + return result != input ? result + number : result; +} + +FORCEINLINE int TruncToNumber(int input, int number) +{ + //(input < 0 && number > 0) ? number *= -1 : 0; + return (input - (input % number)); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for rounding an integer to the specified number. (e.g. 126 rounding to 10 = 130, 1523 rounding to 5 = 1525) +// Input : Integer value to round the current value to. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputRound( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring ROUND because it is disabled\n", GetDebugName() ); + return; + } + + int iMultiple = inputdata.value.Int(); + int iNewValue; + if (iMultiple != 0) + { + // Round to the nearest input number. + iNewValue = RoundToNumber(m_OutValue.Get(), iMultiple); + } + else + { + // 0 just rounds floats. + iNewValue = static_cast(m_OutValue.Get() + 0.5f); + } + + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for flooring an integer to the specified number. (e.g. 126 flooring to 10 = 120, 1528 flooring to 5 = 1525) +// Input : Integer value to floor the current value to. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputFloor( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring FLOOR because it is disabled\n", GetDebugName() ); + return; + } + + int iMultiple = inputdata.value.Int(); + int iNewValue; + if (iMultiple != 0) + { + iNewValue = m_OutValue.Get(); + if (iNewValue >= 0) + { + // Floor to the nearest input number. + iNewValue = FloorToNumber(m_OutValue.Get(), iMultiple); + } + else + { + // We have to do it differently for negatives. + iNewValue = CeilToNumber(m_OutValue.Get(), iMultiple); + } + } + else + { + // 0 just floors floats. + iNewValue = static_cast(m_OutValue.Get()); + } + + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for ceiling an integer to the specified number. (e.g. 126 ceiling to 10 = 130, 1523 ceiling to 50 = 1550) +// Input : Integer value to ceil the current value to. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputCeiling( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring CEIL because it is disabled\n", GetDebugName() ); + return; + } + + int iMultiple = inputdata.value.Int(); + int iNewValue; + if (iMultiple != 0) + { + // Ceil to the nearest input number. + iNewValue = CeilToNumber(m_OutValue.Get(), iMultiple); + } + else + { + // 0 just ceils floats. + iNewValue = static_cast(m_OutValue.Get()) + (m_OutValue.Get() != 0 ? 1 : 0); + } + + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for truncating an integer to the specified number. (e.g. 126 rounding to 10 = 120, -1523 rounding to 5 = 1520) +// Input : Integer value to truncate the current value to. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputTrunc( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring TRUNC because it is disabled\n", GetDebugName() ); + return; + } + + int iMultiple = inputdata.value.Int(); + int iNewValue; + if (iMultiple != 0) + { + // Floor always truncates negative numbers if we don't tell it not to + iNewValue = FloorToNumber(m_OutValue.Get(), iMultiple); + } + else + { + // 0 just ceils floats. + iNewValue = static_cast(m_OutValue.Get()); + if (iNewValue < 0) + iNewValue += 1; + } + + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for applying sine to the current value. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputSine( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SINE because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = sin(m_OutValue.Get()); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for applying cosine to the current value. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputCosine( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SINE because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = cos(m_OutValue.Get()); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for applying tangent to the current value. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputTangent( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring SINE because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = tan(m_OutValue.Get()); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for random int generation. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputRandomInt( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring RANDOMINT because it is disabled\n", GetDebugName() ); + return; + } + + int i1 = 0; + int i2 = 0; + + char szInput[128]; + Q_strncpy( szInput, inputdata.value.String(), sizeof(szInput) ); + char *sSpace = strchr( szInput, ' ' ); + if ( sSpace ) + { + i1 = atoi(szInput); + i2 = atoi(sSpace+1); + } + else + { + // No space, assume anything from 0 to X + i2 = atoi(szInput); + } + + float fNewValue = RandomInt(i1, i2); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for random float generation. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputRandomFloat( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring RANDOMFLOAT because it is disabled\n", GetDebugName() ); + return; + } + + float f1 = 0; + float f2 = 0; + + char szInput[128]; + Q_strncpy( szInput, inputdata.value.String(), sizeof(szInput) ); + char *sSpace = strchr( szInput, ' ' ); + if ( sSpace ) + { + f1 = atof(szInput); + f2 = atof(sSpace+1); + } + else + { + // No space, assume anything from 0 to X + f2 = atof(szInput); + } + + float fNewValue = RandomFloat(f1, f2); + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for random float generation. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::InputLerpTo( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Counter %s ignoring LERPTO because it is disabled\n", GetDebugName() ); + return; + } + + float fNewValue = m_OutValue.Get() + (inputdata.value.Float() - m_OutValue.Get()) * m_flLerpPercent; + UpdateOutValue( inputdata.pActivator, fNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the value to the new value, clamping and firing the output value. +// Input : fNewValue - Value to set. +//----------------------------------------------------------------------------- +void CMathCounterAdvanced::UpdateOutValue(CBaseEntity *pActivator, float fNewValue) +{ + if (m_bAlwaysOutputAsInt) + fNewValue = roundf(fNewValue); + + if (m_bPreserveValue) + { + //float fOriginal = m_OutValue.Get(); + //DevMsg("Preserve Before: %f\n", fOriginal); + //BaseClass::UpdateOutValue(pActivator, fNewValue); + //DevMsg("Preserve After: %f\n", fOriginal); + //m_OutValue.Init(fOriginal); + + variant_t var; + var.SetFloat(fNewValue); + m_OutValue.FireOutput( var, pActivator, this ); + } + else + { + BaseClass::UpdateOutValue(pActivator, fNewValue); + } +} +#endif + + + +//----------------------------------------------------------------------------- +// Purpose: Compares a single string input to up to 16 case values, firing an +// output corresponding to the case value that matched, or a default +// output if the input value didn't match any of the case values. +// +// This can also be used to fire a random output from a set of outputs. +//----------------------------------------------------------------------------- +#define MAX_LOGIC_CASES 16 + +class CLogicCase : public CLogicalEntity +{ + DECLARE_CLASS( CLogicCase, CLogicalEntity ); +private: + string_t m_nCase[MAX_LOGIC_CASES]; + +#ifdef MAPBASE + bool m_bMultipleCasesAllowed; +#endif + + int m_nShuffleCases; + int m_nLastShuffleCase; + unsigned char m_uchShuffleCaseMap[MAX_LOGIC_CASES]; + + void Spawn(void); + + int BuildCaseMap(unsigned char *puchMap); + + // Inputs + void InputValue( inputdata_t &inputdata ); + void InputPickRandom( inputdata_t &inputdata ); + void InputPickRandomShuffle( inputdata_t &inputdata ); + + // Outputs + COutputEvent m_OnCase[MAX_LOGIC_CASES]; // Fired when the input value matches one of the case values. + COutputVariant m_OnDefault; // Fired when no match was found. +#ifdef MAPBASE + COutputVariant m_OnUsed; // Fired when this entity receives any input at all. +#endif + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_case, CLogicCase); + + +BEGIN_DATADESC( CLogicCase ) + +// Silence, Classcheck! +// DEFINE_ARRAY( m_nCase, FIELD_STRING, MAX_LOGIC_CASES ), + + // Keys + DEFINE_KEYFIELD(m_nCase[0], FIELD_STRING, "Case01"), + DEFINE_KEYFIELD(m_nCase[1], FIELD_STRING, "Case02"), + DEFINE_KEYFIELD(m_nCase[2], FIELD_STRING, "Case03"), + DEFINE_KEYFIELD(m_nCase[3], FIELD_STRING, "Case04"), + DEFINE_KEYFIELD(m_nCase[4], FIELD_STRING, "Case05"), + DEFINE_KEYFIELD(m_nCase[5], FIELD_STRING, "Case06"), + DEFINE_KEYFIELD(m_nCase[6], FIELD_STRING, "Case07"), + DEFINE_KEYFIELD(m_nCase[7], FIELD_STRING, "Case08"), + DEFINE_KEYFIELD(m_nCase[8], FIELD_STRING, "Case09"), + DEFINE_KEYFIELD(m_nCase[9], FIELD_STRING, "Case10"), + DEFINE_KEYFIELD(m_nCase[10], FIELD_STRING, "Case11"), + DEFINE_KEYFIELD(m_nCase[11], FIELD_STRING, "Case12"), + DEFINE_KEYFIELD(m_nCase[12], FIELD_STRING, "Case13"), + DEFINE_KEYFIELD(m_nCase[13], FIELD_STRING, "Case14"), + DEFINE_KEYFIELD(m_nCase[14], FIELD_STRING, "Case15"), + DEFINE_KEYFIELD(m_nCase[15], FIELD_STRING, "Case16"), + +#ifdef MAPBASE + DEFINE_KEYFIELD(m_bMultipleCasesAllowed, FIELD_BOOLEAN, "MultipleCasesAllowed"), +#endif + + DEFINE_FIELD( m_nShuffleCases, FIELD_INTEGER ), + DEFINE_FIELD( m_nLastShuffleCase, FIELD_INTEGER ), + DEFINE_ARRAY( m_uchShuffleCaseMap, FIELD_CHARACTER, MAX_LOGIC_CASES ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_INPUT, "InValue", InputValue), + DEFINE_INPUTFUNC(FIELD_VOID, "PickRandom", InputPickRandom), + DEFINE_INPUTFUNC(FIELD_VOID, "PickRandomShuffle", InputPickRandomShuffle), + + // Outputs + DEFINE_OUTPUT(m_OnCase[0], "OnCase01"), + DEFINE_OUTPUT(m_OnCase[1], "OnCase02"), + DEFINE_OUTPUT(m_OnCase[2], "OnCase03"), + DEFINE_OUTPUT(m_OnCase[3], "OnCase04"), + DEFINE_OUTPUT(m_OnCase[4], "OnCase05"), + DEFINE_OUTPUT(m_OnCase[5], "OnCase06"), + DEFINE_OUTPUT(m_OnCase[6], "OnCase07"), + DEFINE_OUTPUT(m_OnCase[7], "OnCase08"), + DEFINE_OUTPUT(m_OnCase[8], "OnCase09"), + DEFINE_OUTPUT(m_OnCase[9], "OnCase10"), + DEFINE_OUTPUT(m_OnCase[10], "OnCase11"), + DEFINE_OUTPUT(m_OnCase[11], "OnCase12"), + DEFINE_OUTPUT(m_OnCase[12], "OnCase13"), + DEFINE_OUTPUT(m_OnCase[13], "OnCase14"), + DEFINE_OUTPUT(m_OnCase[14], "OnCase15"), + DEFINE_OUTPUT(m_OnCase[15], "OnCase16"), + + DEFINE_OUTPUT(m_OnDefault, "OnDefault"), +#ifdef MAPBASE + DEFINE_OUTPUT(m_OnUsed, "OnUsed"), +#endif + +END_DATADESC() + + + + +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after key values have been set. +//----------------------------------------------------------------------------- +void CLogicCase::Spawn( void ) +{ + m_nLastShuffleCase = -1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Evaluates the new input value, firing the appropriate OnCaseX output +// if the input value matches one of the "CaseX" keys. +// Input : Value - Variant value to compare against the values of the case fields. +// We use a variant so that we can convert any input type to a string. +//----------------------------------------------------------------------------- +void CLogicCase::InputValue( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + m_OnUsed.Set(inputdata.value, inputdata.pActivator, this); + bool bFoundCase = false; +#endif + const char *pszValue = inputdata.value.String(); + for (int i = 0; i < MAX_LOGIC_CASES; i++) + { +#ifdef MAPBASE + if ((m_nCase[i] != NULL_STRING) && Matcher_Match(STRING(m_nCase[i]), pszValue)) + { + m_OnCase[i].FireOutput( inputdata.pActivator, this ); + + if (!m_bMultipleCasesAllowed) + return; + else if (!bFoundCase) + bFoundCase = true; + } +#else + if ((m_nCase[i] != NULL_STRING) && !stricmp(STRING(m_nCase[i]), pszValue)) + { + m_OnCase[i].FireOutput( inputdata.pActivator, this ); + return; + } +#endif + } + +#ifdef MAPBASE + if (!bFoundCase) +#endif + m_OnDefault.Set( inputdata.value, inputdata.pActivator, this ); +} + + +//----------------------------------------------------------------------------- +// Count the number of valid cases, building a packed array +// that maps 0..NumCases to the actual CaseX values. +// +// This allows our zany mappers to set up cases sparsely if they desire. +// NOTE: assumes pnMap points to an array of MAX_LOGIC_CASES +//----------------------------------------------------------------------------- +int CLogicCase::BuildCaseMap(unsigned char *puchCaseMap) +{ + memset(puchCaseMap, 0, sizeof(unsigned char) * MAX_LOGIC_CASES); + + int nNumCases = 0; + for (int i = 0; i < MAX_LOGIC_CASES; i++) + { + if (m_OnCase[i].NumberOfElements() > 0) + { + puchCaseMap[nNumCases] = (unsigned char)i; + nNumCases++; + } + } + + return nNumCases; +} + + +//----------------------------------------------------------------------------- +// Purpose: Makes the case statement choose a case at random. +//----------------------------------------------------------------------------- +void CLogicCase::InputPickRandom( inputdata_t &inputdata ) +{ + unsigned char uchCaseMap[MAX_LOGIC_CASES]; + int nNumCases = BuildCaseMap( uchCaseMap ); + + // + // Choose a random case from the ones that were set up by the level designer. + // + if ( nNumCases > 0 ) + { + int nRandom = random->RandomInt(0, nNumCases - 1); + int nCase = (unsigned char)uchCaseMap[nRandom]; + + Assert(nCase < MAX_LOGIC_CASES); + + if (nCase < MAX_LOGIC_CASES) + { + m_OnCase[nCase].FireOutput( inputdata.pActivator, this ); + } + } + else + { + DevMsg( 1, "Firing PickRandom input on logic_case %s with no cases set up\n", GetDebugName() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Makes the case statement choose a case at random. +//----------------------------------------------------------------------------- +void CLogicCase::InputPickRandomShuffle( inputdata_t &inputdata ) +{ + int nAvoidCase = -1; + int nCaseCount = m_nShuffleCases; + + if ( nCaseCount == 0 ) + { + // Starting a new shuffle batch. + nCaseCount = m_nShuffleCases = BuildCaseMap( m_uchShuffleCaseMap ); + + if ( ( m_nShuffleCases > 1 ) && ( m_nLastShuffleCase != -1 ) ) + { + // Remove the previously picked case from the case map for this pick only. + // This avoids repeats across shuffle batch boundaries. + nAvoidCase = m_nLastShuffleCase; + + for (int i = 0; i < m_nShuffleCases; i++ ) + { + if ( m_uchShuffleCaseMap[i] == nAvoidCase ) + { + unsigned char uchSwap = m_uchShuffleCaseMap[i]; + m_uchShuffleCaseMap[i] = m_uchShuffleCaseMap[nCaseCount - 1]; + m_uchShuffleCaseMap[nCaseCount - 1] = uchSwap; + nCaseCount--; + break; + } + } + } + } + + // + // Choose a random case from the ones that were set up by the level designer. + // Never repeat a case within a shuffle batch, nor consecutively across batches. + // + if ( nCaseCount > 0 ) + { + int nRandom = random->RandomInt( 0, nCaseCount - 1 ); + + int nCase = m_uchShuffleCaseMap[nRandom]; + Assert(nCase < MAX_LOGIC_CASES); + + if (nCase < MAX_LOGIC_CASES) + { + m_OnCase[nCase].FireOutput( inputdata.pActivator, this ); + } + + m_uchShuffleCaseMap[nRandom] = m_uchShuffleCaseMap[m_nShuffleCases - 1]; + m_nShuffleCases--; + + m_nLastShuffleCase = nCase; + } + else + { + DevMsg( 1, "Firing PickRandom input on logic_case %s with no cases set up\n", GetDebugName() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Compares a floating point input to a predefined value, firing an +// output to indicate the result of the comparison. +//----------------------------------------------------------------------------- +class CLogicCompare : public CLogicalEntity +{ + DECLARE_CLASS( CLogicCompare, CLogicalEntity ); + +public: + int DrawDebugTextOverlays(void); + +#ifdef MAPBASE + void Spawn(); +#endif + +private: + // Inputs + void InputSetValue( inputdata_t &inputdata ); + void InputSetValueCompare( inputdata_t &inputdata ); + void InputSetCompareValue( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetCompareValueCompare( inputdata_t &inputdata ); +#endif + void InputCompare( inputdata_t &inputdata ); + +#ifdef MAPBASE + void DoCompare(CBaseEntity *pActivator, variant_t value); +#else + void DoCompare(CBaseEntity *pActivator, float flInValue); +#endif + +#ifdef MAPBASE + bool m_bStrLenAllowed = true; + bool m_bGreaterThanOrEqual; + variant_t m_InValue; // Place to hold the last input value for a recomparison. + variant_t m_CompareValue; // The value to compare the input value against. +#else + float m_flInValue; // Place to hold the last input value for a recomparison. + float m_flCompareValue; // The value to compare the input value against. +#endif + + // Outputs +#ifdef MAPBASE + COutputVariant m_OnLessThan; // Fired when the input value is less than the compare value. + COutputVariant m_OnEqualTo; // Fired when the input value is equal to the compare value. + COutputVariant m_OnNotEqualTo; // Fired when the input value is not equal to the compare value. + COutputVariant m_OnGreaterThan; // Fired when the input value is greater than the compare value. + COutputVariant m_OnLessThanOrEqualTo; // Fired when the input value is less than or equal to the compare value. + COutputVariant m_OnGreaterThanOrEqualTo; // Fired when the input value is greater than or equal to the compare value. +#else + COutputFloat m_OnLessThan; // Fired when the input value is less than the compare value. + COutputFloat m_OnEqualTo; // Fired when the input value is equal to the compare value. + COutputFloat m_OnNotEqualTo; // Fired when the input value is not equal to the compare value. + COutputFloat m_OnGreaterThan; // Fired when the input value is greater than the compare value. +#endif + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_compare, CLogicCompare); + + +BEGIN_DATADESC( CLogicCompare ) + + // Keys +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bStrLenAllowed, FIELD_BOOLEAN, "StrLenAllowed" ), + DEFINE_KEYVARIANT(m_CompareValue, "CompareValue"), + DEFINE_KEYVARIANT(m_InValue, "InitialValue"), +#else + DEFINE_KEYFIELD(m_flCompareValue, FIELD_FLOAT, "CompareValue"), + DEFINE_KEYFIELD(m_flInValue, FIELD_FLOAT, "InitialValue"), +#endif + + // Inputs +#ifdef MAPBASE + DEFINE_INPUTFUNC(FIELD_INPUT, "SetValue", InputSetValue), + DEFINE_INPUTFUNC(FIELD_INPUT, "SetValueCompare", InputSetValueCompare), + DEFINE_INPUTFUNC(FIELD_INPUT, "SetCompareValue", InputSetCompareValue), + DEFINE_INPUTFUNC(FIELD_INPUT, "SetCompareValueCompare", InputSetCompareValueCompare), +#else + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetValue", InputSetValue), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetValueCompare", InputSetValueCompare), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetCompareValue", InputSetCompareValue), +#endif + DEFINE_INPUTFUNC(FIELD_VOID, "Compare", InputCompare), + + // Outputs + DEFINE_OUTPUT(m_OnEqualTo, "OnEqualTo"), + DEFINE_OUTPUT(m_OnNotEqualTo, "OnNotEqualTo"), + DEFINE_OUTPUT(m_OnGreaterThan, "OnGreaterThan"), + DEFINE_OUTPUT(m_OnLessThan, "OnLessThan"), +#ifdef MAPBASE + DEFINE_OUTPUT(m_OnGreaterThanOrEqualTo, "OnGreaterThanOrEqualTo"), + DEFINE_OUTPUT(m_OnLessThanOrEqualTo, "OnLessThanOrEqualTo"), +#endif + +END_DATADESC() + + + + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler for a new input value without performing a comparison. +//----------------------------------------------------------------------------- +void CLogicCompare::Spawn() +{ + // Empty initial values are equivalent to 0 + if (m_InValue.FieldType() == FIELD_STRING && m_InValue.String()[0] == '\0') + m_InValue.SetInt( 0 ); + + BaseClass::Spawn(); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Input handler for a new input value without performing a comparison. +//----------------------------------------------------------------------------- +void CLogicCompare::InputSetValue( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + m_InValue = Variant_ParseInput(inputdata); +#else + m_flInValue = inputdata.value.Float(); +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for a setting a new value and doing the comparison. +//----------------------------------------------------------------------------- +void CLogicCompare::InputSetValueCompare( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + m_InValue = Variant_ParseInput(inputdata); + DoCompare( inputdata.pActivator, m_InValue ); +#else + m_flInValue = inputdata.value.Float(); + DoCompare( inputdata.pActivator, m_flInValue ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for a new input value without performing a comparison. +//----------------------------------------------------------------------------- +void CLogicCompare::InputSetCompareValue( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + m_CompareValue = Variant_ParseInput(inputdata); +#else + m_flCompareValue = inputdata.value.Float(); +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler for a new input value and doing the comparison. +//----------------------------------------------------------------------------- +void CLogicCompare::InputSetCompareValueCompare( inputdata_t &inputdata ) +{ + m_CompareValue = Variant_ParseInput(inputdata); + DoCompare( inputdata.pActivator, m_InValue ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Input handler for forcing a recompare of the last input value. +//----------------------------------------------------------------------------- +void CLogicCompare::InputCompare( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + DoCompare( inputdata.pActivator, m_InValue ); +#else + DoCompare( inputdata.pActivator, m_flInValue ); +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Compares the input value to the compare value, firing the appropriate +// output(s) based on the comparison result. +// Input : flInValue - Value to compare against the comparison value. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +void CLogicCompare::DoCompare(CBaseEntity *pActivator, variant_t value) +{ + if (Variant_Equal(value, m_CompareValue, m_bStrLenAllowed)) + { + m_OnEqualTo.Set(value, pActivator, this); + + m_OnLessThanOrEqualTo.Set(value, pActivator, this); + m_OnGreaterThanOrEqualTo.Set(value, pActivator, this); + } + else + { + m_OnNotEqualTo.Set(value, pActivator, this); + + if (Variant_Greater(m_InValue, m_CompareValue, m_bStrLenAllowed)) + { + m_OnGreaterThan.Set(value, pActivator, this); + m_OnGreaterThanOrEqualTo.Set(value, pActivator, this); + } + else + { + m_OnLessThan.Set(value, pActivator, this); + m_OnLessThanOrEqualTo.Set(value, pActivator, this); + } + } +} +#else +void CLogicCompare::DoCompare(CBaseEntity *pActivator, float flInValue) +{ + if (flInValue == m_flCompareValue) + { + m_OnEqualTo.Set(flInValue, pActivator, this); + } + else + { + m_OnNotEqualTo.Set(flInValue, pActivator, this); + + if (flInValue > m_flCompareValue) + { + m_OnGreaterThan.Set(flInValue, pActivator, this); + } + else + { + m_OnLessThan.Set(flInValue, pActivator, this); + } + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CLogicCompare::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print duration +#ifdef MAPBASE + Q_snprintf(tempstr,sizeof(tempstr)," Initial Value: %s", m_InValue.GetDebug()); +#else + Q_snprintf(tempstr,sizeof(tempstr)," Initial Value: %f", m_flInValue); +#endif + EntityText(text_offset,tempstr,0); + text_offset++; + + // print hold time +#ifdef MAPBASE + Q_snprintf(tempstr,sizeof(tempstr)," Compare Value: %s", m_CompareValue.GetDebug()); +#else + Q_snprintf(tempstr,sizeof(tempstr)," Compare Value: %f", m_flCompareValue); +#endif + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: Tests a boolean value, firing an output to indicate whether the +// value was true or false. +//----------------------------------------------------------------------------- +class CLogicBranch : public CLogicalEntity +{ + DECLARE_CLASS( CLogicBranch, CLogicalEntity ); + +public: + + void UpdateOnRemove(); + + void AddLogicBranchListener( CBaseEntity *pEntity ); + inline bool GetLogicBranchState(); + virtual int DrawDebugTextOverlays( void ); + +private: + + enum LogicBranchFire_t + { + LOGIC_BRANCH_FIRE, + LOGIC_BRANCH_NO_FIRE, + }; + + // Inputs + void InputSetValue( inputdata_t &inputdata ); + void InputSetValueTest( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputToggleTest( inputdata_t &inputdata ); + void InputTest( inputdata_t &inputdata ); + + void UpdateValue(bool bNewValue, CBaseEntity *pActivator, LogicBranchFire_t eFire); + + bool m_bInValue; // Place to hold the last input value for a future test. + + CUtlVector m_Listeners; // A list of logic_branch_listeners that are monitoring us. + + // Outputs + COutputEvent m_OnTrue; // Fired when the value is true. + COutputEvent m_OnFalse; // Fired when the value is false. + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_branch, CLogicBranch); + + +BEGIN_DATADESC( CLogicBranch ) + + // Keys + DEFINE_KEYFIELD(m_bInValue, FIELD_BOOLEAN, "InitialValue"), + + DEFINE_UTLVECTOR( m_Listeners, FIELD_EHANDLE ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_BOOLEAN, "SetValue", InputSetValue), + DEFINE_INPUTFUNC(FIELD_BOOLEAN, "SetValueTest", InputSetValueTest), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + DEFINE_INPUTFUNC(FIELD_VOID, "ToggleTest", InputToggleTest), + DEFINE_INPUTFUNC(FIELD_VOID, "Test", InputTest), + + // Outputs + DEFINE_OUTPUT(m_OnTrue, "OnTrue"), + DEFINE_OUTPUT(m_OnFalse, "OnFalse"), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicBranch::UpdateOnRemove() +{ + for ( int i = 0; i < m_Listeners.Count(); i++ ) + { + CBaseEntity *pEntity = m_Listeners.Element( i ).Get(); + if ( pEntity ) + { + g_EventQueue.AddEvent( this, "_OnLogicBranchRemoved", 0, this, this ); + } + } + + BaseClass::UpdateOnRemove(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to set a new input value without firing outputs. +// Input : Boolean value to set. +//----------------------------------------------------------------------------- +void CLogicBranch::InputSetValue( inputdata_t &inputdata ) +{ + UpdateValue( inputdata.value.Bool(), inputdata.pActivator, LOGIC_BRANCH_NO_FIRE ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to set a new input value and fire appropriate outputs. +// Input : Boolean value to set. +//----------------------------------------------------------------------------- +void CLogicBranch::InputSetValueTest( inputdata_t &inputdata ) +{ + UpdateValue( inputdata.value.Bool(), inputdata.pActivator, LOGIC_BRANCH_FIRE ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for toggling the boolean value without firing outputs. +//----------------------------------------------------------------------------- +void CLogicBranch::InputToggle( inputdata_t &inputdata ) +{ + UpdateValue( !m_bInValue, inputdata.pActivator, LOGIC_BRANCH_NO_FIRE ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for toggling the boolean value and then firing the +// appropriate output based on the new value. +//----------------------------------------------------------------------------- +void CLogicBranch::InputToggleTest( inputdata_t &inputdata ) +{ + UpdateValue( !m_bInValue, inputdata.pActivator, LOGIC_BRANCH_FIRE ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for forcing a test of the last input value. +//----------------------------------------------------------------------------- +void CLogicBranch::InputTest( inputdata_t &inputdata ) +{ + UpdateValue( m_bInValue, inputdata.pActivator, LOGIC_BRANCH_FIRE ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Tests the last input value, firing the appropriate output based on +// the test result. +// Input : bInValue - +//----------------------------------------------------------------------------- +void CLogicBranch::UpdateValue( bool bNewValue, CBaseEntity *pActivator, LogicBranchFire_t eFire ) +{ + if ( m_bInValue != bNewValue ) + { + m_bInValue = bNewValue; + + for ( int i = 0; i < m_Listeners.Count(); i++ ) + { + CBaseEntity *pEntity = m_Listeners.Element( i ).Get(); + if ( pEntity ) + { + g_EventQueue.AddEvent( pEntity, "_OnLogicBranchChanged", 0, this, this ); + } + } + } + + if ( eFire == LOGIC_BRANCH_FIRE ) + { + if ( m_bInValue ) + { + m_OnTrue.FireOutput( pActivator, this ); + } + else + { + m_OnFalse.FireOutput( pActivator, this ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Accessor for logic_branchlist to test the value of the branch on demand. +//----------------------------------------------------------------------------- +bool CLogicBranch::GetLogicBranchState() +{ + return m_bInValue; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicBranch::AddLogicBranchListener( CBaseEntity *pEntity ) +{ + if ( m_Listeners.Find( pEntity ) == -1 ) + { + m_Listeners.AddToTail( pEntity ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CLogicBranch::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print refire time + Q_snprintf( tempstr, sizeof(tempstr), "Branch value: %s", (m_bInValue) ? "TRUE" : "FALSE" ); + EntityText( text_offset, tempstr, 0 ); + text_offset++; + } + + return text_offset; +} + +#ifdef MAPBASE +extern void MapbaseGameLog_Record( const char *szContext ); +extern ConVar mapbase_game_log_on_autosave; +#endif + +//----------------------------------------------------------------------------- +// Purpose: Autosaves when triggered +//----------------------------------------------------------------------------- +class CLogicAutosave : public CLogicalEntity +{ + DECLARE_CLASS( CLogicAutosave, CLogicalEntity ); + +protected: + // Inputs + void InputSave( inputdata_t &inputdata ); + void InputSaveDangerous( inputdata_t &inputdata ); + void InputSetMinHitpointsThreshold( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + bool m_bForceNewLevelUnit; + int m_minHitPoints; + int m_minHitPointsToCommit; +}; + +LINK_ENTITY_TO_CLASS(logic_autosave, CLogicAutosave); + +BEGIN_DATADESC( CLogicAutosave ) + DEFINE_KEYFIELD( m_bForceNewLevelUnit, FIELD_BOOLEAN, "NewLevelUnit" ), + DEFINE_KEYFIELD( m_minHitPoints, FIELD_INTEGER, "MinimumHitPoints" ), + DEFINE_KEYFIELD( m_minHitPointsToCommit, FIELD_INTEGER, "MinHitPointsToCommit" ), + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Save", InputSave ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SaveDangerous", InputSaveDangerous ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMinHitpointsThreshold", InputSetMinHitpointsThreshold ), +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Save! +//----------------------------------------------------------------------------- +void CLogicAutosave::InputSave( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + if (mapbase_game_log_on_autosave.GetBool()) + { + MapbaseGameLog_Record( "autosave" ); + } +#endif + + if ( m_bForceNewLevelUnit ) + { + engine->ClearSaveDir(); + } + + engine->ServerCommand( "autosave\n" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Save safely! +//----------------------------------------------------------------------------- +void CLogicAutosave::InputSaveDangerous( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + if (mapbase_game_log_on_autosave.GetBool()) + { + MapbaseGameLog_Record( "autosave_dangerous" ); + } +#endif + + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + + if ( g_ServerGameDLL.m_fAutoSaveDangerousTime != 0.0f && g_ServerGameDLL.m_fAutoSaveDangerousTime >= gpGlobals->curtime ) + { + // A previous dangerous auto save was waiting to become safe + + if ( pPlayer->GetDeathTime() == 0.0f || pPlayer->GetDeathTime() > gpGlobals->curtime ) + { + // The player isn't dead, so make the dangerous auto save safe + engine->ServerCommand( "autosavedangerousissafe\n" ); + } + } + + if ( m_bForceNewLevelUnit ) + { + engine->ClearSaveDir(); + } + + if ( pPlayer->GetHealth() >= m_minHitPoints ) + { + engine->ServerCommand( "autosavedangerous\n" ); + g_ServerGameDLL.m_fAutoSaveDangerousTime = gpGlobals->curtime + inputdata.value.Float(); + + // Player must have this much health when we go to commit, or we don't commit. + g_ServerGameDLL.m_fAutoSaveDangerousMinHealthToCommit = m_minHitPointsToCommit; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Autosaves when triggered +//----------------------------------------------------------------------------- +class CLogicActiveAutosave : public CLogicAutosave +{ + DECLARE_CLASS( CLogicActiveAutosave, CLogicAutosave ); + + void InputEnable( inputdata_t &inputdata ) + { + m_flStartTime = -1; + SetThink( &CLogicActiveAutosave::SaveThink ); + SetNextThink( gpGlobals->curtime ); + } + + void InputDisable( inputdata_t &inputdata ) + { + SetThink( NULL ); + } + + void SaveThink() + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer ) + { + if ( m_flStartTime < 0 ) + { + if ( pPlayer->GetHealth() <= m_minHitPoints ) + { + m_flStartTime = gpGlobals->curtime; + } + } + else + { + if ( pPlayer->GetHealth() >= m_TriggerHitPoints ) + { + inputdata_t inputdata; + DevMsg( 2, "logic_active_autosave (%s, %d) triggered\n", STRING( GetEntityName() ), entindex() ); + if ( !m_flDangerousTime ) + { + InputSave( inputdata ); + } + else + { + inputdata.value.SetFloat( m_flDangerousTime ); + InputSaveDangerous( inputdata ); + } + m_flStartTime = -1; + } + else if ( m_flTimeToTrigger > 0 && gpGlobals->curtime - m_flStartTime > m_flTimeToTrigger ) + { + m_flStartTime = -1; + } + } + } + + float thinkInterval = ( m_flStartTime < 0 ) ? 1.0 : 0.5; + SetNextThink( gpGlobals->curtime + thinkInterval ); + } + + DECLARE_DATADESC(); + + int m_TriggerHitPoints; + float m_flTimeToTrigger; + float m_flStartTime; + float m_flDangerousTime; +}; + +LINK_ENTITY_TO_CLASS(logic_active_autosave, CLogicActiveAutosave); + +BEGIN_DATADESC( CLogicActiveAutosave ) + DEFINE_KEYFIELD( m_TriggerHitPoints, FIELD_INTEGER, "TriggerHitPoints" ), + DEFINE_KEYFIELD( m_flTimeToTrigger, FIELD_FLOAT, "TimeToTrigger" ), + DEFINE_KEYFIELD( m_flDangerousTime, FIELD_FLOAT, "DangerousTime" ), + DEFINE_FIELD( m_flStartTime, FIELD_TIME ), + DEFINE_THINKFUNC( SaveThink ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Keyfield set func +//----------------------------------------------------------------------------- +void CLogicAutosave::InputSetMinHitpointsThreshold( inputdata_t &inputdata ) +{ + int setTo = inputdata.value.Int(); + AssertMsg1(setTo >= 0 && setTo <= 100, "Tried to set autosave MinHitpointsThreshold to %d!\n", setTo); + m_minHitPoints = setTo; +} + +// Finds the named physics object. If no name, returns the world +// If a name is specified and an object not found - errors are reported +IPhysicsObject *FindPhysicsObjectByNameOrWorld( string_t name, CBaseEntity *pErrorEntity ) +{ + if ( !name ) + return g_PhysWorldObject; + + IPhysicsObject *pPhysics = FindPhysicsObjectByName( name.ToCStr(), pErrorEntity ); + if ( !pPhysics ) + { + DevWarning("%s: can't find %s\n", pErrorEntity->GetClassname(), name.ToCStr()); + } + return pPhysics; +} + +class CLogicCollisionPair : public CLogicalEntity +{ + DECLARE_CLASS( CLogicCollisionPair, CLogicalEntity ); +public: + +#ifdef MAPBASE + // !activator, !caller, etc. support + void EnableCollisions( bool bEnable, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ) + { + IPhysicsObject *pPhysics0 = NULL; + IPhysicsObject *pPhysics1 = NULL; + + CBaseEntity *pEntity0 = gEntList.FindEntityByName( NULL, m_nameAttach1, this, pActivator, pCaller ); + if (pEntity0) + pPhysics0 = pEntity0->VPhysicsGetObject(); + + CBaseEntity *pEntity1 = gEntList.FindEntityByName( NULL, m_nameAttach2, this, pActivator, pCaller ); + if (pEntity1) + pPhysics1 = pEntity1->VPhysicsGetObject(); +#else + void EnableCollisions( bool bEnable ) + { + IPhysicsObject *pPhysics0 = FindPhysicsObjectByNameOrWorld( m_nameAttach1, this ); + IPhysicsObject *pPhysics1 = FindPhysicsObjectByNameOrWorld( m_nameAttach2, this ); +#endif + + // need two different objects to do anything + if ( pPhysics0 && pPhysics1 && pPhysics0 != pPhysics1 ) + { + m_disabled = !bEnable; + m_succeeded = true; + if ( bEnable ) + { + PhysEnableEntityCollisions( pPhysics0, pPhysics1 ); + } + else + { + PhysDisableEntityCollisions( pPhysics0, pPhysics1 ); + } + } + else + { + m_succeeded = false; + } + } + + void Activate( void ) + { + if ( m_disabled ) + { + EnableCollisions( false ); + } + BaseClass::Activate(); + } + + void InputDisableCollisions( inputdata_t &inputdata ) + { + if ( m_succeeded && m_disabled ) + return; +#ifdef MAPBASE + EnableCollisions( false, inputdata.pActivator, inputdata.pCaller ); +#else + EnableCollisions( false ); +#endif + } + + void InputEnableCollisions( inputdata_t &inputdata ) + { + if ( m_succeeded && !m_disabled ) + return; +#ifdef MAPBASE + EnableCollisions( true, inputdata.pActivator, inputdata.pCaller ); +#else + EnableCollisions( true ); +#endif + } + // If Activate() becomes PostSpawn() + //void OnRestore() { Activate(); } + + DECLARE_DATADESC(); + +private: + string_t m_nameAttach1; + string_t m_nameAttach2; + bool m_disabled; + bool m_succeeded; +}; + +BEGIN_DATADESC( CLogicCollisionPair ) + DEFINE_KEYFIELD( m_nameAttach1, FIELD_STRING, "attach1" ), + DEFINE_KEYFIELD( m_nameAttach2, FIELD_STRING, "attach2" ), + DEFINE_KEYFIELD( m_disabled, FIELD_BOOLEAN, "startdisabled" ), + DEFINE_FIELD( m_succeeded, FIELD_BOOLEAN ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "DisableCollisions", InputDisableCollisions ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableCollisions", InputEnableCollisions ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( logic_collision_pair, CLogicCollisionPair ); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +#define MAX_LOGIC_BRANCH_NAMES 16 + +class CLogicBranchList : public CLogicalEntity +{ + DECLARE_CLASS( CLogicBranchList, CLogicalEntity ); + + virtual void Spawn(); + virtual void Activate(); + virtual int DrawDebugTextOverlays( void ); + +private: + + enum LogicBranchListenerLastState_t + { + LOGIC_BRANCH_LISTENER_NOT_INIT = 0, + LOGIC_BRANCH_LISTENER_ALL_TRUE, + LOGIC_BRANCH_LISTENER_ALL_FALSE, + LOGIC_BRANCH_LISTENER_MIXED, + }; + + void DoTest( CBaseEntity *pActivator ); + + string_t m_nLogicBranchNames[MAX_LOGIC_BRANCH_NAMES]; + CUtlVector m_LogicBranchList; + LogicBranchListenerLastState_t m_eLastState; + + // Inputs + void Input_OnLogicBranchRemoved( inputdata_t &inputdata ); + void Input_OnLogicBranchChanged( inputdata_t &inputdata ); + void InputTest( inputdata_t &inputdata ); + + // Outputs + COutputEvent m_OnAllTrue; // Fired when all the registered logic_branches become true. + COutputEvent m_OnAllFalse; // Fired when all the registered logic_branches become false. + COutputEvent m_OnMixed; // Fired when one of the registered logic branches changes, but not all are true or false. + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_branch_listener, CLogicBranchList); + + +BEGIN_DATADESC( CLogicBranchList ) + + // Silence, classcheck! + //DEFINE_ARRAY( m_nLogicBranchNames, FIELD_STRING, MAX_LOGIC_BRANCH_NAMES ), + + // Keys + DEFINE_KEYFIELD( m_nLogicBranchNames[0], FIELD_STRING, "Branch01" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[1], FIELD_STRING, "Branch02" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[2], FIELD_STRING, "Branch03" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[3], FIELD_STRING, "Branch04" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[4], FIELD_STRING, "Branch05" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[5], FIELD_STRING, "Branch06" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[6], FIELD_STRING, "Branch07" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[7], FIELD_STRING, "Branch08" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[8], FIELD_STRING, "Branch09" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[9], FIELD_STRING, "Branch10" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[10], FIELD_STRING, "Branch11" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[11], FIELD_STRING, "Branch12" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[12], FIELD_STRING, "Branch13" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[13], FIELD_STRING, "Branch14" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[14], FIELD_STRING, "Branch15" ), + DEFINE_KEYFIELD( m_nLogicBranchNames[15], FIELD_STRING, "Branch16" ), + + DEFINE_UTLVECTOR( m_LogicBranchList, FIELD_EHANDLE ), + + DEFINE_FIELD( m_eLastState, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_INPUT, "Test", InputTest ), + DEFINE_INPUTFUNC( FIELD_INPUT, "_OnLogicBranchChanged", Input_OnLogicBranchChanged ), + DEFINE_INPUTFUNC( FIELD_INPUT, "_OnLogicBranchRemoved", Input_OnLogicBranchRemoved ), + + // Outputs + DEFINE_OUTPUT( m_OnAllTrue, "OnAllTrue" ), + DEFINE_OUTPUT( m_OnAllFalse, "OnAllFalse" ), + DEFINE_OUTPUT( m_OnMixed, "OnMixed" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after key values have been set. +//----------------------------------------------------------------------------- +void CLogicBranchList::Spawn( void ) +{ +} + + +//----------------------------------------------------------------------------- +// Finds all the logic_branches that we are monitoring and register ourselves with them. +//----------------------------------------------------------------------------- +void CLogicBranchList::Activate( void ) +{ + for ( int i = 0; i < MAX_LOGIC_BRANCH_NAMES; i++ ) + { + CBaseEntity *pEntity = NULL; + while ( ( pEntity = gEntList.FindEntityGeneric( pEntity, STRING( m_nLogicBranchNames[i] ), this ) ) != NULL ) + { + if ( FClassnameIs( pEntity, "logic_branch" ) ) + { + CLogicBranch *pBranch = (CLogicBranch *)pEntity; + pBranch->AddLogicBranchListener( this ); + m_LogicBranchList.AddToTail( pBranch ); + } + else + { + DevWarning( "logic_branchlist %s refers to entity %s, which is not a logic_branch\n", GetDebugName(), pEntity->GetDebugName() ); + } + } + } + + BaseClass::Activate(); +} + + +//----------------------------------------------------------------------------- +// Called when a monitored logic branch is deleted from the world, since that +// might affect our final result. +//----------------------------------------------------------------------------- +void CLogicBranchList::Input_OnLogicBranchRemoved( inputdata_t &inputdata ) +{ + int nIndex = m_LogicBranchList.Find( inputdata.pActivator ); + if ( nIndex != -1 ) + { + m_LogicBranchList.FastRemove( nIndex ); + } + + // See if this logic_branch's deletion affects the final result. + DoTest( inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Called when the value of a monitored logic branch changes. +//----------------------------------------------------------------------------- +void CLogicBranchList::Input_OnLogicBranchChanged( inputdata_t &inputdata ) +{ + DoTest( inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Input handler to manually test the monitored logic branches and fire the +// appropriate output. +//----------------------------------------------------------------------------- +void CLogicBranchList::InputTest( inputdata_t &inputdata ) +{ + // Force an output. + m_eLastState = LOGIC_BRANCH_LISTENER_NOT_INIT; + + DoTest( inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicBranchList::DoTest( CBaseEntity *pActivator ) +{ + bool bOneTrue = false; + bool bOneFalse = false; + + for ( int i = 0; i < m_LogicBranchList.Count(); i++ ) + { + CLogicBranch *pBranch = (CLogicBranch *)m_LogicBranchList.Element( i ).Get(); + if ( pBranch && pBranch->GetLogicBranchState() ) + { + bOneTrue = true; + } + else + { + bOneFalse = true; + } + } + + // Only fire the output if the new result differs from the last result. + if ( bOneTrue && !bOneFalse ) + { + if ( m_eLastState != LOGIC_BRANCH_LISTENER_ALL_TRUE ) + { + m_OnAllTrue.FireOutput( pActivator, this ); + m_eLastState = LOGIC_BRANCH_LISTENER_ALL_TRUE; + } + } + else if ( bOneFalse && !bOneTrue ) + { + if ( m_eLastState != LOGIC_BRANCH_LISTENER_ALL_FALSE ) + { + m_OnAllFalse.FireOutput( pActivator, this ); + m_eLastState = LOGIC_BRANCH_LISTENER_ALL_FALSE; + } + } + else + { + if ( m_eLastState != LOGIC_BRANCH_LISTENER_MIXED ) + { + m_OnMixed.FireOutput( pActivator, this ); + m_eLastState = LOGIC_BRANCH_LISTENER_MIXED; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CLogicBranchList::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + for ( int i = 0; i < m_LogicBranchList.Count(); i++ ) + { + CLogicBranch *pBranch = (CLogicBranch *)m_LogicBranchList.Element( i ).Get(); + if ( pBranch ) + { + Q_snprintf( tempstr, sizeof(tempstr), "Branch (%s): %s", STRING(pBranch->GetEntityName()), (pBranch->GetLogicBranchState()) ? "TRUE" : "FALSE" ); + EntityText( text_offset, tempstr, 0 ); + text_offset++; + } + } + } + + return text_offset; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Prints messages to the console. +//----------------------------------------------------------------------------- +class CLogicConsole : public CLogicalEntity +{ +public: + + DECLARE_CLASS( CLogicConsole, CLogicalEntity ); + + // Keys + int m_iDevLevel = 1; + Color m_MsgColor = Color(210, 250, 255, 255); + Color m_WarningColor = Color(255, 210, 210, 255); + bool m_bNewLineNotAuto = false; + + // TODO: Replace "append" with variable arguments? + inline void LCMsg(const char *msg, const char *append = NULL) { ConColorMsg(m_MsgColor, msg, append); } + inline void LCDevMsg(int lvl, const char *msg, const char *append = NULL) { developer.GetInt() >= lvl ? ConColorMsg(m_MsgColor, msg, append) : (void)0; } + inline void LCWarning(const char *msg, const char *append = NULL) { ConColorMsg(m_WarningColor, msg, append); } + inline void LCDevWarning(int lvl, const char *msg, const char *append = NULL) { developer.GetInt() >= lvl ? ConColorMsg(m_WarningColor, msg, append) : (void)0; } + + //inline void LCMsg(const char *msg, const char *append = NULL) { ColorSpewMessage(SPEW_MESSAGE, &m_MsgColor, msg, append); } + //inline void LCDevMsg(int lvl, const char *msg, const char *append = NULL) { developer.GetInt() >= lvl ? ColorSpewMessage(SPEW_MESSAGE, &m_MsgColor, msg, append) : (void)0; } + //inline void LCWarning(const char *msg, const char *append = NULL) { ColorSpewMessage(SPEW_MESSAGE, &m_WarningColor, msg, append); } + //inline void LCDevWarning(int lvl, const char *msg, const char *append = NULL) { developer.GetInt() >= lvl ? ColorSpewMessage(SPEW_MESSAGE, &m_WarningColor, msg, append) : (void)0; } + + // Inputs + void InputSendMsg( inputdata_t &inputdata ) { !m_bNewLineNotAuto ? LCMsg("%s\n", inputdata.value.String()) : LCMsg("%s", inputdata.value.String()); } + void InputSendWarning( inputdata_t &inputdata ) { !m_bNewLineNotAuto ? LCWarning("%s\n", inputdata.value.String()) : LCWarning("%s", inputdata.value.String()); } + void InputSendDevMsg( inputdata_t &inputdata ) { !m_bNewLineNotAuto ? LCDevMsg(m_iDevLevel, "%s\n", inputdata.value.String()) : LCDevMsg(m_iDevLevel, "%s", inputdata.value.String()); } + void InputSendDevWarning( inputdata_t &inputdata ) { !m_bNewLineNotAuto ? LCDevWarning(m_iDevLevel, "%s\n", inputdata.value.String()) : LCDevWarning(m_iDevLevel, "%s", inputdata.value.String()); } + + void InputNewLine( inputdata_t &inputdata ) { LCMsg("\n"); } + void InputDevNewLine( inputdata_t &inputdata ) { LCDevMsg(m_iDevLevel, "\n"); } + + // MAPBASE MP TODO: "ClearConsoleOnTarget" + // (and make this input broadcast to all players) + void InputClearConsole( inputdata_t &inputdata ) { UTIL_GetLocalPlayer() ? engine->ClientCommand(UTIL_GetLocalPlayer()->edict(), "clear") : (void)0; } + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_console, CLogicConsole); + + +BEGIN_DATADESC( CLogicConsole ) + + DEFINE_INPUT( m_iDevLevel, FIELD_INTEGER, "SetDevLvl" ), + DEFINE_INPUT( m_MsgColor, FIELD_COLOR32, "SetMsgColor" ), + DEFINE_INPUT( m_WarningColor, FIELD_COLOR32, "SetWarningColor" ), + DEFINE_INPUT( m_bNewLineNotAuto, FIELD_BOOLEAN, "SetNewLineNotAuto" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SendMsg", InputSendMsg ), + DEFINE_INPUTFUNC( FIELD_STRING, "SendWarning", InputSendWarning ), + DEFINE_INPUTFUNC( FIELD_STRING, "SendDevMsg", InputSendDevMsg ), + DEFINE_INPUTFUNC( FIELD_STRING, "SendDevWarning", InputSendDevWarning ), + + DEFINE_INPUTFUNC( FIELD_VOID, "NewLine", InputNewLine ), + DEFINE_INPUTFUNC( FIELD_VOID, "DevNewLine", InputDevNewLine ), + + DEFINE_INPUTFUNC( FIELD_VOID, "ClearConsole", InputClearConsole ), + +END_DATADESC() + +ConVar sv_allow_logic_convar( "sv_allow_logic_convar", "1", FCVAR_NOT_CONNECTED ); + +//----------------------------------------------------------------------------- +// Purpose: Gets console variables for the evil mapper. +//----------------------------------------------------------------------------- +class CLogicConvar : public CLogicalEntity +{ +public: + + DECLARE_CLASS( CLogicConvar, CLogicalEntity ); + + // Keys + string_t m_iszConVar; + string_t m_iszCompareValue; + + const char *GetConVarString( inputdata_t &inputdata ); + + // Inputs + void InputGetValue( inputdata_t &inputdata ); + void InputTest( inputdata_t &inputdata ); + + // Outputs + COutputEvent m_OnTrue; + COutputEvent m_OnFalse; + COutputString m_OutValue; + COutputEvent m_OnDenied; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_convar, CLogicConvar); + + +BEGIN_DATADESC( CLogicConvar ) + + DEFINE_INPUT( m_iszConVar, FIELD_STRING, "SetConVar" ), + DEFINE_INPUT( m_iszCompareValue, FIELD_STRING, "SetTestValue" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "GetValue", InputGetValue ), + DEFINE_INPUTFUNC( FIELD_VOID, "Test", InputTest ), + + DEFINE_OUTPUT(m_OnTrue, "OnTrue"), + DEFINE_OUTPUT(m_OnFalse, "OnFalse"), + DEFINE_OUTPUT(m_OutValue, "OutValue"), + DEFINE_OUTPUT(m_OnDenied, "OnDenied"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +const char *CLogicConvar::GetConVarString( inputdata_t &inputdata ) +{ + if (!sv_allow_logic_convar.GetBool()) + { + m_OnDenied.FireOutput(this, this); + + //return ConVarRef("", true); + return NULL; + } + + ConVarRef pCVar = ConVarRef(STRING(m_iszConVar), true); + if (!pCVar.IsValid()) + { + const char *pszCVar = STRING( m_iszConVar ); + CBasePlayer *pPlayer = ToBasePlayer( inputdata.pActivator ); + if (!pPlayer && AI_IsSinglePlayer()) + pPlayer = UTIL_PlayerByIndex( 1 ); + + if (pPlayer) + { + // Check if it's a common cheat command a player might be using + if (FStrEq( pszCVar, "god" )) + return (pPlayer->GetFlags() & FL_GODMODE) ? "1" : "0"; + if (FStrEq( pszCVar, "notarget" )) + return (pPlayer->GetFlags() & FL_NOTARGET) ? "1" : "0"; + if (FStrEq( pszCVar, "noclip" )) + return (pPlayer->IsEFlagSet(EFL_NOCLIP_ACTIVE)) ? "1" : "0"; + + // It might be a client convar + // This function returns a blank string if the convar doesn't exist, so we have to put this at the end + const char *pszClientValue = engine->GetClientConVarValue( pPlayer->GetClientIndex(), pszCVar ); + if (pszClientValue) + { + return pszClientValue; + } + } + + //Warning("Warning: %s has invalid convar \"%s\"\n", GetDebugName(), STRING(m_iszConVar)); + } + + return pCVar.GetString(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicConvar::InputGetValue( inputdata_t &inputdata ) +{ + const char *pCVarString = GetConVarString(inputdata); + if (pCVarString != NULL) + m_OutValue.Set( AllocPooledString( pCVarString ), inputdata.pActivator, this ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicConvar::InputTest( inputdata_t &inputdata ) +{ + const char *pCVarString = GetConVarString(inputdata); + if (pCVarString) + { + if (Matcher_Match( STRING( m_iszCompareValue ), pCVarString )) + { + m_OnTrue.FireOutput(inputdata.pActivator, this); + } + else + { + m_OnFalse.FireOutput(inputdata.pActivator, this); + } + } +} + +#define MAX_LOGIC_FORMAT_PARAMETERS 8 +//----------------------------------------------------------------------------- +// Purpose: Takes a string and a bunch of parameters and spits out a formatted string. +//----------------------------------------------------------------------------- +class CLogicFormat : public CLogicalEntity +{ +public: + + DECLARE_CLASS( CLogicFormat, CLogicalEntity ); + + // Keys + string_t m_iszInput; + string_t m_iszParameter[MAX_LOGIC_FORMAT_PARAMETERS]; + string_t m_iszBackupParameter; + + void FormatString(const char *szStringToFormat, char *szOutput, int outputlen); + + // Inputs + void InputGetFormattedString( inputdata_t &inputdata ); + + // Outputs + COutputString m_OutFormattedString; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_format, CLogicFormat); + + +BEGIN_DATADESC( CLogicFormat ) + + DEFINE_INPUT( m_iszInput, FIELD_STRING, "SetInputValue" ), + DEFINE_INPUT( m_iszParameter[0], FIELD_STRING, "SetParameter0" ), + DEFINE_INPUT( m_iszParameter[1], FIELD_STRING, "SetParameter1" ), + DEFINE_INPUT( m_iszParameter[2], FIELD_STRING, "SetParameter2" ), + DEFINE_INPUT( m_iszParameter[3], FIELD_STRING, "SetParameter3" ), + DEFINE_INPUT( m_iszParameter[4], FIELD_STRING, "SetParameter4" ), + DEFINE_INPUT( m_iszParameter[5], FIELD_STRING, "SetParameter5" ), + DEFINE_INPUT( m_iszParameter[6], FIELD_STRING, "SetParameter6" ), + DEFINE_INPUT( m_iszParameter[7], FIELD_STRING, "SetParameter7" ), + DEFINE_INPUT( m_iszBackupParameter, FIELD_STRING, "SetBackupParameter" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "GetFormattedValue", InputGetFormattedString ), + + DEFINE_OUTPUT(m_OutFormattedString, "OutFormattedValue"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicFormat::InputGetFormattedString( inputdata_t &inputdata ) +{ + char szFormatted[256]; + if (m_iszInput != NULL_STRING) + { + FormatString(STRING(m_iszInput), szFormatted, sizeof(szFormatted)); + m_OutFormattedString.Set(AllocPooledString(szFormatted), inputdata.pActivator, this); + } +} + +//----------------------------------------------------------------------------- +// I'm bad at coding. +//----------------------------------------------------------------------------- +void CLogicFormat::FormatString(const char *szStringToFormat, char *szOutput, int outputlen) +{ + const char *szParameters[MAX_LOGIC_FORMAT_PARAMETERS]; + for (int i = 0; i < MAX_LOGIC_FORMAT_PARAMETERS; i++) + { + if (m_iszParameter[i] != NULL_STRING) + { + szParameters[i] = STRING(m_iszParameter[i]); + } + else if (m_iszBackupParameter != NULL_STRING) + { + szParameters[i] = STRING(m_iszBackupParameter); + } + else + { + szParameters[i] = ""; + } + } + + char szFormatted[256] = { 0 }; // Needed so garbage isn't spewed at the beginning + //Q_snprintf(szFormatted, sizeof(szFormatted), szInput, szParameters); + + char szInput[256]; + Q_strncpy(szInput, szStringToFormat, sizeof(szInput)); + + bool inparam = (szInput[0] == '{'); + int curparam = 0; + char *szToken = strtok(szInput, "{"); + while (szToken != NULL) + { + if (inparam) + { + curparam = atoi(szToken); + if (curparam < MAX_LOGIC_FORMAT_PARAMETERS /*&& szParameters[curparam] != NULL*/) //if (curparam < MAX_FORMAT_PARAMETERS) + { + Q_snprintf(szFormatted, sizeof(szFormatted), "%s%s", szFormatted, szParameters[curparam]); + } + else + { + Warning("Warning: Parameter %i out of bounds in \"%s\"\n", curparam, szStringToFormat); + + // This might not be the best way to do this, but + // reaching it is supposed to be the result of a mistake anyway. + m_iszBackupParameter != NULL_STRING ? + Q_snprintf(szFormatted, sizeof(szFormatted), "%s%s", szFormatted, STRING(m_iszBackupParameter)) : + Q_snprintf(szFormatted, sizeof(szFormatted), "%s", szFormatted); + } + + inparam = false; + szToken = strtok(NULL, "{"); + } + else + { + Q_snprintf(szFormatted, sizeof(szFormatted), "%s%s", szFormatted, szToken); + + inparam = true; + szToken = strtok(NULL, "}"); + } + } + + Q_strncpy(szOutput, szFormatted, outputlen); +} + + +//----------------------------------------------------------------------------- +// Purpose: Accesses a keyvalue from a specific entity +// Mostly ported from Half-Laugh. +//----------------------------------------------------------------------------- +class CLogicKeyfieldAccessor : public CLogicalEntity +{ + DECLARE_CLASS(CLogicKeyfieldAccessor, CLogicalEntity); + +protected: + CBaseEntity *GetTarget(CBaseEntity *pCaller, CBaseEntity *pActivator); + + virtual bool TestKey(CBaseEntity *pTarget, const char *szKeyName); + virtual bool SetKeyValue(CBaseEntity *pTarget, const char *szKeyName, const char *szValue); + virtual bool SetKeyValueBits(CBaseEntity *pTarget, const char *szKeyName, int iValue, bool bRemove = false); + + // Inputs + void InputTest(inputdata_t &inputdata); + void InputTestKey(inputdata_t &inputdata); + void InputTestTarget(inputdata_t &inputdata); + + void InputSetKey(inputdata_t &inputdata); + + void InputSetValue(inputdata_t &inputdata); + void InputAddBits(inputdata_t &inputdata); + void InputRemoveBits(inputdata_t &inputdata); + + //bool ReadUnregisteredKeyfields(CBaseEntity *pTarget, const char *szKeyName, variant_t *variant); + + COutputVariant m_OutValue; + COutputEvent m_OnFailed; + + string_t m_iszKey; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_keyfield, CLogicKeyfieldAccessor); + + +BEGIN_DATADESC(CLogicKeyfieldAccessor) + +DEFINE_KEYFIELD( m_iszKey, FIELD_STRING, "keyname" ), + +// Inputs +DEFINE_INPUTFUNC(FIELD_VOID, "Test", InputTest), +DEFINE_INPUTFUNC(FIELD_STRING, "TestKey", InputTestKey), +DEFINE_INPUTFUNC(FIELD_STRING, "TestTarget", InputTestTarget), +DEFINE_INPUTFUNC(FIELD_STRING, "SetKey", InputSetKey), + +DEFINE_INPUTFUNC(FIELD_STRING, "SetValue", InputSetValue), +DEFINE_INPUTFUNC(FIELD_INTEGER, "AddBits", InputAddBits), +DEFINE_INPUTFUNC(FIELD_INTEGER, "RemoveBits", InputRemoveBits), + +DEFINE_OUTPUT( m_OutValue, "OutValue" ), +DEFINE_OUTPUT( m_OnFailed, "OnFailed" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +inline CBaseEntity *CLogicKeyfieldAccessor::GetTarget(CBaseEntity *pCaller, CBaseEntity *pActivator) +{ + return gEntList.FindEntityByName(NULL, m_target, this, pActivator, pCaller); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CLogicKeyfieldAccessor::TestKey(CBaseEntity *pTarget, const char *szKeyName) +{ + variant_t variant; + if (pTarget->ReadKeyField(szKeyName, &variant) || ReadUnregisteredKeyfields(pTarget, szKeyName, &variant)) + { + m_OutValue.Set(variant, pTarget, this); + return true; + } + else + { + m_OnFailed.FireOutput(pTarget, this); + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CLogicKeyfieldAccessor::SetKeyValue(CBaseEntity *pTarget, const char *szKeyName, const char *szValue) +{ + if (pTarget->KeyValue(szKeyName, szValue)) + { + // We'll still fire OutValue + variant_t variant; + if (!pTarget->ReadKeyField(szKeyName, &variant)) + ReadUnregisteredKeyfields(pTarget, szKeyName, &variant); + + m_OutValue.Set(variant, pTarget, this); + return true; + } + else + { + m_OnFailed.FireOutput(pTarget, this); + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CLogicKeyfieldAccessor::SetKeyValueBits(CBaseEntity *pTarget, const char *szKeyName, int iValue, bool bRemove) +{ + variant_t variant; + if ((pTarget->ReadKeyField(szKeyName, &variant) || ReadUnregisteredKeyfields(pTarget, szKeyName, &variant)) && variant.FieldType() == FIELD_INTEGER) + { + if (bRemove) + variant.SetInt(variant.Int() & ~iValue); + else + variant.SetInt(variant.Int() | iValue); + + pTarget->KeyValue(szKeyName, UTIL_VarArgs("%i", variant.Int())); + + m_OutValue.Set(variant, pTarget, this); + return true; + } + else + { + m_OnFailed.FireOutput(pTarget, this); + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicKeyfieldAccessor::InputTest(inputdata_t &inputdata) +{ + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (pTarget && m_iszKey != NULL_STRING) + { + TestKey(pTarget, STRING(m_iszKey)); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicKeyfieldAccessor::InputTestKey(inputdata_t &inputdata) +{ + const char *input = inputdata.value.String(); + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (input && pTarget) + { + TestKey(pTarget, input); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicKeyfieldAccessor::InputTestTarget(inputdata_t &inputdata) +{ + m_target = inputdata.value.StringID(); + CBaseEntity *pTarget = gEntList.FindEntityByName(NULL, inputdata.value.StringID(), this, inputdata.pCaller, inputdata.pActivator); + if (pTarget && m_iszKey != NULL_STRING) + { + TestKey(pTarget, STRING(m_iszKey)); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicKeyfieldAccessor::InputSetKey(inputdata_t &inputdata) +{ + m_iszKey = inputdata.value.StringID(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicKeyfieldAccessor::InputSetValue(inputdata_t &inputdata) +{ + const char *input = inputdata.value.String(); + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (input && pTarget) + { + SetKeyValue(pTarget, STRING(m_iszKey), input); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicKeyfieldAccessor::InputAddBits(inputdata_t &inputdata) +{ + int input = inputdata.value.Int(); + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (input && pTarget) + { + SetKeyValueBits(pTarget, STRING(m_iszKey), input, false); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicKeyfieldAccessor::InputRemoveBits(inputdata_t &inputdata) +{ + int input = inputdata.value.Int(); + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (input && pTarget) + { + SetKeyValueBits(pTarget, STRING(m_iszKey), input, true); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Clamps the input value between two values +//----------------------------------------------------------------------------- +class CMathClamp : public CLogicalEntity +{ +public: + + DECLARE_CLASS( CMathClamp, CLogicalEntity ); + + // Keys + variant_t m_Max; + variant_t m_Min; + + // Inputs + void InputClampValue( inputdata_t &inputdata ); + + float ClampValue(float input, float min, float max, int *bounds); + void ClampValue(variant_t var, inputdata_t *inputdata); + + // Outputs + COutputVariant m_OutValue; + COutputVariant m_OnBeforeMin; + COutputVariant m_OnBeyondMax; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_clamp, CMathClamp); + + +BEGIN_DATADESC( CMathClamp ) + + DEFINE_INPUT( m_Max, FIELD_INPUT, "SetMax" ), + DEFINE_INPUT( m_Min, FIELD_INPUT, "SetMin" ), + + DEFINE_INPUTFUNC( FIELD_INPUT, "ClampValue", InputClampValue ), + + DEFINE_OUTPUT(m_OutValue, "OutValue"), + DEFINE_OUTPUT(m_OnBeforeMin, "OnBeforeMin"), + DEFINE_OUTPUT(m_OnBeyondMax, "OnBeyondMax"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathClamp::InputClampValue( inputdata_t &inputdata ) +{ + ClampValue(inputdata.value, &inputdata); +} + +//----------------------------------------------------------------------------- +// "bounds" returns 1 if the number was less than min, 2 if more than max. Must not be NULL +//----------------------------------------------------------------------------- +inline float CMathClamp::ClampValue(float input, float min, float max, int *bounds) +{ + if ( max < min ) + { + Warning("WARNING: Max value (%f) less than min value (%f) in %s!\n", max, min, GetDebugName()); + return max; + } + else if( input < min ) + { + *bounds = 1; + return min; + } + else if( input > max ) + { + *bounds = 2; + return max; + } + else + return input; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathClamp::ClampValue(variant_t var, inputdata_t *inputdata) +{ + // Don't convert up here in case of invalid type + + int nBounds; + + switch (var.FieldType()) + { + case FIELD_FLOAT: + { + m_Max.Convert(var.FieldType()); + m_Min.Convert(var.FieldType()); + + var.SetFloat(ClampValue(var.Float(), m_Max.Float(), m_Min.Float(), &nBounds)); + } break; + case FIELD_INTEGER: + { + m_Max.Convert(var.FieldType()); + m_Min.Convert(var.FieldType()); + + var.SetInt(ClampValue(var.Int(), m_Max.Int(), m_Min.Int(), &nBounds)); + } break; + case FIELD_VECTOR: + { + m_Max.Convert(var.FieldType()); + m_Min.Convert(var.FieldType()); + + Vector min; + Vector max; + m_Min.Vector3D(min); + m_Max.Vector3D(max); + + Vector vec; + var.Vector3D(vec); + + vec.x = ClampValue(vec.x, min.x, max.x, &nBounds); + vec.y = ClampValue(vec.y, min.y, max.y, &nBounds); + vec.z = ClampValue(vec.z, min.z, max.z, &nBounds); + + var.SetVector3D(vec); + } break; + default: + { + Warning("Error: Unsupported value %s in math_clamp %s\n", var.GetDebug(), STRING(GetEntityName())); + return; + } + } + + if (inputdata) + { + m_OutValue.Set(var, inputdata->pActivator, this); + if (nBounds == 1) + m_OnBeforeMin.Set(var, inputdata->pActivator, this); + else if (nBounds == 2) + m_OnBeyondMax.Set(var, inputdata->pActivator, this); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Bits calculations. +//----------------------------------------------------------------------------- +class CMathBits : public CLogicalEntity +{ + DECLARE_CLASS( CMathBits, CLogicalEntity ); +private: + + bool m_bDisabled; + + bool KeyValue(const char *szKeyName, const char *szValue); + + void UpdateOutValue(CBaseEntity *pActivator, int iNewValue); + + int DrawDebugTextOverlays(void); + + // Inputs + void InputAdd( inputdata_t &inputdata ); + void InputSubtract( inputdata_t &inputdata ); + void InputShiftLeft( inputdata_t &inputdata ); + void InputShiftRight( inputdata_t &inputdata ); + void InputApplyAnd( inputdata_t &inputdata ); + void InputApplyOr( inputdata_t &inputdata ); + void InputSetValue( inputdata_t &inputdata ); + void InputSetValueNoFire( inputdata_t &inputdata ); + void InputGetValue( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputContainsBits( inputdata_t &inputdata ); + void InputContainsAllBits( inputdata_t &inputdata ); + + // Outputs + COutputInt m_OutValue; + COutputInt m_OnGetValue; + COutputEvent m_OnTrue; + COutputEvent m_OnFalse; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_bits, CMathBits); + + +BEGIN_DATADESC( CMathBits ) + + // Keys + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_INTEGER, "Add", InputAdd), + DEFINE_INPUTFUNC(FIELD_INTEGER, "Subtract", InputSubtract), + DEFINE_INPUTFUNC(FIELD_INTEGER, "ShiftLeft", InputShiftLeft), + DEFINE_INPUTFUNC(FIELD_INTEGER, "ShiftRight", InputShiftRight), + DEFINE_INPUTFUNC(FIELD_INTEGER, "ApplyAnd", InputApplyAnd), + DEFINE_INPUTFUNC(FIELD_INTEGER, "ApplyOr", InputApplyOr), + DEFINE_INPUTFUNC(FIELD_INTEGER, "SetValue", InputSetValue), + DEFINE_INPUTFUNC(FIELD_INTEGER, "SetValueNoFire", InputSetValueNoFire), + DEFINE_INPUTFUNC(FIELD_VOID, "GetValue", InputGetValue), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC(FIELD_INTEGER, "ContainsBits", InputContainsBits), + DEFINE_INPUTFUNC(FIELD_INTEGER, "ContainsAllBits", InputContainsAllBits), + + // Outputs + DEFINE_OUTPUT(m_OutValue, "OutValue"), + DEFINE_OUTPUT(m_OnGetValue, "OnGetValue"), + DEFINE_OUTPUT(m_OnTrue, "OnTrue"), + DEFINE_OUTPUT(m_OnFalse, "OnFalse"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Handles key values from the BSP before spawn is called. +//----------------------------------------------------------------------------- +bool CMathBits::KeyValue(const char *szKeyName, const char *szValue) +{ + // + // Set the initial value of the counter. + // + if (!stricmp(szKeyName, "startvalue")) + { + m_OutValue.Init(atoi(szValue)); + return(true); + } + + return(BaseClass::KeyValue(szKeyName, szValue)); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for adding to the accumulator value. +// Input : Bit value to add. +//----------------------------------------------------------------------------- +void CMathBits::InputAdd( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring ADD because it is disabled\n", GetDebugName() ); + return; + } + + int iNewValue = m_OutValue.Get() | inputdata.value.Int(); + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for subtracting from the current value. +// Input : Bit value to subtract. +//----------------------------------------------------------------------------- +void CMathBits::InputSubtract( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring SUBTRACT because it is disabled\n", GetDebugName() ); + return; + } + + int iNewValue = m_OutValue.Get() & ~inputdata.value.Int(); + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for shifting from the current value. +// Input : Bit value to shift by. +//----------------------------------------------------------------------------- +void CMathBits::InputShiftLeft( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring SHIFTLEFT because it is disabled\n", GetDebugName() ); + return; + } + + int iNewValue = m_OutValue.Get() << inputdata.value.Int(); + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for shifting from the current value. +// Input : Bit value to shift by. +//----------------------------------------------------------------------------- +void CMathBits::InputShiftRight( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring SHIFTRIGHT because it is disabled\n", GetDebugName() ); + return; + } + + int iNewValue = m_OutValue.Get() >> inputdata.value.Int(); + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for applying & to the current value. +// Input : Bit value to shift by. +//----------------------------------------------------------------------------- +void CMathBits::InputApplyAnd( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring APPLYAND because it is disabled\n", GetDebugName() ); + return; + } + + int iNewValue = m_OutValue.Get() & inputdata.value.Int(); + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for applying | to the current value. +// Input : Bit value to shift by. +//----------------------------------------------------------------------------- +void CMathBits::InputApplyOr( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring APPLYOR because it is disabled\n", GetDebugName() ); + return; + } + + int iNewValue = m_OutValue.Get() | inputdata.value.Int(); + UpdateOutValue( inputdata.pActivator, iNewValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for updating the value. +// Input : Bit value to set. +//----------------------------------------------------------------------------- +void CMathBits::InputSetValue( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring SETVALUE because it is disabled\n", GetDebugName() ); + return; + } + + UpdateOutValue( inputdata.pActivator, inputdata.value.Int() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for updating the value. +// Input : Bit value to set. +//----------------------------------------------------------------------------- +void CMathBits::InputSetValueNoFire( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring SETVALUENOFIRE because it is disabled\n", GetDebugName() ); + return; + } + + m_OutValue.Init( inputdata.value.Int() ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathBits::InputGetValue( inputdata_t &inputdata ) +{ + int iOutValue = m_OutValue.Get(); + m_OnGetValue.Set( iOutValue, inputdata.pActivator, inputdata.pCaller ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for checking whether a bit is stored. +// Input : Bit value to check. +//----------------------------------------------------------------------------- +void CMathBits::InputContainsBits( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring CONTAINS BITS because it is disabled\n", GetDebugName() ); + return; + } + + if (m_OutValue.Get() & inputdata.value.Int()) + m_OnTrue.FireOutput(inputdata.pActivator, this); + else + m_OnFalse.FireOutput(inputdata.pActivator, this); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for checking whether all of the specified bits are stored. +// Input : Bit value to check. +//----------------------------------------------------------------------------- +void CMathBits::InputContainsAllBits( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Bits %s ignoring CONTAINS ALL BITS because it is disabled\n", GetDebugName() ); + return; + } + + bool bResult = false; + int iInput = inputdata.value.Int(); + int iValue = m_OutValue.Get(); + + for (int i = 1, n = 0; n < 32; (i <<= 1), n++) + { + DevMsg("%i\n", i); + if (iInput & i) + { + if (!(iValue & i)) + { + DevMsg("%i does not go into %i\n", i, iValue); + bResult = false; + break; + } + else if (!bResult) + { + bResult = true; + } + } + } + + if (bResult) + m_OnTrue.FireOutput(inputdata.pActivator, this); + else + m_OnFalse.FireOutput(inputdata.pActivator, this); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathBits::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathBits::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the value to the new value, firing the output value. +// Input : iNewValue - Value to set. +//----------------------------------------------------------------------------- +void CMathBits::UpdateOutValue(CBaseEntity *pActivator, int iNewValue) +{ + m_OutValue.Set(iNewValue, pActivator, this); +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CMathBits::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + Q_snprintf(tempstr,sizeof(tempstr),"current value: %i", m_OutValue.Get()); + EntityText(text_offset,tempstr,0); + text_offset++; + + if( m_bDisabled ) + { + Q_snprintf(tempstr,sizeof(tempstr),"*DISABLED*"); + } + else + { + Q_snprintf(tempstr,sizeof(tempstr),"Enabled."); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + } + return text_offset; +} + +// These spawnflags control math_vector dimensions. +#define SF_MATH_VECTOR_DISABLE_X ( 1 << 0 ) +#define SF_MATH_VECTOR_DISABLE_Y ( 1 << 1 ) +#define SF_MATH_VECTOR_DISABLE_Z ( 1 << 2 ) + +//----------------------------------------------------------------------------- +// Purpose: Vector calculations. +//----------------------------------------------------------------------------- +class CMathVector : public CLogicalEntity +{ + DECLARE_CLASS( CMathVector, CLogicalEntity ); +private: + + bool m_bDisabled; + + bool KeyValue(const char *szKeyName, const char *szValue); + bool KeyValue( const char *szKeyName, const Vector &vecValue ); + + void UpdateOutValue(CBaseEntity *pActivator, Vector vecNewValue); + + int DrawDebugTextOverlays(void); + + // Inputs + void InputAdd( inputdata_t &inputdata ); + void InputSubtract( inputdata_t &inputdata ); + void InputDivide( inputdata_t &inputdata ); + void InputMultiply( inputdata_t &inputdata ); + void InputSetValue( inputdata_t &inputdata ); + void InputSetValueNoFire( inputdata_t &inputdata ); + void InputGetValue( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + void PointAt( Vector &origin, Vector &target, Vector &out ); + void InputPointAtLocation( inputdata_t &inputdata ); + void InputPointAtEntity( inputdata_t &inputdata ); + + void InputNormalize( inputdata_t &inputdata ); + void InputNormalizeAngles( inputdata_t &inputdata ); + void InputVectorAngles( inputdata_t &inputdata ); + void InputAngleVectorForward( inputdata_t &inputdata ); + void InputAngleVectorRight( inputdata_t &inputdata ); + void InputAngleVectorUp( inputdata_t &inputdata ); + + void SetCoordinate(float value, char coord, CBaseEntity *pActivator); + void GetCoordinate(char coord, CBaseEntity *pActivator); + void AddCoordinate(float value, char coord, CBaseEntity *pActivator); + void SubtractCoordinate(float value, char coord, CBaseEntity *pActivator); + + void InputSetX( inputdata_t &inputdata ) { SetCoordinate(inputdata.value.Float(), 'X', inputdata.pActivator); } + void InputSetY( inputdata_t &inputdata ) { SetCoordinate(inputdata.value.Float(), 'Y', inputdata.pActivator); } + void InputSetZ( inputdata_t &inputdata ) { SetCoordinate(inputdata.value.Float(), 'Z', inputdata.pActivator); } + void InputGetX( inputdata_t &inputdata ) { GetCoordinate('X', inputdata.pActivator); } + void InputGetY( inputdata_t &inputdata ) { GetCoordinate('Y', inputdata.pActivator); } + void InputGetZ( inputdata_t &inputdata ) { GetCoordinate('Z', inputdata.pActivator); } + void InputAddX( inputdata_t &inputdata ) { AddCoordinate(inputdata.value.Float(), 'X', inputdata.pActivator); } + void InputAddY( inputdata_t &inputdata ) { AddCoordinate(inputdata.value.Float(), 'Y', inputdata.pActivator); } + void InputAddZ( inputdata_t &inputdata ) { AddCoordinate(inputdata.value.Float(), 'Z', inputdata.pActivator); } + void InputSubtractX( inputdata_t &inputdata ) { SubtractCoordinate(inputdata.value.Float(), 'X', inputdata.pActivator); } + void InputSubtractY( inputdata_t &inputdata ) { SubtractCoordinate(inputdata.value.Float(), 'Y', inputdata.pActivator); } + void InputSubtractZ( inputdata_t &inputdata ) { SubtractCoordinate(inputdata.value.Float(), 'Z', inputdata.pActivator); } + + // Outputs + COutputVector m_OutValue; + COutputFloat m_OutX; + COutputFloat m_OutY; + COutputFloat m_OutZ; + + COutputVector m_OnGetValue; + COutputFloat m_OnGetX; + COutputFloat m_OnGetY; + COutputFloat m_OnGetZ; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_vector, CMathVector); + + +BEGIN_DATADESC( CMathVector ) + + // Keys + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VECTOR, "Add", InputAdd), + DEFINE_INPUTFUNC(FIELD_VECTOR, "Subtract", InputSubtract), + DEFINE_INPUTFUNC(FIELD_VECTOR, "Divide", InputDivide), + DEFINE_INPUTFUNC(FIELD_VECTOR, "Multiply", InputMultiply), + DEFINE_INPUTFUNC(FIELD_VECTOR, "SetValue", InputSetValue), + DEFINE_INPUTFUNC(FIELD_VECTOR, "SetValueNoFire", InputSetValueNoFire), + DEFINE_INPUTFUNC(FIELD_VOID, "GetValue", InputGetValue), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_INPUTFUNC( FIELD_VECTOR, "PointAtLocation", InputPointAtLocation ), + DEFINE_INPUTFUNC( FIELD_EHANDLE, "PointAtEntity", InputPointAtEntity ), + + DEFINE_INPUTFUNC(FIELD_VOID, "Normalize", InputNormalize), + DEFINE_INPUTFUNC(FIELD_VOID, "NormalizeAngles", InputNormalizeAngles), + DEFINE_INPUTFUNC(FIELD_VOID, "VectorAngles", InputVectorAngles), + DEFINE_INPUTFUNC(FIELD_VOID, "AngleVectorForward", InputAngleVectorForward), + DEFINE_INPUTFUNC(FIELD_VOID, "AngleVectorRight", InputAngleVectorRight), + DEFINE_INPUTFUNC(FIELD_VOID, "AngleVectorUp", InputAngleVectorUp), + + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetX", InputSetX), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetY", InputSetY), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SetZ", InputSetZ), + DEFINE_INPUTFUNC(FIELD_VOID, "GetX", InputGetX), + DEFINE_INPUTFUNC(FIELD_VOID, "GetY", InputGetY), + DEFINE_INPUTFUNC(FIELD_VOID, "GetZ", InputGetZ), + DEFINE_INPUTFUNC(FIELD_FLOAT, "AddX", InputAddX), + DEFINE_INPUTFUNC(FIELD_FLOAT, "AddY", InputAddY), + DEFINE_INPUTFUNC(FIELD_FLOAT, "AddZ", InputAddZ), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SubtractX", InputSubtractX), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SubtractY", InputSubtractY), + DEFINE_INPUTFUNC(FIELD_FLOAT, "SubtractZ", InputSubtractZ), + + // Outputs + DEFINE_OUTPUT(m_OutValue, "OutValue"), + DEFINE_OUTPUT(m_OutX, "OutX"), + DEFINE_OUTPUT(m_OutY, "OutY"), + DEFINE_OUTPUT(m_OutZ, "OutZ"), + + DEFINE_OUTPUT(m_OnGetValue, "OnGetValue"), + DEFINE_OUTPUT(m_OnGetX, "OnGetX"), + DEFINE_OUTPUT(m_OnGetY, "OnGetY"), + DEFINE_OUTPUT(m_OnGetZ, "OnGetZ"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Handles key values from the BSP before spawn is called. +//----------------------------------------------------------------------------- +bool CMathVector::KeyValue(const char *szKeyName, const char *szValue) +{ + // + // Set the initial value of the counter. + // + if (!stricmp(szKeyName, "startvalue")) + { + Vector vec; + UTIL_StringToVector( vec.Base(), szValue ); + m_OutValue.Init(vec); + return(true); + } + + return(BaseClass::KeyValue(szKeyName, szValue)); +} + +//----------------------------------------------------------------------------- +// Purpose: Handles key values from the BSP before spawn is called. +//----------------------------------------------------------------------------- +bool CMathVector::KeyValue( const char *szKeyName, const Vector &vecValue ) +{ + // + // Set the initial value of the counter. + // + if (!stricmp(szKeyName, "startvalue")) + { + m_OutValue.Init(vecValue); + return true; + } + + // So, CLogicalEntity descends from CBaseEntity... + // Yup. + // ...and CBaseEntity has a version of KeyValue that takes vectors. + // Yup. + // Since it's virtual, I could easily override it just like I could with a KeyValue that takes strings, right? + // Sounds right to me. + // So let me override it. + // *No suitable function exists* + return CBaseEntity::KeyValue(szKeyName, vecValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for adding to the accumulator value. +// Input : Bit value to add. +//----------------------------------------------------------------------------- +void CMathVector::InputAdd( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring ADD because it is disabled\n", GetDebugName() ); + return; + } + + Vector vec; + inputdata.value.Vector3D(vec); + Vector cur; + m_OutValue.Get(cur); + UpdateOutValue( inputdata.pActivator, cur + vec ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for subtracting from the current value. +// Input : Bit value to subtract. +//----------------------------------------------------------------------------- +void CMathVector::InputSubtract( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring SUBTRACT because it is disabled\n", GetDebugName() ); + return; + } + + Vector vec; + inputdata.value.Vector3D(vec); + Vector cur; + m_OutValue.Get(cur); + UpdateOutValue( inputdata.pActivator, cur - vec ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for multiplying the current value. +// Input : Float value to multiply the value by. +//----------------------------------------------------------------------------- +void CMathVector::InputDivide( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring DIVIDE because it is disabled\n", GetDebugName() ); + return; + } + + Vector vec; + inputdata.value.Vector3D(vec); + Vector cur; + m_OutValue.Get(cur); + + if (vec.x != 0) + cur.x /= vec.x; + if (vec.y != 0) + cur.y /= vec.y; + if (vec.z != 0) + cur.z /= vec.z; + + UpdateOutValue( inputdata.pActivator, cur ); + + //if (vec.x != 0 && vec.y != 0 && vec.z != 0) + //{ + // UpdateOutValue( inputdata.pActivator, cur / vec ); + //} + //else + //{ + // DevMsg( 1, "LEVEL DESIGN ERROR: Divide by zero in math_vector\n" ); + // UpdateOutValue( inputdata.pActivator, cur ); + //} +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for multiplying the current value. +// Input : Float value to multiply the value by. +//----------------------------------------------------------------------------- +void CMathVector::InputMultiply( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring MULTIPLY because it is disabled\n", GetDebugName() ); + return; + } + + Vector vec; + inputdata.value.Vector3D(vec); + Vector cur; + m_OutValue.Get(cur); + UpdateOutValue( inputdata.pActivator, cur * vec ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for updating the value. +// Input : Bit value to set. +//----------------------------------------------------------------------------- +void CMathVector::InputSetValue( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring SETVALUE because it is disabled\n", GetDebugName() ); + return; + } + + Vector vec; + inputdata.value.Vector3D(vec); + UpdateOutValue( inputdata.pActivator, vec ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for updating the value. +// Input : Bit value to set. +//----------------------------------------------------------------------------- +void CMathVector::InputSetValueNoFire( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring SETVALUENOFIRE because it is disabled\n", GetDebugName() ); + return; + } + + Vector vec; + inputdata.value.Vector3D(vec); + m_OutValue.Init( vec ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputGetValue( inputdata_t &inputdata ) +{ + Vector cur; + m_OutValue.Get(cur); + m_OnGetValue.Set( cur, inputdata.pActivator, inputdata.pCaller ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::PointAt( Vector &origin, Vector &target, Vector &out ) +{ + out = origin - target; + VectorNormalize( out ); + + QAngle ang; + VectorAngles( out, ang ); + + out[0] = ang[0]; + out[1] = ang[1]; + out[2] = ang[2]; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputPointAtLocation( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring POINTATLOCATION because it is disabled\n", GetDebugName() ); + return; + } + + Vector cur; + m_OutValue.Get(cur); + + Vector location; + inputdata.value.Vector3D( location ); + + PointAt( cur, location, cur ); + + UpdateOutValue( inputdata.pActivator, cur ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputPointAtEntity( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring POINTATENTITY because it is disabled\n", GetDebugName() ); + return; + } + + if (!inputdata.value.Entity()) + { + Warning("%s received no entity to point at\n", GetDebugName()); + return; + } + + Vector cur; + m_OutValue.Get(cur); + + Vector location = inputdata.value.Entity()->GetAbsOrigin(); + + PointAt( cur, location, cur ); + + UpdateOutValue( inputdata.pActivator, cur ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputNormalize( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring NORMALIZE because it is disabled\n", GetDebugName() ); + return; + } + + Vector cur; + m_OutValue.Get(cur); + VectorNormalize(cur); + UpdateOutValue( inputdata.pActivator, cur ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputNormalizeAngles( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring NORMALIZEANGLES because it is disabled\n", GetDebugName() ); + return; + } + + Vector cur; + m_OutValue.Get(cur); + cur.x = AngleNormalize(cur.x); + cur.y = AngleNormalize(cur.y); + cur.z = AngleNormalize(cur.z); + UpdateOutValue( inputdata.pActivator, cur ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputVectorAngles( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring VECTORANGLES because it is disabled\n", GetDebugName() ); + return; + } + + Vector cur; + QAngle ang; + m_OutValue.Get(cur); + VectorAngles(cur, ang); + UpdateOutValue( inputdata.pActivator, Vector(ang.x, ang.y, ang.z) ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputAngleVectorForward( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring ANGLEVECTORFORWARD because it is disabled\n", GetDebugName() ); + return; + } + + Vector cur; + m_OutValue.Get(cur); + AngleVectors(QAngle(cur.x, cur.y, cur.z), &cur); + UpdateOutValue( inputdata.pActivator, cur ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputAngleVectorRight( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring ANGLEVECTORRIGHT because it is disabled\n", GetDebugName() ); + return; + } + + Vector cur; + m_OutValue.Get(cur); + AngleVectors(QAngle(cur.x, cur.y, cur.z), NULL, &cur, NULL); + UpdateOutValue( inputdata.pActivator, cur ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::InputAngleVectorUp( inputdata_t &inputdata ) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring ANGLEVECTORUP because it is disabled\n", GetDebugName() ); + return; + } + + Vector cur; + m_OutValue.Get(cur); + AngleVectors(QAngle(cur.x, cur.y, cur.z), NULL, NULL, &cur); + UpdateOutValue( inputdata.pActivator, cur ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::SetCoordinate(float value, char coord, CBaseEntity *pActivator) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring SET%c because it is disabled\n", GetDebugName(), coord ); + return; + } + + Vector vec; + m_OutValue.Get(vec); + switch (coord) + { + case 'X': vec.x = value; break; + case 'Y': vec.y = value; break; + case 'Z': vec.z = value; break; + } + UpdateOutValue( pActivator, vec ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::GetCoordinate(char coord, CBaseEntity *pActivator) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring SET%c because it is disabled\n", GetDebugName(), coord ); + return; + } + + Vector vec; + m_OutValue.Get(vec); + switch (coord) + { + case 'X': m_OnGetX.Set(vec.x, pActivator, this); break; + case 'Y': m_OnGetY.Set(vec.y, pActivator, this); break; + case 'Z': m_OnGetZ.Set(vec.z, pActivator, this); break; + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::AddCoordinate(float value, char coord, CBaseEntity *pActivator) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring ADD%c because it is disabled\n", GetDebugName(), coord ); + return; + } + + Vector vec; + m_OutValue.Get(vec); + switch (coord) + { + case 'X': vec.x += value; break; + case 'Y': vec.y += value; break; + case 'Z': vec.z += value; break; + } + UpdateOutValue( pActivator, vec ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathVector::SubtractCoordinate(float value, char coord, CBaseEntity *pActivator) +{ + if( m_bDisabled ) + { + DevMsg("Math Vector %s ignoring SUBTRACT%c because it is disabled\n", GetDebugName(), coord ); + return; + } + + Vector vec; + m_OutValue.Get(vec); + switch (coord) + { + case 'X': vec.x += value; break; + case 'Y': vec.y += value; break; + case 'Z': vec.z += value; break; + } + UpdateOutValue( pActivator, vec ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the value to the new value, firing the output value. +// Input : vecNewValue - Value to set. +//----------------------------------------------------------------------------- +void CMathVector::UpdateOutValue(CBaseEntity *pActivator, Vector vecNewValue) +{ + if (HasSpawnFlags( SF_MATH_VECTOR_DISABLE_X )) + vecNewValue.x = 0; + if (HasSpawnFlags( SF_MATH_VECTOR_DISABLE_Y )) + vecNewValue.y = 0; + if (HasSpawnFlags( SF_MATH_VECTOR_DISABLE_Z )) + vecNewValue.z = 0; + + m_OutValue.Set(vecNewValue, pActivator, this); + + m_OutX.Set(vecNewValue.x, pActivator, this); + m_OutY.Set(vecNewValue.y, pActivator, this); + m_OutZ.Set(vecNewValue.z, pActivator, this); +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CMathVector::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Vector cur; + m_OutValue.Get(cur); + + Q_snprintf(tempstr, sizeof(tempstr), "current value: [%g %g %g]", (double)cur[0], (double)cur[1], (double)cur[2]); + EntityText(text_offset,tempstr,0); + text_offset++; + + if( m_bDisabled ) + { + Q_snprintf(tempstr,sizeof(tempstr),"*DISABLED*"); + } + else + { + Q_snprintf(tempstr,sizeof(tempstr),"Enabled."); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: Accesses/modifies any field in a datadesc based on its internal name. +// Oh boy. +//----------------------------------------------------------------------------- +class CLogicFieldAccessor : public CLogicKeyfieldAccessor +{ + DECLARE_CLASS(CLogicFieldAccessor, CLogicKeyfieldAccessor); + +private: + bool TestKey(CBaseEntity *pTarget, const char *szKeyName); + bool SetKeyValue(CBaseEntity *pTarget, const char *szKeyName, const char *szValue); + bool SetKeyValueBits(CBaseEntity *pTarget, const char *szKeyName, int iValue, bool bRemove = false); + + //DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_datadesc_accessor, CLogicFieldAccessor); + + +//BEGIN_DATADESC(CLogicFieldAccessor) + +//END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CLogicFieldAccessor::TestKey(CBaseEntity *pTarget, const char *szKeyName) +{ + variant_t var; + for ( datamap_t *dmap = pTarget->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the readable fields in the data description, looking for a match + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & (FTYPEDESC_SAVE | FTYPEDESC_KEY) ) + { + DevMsg("Field Name: %s,\n", dmap->dataDesc[i].fieldName); + if ( Matcher_NamesMatch(szKeyName, dmap->dataDesc[i].fieldName) ) + { + fieldtype_t fieldtype = dmap->dataDesc[i].fieldType; + switch (fieldtype) + { + case FIELD_TIME: fieldtype = FIELD_FLOAT; break; + case FIELD_MODELNAME: fieldtype = FIELD_STRING; break; + case FIELD_SOUNDNAME: fieldtype = FIELD_STRING; break; + // There's definitely more of them. Add when demand becomes prevalent + } + + var.Set( fieldtype, ((char*)pTarget) + dmap->dataDesc[i].fieldOffset[ TD_OFFSET_NORMAL ] ); + DevMsg("FIELD TYPE: %i\n", fieldtype); + m_OutValue.Set(var, pTarget, this); + return true; + } + } + } + } + + m_OnFailed.FireOutput(pTarget, this); + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CLogicFieldAccessor::SetKeyValue(CBaseEntity *pTarget, const char *szKeyName, const char *szValue) +{ + for ( datamap_t *dmap = pTarget->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the readable fields in the data description, looking for a match + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & (FTYPEDESC_SAVE | FTYPEDESC_KEY) ) + { + DevMsg("Field Name: %s,\n", dmap->dataDesc[i].fieldName); + if ( Matcher_NamesMatch(szKeyName, dmap->dataDesc[i].fieldName) ) + { + // Copied from ::ParseKeyvalue... + fieldtype_t fieldtype = FIELD_VOID; + typedescription_t *pField = &dmap->dataDesc[i]; + char *data = Datadesc_SetFieldString( szValue, pTarget, pField, &fieldtype ); + + if (!data) + { + Warning( "%s cannot set field of type %i.\n", GetDebugName(), dmap->dataDesc[i].fieldType ); + } + else if (fieldtype != FIELD_VOID) + { + variant_t var; + var.Set(fieldtype, data); + m_OutValue.Set(var, pTarget, this); + return true; + } + } + } + } + } + + m_OnFailed.FireOutput(pTarget, this); + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CLogicFieldAccessor::SetKeyValueBits(CBaseEntity *pTarget, const char *szKeyName, int iValue, bool bRemove) +{ + variant_t var; + for ( datamap_t *dmap = pTarget->GetDataDescMap(); dmap != NULL; dmap = dmap->baseMap ) + { + // search through all the readable fields in the data description, looking for a match + for ( int i = 0; i < dmap->dataNumFields; i++ ) + { + if ( dmap->dataDesc[i].flags & (FTYPEDESC_SAVE | FTYPEDESC_KEY) ) + { + DevMsg("Field Name: %s,\n", dmap->dataDesc[i].fieldName); + if ( Matcher_NamesMatch(szKeyName, dmap->dataDesc[i].fieldName) ) + { + fieldtype_t fieldtype = dmap->dataDesc[i].fieldType; + if (fieldtype != FIELD_INTEGER) + break; + + var.Set( fieldtype, ((char*)pTarget) + dmap->dataDesc[i].fieldOffset[ TD_OFFSET_NORMAL ] ); + + if (bRemove) + var.SetInt(var.Int() & ~iValue); + else + var.SetInt(var.Int() | iValue); + + DevMsg("FIELD TYPE: %i\n", fieldtype); + m_OutValue.Set(var, pTarget, this); + return true; + } + } + } + } + + m_OnFailed.FireOutput(pTarget, this); + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Passes global variables, like curtime. +//----------------------------------------------------------------------------- +class CGameGlobalVars : public CLogicalEntity +{ + DECLARE_CLASS( CGameGlobalVars, CLogicalEntity ); +private: + + // Inputs + void InputGetCurtime( inputdata_t &inputdata ) { m_OutCurtime.Set(gpGlobals->curtime, inputdata.pActivator, this); } + void InputGetFrameCount( inputdata_t &inputdata ) { m_OutFrameCount.Set(gpGlobals->framecount, inputdata.pActivator, this); } + void InputGetFrametime( inputdata_t &inputdata ) { m_OutFrametime.Set(gpGlobals->frametime, inputdata.pActivator, this); } + void InputGetTickCount( inputdata_t &inputdata ) { m_OutTickCount.Set(gpGlobals->tickcount, inputdata.pActivator, this); } + void InputGetIntervalPerTick( inputdata_t &inputdata ) { m_OutIntervalPerTick.Set(gpGlobals->interval_per_tick, inputdata.pActivator, this); } + + // Outputs + COutputFloat m_OutCurtime; + COutputInt m_OutFrameCount; + COutputFloat m_OutFrametime; + COutputInt m_OutTickCount; + COutputInt m_OutIntervalPerTick; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(game_globalvars, CGameGlobalVars); + + +BEGIN_DATADESC( CGameGlobalVars ) + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "GetCurtime", InputGetCurtime ), + DEFINE_INPUTFUNC( FIELD_VOID, "GetFrameCount", InputGetFrameCount ), + DEFINE_INPUTFUNC( FIELD_VOID, "GetFrametime", InputGetFrametime ), + DEFINE_INPUTFUNC( FIELD_VOID, "GetTickCount", InputGetTickCount ), + DEFINE_INPUTFUNC( FIELD_VOID, "GetIntervalPerTick", InputGetIntervalPerTick ), + + // Outputs + DEFINE_OUTPUT(m_OutCurtime, "OutCurtime"), + DEFINE_OUTPUT(m_OutFrameCount, "OutFrameCount"), + DEFINE_OUTPUT(m_OutFrametime, "OutFrametime"), + DEFINE_OUTPUT(m_OutTickCount, "OutTickCount"), + DEFINE_OUTPUT(m_OutIntervalPerTick, "OutIntervalPerTick"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//void CGameGlobalVars::InputGetCurtime( inputdata_t &inputdata ) +//{ +// m_OutCurtime.Set(gpGlobals->curtime, inputdata.pActivator, this); +//} + + +#define MathModCalc(val1, val2, op) \ + switch (op) \ + { \ + case '+': val1 += val2; break; \ + case '-': val1 -= val2; break; \ + case '*': val1 *= val2; break; \ + case '/': val1 /= val2; break; \ + } \ + +//----------------------------------------------------------------------------- +// Purpose: Modifies values on the fly. +//----------------------------------------------------------------------------- +class CMathMod : public CLogicalEntity +{ + DECLARE_CLASS( CMathMod, CLogicalEntity ); +private: + + bool KeyValue(const char *szKeyName, const char *szValue); + + // Inputs + void InputSetMod( inputdata_t &inputdata ); + void InputSetOperator( inputdata_t &inputdata ); + + void InputModInt( inputdata_t &inputdata ); + void InputModFloat( inputdata_t &inputdata ); + void InputModVector( inputdata_t &inputdata ); + + // Outputs + COutputInt m_OutInt; + COutputFloat m_OutFloat; + COutputVector m_OutVector; + + int m_Operator; + + variant_t m_Mod; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(math_mod, CMathMod); + + +BEGIN_DATADESC( CMathMod ) + + DEFINE_KEYFIELD( m_Operator, FIELD_INTEGER, "SetOperator" ), + + DEFINE_VARIANT( m_Mod ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_INPUT, "SetMod", InputSetMod ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetOperator", InputSetOperator ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "ModInt", InputModInt ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "ModFloat", InputModFloat ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "ModVector", InputModVector ), + + // Outputs + DEFINE_OUTPUT(m_OutInt, "OutInt"), + DEFINE_OUTPUT(m_OutFloat, "OutFloat"), + DEFINE_OUTPUT(m_OutVector, "OutVector"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Handles key values from the BSP before spawn is called. +//----------------------------------------------------------------------------- +bool CMathMod::KeyValue(const char *szKeyName, const char *szValue) +{ + if (!stricmp(szKeyName, "startvalue")) + { + // It converts later anyway + m_Mod.SetString(AllocPooledString(szValue)); + return true; + } + + return BaseClass::KeyValue(szKeyName, szValue); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathMod::InputSetMod( inputdata_t &inputdata ) +{ + m_Mod = inputdata.value; + //if (inputdata.value.FieldType() == FIELD_STRING) + // m_Mod = Variant_Parse(inputdata.value.String()); + //else + // m_Mod = inputdata.value; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathMod::InputSetOperator( inputdata_t &inputdata ) +{ + m_Operator = inputdata.value.String()[0]; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathMod::InputModInt( inputdata_t &inputdata ) +{ + m_Mod.Convert(FIELD_INTEGER); + + DevMsg("Operator is %c you see\n", m_Operator); + + int out = inputdata.value.Int(); + MathModCalc(out, m_Mod.Int(), m_Operator); + + m_OutInt.Set( out, inputdata.pActivator, this ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathMod::InputModFloat( inputdata_t &inputdata ) +{ + m_Mod.Convert(FIELD_FLOAT); + + float out = inputdata.value.Float(); + MathModCalc(out, m_Mod.Float(), m_Operator); + + m_OutFloat.Set( out, inputdata.pActivator, this ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathMod::InputModVector( inputdata_t &inputdata ) +{ + m_Mod.Convert(FIELD_VECTOR); + + Vector out; + inputdata.value.Vector3D(out); + Vector mod; + m_Mod.Vector3D(mod); + MathModCalc(out, mod, m_Operator); + + m_OutVector.Set( out, inputdata.pActivator, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Gets an entity's model information. +//----------------------------------------------------------------------------- +class CLogicModelInfo : public CLogicalEntity +{ + DECLARE_CLASS( CLogicModelInfo, CLogicalEntity ); +private: + + CBaseAnimating *GetTarget(inputdata_t &inputdata); + int GetPoseParameterIndex(CBaseAnimating *pTarget); + + // Inputs + //void InputSetTarget( inputdata_t &inputdata ) { BaseClass::InputSetTarget(inputdata); m_hTarget = NULL; } + void InputGetNumSkins( inputdata_t &inputdata ); + void InputLookupSequence( inputdata_t &inputdata ); + void InputLookupActivity( inputdata_t &inputdata ); + + void InputSetPoseParameterName( inputdata_t &inputdata ); + void InputSetPoseParameterValue( inputdata_t &inputdata ); + void InputGetPoseParameter( inputdata_t &inputdata ); + + // Outputs + COutputInt m_OutNumSkins; + COutputInt m_OnHasSequence; + COutputEvent m_OnLacksSequence; + + COutputFloat m_OutPoseParameterValue; + + // KeyValues + + string_t m_iszPoseParameterName; + int m_iPoseParameterIndex = -1; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_modelinfo, CLogicModelInfo); + + +BEGIN_DATADESC( CLogicModelInfo ) + + DEFINE_KEYFIELD( m_iszPoseParameterName, FIELD_STRING, "PoseParameterName" ), + DEFINE_FIELD( m_iPoseParameterIndex, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "GetNumSkins", InputGetNumSkins ), + DEFINE_INPUTFUNC( FIELD_STRING, "LookupSequence", InputLookupSequence ), + DEFINE_INPUTFUNC( FIELD_STRING, "LookupActivity", InputLookupActivity ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetPoseParameterName", InputSetPoseParameterName ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPoseParameterValue", InputSetPoseParameterValue ), + DEFINE_INPUTFUNC( FIELD_VOID, "GetPoseParameter", InputGetPoseParameter ), + + // Outputs + DEFINE_OUTPUT(m_OutNumSkins, "OutNumSkins"), + DEFINE_OUTPUT(m_OnHasSequence, "OnHasSequence"), + DEFINE_OUTPUT(m_OnLacksSequence, "OnLacksSequence"), + DEFINE_OUTPUT(m_OutPoseParameterValue, "OutPoseParameterValue"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +inline CBaseAnimating *CLogicModelInfo::GetTarget(inputdata_t &inputdata) +{ + CBaseEntity *pEntity = gEntList.FindEntityByName(NULL, STRING(m_target), this, inputdata.pActivator, inputdata.pCaller); + if (!pEntity || !pEntity->GetBaseAnimating()) + return NULL; + return pEntity->GetBaseAnimating(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +inline int CLogicModelInfo::GetPoseParameterIndex(CBaseAnimating *pTarget) +{ + if (m_iPoseParameterIndex == -1) + m_iPoseParameterIndex = pTarget->LookupPoseParameter(STRING(m_iszPoseParameterName)); + return m_iPoseParameterIndex; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicModelInfo::InputGetNumSkins( inputdata_t &inputdata ) +{ + CBaseAnimating *pAnimating = GetTarget(inputdata); + if (pAnimating && pAnimating->GetModelPtr()) + { + m_OutNumSkins.Set(pAnimating->GetModelPtr()->numskinfamilies(), pAnimating, this); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicModelInfo::InputLookupSequence( inputdata_t &inputdata ) +{ + CBaseAnimating *pAnimating = GetTarget(inputdata); + if (pAnimating && pAnimating->GetModelPtr()) + { + int index = pAnimating->LookupSequence(inputdata.value.String()); + + if (index != ACT_INVALID) + m_OnHasSequence.Set(index, pAnimating, this); + else + m_OnLacksSequence.FireOutput(pAnimating, this); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicModelInfo::InputLookupActivity( inputdata_t &inputdata ) +{ + CBaseAnimating *pAnimating = GetTarget(inputdata); + if (pAnimating && pAnimating->GetModelPtr()) + { + int iActivity = ActivityList_IndexForName(inputdata.value.String()); + if (iActivity == -1) + { + // Check if it's a raw activity ID + iActivity = atoi(inputdata.value.String()); + if (!ActivityList_NameForIndex(iActivity)) + { + Msg("%s received invalid LookupActivity %s\n", GetDebugName(), inputdata.value.String()); + return; + } + } + + int index = pAnimating->SelectWeightedSequence((Activity)iActivity); + + if (index != ACT_INVALID) + m_OnHasSequence.Set(index, pAnimating, this); + else + m_OnLacksSequence.FireOutput(pAnimating, this); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicModelInfo::InputSetPoseParameterName( inputdata_t &inputdata ) +{ + m_iszPoseParameterName = inputdata.value.StringID(); + m_iPoseParameterIndex = -1; + + CBaseAnimating *pAnimating = GetTarget(inputdata); + if (pAnimating && pAnimating->GetModelPtr()) + { + if (GetPoseParameterIndex(pAnimating) == -1) + Warning("%s: Pose parameter \"%s\" does not exist on %s\n", GetDebugName(), inputdata.value.String(), STRING(pAnimating->GetModelName())); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicModelInfo::InputSetPoseParameterValue( inputdata_t &inputdata ) +{ + CBaseAnimating *pAnimating = GetTarget(inputdata); + if (pAnimating && pAnimating->GetModelPtr()) + { + int index = GetPoseParameterIndex(pAnimating); + if (index != -1) + { + pAnimating->SetPoseParameter( index, inputdata.value.Float() ); + } + else + Warning("%s: Pose parameter \"%s\" does not exist on %s\n", GetDebugName(), STRING(m_iszPoseParameterName), STRING(pAnimating->GetModelName())); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicModelInfo::InputGetPoseParameter( inputdata_t &inputdata ) +{ + CBaseAnimating *pAnimating = GetTarget(inputdata); + if (pAnimating && pAnimating->GetModelPtr()) + { + int index = GetPoseParameterIndex(pAnimating); + if (index != -1) + { + m_OutPoseParameterValue.Set( pAnimating->GetPoseParameter( index ), pAnimating, this ); + } + else + Warning("%s: Pose parameter \"%s\" does not exist on %s\n", GetDebugName(), inputdata.value.String(), STRING(pAnimating->GetModelName())); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Checks and calculates an entity's position. +//----------------------------------------------------------------------------- +class CLogicEntityPosition : public CLogicalEntity +{ + DECLARE_CLASS( CLogicEntityPosition, CLogicalEntity ); +private: + EHANDLE m_hTarget; + + int m_iPositionType; + enum + { + POSITION_ORIGIN = 0, + POSITION_LOCAL, + POSITION_BBOX, + POSITION_EYES, + POSITION_EARS, + POSITION_ATTACHMENT, + }; + + // Something that accompanies the position type, like an attachment name. + string_t m_iszPositionParameter; + + CBaseEntity *GetTarget(CBaseEntity *pActivator, CBaseEntity *pCaller); + + Vector GetPosition(CBaseEntity *pEntity); + QAngle GetAngles(CBaseEntity *pEntity); + + // Inputs + void InputGetPosition( inputdata_t &inputdata ); + void InputSetPosition( inputdata_t &inputdata ); + void InputPredictPosition( inputdata_t &inputdata ); + + void InputSetTarget( inputdata_t &inputdata ) { BaseClass::InputSetTarget(inputdata); m_hTarget = NULL; } + + // Outputs + COutputVector m_OutPosition; + COutputVector m_OutAngles; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_entity_position, CLogicEntityPosition); + +BEGIN_DATADESC( CLogicEntityPosition ) + + // Keys + DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_iPositionType, FIELD_INTEGER, "PositionType" ), + DEFINE_KEYFIELD( m_iszPositionParameter, FIELD_STRING, "PositionParameter" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "GetPosition", InputGetPosition ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetPosition", InputSetPosition ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "PredictPosition", InputPredictPosition ), + + // Outputs + DEFINE_OUTPUT(m_OutPosition, "OutPosition"), + DEFINE_OUTPUT(m_OutAngles, "OutAngles"), + +END_DATADESC() + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +inline CBaseEntity *CLogicEntityPosition::GetTarget(CBaseEntity *pActivator, CBaseEntity *pCaller) +{ + // Always reset with procedurals + if (!m_hTarget || STRING(m_target)[0] == '!') + m_hTarget = gEntList.FindEntityByName(NULL, STRING(m_target), this, pActivator, pCaller); + return m_hTarget.Get(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CLogicEntityPosition::GetPosition(CBaseEntity *pEntity) +{ + switch (m_iPositionType) + { + case POSITION_ORIGIN: return pEntity->GetAbsOrigin(); + case POSITION_LOCAL: return pEntity->GetLocalOrigin(); + case POSITION_BBOX: return pEntity->WorldSpaceCenter(); + case POSITION_EYES: return pEntity->EyePosition(); + case POSITION_EARS: return pEntity->EarPosition(); + case POSITION_ATTACHMENT: + { + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + if (!pAnimating) + { + Warning("%s wants to measure one of %s's attachments, but %s doesn't support them!\n", GetDebugName(), pEntity->GetDebugName(), pEntity->GetDebugName()); + break; + } + + Vector vecPosition; + pAnimating->GetAttachment(STRING(m_iszPositionParameter), vecPosition); + return vecPosition; + } + } + + return vec3_origin; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +QAngle CLogicEntityPosition::GetAngles(CBaseEntity *pEntity) +{ + switch (m_iPositionType) + { + case POSITION_BBOX: + case POSITION_EARS: + case POSITION_ORIGIN: return pEntity->GetAbsAngles(); break; + case POSITION_LOCAL: return pEntity->GetLocalAngles(); break; + case POSITION_EYES: return pEntity->EyeAngles(); break; + case POSITION_ATTACHMENT: + { + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + if (!pAnimating) + { + Warning("%s wants to measure one of %s's attachments, but %s doesn't support them!\n", GetDebugName(), pEntity->GetDebugName(), pEntity->GetDebugName()); + break; + } + + QAngle AttachmentAngles; + matrix3x4_t attachmentToWorld; + pAnimating->GetAttachment( pAnimating->LookupAttachment( STRING( m_iszPositionParameter ) ), attachmentToWorld ); + MatrixAngles( attachmentToWorld, AttachmentAngles ); + return AttachmentAngles; + } break; + } + + return vec3_angle; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicEntityPosition::InputGetPosition( inputdata_t &inputdata ) +{ + CBaseEntity *pEntity = GetTarget(inputdata.pActivator, inputdata.pCaller); + if (!pEntity) + { + m_OutPosition.Set( vec3_origin, NULL, this ); + m_OutAngles.Set( vec3_angle, NULL, this ); + return; + } + + m_OutPosition.Set( GetPosition(pEntity), pEntity, this ); + m_OutAngles.Set( GetAngles(pEntity), pEntity, this ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicEntityPosition::InputSetPosition( inputdata_t &inputdata ) +{ + CBaseEntity *pEntity = GetTarget(inputdata.pActivator, inputdata.pCaller); + if (!pEntity) + { + Warning("%s can't find entity %s for SetPosition!\n", GetDebugName(), STRING(m_target)); + return; + } + + Vector vec; + inputdata.value.Vector3D(vec); + + // If the position is local, they might want to move local origin instead + if (m_iPositionType == POSITION_LOCAL) + pEntity->SetLocalOrigin(vec); + else + pEntity->SetAbsOrigin(vec); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CLogicEntityPosition::InputPredictPosition( inputdata_t &inputdata ) +{ + CBaseEntity *pEntity = GetTarget(inputdata.pActivator, inputdata.pCaller); + if (!pEntity) + { + m_OutPosition.Set( vec3_origin, NULL, this ); + m_OutAngles.Set( vec3_angle, NULL, this ); + return; + } + + Vector vecPosition; + UTIL_PredictedPosition(pEntity, GetPosition(pEntity), inputdata.value.Float(), &vecPosition); + + QAngle angAngles; + UTIL_PredictedAngles(pEntity, GetAngles(pEntity), inputdata.value.Float(), &angAngles); + + m_OutPosition.Set( vecPosition, pEntity, this ); + m_OutAngles.Set( angAngles, pEntity, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Accesses context values +//----------------------------------------------------------------------------- +class CLogicContextAccessor : public CLogicalEntity +{ + DECLARE_CLASS(CLogicContextAccessor, CLogicalEntity); + +public: + CBaseEntity *GetTarget(CBaseEntity *pCaller, CBaseEntity *pActivator); + + bool TestContext(CBaseEntity *pTarget, const char *szKeyName); + void SetContext(CBaseEntity *pTarget, const char *szKeyName, string_t szValue); + + // Inputs + void InputTest(inputdata_t &inputdata); + void InputTestContext(inputdata_t &inputdata); + void InputTestTarget(inputdata_t &inputdata); + + void InputSetContext(inputdata_t &inputdata); + + void InputSetValue(inputdata_t &inputdata); + + //bool ReadUnregisteredKeyfields(CBaseEntity *pTarget, const char *szKeyName, variant_t *variant); + + COutputString m_OutValue; + COutputEvent m_OnFailed; + + string_t m_iszContext; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(logic_context_accessor, CLogicContextAccessor); + + +BEGIN_DATADESC(CLogicContextAccessor) + +DEFINE_KEYFIELD( m_iszContext, FIELD_STRING, "context" ), + +// Inputs +DEFINE_INPUTFUNC(FIELD_VOID, "Test", InputTest), +DEFINE_INPUTFUNC(FIELD_STRING, "TestContext", InputTestContext), +DEFINE_INPUTFUNC(FIELD_STRING, "TestTarget", InputTestTarget), +DEFINE_INPUTFUNC(FIELD_STRING, "SetContext", InputSetContext), +DEFINE_INPUTFUNC(FIELD_STRING, "SetValue", InputSetValue), + +DEFINE_OUTPUT( m_OutValue, "OutValue" ), +DEFINE_OUTPUT( m_OnFailed, "OnFailed" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +inline CBaseEntity *CLogicContextAccessor::GetTarget(CBaseEntity *pCaller, CBaseEntity *pActivator) +{ + return gEntList.FindEntityByName(NULL, m_target, this, pActivator, pCaller); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CLogicContextAccessor::TestContext(CBaseEntity *pTarget, const char *szKeyName) +{ + int idx = pTarget->FindContextByName( szKeyName ); + if ( idx != -1 ) + { + m_OutValue.Set(FindPooledString(pTarget->GetContextValue(idx)), pTarget, this); + return true; + } + else + { + m_OnFailed.FireOutput(pTarget, this); + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicContextAccessor::SetContext(CBaseEntity *pTarget, const char *szKeyName, string_t szValue) +{ + pTarget->AddContext(szKeyName, STRING(szValue)); + + m_OutValue.Set(szValue, pTarget, this); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicContextAccessor::InputTest(inputdata_t &inputdata) +{ + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (pTarget && m_iszContext != NULL_STRING) + { + TestContext(pTarget, STRING(m_iszContext)); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicContextAccessor::InputTestContext(inputdata_t &inputdata) +{ + const char *input = inputdata.value.String(); + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (input && pTarget) + { + TestContext(pTarget, input); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicContextAccessor::InputTestTarget(inputdata_t &inputdata) +{ + m_target = inputdata.value.StringID(); + CBaseEntity *pTarget = gEntList.FindEntityByName(NULL, inputdata.value.StringID(), this, inputdata.pCaller, inputdata.pActivator); + if (pTarget && m_iszContext != NULL_STRING) + { + TestContext(pTarget, STRING(m_iszContext)); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicContextAccessor::InputSetContext(inputdata_t &inputdata) +{ + m_iszContext = inputdata.value.StringID(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicContextAccessor::InputSetValue(inputdata_t &inputdata) +{ + CBaseEntity *pTarget = GetTarget(inputdata.pCaller, inputdata.pActivator); + if (pTarget) + { + SetContext(pTarget, STRING(m_iszContext), inputdata.value.StringID()); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Replicates light pattern functionality. +//----------------------------------------------------------------------------- +class CMathLightPattern : public CLogicalEntity +{ + DECLARE_CLASS( CMathLightPattern, CLogicalEntity ); +private: + + + string_t m_iszPattern; + + bool m_bDisabled; + + void Spawn(); + bool KeyValue( const char *szKeyName, const char *szValue ); + + void OutputCurPattern(); + + void StartPatternThink(); + void PatternThink(); + unsigned char m_NextLetter = 0; + + // How fast the pattern should be + float m_flPatternSpeed = 0.1f; + + inline bool VerifyPatternValid() { return (m_iszPattern != NULL_STRING && STRING( m_iszPattern )[0] != '\0'); } + + // Inputs + void InputSetStyle( inputdata_t &inputdata ); + void InputSetPattern( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + // Outputs + COutputFloat m_OutValue; + COutputString m_OutLetter; + COutputEvent m_OnLightOn; + COutputEvent m_OnLightOff; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( math_lightpattern, CMathLightPattern ); + +BEGIN_DATADESC( CMathLightPattern ) + + // Keys + DEFINE_KEYFIELD(m_iszPattern, FIELD_STRING, "pattern"), + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_KEYFIELD(m_flPatternSpeed, FIELD_FLOAT, "PatternSpeed"), + + // Inputs + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetStyle", InputSetStyle ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetPattern", InputSetPattern ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + + // Outputs + DEFINE_OUTPUT(m_OutValue, "OutValue"), + DEFINE_OUTPUT(m_OutLetter, "OutLetter"), + DEFINE_OUTPUT(m_OnLightOn, "OnLightOn"), + DEFINE_OUTPUT(m_OnLightOff, "OnLightOff"), + + DEFINE_THINKFUNC( PatternThink ), + DEFINE_FIELD( m_NextLetter, FIELD_CHARACTER ), + +END_DATADESC() + +extern const char *GetDefaultLightstyleString( int styleIndex ); + +static const char *s_pLightPatternContext = "PatternContext"; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::Spawn() +{ + BaseClass::Spawn(); + + if (!m_bDisabled && VerifyPatternValid()) + StartPatternThink(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::OutputCurPattern() +{ + // This code looks messy, but it does what it's supposed to and is safe enough. + // First, we get the next letter in the pattern sequence. + // Next, we calculate its integral proximity to the character 'a' (fully dark) + // and calculate its approximate brightness by dividing it by the number of letters in the alphabet other than a. + // We output that brightness value for things like projected textures and other custom intensity values + // so they could replicate the patterns of their corresponding vrad lights. + char cLetter = STRING(m_iszPattern)[m_NextLetter]; + int iValue = (cLetter - 'a'); + float flResult = iValue != 0 ? ((float)iValue / 25.0f) : 0.0f; + m_OutValue.Set(flResult, this, this); + + // User-friendly "Light on, light off" outputs + if (flResult > 0) + m_OnLightOn.FireOutput(this, this); + else + m_OnLightOff.FireOutput(this, this); + + // Create a string with cLetter and a null terminator. + char szLetter[2] = { cLetter, '\0' }; + m_OutLetter.Set( AllocPooledString(szLetter), this, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::StartPatternThink() +{ + // Output our current/next one immediately. + OutputCurPattern(); + + // Start thinking now. + SetContextThink( &CMathLightPattern::PatternThink, gpGlobals->curtime, s_pLightPatternContext ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::PatternThink() +{ + // Output our current/next one + OutputCurPattern(); + + // Increment + m_NextLetter++; + if (STRING(m_iszPattern)[m_NextLetter] == '\0') + m_NextLetter = 0; + + //m_OutLetter.Set(AllocPooledString(UTIL_VarArgs("%c", m_NextLetter)), this, this); + + SetNextThink( gpGlobals->curtime + m_flPatternSpeed, s_pLightPatternContext ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CMathLightPattern::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "style" ) ) + { + m_iszPattern = AllocPooledString(GetDefaultLightstyleString(atoi(szValue))); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::InputSetStyle( inputdata_t &inputdata ) +{ + m_iszPattern = AllocPooledString(GetDefaultLightstyleString(inputdata.value.Int())); + m_NextLetter = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::InputSetPattern( inputdata_t &inputdata ) +{ + m_iszPattern = inputdata.value.StringID(); + m_NextLetter = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::InputEnable( inputdata_t &inputdata ) +{ + if (VerifyPatternValid()) + StartPatternThink(); + else + Warning("%s tried to enable without valid pattern\n", GetDebugName()); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::InputDisable( inputdata_t &inputdata ) +{ + SetContextThink( NULL, TICK_NEVER_THINK, s_pLightPatternContext ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathLightPattern::InputToggle( inputdata_t &inputdata ) +{ + if (GetNextThink(s_pLightPatternContext) != TICK_NEVER_THINK) + InputDisable(inputdata); + else + InputEnable(inputdata); +} + +//----------------------------------------------------------------------------- +// Purpose: Sequences for keypads, etc. +//----------------------------------------------------------------------------- +#define MAX_SEQUENCE_CASES 16 + +class CLogicSequence : public CLogicalEntity +{ + DECLARE_CLASS( CLogicSequence, CLogicalEntity ); +public: + CLogicSequence(); + + void Activate(); + + bool KeyValue( const char *szKeyName, const char *szValue ); + + void TestCase( int iCase, string_t iszValue, CBaseEntity *pActivator ); + void SequenceComplete( string_t iszValue, CBaseEntity *pActivator ); + +private: + string_t m_iszCase[MAX_SEQUENCE_CASES]; + int m_iNumCases; + + bool m_bDisabled; + + bool m_bDontIncrementOnPass; + + // Inputs + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputInValue( inputdata_t &inputdata ); + void InputSetCurrentCase( inputdata_t &inputdata ); + void InputSetCurrentCaseNoFire( inputdata_t &inputdata ); + void InputIncrementSequence( inputdata_t &inputdata ); + void InputResetSequence( inputdata_t &inputdata ); + + // Outputs + COutputInt m_CurCase; + COutputString m_OnCasePass; + COutputString m_OnCaseFail; + COutputString m_OnSequenceComplete; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( logic_sequence, CLogicSequence ); + + +BEGIN_DATADESC( CLogicSequence ) + + // Keys + DEFINE_KEYFIELD( m_iszCase[0], FIELD_STRING, "Case01" ), + DEFINE_KEYFIELD( m_iszCase[1], FIELD_STRING, "Case02" ), + DEFINE_KEYFIELD( m_iszCase[2], FIELD_STRING, "Case03" ), + DEFINE_KEYFIELD( m_iszCase[3], FIELD_STRING, "Case04" ), + DEFINE_KEYFIELD( m_iszCase[4], FIELD_STRING, "Case05" ), + DEFINE_KEYFIELD( m_iszCase[5], FIELD_STRING, "Case06" ), + DEFINE_KEYFIELD( m_iszCase[6], FIELD_STRING, "Case07" ), + DEFINE_KEYFIELD( m_iszCase[7], FIELD_STRING, "Case08" ), + DEFINE_KEYFIELD( m_iszCase[8], FIELD_STRING, "Case09" ), + DEFINE_KEYFIELD( m_iszCase[9], FIELD_STRING, "Case10" ), + DEFINE_KEYFIELD( m_iszCase[10], FIELD_STRING, "Case11" ), + DEFINE_KEYFIELD( m_iszCase[11], FIELD_STRING, "Case12" ), + DEFINE_KEYFIELD( m_iszCase[12], FIELD_STRING, "Case13" ), + DEFINE_KEYFIELD( m_iszCase[13], FIELD_STRING, "Case14" ), + DEFINE_KEYFIELD( m_iszCase[14], FIELD_STRING, "Case15" ), + DEFINE_KEYFIELD( m_iszCase[15], FIELD_STRING, "Case16" ), + + // This doesn't need to be saved, it can be assigned every Activate() + //DEFINE_FIELD( m_iNumCases, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_KEYFIELD( m_bDontIncrementOnPass, FIELD_BOOLEAN, "DontIncrementOnPass" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_STRING, "InValue", InputInValue ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetCurrentCase", InputSetCurrentCase ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetCurrentCaseNoFire", InputSetCurrentCaseNoFire ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "IncrementSequence", InputIncrementSequence ), + DEFINE_INPUTFUNC( FIELD_VOID, "ResetSequence", InputResetSequence ), + + // Outputs + DEFINE_OUTPUT( m_CurCase, "OutCurCase" ), + DEFINE_OUTPUT( m_OnCasePass, "OnCasePass" ), + DEFINE_OUTPUT( m_OnCaseFail, "OnCaseFail" ), + DEFINE_OUTPUT( m_OnSequenceComplete, "OnSequenceComplete" ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CLogicSequence::CLogicSequence() +{ + m_CurCase.Init( 1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::Activate( void ) +{ + BaseClass::Activate(); + + // Count number of cases + for (m_iNumCases = 0; m_iNumCases < MAX_SEQUENCE_CASES; m_iNumCases++) + { + if (m_iszCase[m_iNumCases] == NULL_STRING) + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Cache user entity field values until spawn is called. +// Input : szKeyName - Key to handle. +// szValue - Value for key. +// Output : Returns true if the key was handled, false if not. +//----------------------------------------------------------------------------- +bool CLogicSequence::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq( szKeyName, "StartCase" )) + { + m_CurCase.Init( atoi(szValue) ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::TestCase( int iCase, string_t iszValue, CBaseEntity *pActivator ) +{ + if (m_bDisabled) + { + DevMsg("%s ignoring case test because it is disabled\n", GetDebugName()); + return; + } + + // Arrays are 0-based, so the index is (iCase - 1) + int iIndex = iCase - 1; + if (iIndex >= m_iNumCases) + { + DevMsg("%s ignoring case test because the current case %i is greater than or equal to the number of cases %i\n", GetDebugName(), iCase, m_iNumCases); + return; + } + + if (Matcher_Match( STRING( m_iszCase[iIndex] ), STRING(iszValue) )) + { + m_OnCasePass.Set( iszValue, pActivator, this ); + + if (!m_bDontIncrementOnPass) + { + m_CurCase.Set(iCase + 1, pActivator, this); + + if (m_CurCase.Get() > m_iNumCases) + { + // Sequence complete! + SequenceComplete(iszValue, pActivator); + } + } + else + { + m_CurCase.Set(iCase, pActivator, this); + } + } + else + { + m_OnCaseFail.Set( iszValue, pActivator, this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::SequenceComplete( string_t iszValue, CBaseEntity *pActivator ) +{ + m_OnSequenceComplete.Set( iszValue, pActivator, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputToggle( inputdata_t &inputdata ) +{ + m_bDisabled = (m_bDisabled == false); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputInValue( inputdata_t &inputdata ) +{ + TestCase( m_CurCase.Get(), inputdata.value.StringID(), inputdata.pActivator ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputSetCurrentCase( inputdata_t &inputdata ) +{ + m_CurCase.Set( inputdata.value.Int(), inputdata.pActivator, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputSetCurrentCaseNoFire( inputdata_t &inputdata ) +{ + m_CurCase.Init( inputdata.value.Int() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputIncrementSequence( inputdata_t &inputdata ) +{ + int iInc = inputdata.value.Int(); + m_CurCase.Set( m_CurCase.Get() + (iInc != 0 ? iInc : 1), inputdata.pActivator, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CLogicSequence::InputResetSequence( inputdata_t &inputdata ) +{ + m_CurCase.Set( 1, inputdata.pActivator, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Generates various types of numbers based on existing material proxies +//----------------------------------------------------------------------------- +class CMathGenerate : public CLogicalEntity +{ +public: + DECLARE_CLASS( CMathGenerate, CLogicalEntity ); + CMathGenerate(); + + enum GenerateType_t + { + GENERATE_SINE_WAVE, + GENERATE_LINEAR_RAMP, + GENERATE_UNIFORM_NOISE, + GENERATE_GAUSSIAN_NOISE, + GENERATE_EXPONENTIAL, + }; + + // Keys + float m_flMax; + float m_flMin; + + float m_flParam1; + float m_flParam2; + + bool m_bDisabled; + + GenerateType_t m_iGenerateType; + + // Inputs + void InputSetValue( inputdata_t &inputdata ); + void InputSetValueNoFire( inputdata_t &inputdata ); + void InputGetValue( inputdata_t &inputdata ); + void InputSetGenerateType( inputdata_t &inputdata ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + + void UpdateOutValue( float fNewValue, CBaseEntity *pActivator = NULL ); + void UpdateOutValueSine( float fNewValue, CBaseEntity *pActivator = NULL ); + + // Basic functions + void Spawn(); + bool KeyValue( const char *szKeyName, const char *szValue ); + + void StartGenerating(); + void StopGenerating(); + + // Number generation functions + void GenerateSineWave(); + void GenerateLinearRamp(); + void GenerateUniformNoise(); + void GenerateGaussianNoise(); + void GenerateExponential(); + + // The gaussian stream normally only exists on the client, so we use our own. + static CGaussianRandomStream m_GaussianStream; + + bool m_bHitMin; // Set when we reach or go below our minimum value, cleared if we go above it again. + bool m_bHitMax; // Set when we reach or exceed our maximum value, cleared if we fall below it again. + + // Outputs + COutputFloat m_OutValue; + COutputFloat m_OnGetValue; // Used for polling the counter value. + COutputEvent m_OnHitMin; + COutputEvent m_OnHitMax; + COutputEvent m_OnChangedFromMin; + COutputEvent m_OnChangedFromMax; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( math_generate, CMathGenerate ); + + +BEGIN_DATADESC( CMathGenerate ) + + DEFINE_INPUT( m_flMax, FIELD_FLOAT, "SetHitMax" ), + DEFINE_INPUT( m_flMin, FIELD_FLOAT, "SetHitMin" ), + DEFINE_INPUT( m_flParam1, FIELD_FLOAT, "SetParam1" ), + DEFINE_INPUT( m_flParam2, FIELD_FLOAT, "SetParam2" ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_KEYFIELD( m_iGenerateType, FIELD_INTEGER, "GenerateType" ), + + DEFINE_FIELD( m_bHitMax, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bHitMin, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetValue", InputSetValue ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetValueNoFire", InputSetValueNoFire ), + DEFINE_INPUTFUNC( FIELD_VOID, "GetValue", InputGetValue ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetGenerateType", InputSetGenerateType ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + + DEFINE_OUTPUT( m_OutValue, "OutValue" ), + DEFINE_OUTPUT( m_OnHitMin, "OnHitMin" ), + DEFINE_OUTPUT( m_OnHitMax, "OnHitMax" ), + DEFINE_OUTPUT( m_OnGetValue, "OnGetValue" ), + DEFINE_OUTPUT( m_OnChangedFromMin, "OnChangedFromMin" ), + DEFINE_OUTPUT( m_OnChangedFromMax, "OnChangedFromMax" ), + + DEFINE_THINKFUNC( GenerateSineWave ), + DEFINE_THINKFUNC( GenerateLinearRamp ), + DEFINE_THINKFUNC( GenerateUniformNoise ), + DEFINE_THINKFUNC( GenerateGaussianNoise ), + DEFINE_THINKFUNC( GenerateExponential ), + +END_DATADESC() + +CGaussianRandomStream CMathGenerate::m_GaussianStream; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CMathGenerate::CMathGenerate() +{ + m_GaussianStream.AttachToStream( random ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMathGenerate::Spawn() +{ + BaseClass::Spawn(); + + if (!m_bDisabled) + StartGenerating(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CMathGenerate::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq( szKeyName, "InitialValue" )) + { + m_OutValue.Init( atof(szValue) ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::InputSetValue( inputdata_t &inputdata ) +{ + UpdateOutValue(inputdata.value.Float(), inputdata.pActivator); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::InputSetValueNoFire( inputdata_t &inputdata ) +{ + m_OutValue.Init(inputdata.value.Float()); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::InputGetValue( inputdata_t &inputdata ) +{ + float flOutValue = m_OutValue.Get(); + m_OnGetValue.Set( flOutValue, inputdata.pActivator, inputdata.pCaller ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::InputSetGenerateType( inputdata_t &inputdata ) +{ + m_iGenerateType = (GenerateType_t)inputdata.value.Int(); + + if (GetNextThink() != TICK_NEVER_THINK) + { + // Change our generation function if we're already generating. + // StartGenerating() should set to the new function. + StartGenerating(); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; + StartGenerating(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; + StopGenerating(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::InputToggle( inputdata_t &inputdata ) +{ + m_bDisabled ? InputEnable(inputdata) : InputDisable(inputdata); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the value to the new value, clamping and firing the output value. +// Input : fNewValue - Value to set. +//----------------------------------------------------------------------------- +void CMathGenerate::UpdateOutValue( float fNewValue, CBaseEntity *pActivator ) +{ + if ((m_flMin != 0) || (m_flMax != 0)) + { + // + // Fire an output any time we reach or exceed our maximum value. + // + if ( fNewValue >= m_flMax || (m_iGenerateType == GENERATE_SINE_WAVE && fNewValue >= (m_flMax * 0.995f)) ) + { + if ( !m_bHitMax ) + { + m_bHitMax = true; + m_OnHitMax.FireOutput( pActivator, this ); + } + } + else + { + // Fire an output if we just changed from the maximum value + if ( m_OutValue.Get() == m_flMax ) + { + m_OnChangedFromMax.FireOutput( pActivator, this ); + } + + m_bHitMax = false; + } + + // + // Fire an output any time we reach or go below our minimum value. + // + if ( fNewValue <= m_flMin ) + { + if ( !m_bHitMin ) + { + m_bHitMin = true; + m_OnHitMin.FireOutput( pActivator, this ); + } + } + else + { + // Fire an output if we just changed from the maximum value + if ( m_OutValue.Get() == m_flMin ) + { + m_OnChangedFromMin.FireOutput( pActivator, this ); + } + + m_bHitMin = false; + } + + fNewValue = clamp(fNewValue, m_flMin, m_flMax); + } + + m_OutValue.Set(fNewValue, pActivator, this); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the value to the new value, clamping and firing the output value. +// Sine generation needs to use a different function to account for skips and imprecision. +// Input : fNewValue - Value to set. +//----------------------------------------------------------------------------- +void CMathGenerate::UpdateOutValueSine( float fNewValue, CBaseEntity *pActivator ) +{ + if ((m_flMin != 0) || (m_flMax != 0)) + { + // + // Fire an output any time we reach or exceed our maximum value. + // + if ( fNewValue >= (m_flMax * 0.995f) ) + { + if ( !m_bHitMax ) + { + m_bHitMax = true; + m_OnHitMax.FireOutput( pActivator, this ); + } + } + else + { + // Fire an output if we just changed from the maximum value + if ( m_bHitMax ) + { + m_OnChangedFromMax.FireOutput( pActivator, this ); + } + + m_bHitMax = false; + } + + // + // Fire an output any time we reach or go below our minimum value. + // + if ( fNewValue <= (m_flMin * 1.005f) ) + { + if ( !m_bHitMin ) + { + m_bHitMin = true; + m_OnHitMin.FireOutput( pActivator, this ); + } + } + else + { + // Fire an output if we just changed from the maximum value + if ( m_bHitMin ) + { + m_OnChangedFromMin.FireOutput( pActivator, this ); + } + + m_bHitMin = false; + } + + //fNewValue = clamp(fNewValue, m_flMin, m_flMax); + } + + m_OutValue.Set(fNewValue, pActivator, this); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::StartGenerating() +{ + // Correct any min/max quirks here + if (m_flMin > m_flMax) + { + float flTemp = m_flMin; + m_flMin = m_flMax; + m_flMax = flTemp; + } + + switch (m_iGenerateType) + { + case GENERATE_SINE_WAVE: + SetThink( &CMathGenerate::GenerateSineWave ); + break; + case GENERATE_LINEAR_RAMP: + SetThink( &CMathGenerate::GenerateLinearRamp ); + break; + case GENERATE_UNIFORM_NOISE: + SetThink( &CMathGenerate::GenerateUniformNoise ); + break; + case GENERATE_GAUSSIAN_NOISE: + SetThink( &CMathGenerate::GenerateGaussianNoise ); + break; + case GENERATE_EXPONENTIAL: + SetThink( &CMathGenerate::GenerateExponential ); + break; + + default: + Warning("%s is set to invalid generation type %i! It won't do anything now.\n", GetDebugName(), m_iGenerateType); + StopGenerating(); + return; + } + + // All valid types should fall through to this + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::StopGenerating() +{ + SetThink(NULL); + SetNextThink( TICK_NEVER_THINK ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::GenerateSineWave() +{ + // CSineProxy in mathproxy.cpp + float flSineTimeOffset = m_flParam2; + float flSinePeriod = m_flParam1; + float flValue; + + if (flSinePeriod == 0) + flSinePeriod = 1; + + // get a value in [0,1] + flValue = ( sin( 2.0f * M_PI * (gpGlobals->curtime - flSineTimeOffset) / flSinePeriod ) * 0.5f ) + 0.5f; + // get a value in [min,max] + flValue = ( m_flMax - m_flMin ) * flValue + m_flMin; + + UpdateOutValueSine( flValue ); + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::GenerateLinearRamp() +{ + // CLinearRampProxy in mathproxy.cpp + + // Param1 = rate + float flVal = m_flParam1 * gpGlobals->curtime + m_OutValue.Get(); + + // clamp + if (flVal < m_flMin) + flVal = m_flMin; + else if (flVal > m_flMax) + flVal = m_flMax; + + UpdateOutValue( flVal ); + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::GenerateUniformNoise() +{ + // CUniformNoiseProxy in mathproxy.cpp + + UpdateOutValue( random->RandomFloat( m_flMin, m_flMax ) ); + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::GenerateGaussianNoise() +{ + // CGaussianNoiseProxy in mathproxy.cpp + + float flMean = m_flParam1; + float flStdDev = m_flParam2; + float flVal = m_GaussianStream.RandomFloat( flMean, flStdDev ); + + // clamp + if (flVal < m_flMin) + flVal = m_flMin; + else if (flVal > m_flMax) + flVal = m_flMax; + + UpdateOutValue( flVal ); + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CMathGenerate::GenerateExponential() +{ + // CExponentialProxy in mathproxy.cpp + + // Param1 = scale + // Param2 = offset + float flVal = m_flParam1 * exp( m_OutValue.Get() + m_flParam2 ); + + // clamp + if (flVal < m_flMin) + flVal = m_flMin; + else if (flVal > m_flMax) + flVal = m_flMax; + + UpdateOutValue( flVal ); + + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} +#endif diff --git a/sp/src/game/server/logicrelay.cpp b/sp/src/game/server/logicrelay.cpp new file mode 100644 index 00000000..6daa15f4 --- /dev/null +++ b/sp/src/game/server/logicrelay.cpp @@ -0,0 +1,480 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// A message forwarder. Fires an OnTrigger output when triggered, and can be +// disabled to prevent forwarding outputs. +// +// Useful as an intermediary between one entity and another for turning on or +// off an I/O connection, or as a container for holding a set of outputs that +// can be triggered from multiple places. +// +//============================================================================= + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "soundent.h" +#include "logicrelay.h" +#ifdef MAPBASE +#include "mapbase/variant_tools.h" +#include "saverestore_utlvector.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +const int SF_REMOVE_ON_FIRE = 0x001; // Relay will remove itself after being triggered. +const int SF_ALLOW_FAST_RETRIGGER = 0x002; // Unless set, relay will disable itself until the last output is sent. + +LINK_ENTITY_TO_CLASS(logic_relay, CLogicRelay); + + +BEGIN_DATADESC( CLogicRelay ) + + DEFINE_FIELD(m_bWaitForRefire, FIELD_BOOLEAN), + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled"), +#if RELAY_QUEUE_SYSTEM + DEFINE_KEYFIELD(m_bQueueTrigger, FIELD_BOOLEAN, "QueueDisabledTrigger"), + DEFINE_FIELD(m_bQueueWaiting, FIELD_BOOLEAN), + DEFINE_FIELD(m_flRefireTime, FIELD_TIME), +#endif + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable), + DEFINE_INPUTFUNC(FIELD_VOID, "EnableRefire", InputEnableRefire), + DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + DEFINE_INPUTFUNC(FIELD_VOID, "Trigger", InputTrigger), +#ifdef MAPBASE + DEFINE_INPUTFUNC(FIELD_INPUT, "TriggerWithParameter", InputTriggerWithParameter), +#endif + DEFINE_INPUTFUNC(FIELD_VOID, "CancelPending", InputCancelPending), + + // Outputs + DEFINE_OUTPUT(m_OnTrigger, "OnTrigger"), +#ifdef MAPBASE + DEFINE_OUTPUT(m_OnTriggerParameter, "OnTriggerParameter"), +#endif + DEFINE_OUTPUT(m_OnSpawn, "OnSpawn"), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Constructor. +//----------------------------------------------------------------------------- +CLogicRelay::CLogicRelay(void) +{ +} + + +//------------------------------------------------------------------------------ +// Kickstarts a think if we have OnSpawn connections. +//------------------------------------------------------------------------------ +void CLogicRelay::Activate() +{ + BaseClass::Activate(); + + if ( m_OnSpawn.NumberOfElements() > 0) + { + SetNextThink( gpGlobals->curtime + 0.01 ); + } +} + + +//----------------------------------------------------------------------------- +// If we have OnSpawn connections, this is called shortly after spawning to +// fire the OnSpawn output. +//----------------------------------------------------------------------------- +void CLogicRelay::Think() +{ + // Fire an output when we spawn. This is used for self-starting an entity + // template -- since the logic_relay is inside the template, it gets all the + // name and I/O connection fixup, so can target other entities in the template. + m_OnSpawn.FireOutput( this, this ); + + // We only get here if we had OnSpawn connections, so this is safe. + if ( m_spawnflags & SF_REMOVE_ON_FIRE ) + { + UTIL_Remove(this); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns on the relay, allowing it to fire outputs. +//------------------------------------------------------------------------------ +void CLogicRelay::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; + +#if RELAY_QUEUE_SYSTEM + if (m_bQueueWaiting) + m_OnTrigger.FireOutput( inputdata.pActivator, this ); +#endif +} + +//------------------------------------------------------------------------------ +// Purpose: Enables us to fire again. This input is only posted from our Trigger +// function to prevent rapid refire. +//------------------------------------------------------------------------------ +void CLogicRelay::InputEnableRefire( inputdata_t &inputdata ) +{ + m_bWaitForRefire = false; +} + + +//------------------------------------------------------------------------------ +// Purpose: Cancels any I/O events in the queue that were fired by us. +//------------------------------------------------------------------------------ +void CLogicRelay::InputCancelPending( inputdata_t &inputdata ) +{ + g_EventQueue.CancelEvents( this ); + + // Stop waiting; allow another Trigger. + m_bWaitForRefire = false; +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns off the relay, preventing it from firing outputs. +//------------------------------------------------------------------------------ +void CLogicRelay::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + + +//------------------------------------------------------------------------------ +// Purpose: Toggles the enabled/disabled state of the relay. +//------------------------------------------------------------------------------ +void CLogicRelay::InputToggle( inputdata_t &inputdata ) +{ + m_bDisabled = !m_bDisabled; + +#if RELAY_QUEUE_SYSTEM + if (m_bQueueWaiting) + m_OnTrigger.FireOutput( inputdata.pActivator, this ); +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that triggers the relay. +//----------------------------------------------------------------------------- +void CLogicRelay::InputTrigger( inputdata_t &inputdata ) +{ + if ((!m_bDisabled) && (!m_bWaitForRefire)) + { + m_OnTrigger.FireOutput( inputdata.pActivator, this ); + + if (m_spawnflags & SF_REMOVE_ON_FIRE) + { + UTIL_Remove(this); + } + else if (!(m_spawnflags & SF_ALLOW_FAST_RETRIGGER)) + { + // + // Disable the relay so that it cannot be refired until after the last output + // has been fired and post an input to re-enable ourselves. + // + m_bWaitForRefire = true; + g_EventQueue.AddEvent(this, "EnableRefire", m_OnTrigger.GetMaxDelay() + 0.001, this, this); +#if RELAY_QUEUE_SYSTEM + if (m_bQueueTrigger) + m_flRefireTime = gpGlobals->curtime + m_OnTrigger.GetMaxDelay() + 0.002; +#endif + } + } +#if RELAY_QUEUE_SYSTEM + else if (m_bQueueTrigger) + { + if (m_bDisabled) + m_bQueueWaiting = true; + else // m_bWaitForRefire + m_OnTrigger.FireOutput( inputdata.pActivator, this, (gpGlobals->curtime - m_flRefireTime) ); + } +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler that triggers the relay. +//----------------------------------------------------------------------------- +void CLogicRelay::InputTriggerWithParameter( inputdata_t &inputdata ) +{ + if ((!m_bDisabled) && (!m_bWaitForRefire)) + { + m_OnTriggerParameter.Set( inputdata.value, inputdata.pActivator, this ); + + if (m_spawnflags & SF_REMOVE_ON_FIRE) + { + UTIL_Remove(this); + } + else if (!(m_spawnflags & SF_ALLOW_FAST_RETRIGGER)) + { + // + // Disable the relay so that it cannot be refired until after the last output + // has been fired and post an input to re-enable ourselves. + // + m_bWaitForRefire = true; + g_EventQueue.AddEvent(this, "EnableRefire", m_OnTrigger.GetMaxDelay() + 0.001, this, this); + } + } +} +#endif + +#ifdef MAPBASE + +BEGIN_SIMPLE_DATADESC( LogicRelayQueueInfo_t ) + + DEFINE_FIELD( TriggerWithParameter, FIELD_BOOLEAN ), + DEFINE_FIELD( pActivator, FIELD_CLASSPTR ), + DEFINE_VARIANT( value ), + DEFINE_FIELD( outputID, FIELD_INTEGER ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS(logic_relay_queue, CLogicRelayQueue); + + +BEGIN_DATADESC( CLogicRelayQueue ) + + DEFINE_FIELD(m_bWaitForRefire, FIELD_BOOLEAN), + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled"), + + DEFINE_INPUT(m_iMaxQueueItems, FIELD_INTEGER, "SetMaxQueueItems"), + DEFINE_KEYFIELD(m_bDontQueueWhenDisabled, FIELD_BOOLEAN, "DontQueueWhenDisabled"), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable), + DEFINE_INPUTFUNC(FIELD_VOID, "EnableRefire", InputEnableRefire), + DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + DEFINE_INPUTFUNC(FIELD_VOID, "Trigger", InputTrigger), + DEFINE_INPUTFUNC(FIELD_INPUT, "TriggerWithParameter", InputTriggerWithParameter), + DEFINE_INPUTFUNC(FIELD_VOID, "CancelPending", InputCancelPending), + DEFINE_INPUTFUNC(FIELD_VOID, "ClearQueue", InputClearQueue), + + // Outputs + DEFINE_OUTPUT(m_OnTrigger, "OnTrigger"), + DEFINE_OUTPUT(m_OnTriggerParameter, "OnTriggerParameter"), + + DEFINE_UTLVECTOR(m_QueueItems, FIELD_EMBEDDED), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Constructor. +//----------------------------------------------------------------------------- +CLogicRelayQueue::CLogicRelayQueue(void) +{ +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns on the relay, allowing it to fire outputs. +//------------------------------------------------------------------------------ +void CLogicRelayQueue::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; + + if (!m_bWaitForRefire && m_QueueItems.Count() > 0) + HandleNextQueueItem(); +} + +//------------------------------------------------------------------------------ +// Purpose: Enables us to fire again. This input is only posted from our Trigger +// function to prevent rapid refire. +//------------------------------------------------------------------------------ +void CLogicRelayQueue::InputEnableRefire( inputdata_t &inputdata ) +{ + m_bWaitForRefire = false; + + if (!m_bDisabled && m_QueueItems.Count() > 0) + HandleNextQueueItem(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Cancels any I/O events in the queue that were fired by us. +//------------------------------------------------------------------------------ +void CLogicRelayQueue::InputCancelPending( inputdata_t &inputdata ) +{ + g_EventQueue.CancelEvents( this ); + + // Stop waiting; allow another Trigger. + m_bWaitForRefire = false; + + if (!m_bDisabled && m_QueueItems.Count() > 0) + HandleNextQueueItem(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Clears the queue. +//------------------------------------------------------------------------------ +void CLogicRelayQueue::InputClearQueue( inputdata_t &inputdata ) +{ + m_QueueItems.RemoveAll(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Turns off the relay, preventing it from firing outputs. +//------------------------------------------------------------------------------ +void CLogicRelayQueue::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + + +//------------------------------------------------------------------------------ +// Purpose: Toggles the enabled/disabled state of the relay. +//------------------------------------------------------------------------------ +void CLogicRelayQueue::InputToggle( inputdata_t &inputdata ) +{ + m_bDisabled = !m_bDisabled; + + if (!m_bDisabled && !m_bWaitForRefire && m_QueueItems.Count() > 0) + HandleNextQueueItem(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that triggers the relay. +//----------------------------------------------------------------------------- +void CLogicRelayQueue::InputTrigger( inputdata_t &inputdata ) +{ + if ((!m_bDisabled) && (!m_bWaitForRefire)) + { + m_OnTrigger.FireOutput( inputdata.pActivator, this ); + + // + // Disable the relay so that it cannot be refired until after the last output + // has been fired and post an input to re-enable ourselves. + // + m_bWaitForRefire = true; + g_EventQueue.AddEvent(this, "EnableRefire", m_OnTrigger.GetMaxDelay() + 0.001, this, this); + } + else if ( (!m_bDisabled || !m_bDontQueueWhenDisabled) && (m_QueueItems.Count() < m_iMaxQueueItems) ) + { + AddQueueItem(inputdata.pActivator, inputdata.nOutputID); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler that triggers the relay. +//----------------------------------------------------------------------------- +void CLogicRelayQueue::InputTriggerWithParameter( inputdata_t &inputdata ) +{ + if ((!m_bDisabled) && (!m_bWaitForRefire)) + { + m_OnTriggerParameter.Set( inputdata.value, inputdata.pActivator, this ); + + // + // Disable the relay so that it cannot be refired until after the last output + // has been fired and post an input to re-enable ourselves. + // + m_bWaitForRefire = true; + g_EventQueue.AddEvent(this, "EnableRefire", m_OnTrigger.GetMaxDelay() + 0.001, this, this); + } + else if ( (!m_bDisabled || !m_bDontQueueWhenDisabled) && (m_QueueItems.Count() < m_iMaxQueueItems) ) + { + AddQueueItem(inputdata.pActivator, inputdata.nOutputID, inputdata.value); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handles next queue item. +//----------------------------------------------------------------------------- +void CLogicRelayQueue::HandleNextQueueItem() +{ + LogicRelayQueueInfo_t info = m_QueueItems.Element(0); + + //if (!info.TriggerWithParameter) + //{ + // m_OnTrigger.FireOutput(info.pActivator, this); + //} + //else + //{ + // m_OnTriggerParameter.Set(info.value, info.pActivator, this); + //} + + AcceptInput(info.TriggerWithParameter ? "TriggerWithParameter" : "Trigger", info.pActivator, this, info.value, info.outputID); + + m_QueueItems.Remove(0); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a queue item. +//----------------------------------------------------------------------------- +void CLogicRelayQueue::AddQueueItem(CBaseEntity *pActivator, int outputID, variant_t &value) +{ + LogicRelayQueueInfo_t info; + info.pActivator = pActivator; + info.outputID = outputID; + + info.value = value; + info.TriggerWithParameter = true; + + m_QueueItems.AddToTail(info); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a queue item without a parameter. +//----------------------------------------------------------------------------- +void CLogicRelayQueue::AddQueueItem(CBaseEntity *pActivator, int outputID) +{ + LogicRelayQueueInfo_t info; + info.pActivator = pActivator; + info.outputID = outputID; + + info.TriggerWithParameter = false; + + m_QueueItems.AddToTail(info); +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CLogicRelayQueue::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + // -------------- + // Print Target + // -------------- + char tempstr[255]; + + if (m_QueueItems.Count() > 0) + { + Q_snprintf(tempstr, sizeof(tempstr), "Queue Items: %i (%i)", m_QueueItems.Count(), m_iMaxQueueItems); + EntityText(text_offset, tempstr, 0); + text_offset++; + + for (int i = 0; i < m_QueueItems.Count(); i++) + { + Q_snprintf(tempstr, sizeof(tempstr), " Input: %s, Activator: %s, Output ID: %i", + m_QueueItems[i].TriggerWithParameter ? "TriggerWithParameter" : "Trigger", + m_QueueItems[i].pActivator ? m_QueueItems[i].pActivator->GetDebugName() : "None", + m_QueueItems[i].outputID); + EntityText(text_offset, tempstr, 0); + text_offset++; + } + } + else + { + Q_snprintf(tempstr, sizeof(tempstr), "Queue Items: 0 (%i)", m_iMaxQueueItems); + EntityText(text_offset, tempstr, 0); + text_offset++; + } + } + return text_offset; +} +#endif + diff --git a/sp/src/game/server/logicrelay.h b/sp/src/game/server/logicrelay.h new file mode 100644 index 00000000..00791579 --- /dev/null +++ b/sp/src/game/server/logicrelay.h @@ -0,0 +1,125 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef LOGICRELAY_H +#define LOGICRELAY_H + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" + +#ifdef MAPBASE + +// I was originally going to add something similar to that queue thing to logic_relay directly, but I later decided to relegate it to a derivative entity. +#define RELAY_QUEUE_SYSTEM 0 + +#endif + +class CLogicRelay : public CLogicalEntity +{ +public: + DECLARE_CLASS( CLogicRelay, CLogicalEntity ); + + CLogicRelay(); + + void Activate(); + void Think(); + + // Input handlers + void InputEnable( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputEnableRefire( inputdata_t &inputdata ); +#else + void InputEnableRefire( inputdata_t &inputdata ); // Private input handler, not in FGD +#endif + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputTrigger( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputTriggerWithParameter( inputdata_t &inputdata ); +#endif + void InputCancelPending( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + // Outputs + COutputEvent m_OnTrigger; +#ifdef MAPBASE + COutputVariant m_OnTriggerParameter; +#endif + COutputEvent m_OnSpawn; + + bool IsDisabled( void ){ return m_bDisabled; } + +private: + + bool m_bDisabled; + bool m_bWaitForRefire; // Set to disallow a refire while we are waiting for our outputs to finish firing. +#if RELAY_QUEUE_SYSTEM + bool m_bQueueTrigger; + bool m_bQueueWaiting; + float m_flRefireTime; +#endif +}; + +#ifdef MAPBASE +struct LogicRelayQueueInfo_t +{ + DECLARE_SIMPLE_DATADESC(); + + bool TriggerWithParameter; + CBaseEntity *pActivator; + variant_t value; + int outputID; +}; + +//#define LOGIC_RELAY_QUEUE_LIMIT 16 + +class CLogicRelayQueue : public CLogicalEntity +{ +public: + DECLARE_CLASS( CLogicRelayQueue, CLogicalEntity ); + + CLogicRelayQueue(); + + // Input handlers + void InputEnable( inputdata_t &inputdata ); + void InputEnableRefire( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputTrigger( inputdata_t &inputdata ); + void InputTriggerWithParameter( inputdata_t &inputdata ); + void InputCancelPending( inputdata_t &inputdata ); + void InputClearQueue( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + // Outputs + COutputEvent m_OnTrigger; + COutputVariant m_OnTriggerParameter; + + bool IsDisabled( void ){ return m_bDisabled; } + + void HandleNextQueueItem(); + void AddQueueItem(CBaseEntity *pActivator, int outputID, variant_t &value); + void AddQueueItem(CBaseEntity *pActivator, int outputID); + + int DrawDebugTextOverlays( void ); + +private: + + bool m_bDisabled; + bool m_bWaitForRefire; // Set to disallow a refire while we are waiting for our outputs to finish firing. + + int m_iMaxQueueItems; + bool m_bDontQueueWhenDisabled; // Don't add to queue while disabled, only when waiting for refire + CUtlVector m_QueueItems; +}; +#endif + +#endif //LOGICRELAY_H diff --git a/sp/src/game/server/mapentities.cpp b/sp/src/game/server/mapentities.cpp new file mode 100644 index 00000000..0ee82206 --- /dev/null +++ b/sp/src/game/server/mapentities.cpp @@ -0,0 +1,601 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Controls the loading, parsing and creation of the entities from the BSP. +// +//=============================================================================// + +#include "cbase.h" +#include "entitylist.h" +#include "mapentities_shared.h" +#include "soundent.h" +#include "TemplateEntities.h" +#include "point_template.h" +#include "ai_initutils.h" +#include "lights.h" +#include "mapentities.h" +#include "wcedit.h" +#include "stringregistry.h" +#include "datacache/imdlcache.h" +#include "world.h" +#include "toolframework/iserverenginetools.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +struct HierarchicalSpawnMapData_t +{ + const char *m_pMapData; + int m_iMapDataLength; +}; + +static CStringRegistry *g_pClassnameSpawnPriority = NULL; +extern edict_t *g_pForceAttachEdict; + +// creates an entity by string name, but does not spawn it +CBaseEntity *CreateEntityByName( const char *className, int iForceEdictIndex ) +{ + if ( iForceEdictIndex != -1 ) + { + g_pForceAttachEdict = engine->CreateEdict( iForceEdictIndex ); + if ( !g_pForceAttachEdict ) + Error( "CreateEntityByName( %s, %d ) - CreateEdict failed.", className, iForceEdictIndex ); + } + + IServerNetworkable *pNetwork = EntityFactoryDictionary()->Create( className ); + g_pForceAttachEdict = NULL; + + if ( !pNetwork ) + return NULL; + + CBaseEntity *pEntity = pNetwork->GetBaseEntity(); + Assert( pEntity ); + return pEntity; +} + +CBaseNetworkable *CreateNetworkableByName( const char *className ) +{ + IServerNetworkable *pNetwork = EntityFactoryDictionary()->Create( className ); + if ( !pNetwork ) + return NULL; + + CBaseNetworkable *pNetworkable = pNetwork->GetBaseNetworkable(); + Assert( pNetworkable ); + return pNetworkable; +} + +void FreeContainingEntity( edict_t *ed ) +{ + if ( ed ) + { + CBaseEntity *ent = GetContainingEntity( ed ); + if ( ent ) + { + ed->SetEdict( NULL, false ); + CBaseEntity::PhysicsRemoveTouchedList( ent ); + CBaseEntity::PhysicsRemoveGroundList( ent ); + UTIL_RemoveImmediate( ent ); + } + } +} + +// parent name may have a , in it to include an attachment point +string_t ExtractParentName(string_t parentName) +{ + if ( !strchr(STRING(parentName), ',') ) + return parentName; + + char szToken[256]; + nexttoken(szToken, STRING(parentName), ',', sizeof(szToken)); + return AllocPooledString(szToken); +} + +//----------------------------------------------------------------------------- +// Purpose: Callback function for qsort, used to sort entities by their depth +// in the movement hierarchy. +// Input : pEnt1 - +// pEnt2 - +// Output : Returns -1, 0, or 1 per qsort spec. +//----------------------------------------------------------------------------- +static int __cdecl CompareSpawnOrder(HierarchicalSpawn_t *pEnt1, HierarchicalSpawn_t *pEnt2) +{ + if (pEnt1->m_nDepth == pEnt2->m_nDepth) + { + if ( g_pClassnameSpawnPriority ) + { + int o1 = pEnt1->m_pEntity ? g_pClassnameSpawnPriority->GetStringID( pEnt1->m_pEntity->GetClassname() ) : -1; + int o2 = pEnt2->m_pEntity ? g_pClassnameSpawnPriority->GetStringID( pEnt2->m_pEntity->GetClassname() ) : -1; + if ( o1 < o2 ) + return 1; + if ( o2 < o1 ) + return -1; + } + return 0; + } + + if (pEnt1->m_nDepth > pEnt2->m_nDepth) + return 1; + + return -1; +} + + +//----------------------------------------------------------------------------- +// Computes the hierarchical depth of the entities to spawn.. +//----------------------------------------------------------------------------- +static int ComputeSpawnHierarchyDepth_r( CBaseEntity *pEntity ) +{ + if ( !pEntity ) + return 1; + + if (pEntity->m_iParent == NULL_STRING) + return 1; + + CBaseEntity *pParent = gEntList.FindEntityByName( NULL, ExtractParentName(pEntity->m_iParent) ); + if (!pParent) + return 1; + + if (pParent == pEntity) + { + Warning( "LEVEL DESIGN ERROR: Entity %s is parented to itself!\n", pEntity->GetDebugName() ); + return 1; + } + + return 1 + ComputeSpawnHierarchyDepth_r( pParent ); +} + +static void ComputeSpawnHierarchyDepth( int nEntities, HierarchicalSpawn_t *pSpawnList ) +{ + // NOTE: This isn't particularly efficient, but so what? It's at the beginning of time + // I did it this way because it simplified the parent setting in hierarchy (basically + // eliminated questions about whether you should transform origin from global to local or not) + int nEntity; + for (nEntity = 0; nEntity < nEntities; nEntity++) + { + CBaseEntity *pEntity = pSpawnList[nEntity].m_pEntity; + if (pEntity && !pEntity->IsDormant()) + { + pSpawnList[nEntity].m_nDepth = ComputeSpawnHierarchyDepth_r( pEntity ); + } + else + { + pSpawnList[nEntity].m_nDepth = 1; + } + } +} + +static void SortSpawnListByHierarchy( int nEntities, HierarchicalSpawn_t *pSpawnList ) +{ + MEM_ALLOC_CREDIT(); + g_pClassnameSpawnPriority = new CStringRegistry; + // this will cause the entities to be spawned in the indicated order + // Highest string ID spawns first. String ID is spawn priority. + // by default, anything not in this list has priority -1 + g_pClassnameSpawnPriority->AddString( "func_wall", 10 ); + g_pClassnameSpawnPriority->AddString( "scripted_sequence", 9 ); + g_pClassnameSpawnPriority->AddString( "phys_hinge", 8 ); + g_pClassnameSpawnPriority->AddString( "phys_ballsocket", 8 ); + g_pClassnameSpawnPriority->AddString( "phys_slideconstraint", 8 ); + g_pClassnameSpawnPriority->AddString( "phys_constraint", 8 ); + g_pClassnameSpawnPriority->AddString( "phys_pulleyconstraint", 8 ); + g_pClassnameSpawnPriority->AddString( "phys_lengthconstraint", 8 ); + g_pClassnameSpawnPriority->AddString( "phys_ragdollconstraint", 8 ); + g_pClassnameSpawnPriority->AddString( "info_mass_center", 8 ); // spawn these before physbox/prop_physics + g_pClassnameSpawnPriority->AddString( "trigger_vphysics_motion", 8 ); // spawn these before physbox/prop_physics + + g_pClassnameSpawnPriority->AddString( "prop_physics", 7 ); + g_pClassnameSpawnPriority->AddString( "prop_ragdoll", 7 ); + // Sort the entities (other than the world) by hierarchy depth, in order to spawn them in + // that order. This insures that each entity's parent spawns before it does so that + // it can properly set up anything that relies on hierarchy. +#ifdef _WIN32 + qsort(&pSpawnList[0], nEntities, sizeof(pSpawnList[0]), (int (__cdecl *)(const void *, const void *))CompareSpawnOrder); +#elif POSIX + qsort(&pSpawnList[0], nEntities, sizeof(pSpawnList[0]), (int (*)(const void *, const void *))CompareSpawnOrder); +#endif + delete g_pClassnameSpawnPriority; + g_pClassnameSpawnPriority = NULL; +} + +void SetupParentsForSpawnList( int nEntities, HierarchicalSpawn_t *pSpawnList ) +{ + int nEntity; + for (nEntity = nEntities - 1; nEntity >= 0; nEntity--) + { + CBaseEntity *pEntity = pSpawnList[nEntity].m_pEntity; + if ( pEntity ) + { + if ( strchr(STRING(pEntity->m_iParent), ',') ) + { + char szToken[256]; + const char *pAttachmentName = nexttoken(szToken, STRING(pEntity->m_iParent), ',', sizeof(szToken)); + pEntity->m_iParent = AllocPooledString(szToken); + CBaseEntity *pParent = gEntList.FindEntityByName( NULL, pEntity->m_iParent ); + + // setparent in the spawn pass instead - so the model will have been set & loaded + pSpawnList[nEntity].m_pDeferredParent = pParent; + pSpawnList[nEntity].m_pDeferredParentAttachment = pAttachmentName; + } + else + { + CBaseEntity *pParent = gEntList.FindEntityByName( NULL, pEntity->m_iParent ); + + if ((pParent != NULL) && (pParent->edict() != NULL)) + { + pEntity->SetParent( pParent ); + } + } + } + } +} + +// this is a hook for edit mode +void RememberInitialEntityPositions( int nEntities, HierarchicalSpawn_t *pSpawnList ) +{ + for (int nEntity = 0; nEntity < nEntities; nEntity++) + { + CBaseEntity *pEntity = pSpawnList[nEntity].m_pEntity; + + if ( pEntity ) + { + NWCEdit::RememberEntityPosition( pEntity ); + } + } +} + + +void SpawnAllEntities( int nEntities, HierarchicalSpawn_t *pSpawnList, bool bActivateEntities ) +{ + int nEntity; + for (nEntity = 0; nEntity < nEntities; nEntity++) + { + VPROF( "MapEntity_ParseAllEntities_Spawn"); + CBaseEntity *pEntity = pSpawnList[nEntity].m_pEntity; + + if ( pSpawnList[nEntity].m_pDeferredParent ) + { + // UNDONE: Promote this up to the root of this function? + MDLCACHE_CRITICAL_SECTION(); + CBaseEntity *pParent = pSpawnList[nEntity].m_pDeferredParent; + int iAttachment = -1; + CBaseAnimating *pAnim = pParent->GetBaseAnimating(); + if ( pAnim ) + { + iAttachment = pAnim->LookupAttachment(pSpawnList[nEntity].m_pDeferredParentAttachment); + } + pEntity->SetParent( pParent, iAttachment ); + } + if ( pEntity ) + { + if (DispatchSpawn(pEntity) < 0) + { + for ( int i = nEntity+1; i < nEntities; i++ ) + { + // this is a child object that will be deleted now + if ( pSpawnList[i].m_pEntity && pSpawnList[i].m_pEntity->IsMarkedForDeletion() ) + { + pSpawnList[i].m_pEntity = NULL; + } + } + // Spawn failed. + gEntList.CleanupDeleteList(); + // Remove the entity from the spawn list + pSpawnList[nEntity].m_pEntity = NULL; + } + } + } + + if ( bActivateEntities ) + { + VPROF( "MapEntity_ParseAllEntities_Activate"); + bool bAsyncAnims = mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, false ); + for (nEntity = 0; nEntity < nEntities; nEntity++) + { + CBaseEntity *pEntity = pSpawnList[nEntity].m_pEntity; + + if ( pEntity ) + { + MDLCACHE_CRITICAL_SECTION(); + pEntity->Activate(); + } + } + mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, bAsyncAnims ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Only called on BSP load. Parses and spawns all the entities in the BSP. +// Input : pMapData - Pointer to the entity data block to parse. +//----------------------------------------------------------------------------- +void MapEntity_ParseAllEntities(const char *pMapData, IMapEntityFilter *pFilter, bool bActivateEntities) +{ + VPROF("MapEntity_ParseAllEntities"); + + HierarchicalSpawnMapData_t *pSpawnMapData = new HierarchicalSpawnMapData_t[NUM_ENT_ENTRIES]; + HierarchicalSpawn_t *pSpawnList = new HierarchicalSpawn_t[NUM_ENT_ENTRIES]; + + CUtlVector< CPointTemplate* > pPointTemplates; + int nEntities = 0; + + char szTokenBuffer[MAPKEY_MAXLENGTH]; + + // Allow the tools to spawn different things + if ( serverenginetools ) + { + pMapData = serverenginetools->GetEntityData( pMapData ); + } + + // Loop through all entities in the map data, creating each. + for ( ; true; pMapData = MapEntity_SkipToNextEntity(pMapData, szTokenBuffer) ) + { + // + // Parse the opening brace. + // + char token[MAPKEY_MAXLENGTH]; + pMapData = MapEntity_ParseToken( pMapData, token ); + + // + // Check to see if we've finished or not. + // + if (!pMapData) + break; + + if (token[0] != '{') + { + Error( "MapEntity_ParseAllEntities: found %s when expecting {", token); + continue; + } + + // + // Parse the entity and add it to the spawn list. + // + CBaseEntity *pEntity; + const char *pCurMapData = pMapData; + pMapData = MapEntity_ParseEntity(pEntity, pMapData, pFilter); + if (pEntity == NULL) + continue; + + if (pEntity->IsTemplate()) + { + // It's a template entity. Squirrel away its keyvalue text so that we can + // recreate the entity later via a spawner. pMapData points at the '}' + // so we must add one to include it in the string. + Templates_Add(pEntity, pCurMapData, (pMapData - pCurMapData) + 2); + + // Remove the template entity so that it does not show up in FindEntityXXX searches. + UTIL_Remove(pEntity); + gEntList.CleanupDeleteList(); + continue; + } + + // To + if ( dynamic_cast( pEntity ) ) + { + VPROF( "MapEntity_ParseAllEntities_SpawnWorld"); + + pEntity->m_iParent = NULL_STRING; // don't allow a parent on the first entity (worldspawn) + + DispatchSpawn(pEntity); + continue; + } + + CNodeEnt *pNode = dynamic_cast(pEntity); + if ( pNode ) + { + VPROF( "MapEntity_ParseAllEntities_SpawnTransients"); + + // We overflow the max edicts on large maps that have lots of entities. + // Nodes & Lights remove themselves immediately on Spawn(), so dispatch their + // spawn now, to free up the slot inside this loop. + // NOTE: This solution prevents nodes & lights from being used inside point_templates. + // + // NOTE: Nodes spawn other entities (ai_hint) if they need to have a persistent presence. + // To ensure keys are copied over into the new entity, we pass the mapdata into the + // node spawn function. + if ( pNode->Spawn( pCurMapData ) < 0 ) + { + gEntList.CleanupDeleteList(); + } + continue; + } + + if ( dynamic_cast(pEntity) ) + { + VPROF( "MapEntity_ParseAllEntities_SpawnTransients"); + + // We overflow the max edicts on large maps that have lots of entities. + // Nodes & Lights remove themselves immediately on Spawn(), so dispatch their + // spawn now, to free up the slot inside this loop. + // NOTE: This solution prevents nodes & lights from being used inside point_templates. + if (DispatchSpawn(pEntity) < 0) + { + gEntList.CleanupDeleteList(); + } + continue; + } + + // Build a list of all point_template's so we can spawn them before everything else + CPointTemplate *pTemplate = dynamic_cast< CPointTemplate* >(pEntity); + if ( pTemplate ) + { + pPointTemplates.AddToTail( pTemplate ); + } + else + { + // Queue up this entity for spawning + pSpawnList[nEntities].m_pEntity = pEntity; + pSpawnList[nEntities].m_nDepth = 0; + pSpawnList[nEntities].m_pDeferredParentAttachment = NULL; + pSpawnList[nEntities].m_pDeferredParent = NULL; + + pSpawnMapData[nEntities].m_pMapData = pCurMapData; + pSpawnMapData[nEntities].m_iMapDataLength = (pMapData - pCurMapData) + 2; + nEntities++; + } + } + + // Now loop through all our point_template entities and tell them to make templates of everything they're pointing to + int iTemplates = pPointTemplates.Count(); + for ( int i = 0; i < iTemplates; i++ ) + { + VPROF( "MapEntity_ParseAllEntities_SpawnTemplates"); + CPointTemplate *pPointTemplate = pPointTemplates[i]; + + // First, tell the Point template to Spawn + if ( DispatchSpawn(pPointTemplate) < 0 ) + { + UTIL_Remove(pPointTemplate); + gEntList.CleanupDeleteList(); + continue; + } + + pPointTemplate->StartBuildingTemplates(); + + // Now go through all it's templates and turn the entities into templates + int iNumTemplates = pPointTemplate->GetNumTemplateEntities(); + for ( int iTemplateNum = 0; iTemplateNum < iNumTemplates; iTemplateNum++ ) + { + // Find it in the spawn list + CBaseEntity *pEntity = pPointTemplate->GetTemplateEntity( iTemplateNum ); + for ( int iEntNum = 0; iEntNum < nEntities; iEntNum++ ) + { + if ( pSpawnList[iEntNum].m_pEntity == pEntity ) + { + // Give the point_template the mapdata + pPointTemplate->AddTemplate( pEntity, pSpawnMapData[iEntNum].m_pMapData, pSpawnMapData[iEntNum].m_iMapDataLength ); + + if ( pPointTemplate->ShouldRemoveTemplateEntities() ) + { + // Remove the template entity so that it does not show up in FindEntityXXX searches. + UTIL_Remove(pEntity); + gEntList.CleanupDeleteList(); + + // Remove the entity from the spawn list + pSpawnList[iEntNum].m_pEntity = NULL; + } + break; + } + } + } + + pPointTemplate->FinishBuildingTemplates(); + } + + SpawnHierarchicalList( nEntities, pSpawnList, bActivateEntities ); + + delete [] pSpawnMapData; + delete [] pSpawnList; +} + +void SpawnHierarchicalList( int nEntities, HierarchicalSpawn_t *pSpawnList, bool bActivateEntities ) +{ + // Compute the hierarchical depth of all entities hierarchically attached + ComputeSpawnHierarchyDepth( nEntities, pSpawnList ); + + // Sort the entities (other than the world) by hierarchy depth, in order to spawn them in + // that order. This insures that each entity's parent spawns before it does so that + // it can properly set up anything that relies on hierarchy. + SortSpawnListByHierarchy( nEntities, pSpawnList ); + + // save off entity positions if in edit mode + if ( engine->IsInEditMode() ) + { + RememberInitialEntityPositions( nEntities, pSpawnList ); + } + // Set up entity movement hierarchy in reverse hierarchy depth order. This allows each entity + // to use its parent's world spawn origin to calculate its local origin. + SetupParentsForSpawnList( nEntities, pSpawnList ); + + // Spawn all the entities in hierarchy depth order so that parents spawn before their children. + SpawnAllEntities( nEntities, pSpawnList, bActivateEntities ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEntData - +//----------------------------------------------------------------------------- +void MapEntity_PrecacheEntity( const char *pEntData, int &nStringSize ) +{ + CEntityMapData entData( (char*)pEntData, nStringSize ); + char className[MAPKEY_MAXLENGTH]; + + if (!entData.ExtractValue("classname", className)) + { + Error( "classname missing from entity!\n" ); + } + + // Construct via the LINK_ENTITY_TO_CLASS factory. + CBaseEntity *pEntity = CreateEntityByName(className); + + // + // Set up keyvalues, which can set the model name, which is why we don't just do UTIL_PrecacheOther here... + // + if ( pEntity != NULL ) + { + pEntity->ParseMapData(&entData); + pEntity->Precache(); + UTIL_RemoveImmediate( pEntity ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Takes a block of character data as the input +// Input : pEntity - Receives the newly constructed entity, NULL on failure. +// pEntData - Data block to parse to extract entity keys. +// Output : Returns the current position in the entity data block. +//----------------------------------------------------------------------------- +const char *MapEntity_ParseEntity(CBaseEntity *&pEntity, const char *pEntData, IMapEntityFilter *pFilter) +{ + CEntityMapData entData( (char*)pEntData ); + char className[MAPKEY_MAXLENGTH]; + + if (!entData.ExtractValue("classname", className)) + { + Error( "classname missing from entity!\n" ); + } + + pEntity = NULL; + if ( !pFilter || pFilter->ShouldCreateEntity( className ) ) + { + // + // Construct via the LINK_ENTITY_TO_CLASS factory. + // + if ( pFilter ) + pEntity = pFilter->CreateNextEntity( className ); + else + pEntity = CreateEntityByName(className); + + // + // Set up keyvalues. + // + if (pEntity != NULL) + { + pEntity->ParseMapData(&entData); + } + else + { + Warning("Can't init %s\n", className); + } + } + else + { + // Just skip past all the keys. + char keyName[MAPKEY_MAXLENGTH]; + char value[MAPKEY_MAXLENGTH]; + if ( entData.GetFirstKey(keyName, value) ) + { + do + { + } + while ( entData.GetNextKey(keyName, value) ); + } + } + + // + // Return the current parser position in the data block + // + return entData.CurrentBufferPosition(); +} + + diff --git a/sp/src/game/server/mapentities.h b/sp/src/game/server/mapentities.h new file mode 100644 index 00000000..d8fa4288 --- /dev/null +++ b/sp/src/game/server/mapentities.h @@ -0,0 +1,47 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef MAPENTITIES_H +#define MAPENTITIES_H +#ifdef _WIN32 +#pragma once +#endif + +#include "mapentities_shared.h" + +// This class provides hooks into the map-entity loading process that allows CS to do some tricks +// when restarting the round. The main trick it tries to do is recreate all +abstract_class IMapEntityFilter +{ +public: + virtual bool ShouldCreateEntity( const char *pClassname ) = 0; + virtual CBaseEntity* CreateNextEntity( const char *pClassname ) = 0; +}; + +// Use the filter so you can prevent certain entities from being created out of the map. +// CSPort does this when restarting rounds. It wants to reload most entities from the map, but certain +// entities like the world entity need to be left intact. +void MapEntity_ParseAllEntities( const char *pMapData, IMapEntityFilter *pFilter=NULL, bool bActivateEntities=false ); + +const char *MapEntity_ParseEntity( CBaseEntity *&pEntity, const char *pEntData, IMapEntityFilter *pFilter ); +void MapEntity_PrecacheEntity( const char *pEntData, int &nStringSize ); + + +//----------------------------------------------------------------------------- +// Hierarchical spawn +//----------------------------------------------------------------------------- +struct HierarchicalSpawn_t +{ + CBaseEntity *m_pEntity; + int m_nDepth; + CBaseEntity *m_pDeferredParent; // attachment parents can't be set until the parents are spawned + const char *m_pDeferredParentAttachment; // so defer setting them up until the second pass +}; + +void SpawnHierarchicalList( int nEntities, HierarchicalSpawn_t *pSpawnList, bool bActivateEntities ); + +#endif // MAPENTITIES_H diff --git a/sp/src/game/server/maprules.cpp b/sp/src/game/server/maprules.cpp new file mode 100644 index 00000000..91049ad8 --- /dev/null +++ b/sp/src/game/server/maprules.cpp @@ -0,0 +1,826 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Contains entities for implementing/changing game rules dynamically within each BSP. +// +//=============================================================================// + +#include "cbase.h" +#include "datamap.h" +#include "gamerules.h" +#include "maprules.h" +#include "player.h" +#include "entitylist.h" +#include "ai_hull.h" +#include "entityoutput.h" +#ifdef MAPBASE +#include "eventqueue.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CRuleEntity : public CBaseEntity +{ +public: + DECLARE_CLASS( CRuleEntity, CBaseEntity ); + + void Spawn( void ); + + DECLARE_DATADESC(); + + void SetMaster( string_t iszMaster ) { m_iszMaster = iszMaster; } + +protected: + bool CanFireForActivator( CBaseEntity *pActivator ); + +private: + string_t m_iszMaster; +}; + +BEGIN_DATADESC( CRuleEntity ) + + DEFINE_KEYFIELD( m_iszMaster, FIELD_STRING, "master" ), + +END_DATADESC() + + + +void CRuleEntity::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + AddEffects( EF_NODRAW ); +} + + +bool CRuleEntity::CanFireForActivator( CBaseEntity *pActivator ) +{ + if ( m_iszMaster != NULL_STRING ) + { + if ( UTIL_IsMasterTriggered( m_iszMaster, pActivator ) ) + return true; + else + return false; + } + + return true; +} + +// +// CRulePointEntity -- base class for all rule "point" entities (not brushes) +// +class CRulePointEntity : public CRuleEntity +{ +public: + DECLARE_DATADESC(); + DECLARE_CLASS( CRulePointEntity, CRuleEntity ); + + int m_Score; + void Spawn( void ); +}; + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CRulePointEntity ) + + DEFINE_FIELD( m_Score, FIELD_INTEGER ), + +END_DATADESC() + + +void CRulePointEntity::Spawn( void ) +{ + BaseClass::Spawn(); + SetModelName( NULL_STRING ); + m_Score = 0; +} + +// +// CRuleBrushEntity -- base class for all rule "brush" entities (not brushes) +// Default behavior is to set up like a trigger, invisible, but keep the model for volume testing +// +class CRuleBrushEntity : public CRuleEntity +{ +public: + DECLARE_CLASS( CRuleBrushEntity, CRuleEntity ); + + void Spawn( void ); + +private: +}; + +void CRuleBrushEntity::Spawn( void ) +{ + SetModel( STRING( GetModelName() ) ); + BaseClass::Spawn(); +} + + +// CGameScore / game_score -- award points to player / team +// Points +/- total +// Flag: Allow negative scores SF_SCORE_NEGATIVE +// Flag: Award points to team in teamplay SF_SCORE_TEAM + +#define SF_SCORE_NEGATIVE 0x0001 +#define SF_SCORE_TEAM 0x0002 + +class CGameScore : public CRulePointEntity +{ +public: + DECLARE_CLASS( CGameScore, CRulePointEntity ); + DECLARE_DATADESC(); + + void Spawn( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + inline int Points( void ) { return m_Score; } + inline bool AllowNegativeScore( void ) { return m_spawnflags & SF_SCORE_NEGATIVE; } + inline int AwardToTeam( void ) { return (m_spawnflags & SF_SCORE_TEAM); } + + inline void SetPoints( int points ) { m_Score = points; } + + void InputApplyScore( inputdata_t &inputdata ); + +private: +}; + +LINK_ENTITY_TO_CLASS( game_score, CGameScore ); + +BEGIN_DATADESC( CGameScore ) + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "ApplyScore", InputApplyScore ), +END_DATADESC() + +void CGameScore::Spawn( void ) +{ + int iScore = Points(); + BaseClass::Spawn(); + SetPoints( iScore ); +} + + +bool CGameScore::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "points")) + { + SetPoints( atoi(szValue) ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +void CGameScore::InputApplyScore( inputdata_t &inputdata ) +{ + CBaseEntity *pActivator = inputdata.pActivator; + + if ( pActivator == NULL ) + return; + + if ( CanFireForActivator( pActivator ) == false ) + return; + + // Only players can use this + if ( pActivator->IsPlayer() ) + { + if ( AwardToTeam() ) + { + pActivator->AddPointsToTeam( Points(), AllowNegativeScore() ); + } + else + { + pActivator->AddPoints( Points(), AllowNegativeScore() ); + } + } +} + +void CGameScore::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( !CanFireForActivator( pActivator ) ) + return; + + // Only players can use this + if ( pActivator->IsPlayer() ) + { + if ( AwardToTeam() ) + { + pActivator->AddPointsToTeam( Points(), AllowNegativeScore() ); + } + else + { + pActivator->AddPoints( Points(), AllowNegativeScore() ); + } + } +} + + +// CGameEnd / game_end -- Ends the game in MP + +class CGameEnd : public CRulePointEntity +{ + DECLARE_CLASS( CGameEnd, CRulePointEntity ); + +public: + DECLARE_DATADESC(); + + void InputGameEnd( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputGameEndSP( inputdata_t &inputdata ); +#endif + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); +private: +}; + +BEGIN_DATADESC( CGameEnd ) + + // inputs + DEFINE_INPUTFUNC( FIELD_VOID, "EndGame", InputGameEnd ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "EndGameSP", InputGameEndSP ), +#endif + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( game_end, CGameEnd ); + + +void CGameEnd::InputGameEnd( inputdata_t &inputdata ) +{ + g_pGameRules->EndMultiplayerGame(); +} + +#ifdef MAPBASE +void CGameEnd::InputGameEndSP( inputdata_t &inputdata ) +{ + // This basically just acts as a shortcut for the "startupmenu force"/disconnection command. + // Things like mapping competitions could change this code based on given strings for specific endings (e.g. background maps). + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + if (pPlayer) + engine->ClientCommand(pPlayer->edict(), "startupmenu force"); +} +#endif + +void CGameEnd::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( !CanFireForActivator( pActivator ) ) + return; + + g_pGameRules->EndMultiplayerGame(); +} + + +// +// CGameText / game_text -- NON-Localized HUD Message (use env_message to display a titles.txt message) +// Flag: All players SF_ENVTEXT_ALLPLAYERS +// +#define SF_ENVTEXT_ALLPLAYERS 0x0001 + + +class CGameText : public CRulePointEntity +{ +public: + DECLARE_CLASS( CGameText, CRulePointEntity ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + + DECLARE_DATADESC(); + + inline bool MessageToAll( void ) { return (m_spawnflags & SF_ENVTEXT_ALLPLAYERS); } + inline void MessageSet( const char *pMessage ) { m_iszMessage = AllocPooledString(pMessage); } + inline const char *MessageGet( void ) { return STRING( m_iszMessage ); } + + void InputDisplay( inputdata_t &inputdata ); + void Display( CBaseEntity *pActivator ); +#ifdef MAPBASE + void InputSetText ( inputdata_t &inputdata ); + void SetText( const char* pszStr ); + + void InputSetFont( inputdata_t &inputdata ) { m_strFont = inputdata.value.StringID(); } +#endif + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) + { + Display( pActivator ); + } + +private: + + string_t m_iszMessage; + hudtextparms_t m_textParms; + +#ifdef MAPBASE + string_t m_strFont; + bool m_bAutobreak; +#endif +}; + +LINK_ENTITY_TO_CLASS( game_text, CGameText ); + +// Save parms as a block. Will break save/restore if the structure changes, but this entity didn't ship with Half-Life, so +// it can't impact saved Half-Life games. +BEGIN_DATADESC( CGameText ) + + DEFINE_KEYFIELD( m_iszMessage, FIELD_STRING, "message" ), + + DEFINE_KEYFIELD( m_textParms.channel, FIELD_INTEGER, "channel" ), + DEFINE_KEYFIELD( m_textParms.x, FIELD_FLOAT, "x" ), + DEFINE_KEYFIELD( m_textParms.y, FIELD_FLOAT, "y" ), + DEFINE_KEYFIELD( m_textParms.effect, FIELD_INTEGER, "effect" ), + DEFINE_KEYFIELD( m_textParms.fadeinTime, FIELD_FLOAT, "fadein" ), + DEFINE_KEYFIELD( m_textParms.fadeoutTime, FIELD_FLOAT, "fadeout" ), + DEFINE_KEYFIELD( m_textParms.holdTime, FIELD_FLOAT, "holdtime" ), + DEFINE_KEYFIELD( m_textParms.fxTime, FIELD_FLOAT, "fxtime" ), + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_strFont, FIELD_STRING, "font" ), + DEFINE_KEYFIELD( m_bAutobreak, FIELD_BOOLEAN, "autobreak" ), +#endif + + DEFINE_ARRAY( m_textParms, FIELD_CHARACTER, sizeof(hudtextparms_t) ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Display", InputDisplay ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetText", InputSetText ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetFont", InputSetFont ), +#endif + +END_DATADESC() + + + +bool CGameText::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "color")) + { + int color[4]; + UTIL_StringToIntArray( color, 4, szValue ); + m_textParms.r1 = color[0]; + m_textParms.g1 = color[1]; + m_textParms.b1 = color[2]; + m_textParms.a1 = color[3]; + } + else if (FStrEq(szKeyName, "color2")) + { + int color[4]; + UTIL_StringToIntArray( color, 4, szValue ); + m_textParms.r2 = color[0]; + m_textParms.g2 = color[1]; + m_textParms.b2 = color[2]; + m_textParms.a2 = color[3]; + } +#ifdef MAPBASE + else if (FStrEq( szKeyName, "message" )) + { + // Needed for newline support + SetText( szValue ); + } +#endif + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + +void CGameText::InputDisplay( inputdata_t &inputdata ) +{ + Display( inputdata.pActivator ); +} + +void CGameText::Display( CBaseEntity *pActivator ) +{ + if ( !CanFireForActivator( pActivator ) ) + return; + + if ( MessageToAll() ) + { +#ifdef MAPBASE + UTIL_HudMessageAll( m_textParms, MessageGet(), STRING(m_strFont), m_bAutobreak ); +#else + UTIL_HudMessageAll( m_textParms, MessageGet() ); +#endif + } + else + { + // If we're in singleplayer, show the message to the player. + if ( gpGlobals->maxClients == 1 ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); +#ifdef MAPBASE + UTIL_HudMessage( pPlayer, m_textParms, MessageGet(), STRING(m_strFont), m_bAutobreak ); +#else + UTIL_HudMessage( pPlayer, m_textParms, MessageGet() ); +#endif + } + // Otherwise show the message to the player that triggered us. + else if ( pActivator && pActivator->IsNetClient() ) + { +#ifdef MAPBASE + UTIL_HudMessage( ToBasePlayer( pActivator ), m_textParms, MessageGet(), STRING(m_strFont), m_bAutobreak ); +#else + UTIL_HudMessage( ToBasePlayer( pActivator ), m_textParms, MessageGet() ); +#endif + } + } +} + +#ifdef MAPBASE +void CGameText::InputSetText( inputdata_t &inputdata ) +{ + SetText( inputdata.value.String() ); +} + +void CGameText::SetText( const char* pszStr ) +{ + // Replace /n with \n + if (Q_strstr( pszStr, "/n" )) + { + CUtlStringList vecLines; + Q_SplitString( pszStr, "/n", vecLines ); + + char szMsg[512]; + Q_strncpy( szMsg, vecLines[0], sizeof( szMsg ) ); + + for (int i = 1; i < vecLines.Count(); i++) + { + Q_snprintf( szMsg, sizeof( szMsg ), "%s\n%s", szMsg, vecLines[i] ); + } + m_iszMessage = AllocPooledString( szMsg ); + } + else + { + m_iszMessage = AllocPooledString( pszStr ); + } +} +#endif + + +/* TODO: Replace with an entity I/O version +// +// CGameTeamSet / game_team_set -- Changes the team of the entity it targets to the activator's team +// Flag: Fire once +// Flag: Clear team -- Sets the team to "NONE" instead of activator + +#define SF_TEAMSET_FIREONCE 0x0001 +#define SF_TEAMSET_CLEARTEAM 0x0002 + +class CGameTeamSet : public CRulePointEntity +{ +public: + DECLARE_CLASS( CGameTeamSet, CRulePointEntity ); + + inline bool RemoveOnFire( void ) { return (m_spawnflags & SF_TEAMSET_FIREONCE) ? true : false; } + inline bool ShouldClearTeam( void ) { return (m_spawnflags & SF_TEAMSET_CLEARTEAM) ? true : false; } + void InputTrigger( inputdata_t &inputdata ); + +private: + COutputEvent m_OnTrigger; +}; + +LINK_ENTITY_TO_CLASS( game_team_set, CGameTeamSet ); + + +void CGameTeamSet::InputTrigger( inputdata_t &inputdata ) +{ + if ( !CanFireForActivator( inputdata.pActivator ) ) + return; + + if ( ShouldClearTeam() ) + { + // clear the team of our target + } + else + { + // set the team of our target to our activator's team + } + + m_OnTrigger.FireOutput(pActivator, this); + + if ( RemoveOnFire() ) + { + UTIL_Remove( this ); + } +} +*/ + + +// +// CGamePlayerZone / game_player_zone -- players in the zone fire my target when I'm fired +// +// Needs master? +class CGamePlayerZone : public CRuleBrushEntity +{ +public: + DECLARE_CLASS( CGamePlayerZone, CRuleBrushEntity ); + void InputCountPlayersInZone( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + + COutputEvent m_OnPlayerInZone; + COutputEvent m_OnPlayerOutZone; + + COutputInt m_PlayersInCount; + COutputInt m_PlayersOutCount; +}; + +LINK_ENTITY_TO_CLASS( game_zone_player, CGamePlayerZone ); +BEGIN_DATADESC( CGamePlayerZone ) + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "CountPlayersInZone", InputCountPlayersInZone), + + // Outputs + DEFINE_OUTPUT(m_OnPlayerInZone, "OnPlayerInZone"), + DEFINE_OUTPUT(m_OnPlayerOutZone, "OnPlayerOutZone"), + DEFINE_OUTPUT(m_PlayersInCount, "PlayersInCount"), + DEFINE_OUTPUT(m_PlayersOutCount, "PlayersOutCount"), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Counts all the players in the zone. Fires one output per player +// in the zone, one output per player out of the zone, and outputs +// with the total counts of players in and out of the zone. +//----------------------------------------------------------------------------- +void CGamePlayerZone::InputCountPlayersInZone( inputdata_t &inputdata ) +{ + int playersInCount = 0; + int playersOutCount = 0; + + if ( !CanFireForActivator( inputdata.pActivator ) ) + return; + + CBaseEntity *pPlayer = NULL; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer ) + { + trace_t trace; + Hull_t hullType; + + hullType = HULL_HUMAN; + if ( pPlayer->GetFlags() & FL_DUCKING ) + { + hullType = HULL_SMALL_CENTERED; + } + + UTIL_TraceModel( pPlayer->GetAbsOrigin(), pPlayer->GetAbsOrigin(), NAI_Hull::Mins(hullType), + NAI_Hull::Maxs(hullType), this, COLLISION_GROUP_NONE, &trace ); + + if ( trace.startsolid ) + { + playersInCount++; + m_OnPlayerInZone.FireOutput(pPlayer, this); + } + else + { + playersOutCount++; + m_OnPlayerOutZone.FireOutput(pPlayer, this); + } + } + } + + m_PlayersInCount.Set(playersInCount, inputdata.pActivator, this); + m_PlayersOutCount.Set(playersOutCount, inputdata.pActivator, this); +} + + +/* +// Disable. Eventually will be replace by new activator filter entities. (LHL) +// +// CGamePlayerHurt / game_player_hurt -- Damages the player who fires it +// Flag: Fire once + +#define SF_PKILL_FIREONCE 0x0001 +class CGamePlayerHurt : public CRulePointEntity +{ +public: + DECLARE_CLASS( CGamePlayerHurt, CRulePointEntity ); + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + inline bool RemoveOnFire( void ) { return (m_spawnflags & SF_PKILL_FIREONCE) ? true : false; } + + DECLARE_DATADESC(); + +private: + + float m_flDamage; // Damage to inflict, negative values give health. + + COutputEvent m_OnUse; +}; + +LINK_ENTITY_TO_CLASS( game_player_hurt, CGamePlayerHurt ); + + +BEGIN_DATADESC( CGamePlayerHurt ) + + DEFINE_KEYFIELD( m_flDamage, FIELD_FLOAT, "dmg" ), + +END_DATADESC() + + + +void CGamePlayerHurt::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( !CanFireForActivator( pActivator ) ) + return; + + if ( pActivator->IsPlayer() ) + { + if ( m_flDamage < 0 ) + { + pActivator->TakeHealth( -m_flDamage, DMG_GENERIC ); + } + else + { + pActivator->TakeDamage( this, this, m_flDamage, DMG_GENERIC ); + } + } + + SUB_UseTargets( pActivator, useType, value ); + m_OnUse.FireOutput(pActivator, this); // dvsents2: handle useType and value here - they are passed through + + if ( RemoveOnFire() ) + { + UTIL_Remove( this ); + } +} +*/ + +// +// CGamePlayerEquip / game_playerequip -- Sets the default player equipment +// Flag: USE Only + +#define SF_PLAYEREQUIP_USEONLY 0x0001 +#define MAX_EQUIP 32 + +class CGamePlayerEquip : public CRulePointEntity +{ + DECLARE_DATADESC(); + +public: + DECLARE_CLASS( CGamePlayerEquip, CRulePointEntity ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Touch( CBaseEntity *pOther ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + inline bool UseOnly( void ) { return (m_spawnflags & SF_PLAYEREQUIP_USEONLY) ? true : false; } + +private: + + void EquipPlayer( CBaseEntity *pPlayer ); + + string_t m_weaponNames[MAX_EQUIP]; + int m_weaponCount[MAX_EQUIP]; +}; + +LINK_ENTITY_TO_CLASS( game_player_equip, CGamePlayerEquip ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CGamePlayerEquip ) + + DEFINE_AUTO_ARRAY( m_weaponNames, FIELD_STRING ), + DEFINE_AUTO_ARRAY( m_weaponCount, FIELD_INTEGER ), + +END_DATADESC() + + + + +bool CGamePlayerEquip::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( !BaseClass::KeyValue( szKeyName, szValue ) ) + { + for ( int i = 0; i < MAX_EQUIP; i++ ) + { + if ( !m_weaponNames[i] ) + { + char tmp[128]; + + UTIL_StripToken( szKeyName, tmp ); + + m_weaponNames[i] = AllocPooledString(tmp); + m_weaponCount[i] = atoi(szValue); + m_weaponCount[i] = MAX(1,m_weaponCount[i]); + return true; + } + } + } + + return false; +} + + +void CGamePlayerEquip::Touch( CBaseEntity *pOther ) +{ + if ( !CanFireForActivator( pOther ) ) + return; + + if ( UseOnly() ) + return; + + EquipPlayer( pOther ); +} + +void CGamePlayerEquip::EquipPlayer( CBaseEntity *pEntity ) +{ + CBasePlayer *pPlayer = ToBasePlayer(pEntity); + + if ( !pPlayer ) + return; + + for ( int i = 0; i < MAX_EQUIP; i++ ) + { + if ( !m_weaponNames[i] ) + break; + for ( int j = 0; j < m_weaponCount[i]; j++ ) + { + pPlayer->GiveNamedItem( STRING(m_weaponNames[i]) ); + } + } +} + + +void CGamePlayerEquip::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + EquipPlayer( pActivator ); +} + + +// +// CGamePlayerTeam / game_player_team -- Changes the team of the player who fired it +// Flag: Fire once +// Flag: Kill Player +// Flag: Gib Player + +#define SF_PTEAM_FIREONCE 0x0001 +#define SF_PTEAM_KILL 0x0002 +#define SF_PTEAM_GIB 0x0004 + +class CGamePlayerTeam : public CRulePointEntity +{ +public: + DECLARE_CLASS( CGamePlayerTeam, CRulePointEntity ); + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + +private: + + inline bool RemoveOnFire( void ) { return (m_spawnflags & SF_PTEAM_FIREONCE) ? true : false; } + inline bool ShouldKillPlayer( void ) { return (m_spawnflags & SF_PTEAM_KILL) ? true : false; } + inline bool ShouldGibPlayer( void ) { return (m_spawnflags & SF_PTEAM_GIB) ? true : false; } + + const char *TargetTeamName( const char *pszTargetName, CBaseEntity *pActivator ); +}; + +LINK_ENTITY_TO_CLASS( game_player_team, CGamePlayerTeam ); + + +const char *CGamePlayerTeam::TargetTeamName( const char *pszTargetName, CBaseEntity *pActivator ) +{ + CBaseEntity *pTeamEntity = NULL; + + while ((pTeamEntity = gEntList.FindEntityByName( pTeamEntity, pszTargetName, NULL, pActivator )) != NULL) + { + if ( FClassnameIs( pTeamEntity, "game_team_master" ) ) + return pTeamEntity->TeamID(); + } + + return NULL; +} + + +void CGamePlayerTeam::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( !CanFireForActivator( pActivator ) ) + return; + + if ( pActivator->IsPlayer() ) + { + const char *pszTargetTeam = TargetTeamName( STRING(m_target), pActivator ); + if ( pszTargetTeam ) + { + CBasePlayer *pPlayer = (CBasePlayer *)pActivator; + g_pGameRules->ChangePlayerTeam( pPlayer, pszTargetTeam, ShouldKillPlayer(), ShouldGibPlayer() ); + } + } + + if ( RemoveOnFire() ) + { + UTIL_Remove( this ); + } +} + + diff --git a/sp/src/game/server/maprules.h b/sp/src/game/server/maprules.h new file mode 100644 index 00000000..7a4b1553 --- /dev/null +++ b/sp/src/game/server/maprules.h @@ -0,0 +1,15 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef MAPRULES_H +#define MAPRULES_H + + + +#endif // MAPRULES_H + diff --git a/sp/src/game/server/message_entity.cpp b/sp/src/game/server/message_entity.cpp new file mode 100644 index 00000000..903947c1 --- /dev/null +++ b/sp/src/game/server/message_entity.cpp @@ -0,0 +1,340 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "basecombatweapon.h" +#include "explode.h" +#include "eventqueue.h" +#include "gamerules.h" +#include "ammodef.h" +#include "in_buttons.h" +#include "soundent.h" +#include "ndebugoverlay.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "game.h" +#ifdef MAPBASE +#include +#include +#include "utlbuffer.h" +#include "saverestore_utlvector.h" +#endif + +#include "player.h" +#include "entitylist.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Spawnflags +#define SF_MESSAGE_DISABLED 1 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CMessageEntity : public CPointEntity +{ + DECLARE_CLASS( CMessageEntity, CPointEntity ); + +public: + void Spawn( void ); + void Activate( void ); + void Think( void ); +#ifdef MAPBASE + virtual +#endif + void DrawOverlays(void); + + virtual void UpdateOnRemove(); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); +#ifdef MAPBASE + virtual void InputSetMessage( inputdata_t &inputdata ); +#endif + + DECLARE_DATADESC(); + +protected: + int m_radius; + string_t m_messageText; + bool m_drawText; + bool m_bDeveloperOnly; + bool m_bEnabled; +}; + +LINK_ENTITY_TO_CLASS( point_message, CMessageEntity ); + +BEGIN_DATADESC( CMessageEntity ) + + DEFINE_KEYFIELD( m_radius, FIELD_INTEGER, "radius" ), + DEFINE_KEYFIELD( m_messageText, FIELD_STRING, "message" ), + DEFINE_KEYFIELD( m_bDeveloperOnly, FIELD_BOOLEAN, "developeronly" ), + DEFINE_FIELD( m_drawText, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetMessage", InputSetMessage ), +#endif + +END_DATADESC() + +static CUtlVector< CHandle< CMessageEntity > > g_MessageEntities; + +//----------------------------------------- +// Spawn +//----------------------------------------- +void CMessageEntity::Spawn( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + m_drawText = false; + m_bDeveloperOnly = false; + m_bEnabled = !HasSpawnFlags( SF_MESSAGE_DISABLED ); + //m_debugOverlays |= OVERLAY_TEXT_BIT; // make sure we always show the text +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessageEntity::Activate( void ) +{ + BaseClass::Activate(); + + CHandle< CMessageEntity > h; + h = this; + g_MessageEntities.AddToTail( h ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessageEntity::UpdateOnRemove() +{ + BaseClass::UpdateOnRemove(); + + CHandle< CMessageEntity > h; + h = this; + g_MessageEntities.FindAndRemove( h ); + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------- +// Think +//----------------------------------------- +void CMessageEntity::Think( void ) +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + // check for player distance + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( !pPlayer || ( pPlayer->GetFlags() & FL_NOTARGET ) ) + return; + + Vector worldTargetPosition = pPlayer->EyePosition(); + + // bail if player is too far away + if ( (worldTargetPosition - GetAbsOrigin()).Length() > m_radius ) + { + m_drawText = false; + return; + } + + // turn on text + m_drawText = true; +} + +//------------------------------------------- +//------------------------------------------- +void CMessageEntity::DrawOverlays(void) +{ + if ( !m_drawText ) + return; + + if ( m_bDeveloperOnly && !g_pDeveloper->GetInt() ) + return; + + if ( !m_bEnabled ) + return; + + // display text if they are within range + char tempstr[512]; + Q_snprintf( tempstr, sizeof(tempstr), "%s", STRING(m_messageText) ); + EntityText( 0, tempstr, 0); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessageEntity::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessageEntity::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessageEntity::InputSetMessage( inputdata_t &inputdata ) +{ + char newmessage[256]; + Q_strncpy(newmessage, inputdata.value.String(), sizeof(newmessage)); + + m_messageText = AllocPooledString(newmessage); +} +#endif + +// This is a hack to make point_message stuff appear in developer 0 release builds +// for now +void DrawMessageEntities() +{ + int c = g_MessageEntities.Count(); + for ( int i = c - 1; i >= 0; i-- ) + { + CMessageEntity *me = g_MessageEntities[ i ]; + if ( !me ) + { + g_MessageEntities.Remove( i ); + continue; + } + + me->DrawOverlays(); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CMessageEntityLocalized : public CMessageEntity +{ + DECLARE_CLASS( CMessageEntityLocalized, CMessageEntity ); + +public: + bool KeyValue(const char *szKeyName, const char *szValue); + void SetMessage(const char *szValue); + void DrawOverlays(void); + void InputSetMessage( inputdata_t &inputdata ); + + CUtlVector m_messageLines; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( point_message_localized, CMessageEntityLocalized ); + +BEGIN_DATADESC( CMessageEntityLocalized ) + + DEFINE_UTLVECTOR( m_messageLines, FIELD_STRING ), + + //DEFINE_INPUTFUNC( FIELD_STRING, "SetMessage", InputSetMessage ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Handles key values from the BSP before spawn is called. +//----------------------------------------------------------------------------- +bool CMessageEntityLocalized::KeyValue(const char *szKeyName, const char *szValue) +{ + if (FStrEq(szKeyName, "message")) + { + SetMessage(szValue); + return true; + } + + return BaseClass::KeyValue(szKeyName, szValue); +} + +// I would use "\\n", but Hammer doesn't let you use back slashes. +#define CONVERSION_CHAR "/n" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessageEntityLocalized::SetMessage(const char *szValue) +{ + // Find a localization token matching this string + wchar_t *pszMessage = g_pVGuiLocalize->Find(szValue); + + // If this is a localized string, convert it back to char. + // If it isn't, just copy it right into this. + char szBackToChar[256]; + if (pszMessage) + g_pVGuiLocalize->ConvertUnicodeToANSI(pszMessage, szBackToChar, sizeof(szBackToChar)); + else + Q_strncpy(szBackToChar, szValue, sizeof(szBackToChar)); + + // szTemp is used to turn \n from localized strings into /n. + char szTemp[256]; + if (Q_strstr(szBackToChar, "\n")) + { + char *token = strtok(szBackToChar, "\n"); + while (token) + { + Q_snprintf(szTemp, sizeof(szTemp), "%s%s%s", szTemp, token, CONVERSION_CHAR); + token = strtok(NULL, "\n"); + } + } + else + { + Q_strncpy(szTemp, szBackToChar, sizeof(szTemp)); + } + + m_messageLines.RemoveAll(); + + CUtlStringList vecLines; + Q_SplitString(szTemp, CONVERSION_CHAR, vecLines); + FOR_EACH_VEC( vecLines, i ) + { + m_messageLines.AddToTail( AllocPooledString(vecLines[i]) ); + } + + vecLines.PurgeAndDeleteElements(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CMessageEntityLocalized::InputSetMessage( inputdata_t &inputdata ) +{ + SetMessage(inputdata.value.String()); +} + +//------------------------------------------- +//------------------------------------------- +void CMessageEntityLocalized::DrawOverlays(void) +{ + if ( !m_drawText ) + return; + + if ( m_bDeveloperOnly && !g_pDeveloper->GetInt() ) + return; + + if ( !m_bEnabled ) + return; + + // display text if they are within range + int offset = 0; + FOR_EACH_VEC( m_messageLines, i ) + { + EntityText( offset, STRING(m_messageLines[i]), 0 ); + offset++; + } +} +#endif diff --git a/sp/src/game/server/modelentities.cpp b/sp/src/game/server/modelentities.cpp new file mode 100644 index 00000000..3b6e3e3b --- /dev/null +++ b/sp/src/game/server/modelentities.cpp @@ -0,0 +1,364 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "entityoutput.h" +#include "ndebugoverlay.h" +#include "modelentities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar ent_debugkeys; +extern ConVar showtriggers; + + + +LINK_ENTITY_TO_CLASS( func_brush, CFuncBrush ); + +BEGIN_DATADESC( CFuncBrush ) + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_KEYFIELD( m_iDisabled, FIELD_INTEGER, "StartDisabled" ), + DEFINE_KEYFIELD( m_iSolidity, FIELD_INTEGER, "Solidity" ), + DEFINE_KEYFIELD( m_bSolidBsp, FIELD_BOOLEAN, "solidbsp" ), + DEFINE_KEYFIELD( m_iszExcludedClass, FIELD_STRING, "excludednpc" ), + DEFINE_KEYFIELD( m_bInvertExclusion, FIELD_BOOLEAN, "invert_exclusion" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetExcluded", InputSetExcluded ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetInvert", InputSetInvert ), + +END_DATADESC() + + +void CFuncBrush::Spawn( void ) +{ + SetMoveType( MOVETYPE_PUSH ); // so it doesn't get pushed by anything + + SetSolid( SOLID_VPHYSICS ); + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); + + if ( m_iSolidity == BRUSHSOLID_NEVER ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + SetModel( STRING( GetModelName() ) ); + + if ( m_iDisabled ) + TurnOff(); + + // If it can't move/go away, it's really part of the world + if ( !GetEntityName() || !m_iParent ) + AddFlag( FL_WORLDBRUSH ); + + CreateVPhysics(); + + // Slam the object back to solid - if we really want it to be solid. + if ( m_bSolidBsp ) + { + SetSolid( SOLID_BSP ); + } +} + +//----------------------------------------------------------------------------- + +bool CFuncBrush::CreateVPhysics( void ) +{ + // NOTE: Don't init this static. It's pretty common for these to be constrained + // and dynamically parented. Initing shadow avoids having to destroy the physics + // object later and lose the constraints. + IPhysicsObject *pPhys = VPhysicsInitShadow(false, false); + if ( pPhys ) + { + int contents = modelinfo->GetModelContents( GetModelIndex() ); + if ( ! (contents & (MASK_SOLID|MASK_PLAYERSOLID|MASK_NPCSOLID)) ) + { + // leave the physics shadow there in case it has crap constrained to it + // but disable collisions with it + pPhys->EnableCollisions( false ); + } + } + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CFuncBrush::DrawDebugTextOverlays( void ) +{ + int nOffset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf( tempstr,sizeof(tempstr), "angles: %g %g %g", (double)GetLocalAngles()[PITCH], (double)GetLocalAngles()[YAW], (double)GetLocalAngles()[ROLL] ); + EntityText( nOffset, tempstr, 0 ); + nOffset++; + } + + return nOffset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for toggling the hidden/shown state of the brush. +//----------------------------------------------------------------------------- +void CFuncBrush::InputToggle( inputdata_t &inputdata ) +{ + if ( IsOn() ) + { + TurnOff(); + return; + } + + TurnOn(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for hiding the brush. +//----------------------------------------------------------------------------- +void CFuncBrush::InputTurnOff( inputdata_t &inputdata ) +{ + TurnOff(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for showing the brush. +//----------------------------------------------------------------------------- +void CFuncBrush::InputTurnOn( inputdata_t &inputdata ) +{ + TurnOn(); +} + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CFuncBrush::InputSetExcluded( inputdata_t &inputdata ) +{ + m_iszExcludedClass = inputdata.value.StringID(); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CFuncBrush::InputSetInvert( inputdata_t &inputdata ) +{ + m_bInvertExclusion = inputdata.value.Bool(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Hides the brush. +//----------------------------------------------------------------------------- +void CFuncBrush::TurnOff( void ) +{ + if ( !IsOn() ) + return; + + if ( m_iSolidity != BRUSHSOLID_ALWAYS ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + AddEffects( EF_NODRAW ); + m_iDisabled = TRUE; +} + + +//----------------------------------------------------------------------------- +// Purpose: Shows the brush. +//----------------------------------------------------------------------------- +void CFuncBrush::TurnOn( void ) +{ + if ( IsOn() ) + return; + + if ( m_iSolidity != BRUSHSOLID_NEVER ) + { + RemoveSolidFlags( FSOLID_NOT_SOLID ); + } + + RemoveEffects( EF_NODRAW ); +} + + +bool CFuncBrush::IsOn( void ) +{ + return !IsEffectActive( EF_NODRAW ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Invisible field that activates when touched +// All inputs are passed up to the main entity, unless filtered out +//----------------------------------------------------------------------------- +// DVS TODO: obsolete, remove +class CTriggerBrush : public CBaseEntity +{ + // + // Filters controlling what this trigger responds to. + // + enum TriggerFilters_e + { + TRIGGER_IGNOREPLAYERS = 0x01, + TRIGGER_IGNORENPCS = 0x02, + TRIGGER_IGNOREPUSHABLES = 0x04, + TRIGGER_IGNORETOUCH = 0x08, + TRIGGER_IGNOREUSE = 0x10, + TRIGGER_IGNOREALL = 0x20, + }; + +public: + DECLARE_CLASS( CTriggerBrush, CBaseEntity ); + + // engine inputs + void Spawn( void ); + void StartTouch( CBaseEntity *pOther ); + void EndTouch( CBaseEntity *pOther ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + // input filtering (use/touch/blocked) + bool PassesInputFilter( CBaseEntity *pOther, int filter ); + + // input functions + void InputEnable( inputdata_t &inputdata ) + { + RemoveFlag( FL_DONTTOUCH ); + } + + void InputDisable( inputdata_t &inputdata ) + { + // this ensures that all the remaining EndTouch() calls still get passed through + AddFlag( FL_DONTTOUCH ); + } + + // outputs + COutputEvent m_OnStartTouch; + COutputEvent m_OnEndTouch; + COutputEvent m_OnUse; + + // data + int m_iInputFilter; + int m_iDontMessageParent; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( trigger_brush, CTriggerBrush ); + +BEGIN_DATADESC( CTriggerBrush ) + + DEFINE_KEYFIELD( m_iInputFilter, FIELD_INTEGER, "InputFilter" ), + DEFINE_KEYFIELD( m_iDontMessageParent, FIELD_INTEGER, "DontMessageParent" ), + + DEFINE_OUTPUT( m_OnStartTouch, "OnStartTouch" ), + DEFINE_OUTPUT( m_OnEndTouch, "OnEndTouch" ), + DEFINE_OUTPUT( m_OnUse, "OnUse" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + +END_DATADESC() + + +void CTriggerBrush::Spawn( void ) +{ + SetSolid( SOLID_BSP ); + AddSolidFlags( FSOLID_TRIGGER ); + SetMoveType( MOVETYPE_NONE ); + + SetModel( STRING( GetModelName() ) ); // set size and link into world + + if ( !showtriggers.GetInt() ) + { + AddEffects( EF_NODRAW ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when an entity starts touching us. +// Input : pOther - the entity that is now touching us. +//----------------------------------------------------------------------------- +void CTriggerBrush::StartTouch( CBaseEntity *pOther ) +{ + if ( PassesInputFilter(pOther, m_iInputFilter) && !(m_iInputFilter & TRIGGER_IGNORETOUCH) ) + { + m_OnStartTouch.FireOutput( pOther, this ); + if ( !m_iDontMessageParent ) + BaseClass::StartTouch( pOther ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when an entity stops touching us. +// Input : pOther - the entity that was touching us. +//----------------------------------------------------------------------------- +void CTriggerBrush::EndTouch( CBaseEntity *pOther ) +{ + if ( PassesInputFilter(pOther, m_iInputFilter) && !(m_iInputFilter & TRIGGER_IGNORETOUCH) ) + { + m_OnEndTouch.FireOutput( pOther, this ); + + if ( !m_iDontMessageParent ) + BaseClass::EndTouch( pOther ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when we are triggered by another entity or used by the player. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CTriggerBrush::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( PassesInputFilter(pActivator, m_iInputFilter) && !(m_iInputFilter & TRIGGER_IGNOREUSE) ) + { + m_OnUse.FireOutput( pActivator, this ); + if ( !m_iDontMessageParent ) + { + BaseClass::Use( pActivator, pCaller, useType, value ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Checks an entity input, against a filter and the current entity +// Input : *pOther - the entity that is part of the input (touch/untouch/block/use) +// filter - a field of standard filters (TriggerFilters_e) +// Output : Returns true if the input passes, false if it should be ignored +//----------------------------------------------------------------------------- +bool CTriggerBrush::PassesInputFilter( CBaseEntity *pOther, int filter ) +{ + if ( !filter ) + return true; + + // check for players + if ( (filter & TRIGGER_IGNOREPLAYERS) && pOther->IsPlayer() ) + return false; + + // NPCs + if ( (filter & TRIGGER_IGNORENPCS) && pOther->edict() && (pOther->GetFlags() & FL_NPC) ) + return false; + + // pushables + if ( (filter & TRIGGER_IGNOREPUSHABLES) && FStrEq(STRING(pOther->m_iClassname), "func_pushable") ) + return false; + + return true; +} + diff --git a/sp/src/game/server/modelentities.h b/sp/src/game/server/modelentities.h new file mode 100644 index 00000000..a8003b2d --- /dev/null +++ b/sp/src/game/server/modelentities.h @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef MODELENTITIES_H +#define MODELENTITIES_H +#ifdef _WIN32 +#pragma once +#endif + +//!! replace this with generic start enabled/disabled +#define SF_WALL_START_OFF 0x0001 +#define SF_IGNORE_PLAYERUSE 0x0002 + +//----------------------------------------------------------------------------- +// Purpose: basic solid geometry +// enabled state: brush is visible +// disabled staute: brush not visible +//----------------------------------------------------------------------------- +class CFuncBrush : public CBaseEntity +{ +public: + DECLARE_CLASS( CFuncBrush, CBaseEntity ); + + virtual void Spawn( void ); + bool CreateVPhysics( void ); + + virtual int ObjectCaps( void ) { return HasSpawnFlags(SF_IGNORE_PLAYERUSE) ? BaseClass::ObjectCaps() : BaseClass::ObjectCaps() | FCAP_IMPULSE_USE; } + + virtual int DrawDebugTextOverlays( void ); + + void TurnOff( void ); + void TurnOn( void ); + + // Input handlers + void InputTurnOff( inputdata_t &inputdata ); + void InputTurnOn( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputSetExcluded( inputdata_t &inputdata ); + void InputSetInvert( inputdata_t &inputdata ); + + enum BrushSolidities_e { + BRUSHSOLID_TOGGLE = 0, + BRUSHSOLID_NEVER = 1, + BRUSHSOLID_ALWAYS = 2, + }; + + BrushSolidities_e m_iSolidity; + int m_iDisabled; + bool m_bSolidBsp; + string_t m_iszExcludedClass; + bool m_bInvertExclusion; + + DECLARE_DATADESC(); + + virtual bool IsOn( void ); +}; + + +#endif // MODELENTITIES_H diff --git a/sp/src/game/server/monstermaker.cpp b/sp/src/game/server/monstermaker.cpp new file mode 100644 index 00000000..5d56eabf --- /dev/null +++ b/sp/src/game/server/monstermaker.cpp @@ -0,0 +1,1095 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: An entity that creates NPCs in the game. There are two types of NPC +// makers -- one which creates NPCs using a template NPC, and one which +// creates an NPC via a classname. +// +//=============================================================================// + +#include "cbase.h" +#include "datacache/imdlcache.h" +#include "entityapi.h" +#include "entityoutput.h" +#include "ai_basenpc.h" +#include "monstermaker.h" +#include "TemplateEntities.h" +#include "ndebugoverlay.h" +#include "mapentities.h" +#include "IEffects.h" +#include "props.h" + +#include "point_template.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static void DispatchActivate( CBaseEntity *pEntity ) +{ + bool bAsyncAnims = mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, false ); + pEntity->Activate(); + mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, bAsyncAnims ); +} + +ConVar ai_inhibit_spawners( "ai_inhibit_spawners", "0", FCVAR_CHEAT ); + + +LINK_ENTITY_TO_CLASS( info_npc_spawn_destination, CNPCSpawnDestination ); + +BEGIN_DATADESC( CNPCSpawnDestination ) + DEFINE_KEYFIELD( m_ReuseDelay, FIELD_FLOAT, "ReuseDelay" ), + DEFINE_KEYFIELD( m_RenameNPC,FIELD_STRING, "RenameNPC" ), + DEFINE_FIELD( m_TimeNextAvailable, FIELD_TIME ), + + DEFINE_OUTPUT( m_OnSpawnNPC, "OnSpawnNPC" ), +END_DATADESC() + +//--------------------------------------------------------- +//--------------------------------------------------------- +CNPCSpawnDestination::CNPCSpawnDestination() +{ + // Available right away, the first time. + m_TimeNextAvailable = gpGlobals->curtime; +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +bool CNPCSpawnDestination::IsAvailable() +{ + if( m_TimeNextAvailable > gpGlobals->curtime ) + { + return false; + } + + return true; +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPCSpawnDestination::OnSpawnedNPC( CAI_BaseNPC *pNPC ) +{ + // Rename the NPC + if( m_RenameNPC != NULL_STRING ) + { + pNPC->SetName( m_RenameNPC ); + } + + m_OnSpawnNPC.FireOutput( pNPC, this ); + m_TimeNextAvailable = gpGlobals->curtime + m_ReuseDelay; +} + +//------------------------------------- +BEGIN_DATADESC( CBaseNPCMaker ) + + DEFINE_KEYFIELD( m_nMaxNumNPCs, FIELD_INTEGER, "MaxNPCCount" ), + DEFINE_KEYFIELD( m_nMaxLiveChildren, FIELD_INTEGER, "MaxLiveChildren" ), + DEFINE_KEYFIELD( m_flSpawnFrequency, FIELD_FLOAT, "SpawnFrequency" ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_FIELD( m_nLiveChildren, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Spawn", InputSpawnNPC ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxChildren", InputSetMaxChildren ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddMaxChildren", InputAddMaxChildren ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxLiveChildren", InputSetMaxLiveChildren ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpawnFrequency", InputSetSpawnFrequency ), + + // Outputs + DEFINE_OUTPUT( m_OnAllSpawned, "OnAllSpawned" ), + DEFINE_OUTPUT( m_OnAllSpawnedDead, "OnAllSpawnedDead" ), + DEFINE_OUTPUT( m_OnAllLiveChildrenDead, "OnAllLiveChildrenDead" ), + DEFINE_OUTPUT( m_OnSpawnNPC, "OnSpawnNPC" ), + + // Function Pointers + DEFINE_THINKFUNC( MakerThink ), + + DEFINE_FIELD( m_hIgnoreEntity, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_iszIngoreEnt, FIELD_STRING, "IgnoreEntity" ), +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Spawn +//----------------------------------------------------------------------------- +void CBaseNPCMaker::Spawn( void ) +{ + ScriptInstallPreSpawnHook(); + + SetSolid( SOLID_NONE ); + m_nLiveChildren = 0; + Precache(); + + // If I can make an infinite number of NPC, force them to fade + if ( m_spawnflags & SF_NPCMAKER_INF_CHILD ) + { + m_spawnflags |= SF_NPCMAKER_FADE; + } + + //Start on? + if ( m_bDisabled == false ) + { + SetThink ( &CBaseNPCMaker::MakerThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else + { + //wait to be activated. + SetThink ( &CBaseNPCMaker::SUB_DoNothing ); + } +} + +//----------------------------------------------------------------------------- +// A not-very-robust check to see if a human hull could fit at this location. +// used to validate spawn destinations. +//----------------------------------------------------------------------------- +bool CBaseNPCMaker::HumanHullFits( const Vector &vecLocation ) +{ + trace_t tr; + UTIL_TraceHull( vecLocation, + vecLocation + Vector( 0, 0, 1 ), + NAI_Hull::Mins(HULL_HUMAN), + NAI_Hull::Maxs(HULL_HUMAN), + MASK_NPCSOLID, + m_hIgnoreEntity, + COLLISION_GROUP_NONE, + &tr ); + + if( tr.fraction == 1.0 ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not it is OK to make an NPC at this instant. +//----------------------------------------------------------------------------- +bool CBaseNPCMaker::CanMakeNPC( bool bIgnoreSolidEntities ) +{ + if( ai_inhibit_spawners.GetBool() ) + return false; + + if ( m_nMaxLiveChildren > 0 && m_nLiveChildren >= m_nMaxLiveChildren ) + {// not allowed to make a new one yet. Too many live ones out right now. + return false; + } + + if ( m_iszIngoreEnt != NULL_STRING ) + { + m_hIgnoreEntity = gEntList.FindEntityByName( NULL, m_iszIngoreEnt ); + } + + Vector mins = GetAbsOrigin() - Vector( 34, 34, 0 ); + Vector maxs = GetAbsOrigin() + Vector( 34, 34, 0 ); + maxs.z = GetAbsOrigin().z; + + // If we care about not hitting solid entities, look for 'em + if ( !bIgnoreSolidEntities ) + { + CBaseEntity *pList[128]; + + int count = UTIL_EntitiesInBox( pList, 128, mins, maxs, FL_CLIENT|FL_NPC ); + if ( count ) + { + //Iterate through the list and check the results + for ( int i = 0; i < count; i++ ) + { + //Don't build on top of another entity + if ( pList[i] == NULL ) + continue; + + //If one of the entities is solid, then we may not be able to spawn now + if ( ( pList[i]->GetSolidFlags() & FSOLID_NOT_SOLID ) == false ) + { + // Since the outer method doesn't work well around striders on account of their huge bounding box. + // Find the ground under me and see if a human hull would fit there. + trace_t tr; + UTIL_TraceHull( GetAbsOrigin() + Vector( 0, 0, 2 ), + GetAbsOrigin() - Vector( 0, 0, 8192 ), + NAI_Hull::Mins(HULL_HUMAN), + NAI_Hull::Maxs(HULL_HUMAN), + MASK_NPCSOLID, + m_hIgnoreEntity, + COLLISION_GROUP_NONE, + &tr ); + + if( !HumanHullFits( tr.endpos + Vector( 0, 0, 1 ) ) ) + { + return false; + } + } + } + } + } + + // Do we need to check to see if the player's looking? + if ( HasSpawnFlags( SF_NPCMAKER_HIDEFROMPLAYER ) ) + { + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); + if ( pPlayer ) + { + // Only spawn if the player's looking away from me + if( pPlayer->FInViewCone( GetAbsOrigin() ) && pPlayer->FVisible( GetAbsOrigin() ) ) + { + if ( !(pPlayer->GetFlags() & FL_NOTARGET) ) + return false; + DevMsg( 2, "Spawner %s spawning even though seen due to notarget\n", STRING( GetEntityName() ) ); + } + } + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: If this had a finite number of children, return true if they've all +// been created. +//----------------------------------------------------------------------------- +bool CBaseNPCMaker::IsDepleted() +{ + if ( (m_spawnflags & SF_NPCMAKER_INF_CHILD) || m_nMaxNumNPCs > 0 ) + return false; + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Toggle the spawner's state +//----------------------------------------------------------------------------- +void CBaseNPCMaker::Toggle( void ) +{ + if ( m_bDisabled ) + { + Enable(); + } + else + { + Disable(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Start the spawner +//----------------------------------------------------------------------------- +void CBaseNPCMaker::Enable( void ) +{ + // can't be enabled once depleted + if ( IsDepleted() ) + return; + + m_bDisabled = false; + SetThink ( &CBaseNPCMaker::MakerThink ); + SetNextThink( gpGlobals->curtime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Stop the spawner +//----------------------------------------------------------------------------- +void CBaseNPCMaker::Disable( void ) +{ + m_bDisabled = true; + SetThink ( NULL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that spawns an NPC. +//----------------------------------------------------------------------------- +void CBaseNPCMaker::InputSpawnNPC( inputdata_t &inputdata ) +{ + if( !IsDepleted() ) + { + MakeNPC(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input hander that starts the spawner +//----------------------------------------------------------------------------- +void CBaseNPCMaker::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input hander that stops the spawner +//----------------------------------------------------------------------------- +void CBaseNPCMaker::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input hander that toggles the spawner +//----------------------------------------------------------------------------- +void CBaseNPCMaker::InputToggle( inputdata_t &inputdata ) +{ + Toggle(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseNPCMaker::InputSetMaxChildren( inputdata_t &inputdata ) +{ + m_nMaxNumNPCs = inputdata.value.Int(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseNPCMaker::InputAddMaxChildren( inputdata_t &inputdata ) +{ + m_nMaxNumNPCs += inputdata.value.Int(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseNPCMaker::InputSetMaxLiveChildren( inputdata_t &inputdata ) +{ + m_nMaxLiveChildren = inputdata.value.Int(); +} + +void CBaseNPCMaker::InputSetSpawnFrequency( inputdata_t &inputdata ) +{ + m_flSpawnFrequency = inputdata.value.Float(); +} + +LINK_ENTITY_TO_CLASS( npc_maker, CNPCMaker ); + +BEGIN_DATADESC( CNPCMaker ) + + DEFINE_KEYFIELD( m_iszNPCClassname, FIELD_STRING, "NPCType" ), + DEFINE_KEYFIELD( m_ChildTargetName, FIELD_STRING, "NPCTargetname" ), + DEFINE_KEYFIELD( m_SquadName, FIELD_STRING, "NPCSquadName" ), + DEFINE_KEYFIELD( m_spawnEquipment, FIELD_STRING, "additionalequipment" ), + DEFINE_KEYFIELD( m_strHintGroup, FIELD_STRING, "NPCHintGroup" ), + DEFINE_KEYFIELD( m_RelationshipString, FIELD_STRING, "Relationship" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CNPCMaker::CNPCMaker( void ) +{ + m_spawnEquipment = NULL_STRING; +} + + +//----------------------------------------------------------------------------- +// Purpose: Precache the target NPC +//----------------------------------------------------------------------------- +void CNPCMaker::Precache( void ) +{ + BaseClass::Precache(); + + const char *pszNPCName = STRING( m_iszNPCClassname ); + if ( !pszNPCName || !pszNPCName[0] ) + { + Warning("npc_maker %s has no specified NPC-to-spawn classname.\n", STRING(GetEntityName()) ); + } + else + { + UTIL_PrecacheOther( pszNPCName ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Creates the NPC. +//----------------------------------------------------------------------------- +void CNPCMaker::MakeNPC( void ) +{ + if (!CanMakeNPC()) + return; + + CAI_BaseNPC *pent = (CAI_BaseNPC*)CreateEntityByName( STRING(m_iszNPCClassname) ); + + if ( !pent ) + { + Warning("NULL Ent in NPCMaker!\n" ); + return; + } + + // ------------------------------------------------ + // Intialize spawned NPC's relationships + // ------------------------------------------------ + pent->SetRelationshipString( m_RelationshipString ); + + m_OnSpawnNPC.Set( pent, pent, this ); + + pent->SetAbsOrigin( GetAbsOrigin() ); + + // Strip pitch and roll from the spawner's angles. Pass only yaw to the spawned NPC. + QAngle angles = GetAbsAngles(); + angles.x = 0.0; + angles.z = 0.0; + pent->SetAbsAngles( angles ); + + pent->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); + + if ( m_spawnflags & SF_NPCMAKER_FADE ) + { + pent->AddSpawnFlags( SF_NPC_FADE_CORPSE ); + } + + pent->m_spawnEquipment = m_spawnEquipment; + pent->SetSquadName( m_SquadName ); + pent->SetHintGroup( m_strHintGroup ); + + ChildPreSpawn( pent ); + + DispatchSpawn( pent ); + pent->SetOwnerEntity( this ); + DispatchActivate( pent ); + + if ( m_ChildTargetName != NULL_STRING ) + { + // if I have a netname (overloaded), give the child NPC that name as a targetname + pent->SetName( m_ChildTargetName ); + } + + ChildPostSpawn( pent ); + + m_nLiveChildren++;// count this NPC + + if (!(m_spawnflags & SF_NPCMAKER_INF_CHILD)) + { + m_nMaxNumNPCs--; + + if ( IsDepleted() ) + { + m_OnAllSpawned.FireOutput( this, this ); + + // Disable this forever. Don't kill it because it still gets death notices + SetThink( NULL ); + SetUse( NULL ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pChild - +//----------------------------------------------------------------------------- +void CBaseNPCMaker::ChildPostSpawn( CAI_BaseNPC *pChild ) +{ + // If I'm stuck inside any props, remove them + bool bFound = true; + while ( bFound ) + { + trace_t tr; + UTIL_TraceHull( pChild->GetAbsOrigin(), pChild->GetAbsOrigin(), pChild->WorldAlignMins(), pChild->WorldAlignMaxs(), MASK_NPCSOLID, pChild, COLLISION_GROUP_NONE, &tr ); + //NDebugOverlay::Box( pChild->GetAbsOrigin(), pChild->WorldAlignMins(), pChild->WorldAlignMaxs(), 0, 255, 0, 32, 5.0 ); + if ( tr.fraction != 1.0 && tr.m_pEnt ) + { + if ( FClassnameIs( tr.m_pEnt, "prop_physics" ) ) + { + // Set to non-solid so this loop doesn't keep finding it + tr.m_pEnt->AddSolidFlags( FSOLID_NOT_SOLID ); + UTIL_RemoveImmediate( tr.m_pEnt ); + continue; + } + } + + bFound = false; + } + if ( m_hIgnoreEntity != NULL ) + { + pChild->SetOwnerEntity( m_hIgnoreEntity ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Creates a new NPC every so often. +//----------------------------------------------------------------------------- +void CBaseNPCMaker::MakerThink ( void ) +{ + SetNextThink( gpGlobals->curtime + m_flSpawnFrequency ); + + MakeNPC(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pVictim - +//----------------------------------------------------------------------------- +void CBaseNPCMaker::DeathNotice( CBaseEntity *pVictim ) +{ + // ok, we've gotten the deathnotice from our child, now clear out its owner if we don't want it to fade. + m_nLiveChildren--; + + // If we're here, we're getting erroneous death messages from children we haven't created + AssertMsg( m_nLiveChildren >= 0, "npc_maker receiving child death notice but thinks has no children\n" ); + + if ( m_nLiveChildren <= 0 ) + { + m_OnAllLiveChildrenDead.FireOutput( this, this ); + + // See if we've exhausted our supply of NPCs + if ( ( (m_spawnflags & SF_NPCMAKER_INF_CHILD) == false ) && IsDepleted() ) + { + // Signal that all our children have been spawned and are now dead + m_OnAllSpawnedDead.FireOutput( this, this ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Creates new NPCs from a template NPC. The template NPC must be marked +// as a template (spawnflag) and does not spawn. +//----------------------------------------------------------------------------- + +LINK_ENTITY_TO_CLASS( npc_template_maker, CTemplateNPCMaker ); + +BEGIN_DATADESC( CTemplateNPCMaker ) + + DEFINE_KEYFIELD( m_iszTemplateName, FIELD_STRING, "TemplateName" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + DEFINE_FIELD( m_iszTemplateData, FIELD_STRING ), + DEFINE_KEYFIELD( m_iszDestinationGroup, FIELD_STRING, "DestinationGroup" ), + DEFINE_KEYFIELD( m_CriterionVisibility, FIELD_INTEGER, "CriterionVisibility" ), + DEFINE_KEYFIELD( m_CriterionDistance, FIELD_INTEGER, "CriterionDistance" ), + DEFINE_KEYFIELD( m_iMinSpawnDistance, FIELD_INTEGER, "MinSpawnDistance" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "SpawnNPCInRadius", InputSpawnInRadius ), + DEFINE_INPUTFUNC( FIELD_VOID, "SpawnNPCInLine", InputSpawnInLine ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SpawnMultiple", InputSpawnMultiple ), + DEFINE_INPUTFUNC( FIELD_STRING, "ChangeDestinationGroup", InputChangeDestinationGroup ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMinimumSpawnDistance", InputSetMinimumSpawnDistance ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// A hook that lets derived NPC makers do special stuff when precaching. +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::PrecacheTemplateEntity( CBaseEntity *pEntity ) +{ + pEntity->Precache(); +} + + +void CTemplateNPCMaker::Precache() +{ + BaseClass::Precache(); + + if ( !m_iszTemplateData ) + { + // + // This must be the first time we're activated, not a load from save game. + // Look up the template in the template database. + // + if (!m_iszTemplateName) + { + Warning( "npc_template_maker %s has no template NPC!\n", STRING(GetEntityName()) ); + UTIL_Remove( this ); + return; + } + else + { + m_iszTemplateData = Templates_FindByTargetName(STRING(m_iszTemplateName)); + if ( m_iszTemplateData == NULL_STRING ) + { + DevWarning( "npc_template_maker %s: template NPC %s not found!\n", STRING(GetEntityName()), STRING(m_iszTemplateName) ); + UTIL_Remove( this ); + return; + } + } + } + + Assert( m_iszTemplateData != NULL_STRING ); + + // If the mapper marked this as "preload", then instance the entity preache stuff and delete the entity + //if ( !HasSpawnFlags(SF_NPCMAKER_NOPRELOADMODELS) ) + if ( m_iszTemplateData != NULL_STRING ) + { + CBaseEntity *pEntity = NULL; + MapEntity_ParseEntity( pEntity, STRING(m_iszTemplateData), NULL ); + if ( pEntity != NULL ) + { + PrecacheTemplateEntity( pEntity ); + UTIL_RemoveImmediate( pEntity ); + } + } +} + +#define MAX_DESTINATION_ENTS 100 +CNPCSpawnDestination *CTemplateNPCMaker::FindSpawnDestination() +{ + CNPCSpawnDestination *pDestinations[ MAX_DESTINATION_ENTS ]; + CBaseEntity *pEnt = NULL; + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + int count = 0; + + if( !pPlayer ) + { + return NULL; + } + + // Collect all the qualifiying destination ents + pEnt = gEntList.FindEntityByName( NULL, m_iszDestinationGroup ); + + if( !pEnt ) + { + DevWarning("Template NPC Spawner (%s) doesn't have any spawn destinations!\n", GetDebugName() ); + return NULL; + } + + while( pEnt ) + { + CNPCSpawnDestination *pDestination; + + pDestination = dynamic_cast (pEnt); + + if( pDestination && pDestination->IsAvailable() ) + { + bool fValid = true; + Vector vecTest = pDestination->GetAbsOrigin(); + + if( m_CriterionVisibility != TS_YN_DONT_CARE ) + { + // Right now View Cone check is omitted intentionally. + Vector vecTopOfHull = NAI_Hull::Maxs( HULL_HUMAN ); + vecTopOfHull.x = 0; + vecTopOfHull.y = 0; + bool fVisible = (pPlayer->FVisible( vecTest ) || pPlayer->FVisible( vecTest + vecTopOfHull ) ); + + if( m_CriterionVisibility == TS_YN_YES ) + { + if( !fVisible ) + fValid = false; + } + else + { + if( fVisible ) + { + if ( !(pPlayer->GetFlags() & FL_NOTARGET) ) + fValid = false; + else + DevMsg( 2, "Spawner %s spawning even though seen due to notarget\n", STRING( GetEntityName() ) ); + } + } + } + + if( fValid ) + { + pDestinations[ count ] = pDestination; + count++; + } + } + + pEnt = gEntList.FindEntityByName( pEnt, m_iszDestinationGroup ); + } + + if( count < 1 ) + return NULL; + + // Now find the nearest/farthest based on distance criterion + if( m_CriterionDistance == TS_DIST_DONT_CARE ) + { + // Pretty lame way to pick randomly. Try a few times to find a random + // location where a hull can fit. Don't try too many times due to performance + // concerns. + for( int i = 0 ; i < 5 ; i++ ) + { + CNPCSpawnDestination *pRandomDest = pDestinations[ rand() % count ]; + + if( HumanHullFits( pRandomDest->GetAbsOrigin() ) ) + { + return pRandomDest; + } + } + + return NULL; + } + else + { + if( m_CriterionDistance == TS_DIST_NEAREST ) + { + float flNearest = FLT_MAX; + CNPCSpawnDestination *pNearest = NULL; + + for( int i = 0 ; i < count ; i++ ) + { + Vector vecTest = pDestinations[ i ]->GetAbsOrigin(); + float flDist = ( vecTest - pPlayer->GetAbsOrigin() ).Length(); + + if ( m_iMinSpawnDistance != 0 && m_iMinSpawnDistance > flDist ) + continue; + + if( flDist < flNearest && HumanHullFits( vecTest ) ) + { + flNearest = flDist; + pNearest = pDestinations[ i ]; + } + } + + return pNearest; + } + else + { + float flFarthest = 0; + CNPCSpawnDestination *pFarthest = NULL; + + for( int i = 0 ; i < count ; i++ ) + { + Vector vecTest = pDestinations[ i ]->GetAbsOrigin(); + float flDist = ( vecTest - pPlayer->GetAbsOrigin() ).Length(); + + if ( m_iMinSpawnDistance != 0 && m_iMinSpawnDistance > flDist ) + continue; + + if( flDist > flFarthest && HumanHullFits( vecTest ) ) + { + flFarthest = flDist; + pFarthest = pDestinations[ i ]; + } + } + + return pFarthest; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::MakeNPC( void ) +{ + // If we should be using the radius spawn method instead, do so + if ( m_flRadius && HasSpawnFlags(SF_NPCMAKER_ALWAYSUSERADIUS) ) + { + MakeNPCInRadius(); + return; + } + + if (!CanMakeNPC( ( m_iszDestinationGroup != NULL_STRING ) )) + return; + + CNPCSpawnDestination *pDestination = NULL; + if ( m_iszDestinationGroup != NULL_STRING ) + { + pDestination = FindSpawnDestination(); + if ( !pDestination ) + { + DevMsg( 2, "%s '%s' failed to find a valid spawnpoint in destination group: '%s'\n", GetClassname(), STRING(GetEntityName()), STRING(m_iszDestinationGroup) ); + return; + } + } + + CAI_BaseNPC *pent = NULL; + CBaseEntity *pEntity = NULL; + MapEntity_ParseEntity( pEntity, STRING(m_iszTemplateData), NULL ); + if ( pEntity != NULL ) + { + pent = (CAI_BaseNPC *)pEntity; + } + + if ( !pent ) + { + Warning("NULL Ent in NPCMaker!\n" ); + return; + } + + if ( pDestination ) + { + pent->SetAbsOrigin( pDestination->GetAbsOrigin() ); + + // Strip pitch and roll from the spawner's angles. Pass only yaw to the spawned NPC. + QAngle angles = pDestination->GetAbsAngles(); + angles.x = 0.0; + angles.z = 0.0; + pent->SetAbsAngles( angles ); + + pDestination->OnSpawnedNPC( pent ); + } + else + { + pent->SetAbsOrigin( GetAbsOrigin() ); + + // Strip pitch and roll from the spawner's angles. Pass only yaw to the spawned NPC. + QAngle angles = GetAbsAngles(); + angles.x = 0.0; + angles.z = 0.0; + pent->SetAbsAngles( angles ); + } + + if ( !ScriptPreInstanceSpawn( &m_ScriptScope, pEntity, m_iszTemplateData ) ) + { + UTIL_RemoveImmediate( pEntity ); + return; + } + + m_OnSpawnNPC.Set( pEntity, pEntity, this ); + + if ( m_spawnflags & SF_NPCMAKER_FADE ) + { + pent->AddSpawnFlags( SF_NPC_FADE_CORPSE ); + } + + pent->RemoveSpawnFlags( SF_NPC_TEMPLATE ); + + if ( ( m_spawnflags & SF_NPCMAKER_NO_DROP ) == false ) + { + pent->RemoveSpawnFlags( SF_NPC_FALL_TO_GROUND ); // don't fall, slam + } + + ChildPreSpawn( pent ); + + DispatchSpawn( pent ); + pent->SetOwnerEntity( this ); + DispatchActivate( pent ); + + ChildPostSpawn( pent ); + + m_nLiveChildren++;// count this NPC + + if (!(m_spawnflags & SF_NPCMAKER_INF_CHILD)) + { + m_nMaxNumNPCs--; + + if ( IsDepleted() ) + { + m_OnAllSpawned.FireOutput( this, this ); + + // Disable this forever. Don't kill it because it still gets death notices + SetThink( NULL ); + SetUse( NULL ); + } + } + + ScriptPostSpawn( &m_ScriptScope, &pEntity, 1 ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::MakeNPCInLine( void ) +{ + if (!CanMakeNPC(true)) + return; + + CAI_BaseNPC *pent = NULL; + CBaseEntity *pEntity = NULL; + MapEntity_ParseEntity( pEntity, STRING(m_iszTemplateData), NULL ); + if ( pEntity != NULL ) + { + pent = (CAI_BaseNPC *)pEntity; + } + + if ( !pent ) + { + Warning("NULL Ent in NPCMaker!\n" ); + return; + } + + m_OnSpawnNPC.Set( pEntity, pEntity, this ); + + PlaceNPCInLine( pent ); + + pent->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); + + pent->RemoveSpawnFlags( SF_NPC_TEMPLATE ); + ChildPreSpawn( pent ); + + DispatchSpawn( pent ); + pent->SetOwnerEntity( this ); + DispatchActivate( pent ); + + ChildPostSpawn( pent ); + + m_nLiveChildren++;// count this NPC + + if (!(m_spawnflags & SF_NPCMAKER_INF_CHILD)) + { + m_nMaxNumNPCs--; + + if ( IsDepleted() ) + { + m_OnAllSpawned.FireOutput( this, this ); + + // Disable this forever. Don't kill it because it still gets death notices + SetThink( NULL ); + SetUse( NULL ); + } + } +} + +//----------------------------------------------------------------------------- +bool CTemplateNPCMaker::PlaceNPCInLine( CAI_BaseNPC *pNPC ) +{ + Vector vecPlace; + Vector vecLine; + + GetVectors( &vecLine, NULL, NULL ); + + // invert this, line up NPC's BEHIND the maker. + vecLine *= -1; + + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector( 0, 0, 8192 ), MASK_SHOT, pNPC, COLLISION_GROUP_NONE, &tr ); + vecPlace = tr.endpos; + float flStepSize = pNPC->GetHullWidth(); + + // Try 10 times to place this npc. + for( int i = 0 ; i < 10 ; i++ ) + { + UTIL_TraceHull( vecPlace, + vecPlace + Vector( 0, 0, 10 ), + pNPC->GetHullMins(), + pNPC->GetHullMaxs(), + MASK_SHOT, + pNPC, + COLLISION_GROUP_NONE, + &tr ); + + if( tr.fraction == 1.0 ) + { + pNPC->SetAbsOrigin( tr.endpos ); + return true; + } + + vecPlace += vecLine * flStepSize; + } + + DevMsg("**Failed to place NPC in line!\n"); + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Place NPC somewhere on the perimeter of my radius. +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::MakeNPCInRadius( void ) +{ + if ( !CanMakeNPC(true)) + return; + + CAI_BaseNPC *pent = NULL; + CBaseEntity *pEntity = NULL; + MapEntity_ParseEntity( pEntity, STRING(m_iszTemplateData), NULL ); + if ( pEntity != NULL ) + { + pent = (CAI_BaseNPC *)pEntity; + } + + if ( !pent ) + { + Warning("NULL Ent in NPCMaker!\n" ); + return; + } + + if ( !PlaceNPCInRadius( pent ) ) + { + // Failed to place the NPC. Abort + UTIL_RemoveImmediate( pent ); + return; + } + + m_OnSpawnNPC.Set( pEntity, pEntity, this ); + + pent->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); + + pent->RemoveSpawnFlags( SF_NPC_TEMPLATE ); + ChildPreSpawn( pent ); + + DispatchSpawn( pent ); + + pent->SetOwnerEntity( this ); + DispatchActivate( pent ); + + ChildPostSpawn( pent ); + + m_nLiveChildren++;// count this NPC + + if (!(m_spawnflags & SF_NPCMAKER_INF_CHILD)) + { + m_nMaxNumNPCs--; + + if ( IsDepleted() ) + { + m_OnAllSpawned.FireOutput( this, this ); + + // Disable this forever. Don't kill it because it still gets death notices + SetThink( NULL ); + SetUse( NULL ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Find a place to spawn an npc within my radius. +// Right now this function tries to place them on the perimeter of radius. +// Output : false if we couldn't find a spot! +//----------------------------------------------------------------------------- +bool CTemplateNPCMaker::PlaceNPCInRadius( CAI_BaseNPC *pNPC ) +{ + Vector vPos; + + if ( CAI_BaseNPC::FindSpotForNPCInRadius( &vPos, GetAbsOrigin(), pNPC, m_flRadius ) ) + { + pNPC->SetAbsOrigin( vPos ); + return true; + } + + DevMsg("**Failed to place NPC in radius!\n"); + return false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::MakeMultipleNPCS( int nNPCs ) +{ + bool bInRadius = ( m_iszDestinationGroup == NULL_STRING && m_flRadius > 0.1 ); + while ( nNPCs-- ) + { + if ( !bInRadius ) + { + MakeNPC(); + } + else + { + MakeNPCInRadius(); + } + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::InputSpawnMultiple( inputdata_t &inputdata ) +{ + MakeMultipleNPCS( inputdata.value.Int() ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::InputChangeDestinationGroup( inputdata_t &inputdata ) +{ + m_iszDestinationGroup = inputdata.value.StringID(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CTemplateNPCMaker::InputSetMinimumSpawnDistance( inputdata_t &inputdata ) +{ + m_iMinSpawnDistance = inputdata.value.Int(); +} diff --git a/sp/src/game/server/monstermaker.h b/sp/src/game/server/monstermaker.h new file mode 100644 index 00000000..e866662e --- /dev/null +++ b/sp/src/game/server/monstermaker.h @@ -0,0 +1,184 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef MONSTERMAKER_H +#define MONSTERMAKER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "cbase.h" + + +//----------------------------------------------------------------------------- +// Spawnflags +//----------------------------------------------------------------------------- +#define SF_NPCMAKER_START_ON 1 // start active ( if has targetname ) +#define SF_NPCMAKER_NPCCLIP 8 // Children are blocked by NPCclip +#define SF_NPCMAKER_FADE 16 // Children's corpses fade +#define SF_NPCMAKER_INF_CHILD 32 // Infinite number of children +#define SF_NPCMAKER_NO_DROP 64 // Do not adjust for the ground's position when checking for spawn +#define SF_NPCMAKER_HIDEFROMPLAYER 128 // Don't spawn if the player's looking at me +#define SF_NPCMAKER_ALWAYSUSERADIUS 256 // Use radius spawn whenever spawning +#define SF_NPCMAKER_NOPRELOADMODELS 512 // Suppress preloading into the cache of all referenced .mdl files + +//========================================================= +//========================================================= +class CNPCSpawnDestination : public CPointEntity +{ + DECLARE_CLASS( CNPCSpawnDestination, CPointEntity ); + +public: + CNPCSpawnDestination(); + bool IsAvailable(); // Is this spawn destination available for selection? + void OnSpawnedNPC( CAI_BaseNPC *pNPC ); // Notify this spawn destination that an NPC has spawned here. + + float m_ReuseDelay; // How long to be unavailable after being selected + string_t m_RenameNPC; // If not NULL, rename the NPC that spawns here to this. + float m_TimeNextAvailable;// The time at which this destination will be available again. + + COutputEvent m_OnSpawnNPC; + + DECLARE_DATADESC(); +}; + +abstract_class CBaseNPCMaker : public CBaseEntity +{ +public: + DECLARE_CLASS( CBaseNPCMaker, CBaseEntity ); + + void Spawn( void ); + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + void MakerThink( void ); + bool HumanHullFits( const Vector &vecLocation ); + bool CanMakeNPC( bool bIgnoreSolidEntities = false ); + + virtual void DeathNotice( CBaseEntity *pChild );// NPC maker children use this to tell the NPC maker that they have died. + virtual void MakeNPC( void ) = 0; + + virtual void ChildPreSpawn( CAI_BaseNPC *pChild ) {}; + virtual void ChildPostSpawn( CAI_BaseNPC *pChild ); + + CBaseNPCMaker(void) {} + + // Input handlers + void InputSpawnNPC( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputSetMaxChildren( inputdata_t &inputdata ); + void InputAddMaxChildren( inputdata_t &inputdata ); + void InputSetMaxLiveChildren( inputdata_t &inputdata ); + void InputSetSpawnFrequency( inputdata_t &inputdata ); + + // State changers + void Toggle( void ); + virtual void Enable( void ); + virtual void Disable( void ); + + virtual bool IsDepleted( void ); + + DECLARE_DATADESC(); + + int m_nMaxNumNPCs; // max number of NPCs this ent can create + float m_flSpawnFrequency; // delay (in secs) between spawns + + COutputEHANDLE m_OnSpawnNPC; + COutputEvent m_OnAllSpawned; + COutputEvent m_OnAllSpawnedDead; + COutputEvent m_OnAllLiveChildrenDead; + + int m_nLiveChildren; // how many NPCs made by this NPC maker that are currently alive + int m_nMaxLiveChildren; // max number of NPCs that this maker may have out at one time. + + bool m_bDisabled; + + EHANDLE m_hIgnoreEntity; + string_t m_iszIngoreEnt; +}; + + +class CNPCMaker : public CBaseNPCMaker +{ +public: + DECLARE_CLASS( CNPCMaker, CBaseNPCMaker ); + + CNPCMaker( void ); + + void Precache( void ); + + virtual void MakeNPC( void ); + + DECLARE_DATADESC(); + + string_t m_iszNPCClassname; // classname of the NPC(s) that will be created. + string_t m_SquadName; + string_t m_strHintGroup; + string_t m_spawnEquipment; + string_t m_RelationshipString; // Used to load up relationship keyvalues + string_t m_ChildTargetName; +}; + +class CTemplateNPCMaker : public CBaseNPCMaker +{ +public: + DECLARE_CLASS( CTemplateNPCMaker, CBaseNPCMaker ); + + CTemplateNPCMaker( void ) + { + m_iMinSpawnDistance = 0; + } + + virtual void Precache(); + + virtual CNPCSpawnDestination *FindSpawnDestination(); + virtual void MakeNPC( void ); + void MakeNPCInRadius( void ); + void MakeNPCInLine( void ); + virtual void MakeMultipleNPCS( int nNPCs ); + +protected: + virtual void PrecacheTemplateEntity( CBaseEntity *pEntity ); + + bool PlaceNPCInRadius( CAI_BaseNPC *pNPC ); + bool PlaceNPCInLine( CAI_BaseNPC *pNPC ); + + // Inputs + void InputSpawnInRadius( inputdata_t &inputdata ) { MakeNPCInRadius(); } + void InputSpawnInLine( inputdata_t &inputdata ) { MakeNPCInLine(); } + void InputSpawnMultiple( inputdata_t &inputdata ); + void InputChangeDestinationGroup( inputdata_t &inputdata ); + void InputSetMinimumSpawnDistance( inputdata_t &inputdata ); + + float m_flRadius; + + DECLARE_DATADESC(); + + string_t m_iszTemplateName; // The name of the NPC that will be used as the template. + string_t m_iszTemplateData; // The keyvalue data blob from the template NPC that will be used to spawn new ones. + string_t m_iszDestinationGroup; + + int m_iMinSpawnDistance; + + enum ThreeStateYesNo_t + { + TS_YN_YES = 0, + TS_YN_NO, + TS_YN_DONT_CARE, + }; + + enum ThreeStateDist_t + { + TS_DIST_NEAREST = 0, + TS_DIST_FARTHEST, + TS_DIST_DONT_CARE, + }; + + ThreeStateYesNo_t m_CriterionVisibility; + ThreeStateDist_t m_CriterionDistance; +}; + +#endif // MONSTERMAKER_H diff --git a/sp/src/game/server/movehelper_server.cpp b/sp/src/game/server/movehelper_server.cpp new file mode 100644 index 00000000..acae9866 --- /dev/null +++ b/sp/src/game/server/movehelper_server.cpp @@ -0,0 +1,416 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include +#include "gamerules.h" +#include "player.h" +#include "model_types.h" +#include "movehelper_server.h" +#include "shake.h" // For screen fade constants +#include "engine/IEngineSound.h" + +//============================================================================= +// HPE_BEGIN +// [dwenger] Necessary for stats tracking +//============================================================================= +#ifdef CSTRIKE_DLL + +#include "cs_gamestats.h" +#include "cs_achievement_constants.h" + +#endif +//============================================================================= +// HPE_END +//============================================================================= + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern IPhysicsCollision *physcollision; + + +//----------------------------------------------------------------------------- +// Implementation of the movehelper on the server +//----------------------------------------------------------------------------- + +class CMoveHelperServer : public IMoveHelperServer +{ +public: + CMoveHelperServer( void ); + virtual ~CMoveHelperServer(); + + // Methods associated with a particular entity + virtual char const* GetName( EntityHandle_t handle ) const; + + // Touch list... + virtual void ResetTouchList( void ); + virtual bool AddToTouched( const trace_t &tr, const Vector& impactvelocity ); + virtual void ProcessImpacts( void ); + + virtual bool PlayerFallingDamage( void ); + virtual void PlayerSetAnimation( PLAYER_ANIM eAnim ); + + // Numbered line printf + virtual void Con_NPrintf( int idx, char const* fmt, ... ); + + // These have separate server vs client impementations + virtual void StartSound( const Vector& origin, int channel, char const* sample, float volume, soundlevel_t soundlevel, int fFlags, int pitch ); + virtual void StartSound( const Vector& origin, const char *soundname ); + + virtual void PlaybackEventFull( int flags, int clientindex, unsigned short eventindex, float delay, Vector& origin, Vector& angles, float fparam1, float fparam2, int iparam1, int iparam2, int bparam1, int bparam2 ); + virtual IPhysicsSurfaceProps *GetSurfaceProps( void ); + + void SetHost( CBasePlayer *host ); + + virtual bool IsWorldEntity( const CBaseHandle &handle ); + +private: + CBasePlayer* m_pHostPlayer; + + // results, tallied on client and server, but only used by server to run SV_Impact. + // we store off our velocity in the trace_t structure so that we can determine results + // of shoving boxes etc. around. + struct touchlist_t + { + Vector deltavelocity; + trace_t trace; + }; + + CUtlVector m_TouchList; +}; + + +//----------------------------------------------------------------------------- +// Singleton +//----------------------------------------------------------------------------- + +IMPLEMENT_MOVEHELPER(); + +IMoveHelperServer* MoveHelperServer() +{ + static CMoveHelperServer s_MoveHelperServer; + return &s_MoveHelperServer; +} + + +//----------------------------------------------------------------------------- +// Converts the entity handle into a edict_t +//----------------------------------------------------------------------------- + +static inline edict_t* GetEdict( EntityHandle_t handle ) +{ + return gEntList.GetEdict( handle ); +} + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- + +CMoveHelperServer::CMoveHelperServer( void ) : m_TouchList( 0, 128 ) +{ + m_pHostPlayer = 0; + SetSingleton( this ); +} + +CMoveHelperServer::~CMoveHelperServer( void ) +{ + SetSingleton( 0 ); +} + +//----------------------------------------------------------------------------- +// Indicates which player we're going to move +//----------------------------------------------------------------------------- + +void CMoveHelperServer::SetHost( CBasePlayer *host ) +{ + m_pHostPlayer = host; + + // In case any stuff is ever left over, sigh... + ResetTouchList(); +} + + +//----------------------------------------------------------------------------- +// Returns the name for debugging purposes +//----------------------------------------------------------------------------- +char const* CMoveHelperServer::GetName( EntityHandle_t handle ) const +{ + // This ain't pertickulerly fast, but it's for debugging anyways + edict_t* pEdict = GetEdict(handle); + CBaseEntity *ent = CBaseEntity::Instance( pEdict ); + + // Is it the world? + if (ENTINDEX(pEdict) == 0) + return STRING(gpGlobals->mapname); + + // Is it a model? + if ( ent && ent->GetModelName() != NULL_STRING ) + return STRING( ent->GetModelName() ); + + if ( ent->GetClassname() != NULL ) + { + return ent->GetClassname(); + } + + return "?"; +} + +//----------------------------------------------------------------------------- +// When we do a collision test, we report everything we hit.. +//----------------------------------------------------------------------------- + +void CMoveHelperServer::ResetTouchList( void ) +{ + m_TouchList.RemoveAll(); +} + + +//----------------------------------------------------------------------------- +// When a collision occurs, we add it to the touched list +//----------------------------------------------------------------------------- + +bool CMoveHelperServer::AddToTouched( const trace_t &tr, const Vector& impactvelocity ) +{ + Assert( m_pHostPlayer ); + + // Trace missed + if ( !tr.m_pEnt ) + return false; + + if ( tr.m_pEnt == m_pHostPlayer ) + { + Assert( !"CMoveHelperServer::AddToTouched: Tried to add self to touchlist!!!" ); + return false; + } + + // Check for duplicate entities + for ( int j = m_TouchList.Size(); --j >= 0; ) + { + if ( m_TouchList[j].trace.m_pEnt == tr.m_pEnt ) + { + return false; + } + } + + int i = m_TouchList.AddToTail(); + m_TouchList[i].trace = tr; + VectorCopy( impactvelocity, m_TouchList[i].deltavelocity ); + + return true; +} + + +//----------------------------------------------------------------------------- +// After we built the touch list, deal with all the impacts... +//----------------------------------------------------------------------------- +void CMoveHelperServer::ProcessImpacts( void ) +{ + Assert( m_pHostPlayer ); + + // Relink in order to build absorigin and absmin/max to reflect any changes + // from prediction. Relink will early out on SOLID_NOT + m_pHostPlayer->PhysicsTouchTriggers(); + + // Don't bother if the player ain't solid + if ( m_pHostPlayer->IsSolidFlagSet( FSOLID_NOT_SOLID ) ) + return; + + // Save off the velocity, cause we need to temporarily reset it + Vector vel = m_pHostPlayer->GetAbsVelocity(); + + // Touch other objects that were intersected during the movement. + for (int i = 0 ; i < m_TouchList.Size(); i++) + { + CBaseHandle entindex = m_TouchList[i].trace.m_pEnt->GetRefEHandle(); + + // We should have culled negative indices by now + Assert( entindex.IsValid() ); + + edict_t* ent = GetEdict( entindex ); + if (!ent) + continue; + + // Run the impact function as if we had run it during movement. + CBaseEntity *entity = GetContainingEntity( ent ); + if ( !entity ) + continue; + + Assert( entity != m_pHostPlayer ); + // Don't ever collide with self!!!! + if ( entity == m_pHostPlayer ) + continue; + + // Reconstruct trace results. + m_TouchList[i].trace.m_pEnt = CBaseEntity::Instance( ent ); + + // Use the velocity we had when we collided, so boxes will move, etc. + m_pHostPlayer->SetAbsVelocity( m_TouchList[i].deltavelocity ); + + entity->PhysicsImpact( m_pHostPlayer, m_TouchList[i].trace ); + } + + // Restore the velocity + m_pHostPlayer->SetAbsVelocity( vel ); + + // So no stuff is ever left over, sigh... + ResetTouchList(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : origin - +// *soundname - +//----------------------------------------------------------------------------- +void CMoveHelperServer::StartSound( const Vector& origin, const char *soundname ) +{ + //MDB - Changing this to send to PAS, as the overloaded function below has done. + //Also removed the UsePredictionRules, client does not yet play the equivalent sound + + CRecipientFilter filter; + filter.AddRecipientsByPAS( origin ); + + CBaseEntity::EmitSound( filter, m_pHostPlayer->entindex(), soundname ); +} + +//----------------------------------------------------------------------------- +// plays a sound +//----------------------------------------------------------------------------- +void CMoveHelperServer::StartSound( const Vector& origin, int channel, char const* sample, + float volume, soundlevel_t soundlevel, int fFlags, int pitch ) +{ + + CRecipientFilter filter; + filter.AddRecipientsByPAS( origin ); + // FIXME, these sounds should not go to the host entity ( SND_NOTHOST ) + if ( gpGlobals->maxClients == 1 ) + { + // Always send sounds down in SP + + EmitSound_t ep; + ep.m_nChannel = channel; + ep.m_pSoundName = sample; + ep.m_flVolume = volume; + ep.m_SoundLevel = soundlevel; + ep.m_nFlags = fFlags; + ep.m_nPitch = pitch; + ep.m_pOrigin = &origin; + + CBaseEntity::EmitSound( filter, m_pHostPlayer->entindex(), ep ); + } + else + { + filter.UsePredictionRules(); + + EmitSound_t ep; + ep.m_nChannel = channel; + ep.m_pSoundName = sample; + ep.m_flVolume = volume; + ep.m_SoundLevel = soundlevel; + ep.m_nFlags = fFlags; + ep.m_nPitch = pitch; + ep.m_pOrigin = &origin; + + CBaseEntity::EmitSound( filter, m_pHostPlayer->entindex(), ep ); + } +} + + +//----------------------------------------------------------------------------- +// Umm... +//----------------------------------------------------------------------------- +void CMoveHelperServer::PlaybackEventFull( int flags, int clientindex, unsigned short eventindex, float delay, Vector& origin, Vector& angles, float fparam1, float fparam2, int iparam1, int iparam2, int bparam1, int bparam2 ) +{ + // FIXME, Redo with new event system parameter stuff +} + +IPhysicsSurfaceProps *CMoveHelperServer::GetSurfaceProps( void ) +{ + extern IPhysicsSurfaceProps *physprops; + return physprops; +} + +//----------------------------------------------------------------------------- +// Purpose: Note that this only works on a listen server (since it requires graphical output) +// *pFormat - +// ... - +//----------------------------------------------------------------------------- +void CMoveHelperServer::Con_NPrintf( int idx, char const* pFormat, ...) +{ + va_list marker; + char msg[8192]; + + va_start(marker, pFormat); + Q_vsnprintf(msg, sizeof( msg ), pFormat, marker); + va_end(marker); + + engine->Con_NPrintf( idx, msg ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called when the player falls onto a surface fast enough to take +// damage, according to the rules in CGameMovement::CheckFalling. +// Output : Returns true if the player survived the fall, false if they died. +//----------------------------------------------------------------------------- +bool CMoveHelperServer::PlayerFallingDamage( void ) +{ + float flFallDamage = g_pGameRules->FlPlayerFallDamage( m_pHostPlayer ); + if ( flFallDamage > 0 ) + { + m_pHostPlayer->TakeDamage( CTakeDamageInfo( GetContainingEntity(INDEXENT(0)), GetContainingEntity(INDEXENT(0)), flFallDamage, DMG_FALL ) ); + StartSound( m_pHostPlayer->GetAbsOrigin(), "Player.FallDamage" ); + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Needed for fun-fact implementation + //============================================================================= + +#ifdef CSTRIKE_DLL + + // Increment the stat for fall damage + CCSPlayer* pPlayer = ToCSPlayer(m_pHostPlayer); + + if ( pPlayer ) + { + CCS_GameStats.IncrementStat( pPlayer, CSSTAT_FALL_DAMAGE, (int)flFallDamage ); + } + +#endif + //============================================================================= + // HPE_END + //============================================================================= + + } + + if ( m_pHostPlayer->m_iHealth <= 0 ) + { + if ( g_pGameRules->FlPlayerFallDeathDoesScreenFade( m_pHostPlayer ) ) + { + color32 black = {0, 0, 0, 255}; + UTIL_ScreenFade( m_pHostPlayer, black, 0, 9999, FFADE_OUT | FFADE_STAYOUT ); + } + return(false); + } + + return(true); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets an animation in the player. +// Input : eAnim - Animation to set. +//----------------------------------------------------------------------------- +void CMoveHelperServer::PlayerSetAnimation( PLAYER_ANIM eAnim ) +{ + m_pHostPlayer->SetAnimation( eAnim ); +} + +bool CMoveHelperServer::IsWorldEntity( const CBaseHandle &handle ) +{ + return handle == CBaseEntity::Instance( 0 ); +} diff --git a/sp/src/game/server/movehelper_server.h b/sp/src/game/server/movehelper_server.h new file mode 100644 index 00000000..a203f77a --- /dev/null +++ b/sp/src/game/server/movehelper_server.h @@ -0,0 +1,43 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef MOVEHELPER_SERVER_H +#define MOVEHELPER_SERVER_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "imovehelper.h" + + +//----------------------------------------------------------------------------- +// Forward declarations +//----------------------------------------------------------------------------- + +class CBasePlayer; +class CBaseEntity; + + +//----------------------------------------------------------------------------- +// Implementation of the movehelper on the server +//----------------------------------------------------------------------------- + +abstract_class IMoveHelperServer : public IMoveHelper +{ +public: + virtual void SetHost( CBasePlayer *host ) = 0; +}; + +//----------------------------------------------------------------------------- +// Singleton access +//----------------------------------------------------------------------------- + +IMoveHelperServer* MoveHelperServer(); + + +#endif // MOVEHELPER_SERVER_H diff --git a/sp/src/game/server/movement.cpp b/sp/src/game/server/movement.cpp new file mode 100644 index 00000000..da12420e --- /dev/null +++ b/sp/src/game/server/movement.cpp @@ -0,0 +1,658 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: MOVEMENT ENTITIES TEST +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "entitylist.h" +#include "entityoutput.h" +#include "keyframe/keyframe.h" // BUG: this needs to move if keyframe is a standard thing + +#include "mathlib/mathlib.h" // FIXME: why do we still need this? + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Hack, sort of. These interpolators don't get to hold state, but the ones +// that need state (like the rope simulator) should NOT be used as paths here. +IPositionInterpolator *g_pPositionInterpolators[8] = {0,0,0,0,0,0,0,0}; + +IPositionInterpolator* GetPositionInterpolator( int iInterp ) +{ + if( !g_pPositionInterpolators[iInterp] ) + g_pPositionInterpolators[iInterp] = Motion_GetPositionInterpolator( iInterp ); + + return g_pPositionInterpolators[iInterp]; +} + + +static float Fix( float angle ) +{ + while ( angle < 0 ) + angle += 360; + while ( angle > 360 ) + angle -= 360; + + return angle; +} + +void FixupAngles( QAngle &v ) +{ + v.x = Fix( v.x ); + v.y = Fix( v.y ); + v.z = Fix( v.z ); +} + + +//----------------------------------------------------------------------------- +// +// Purpose: Contains a description of a keyframe +// has no networked representation, so has to store origin, etc. itself +// +//----------------------------------------------------------------------------- +class CPathKeyFrame : public CLogicalEntity +{ +public: + DECLARE_CLASS( CPathKeyFrame, CLogicalEntity ); + + void Spawn( void ); + void Activate( void ); + void Link( void ); + + Vector m_Origin; + QAngle m_Angles; // euler angles PITCH YAW ROLL (Y Z X) + Quaternion m_qAngle; // quaternion angle (generated from m_Angles) + + string_t m_iNextKey; + float m_flNextTime; + + CPathKeyFrame *NextKey( int direction ); + CPathKeyFrame *PrevKey( int direction ); + + float Speed( void ) { return m_flSpeed; } + void SetKeyAngles( QAngle angles ); + + CPathKeyFrame *InsertNewKey( Vector newPos, QAngle newAngles ); + void CalculateFrameDuration( void ); + +protected: + CPathKeyFrame *m_pNextKey; + CPathKeyFrame *m_pPrevKey; + + float m_flSpeed; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( keyframe_track, CPathKeyFrame ); + +BEGIN_DATADESC( CPathKeyFrame ) + + DEFINE_FIELD( m_Origin, FIELD_VECTOR ), + DEFINE_FIELD( m_Angles, FIELD_VECTOR ), + DEFINE_FIELD( m_qAngle, FIELD_QUATERNION ), + + DEFINE_KEYFIELD( m_iNextKey, FIELD_STRING, "NextKey" ), + DEFINE_FIELD( m_flNextTime, FIELD_FLOAT ), // derived from speed + DEFINE_KEYFIELD( m_flSpeed, FIELD_FLOAT, "MoveSpeed" ), + DEFINE_FIELD( m_pNextKey, FIELD_CLASSPTR ), + DEFINE_FIELD( m_pPrevKey, FIELD_CLASSPTR ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Converts inputed euler angles to internal angle format (quaternions) +//----------------------------------------------------------------------------- +void CPathKeyFrame::Spawn( void ) +{ + m_Origin = GetLocalOrigin(); + m_Angles = GetLocalAngles(); + + SetKeyAngles( m_Angles ); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds the keyframe into the path after all the other keys have spawned +//----------------------------------------------------------------------------- +void CPathKeyFrame::Activate( void ) +{ + BaseClass::Activate(); + + Link(); + + CalculateFrameDuration(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathKeyFrame::CalculateFrameDuration( void ) +{ + // calculate time from speed + if ( m_pNextKey && m_flSpeed > 0 ) + { + m_flNextTime = (m_Origin - m_pNextKey->m_Origin).Length() / m_flSpeed; + + // couldn't get time from distance, get it from rotation instead + if ( !m_flNextTime ) + { + // speed is in degrees per second + // find the largest rotation component and use that + QAngle ang = m_Angles - m_pNextKey->m_Angles; + FixupAngles( ang ); + float x = 0; + for ( int i = 0; i < 3; i++ ) + { + if ( abs(ang[i]) > x ) + { + x = abs(ang[i]); + } + } + + m_flNextTime = x / m_flSpeed; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Links the key frame into the key frame list +//----------------------------------------------------------------------------- +void CPathKeyFrame::Link( void ) +{ + m_pNextKey = dynamic_cast( gEntList.FindEntityByName(NULL, m_iNextKey ) ); + + if ( m_pNextKey ) + { + m_pNextKey->m_pPrevKey = this; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : angles - +//----------------------------------------------------------------------------- +void CPathKeyFrame::SetKeyAngles( QAngle angles ) +{ + m_Angles = angles; + AngleQuaternion( m_Angles, m_qAngle ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : direction - +// Output : CPathKeyFrame +//----------------------------------------------------------------------------- +CPathKeyFrame* CPathKeyFrame::NextKey( int direction ) +{ + if ( direction == 1 ) + { + return m_pNextKey; + } + else if ( direction == -1 ) + { + return m_pPrevKey; + } + + return this; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : direction - +// Output : CPathKeyFrame +//----------------------------------------------------------------------------- +CPathKeyFrame *CPathKeyFrame::PrevKey( int direction ) +{ + if ( direction == 1 ) + { + return m_pPrevKey; + } + else if ( direction == -1 ) + { + return m_pNextKey; + } + + return this; +} + +//----------------------------------------------------------------------------- +// Purpose: Creates and insterts a new keyframe into the sequence +// Input : newPos - +// newAngles - +// Output : CPathKeyFrame +//----------------------------------------------------------------------------- +CPathKeyFrame *CPathKeyFrame::InsertNewKey( Vector newPos, QAngle newAngles ) +{ + CPathKeyFrame *newKey = CREATE_ENTITY( CPathKeyFrame, "keyframe_track" ); + + // copy data across + newKey->SetKeyAngles( newAngles ); + newKey->m_Origin = newPos; + newKey->m_flSpeed = m_flSpeed; + newKey->SetEFlags( GetEFlags() ); + if ( m_iParent != NULL_STRING ) + { + newKey->SetParent( m_iParent, NULL ); + } + + // link forward + newKey->m_pNextKey = m_pNextKey; + m_pNextKey->m_pPrevKey = newKey; + + // link back + m_pNextKey = newKey; + newKey->m_pPrevKey = this; + + // calculate new times + CalculateFrameDuration(); + newKey->CalculateFrameDuration(); + + return newKey; +} + + +//----------------------------------------------------------------------------- +// +// Purpose: Basic keyframed movement behavior +// +//----------------------------------------------------------------------------- +class CBaseMoveBehavior : public CPathKeyFrame +{ +public: + DECLARE_CLASS( CBaseMoveBehavior, CPathKeyFrame ); + + void Spawn( void ); + void Activate( void ); + void MoveDone( void ); + float SetObjectPhysicsVelocity( float moveTime ); + + // methods + virtual bool StartMoving( int direction ); + virtual void StopMoving( void ); + virtual bool IsMoving( void ); + + // derived classes should override this to get notification of arriving at new keyframes +// virtual void ArrivedAtKeyFrame( CPathKeyFrame * ) {} + + bool IsAtSequenceStart( void ); + bool IsAtSequenceEnd( void ); + + // interpolation functions +// int m_iTimeModifier; + int m_iPositionInterpolator; + int m_iRotationInterpolator; + + // animation vars + float m_flAnimStartTime; + float m_flAnimEndTime; + float m_flAverageSpeedAcrossFrame; // for advancing time with speed (not the normal visa-versa) + CPathKeyFrame *m_pCurrentKeyFrame; // keyframe currently moving from + CPathKeyFrame *m_pTargetKeyFrame; // keyframe being moved to + CPathKeyFrame *m_pPreKeyFrame, *m_pPostKeyFrame; // pre- and post-keyframe's for spline interpolation + float m_flTimeIntoFrame; + + int m_iDirection; // 1 for forward, -1 for backward, and 0 for at rest + + float CalculateTimeAdvancementForSpeed( float moveTime, float speed ); + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( move_keyframed, CBaseMoveBehavior ); + +BEGIN_DATADESC( CBaseMoveBehavior ) + +// DEFINE_KEYFIELD( m_iTimeModifier, FIELD_INTEGER, "TimeModifier" ), + DEFINE_KEYFIELD( m_iPositionInterpolator, FIELD_INTEGER, "PositionInterpolator" ), + DEFINE_KEYFIELD( m_iRotationInterpolator, FIELD_INTEGER, "RotationInterpolator" ), + + DEFINE_FIELD( m_pCurrentKeyFrame, FIELD_CLASSPTR ), + DEFINE_FIELD( m_pTargetKeyFrame, FIELD_CLASSPTR ), + DEFINE_FIELD( m_pPreKeyFrame, FIELD_CLASSPTR ), + DEFINE_FIELD( m_pPostKeyFrame, FIELD_CLASSPTR ), + + DEFINE_FIELD( m_flAnimStartTime, FIELD_FLOAT ), + DEFINE_FIELD( m_flAnimEndTime, FIELD_FLOAT ), + DEFINE_FIELD( m_flAverageSpeedAcrossFrame, FIELD_FLOAT ), + DEFINE_FIELD( m_flTimeIntoFrame, FIELD_FLOAT ), + DEFINE_FIELD( m_iDirection, FIELD_INTEGER ), + +END_DATADESC() + + +void CBaseMoveBehavior::Spawn( void ) +{ + m_pCurrentKeyFrame = this; + m_flTimeIntoFrame = 0; + SetMoveType( MOVETYPE_PUSH ); + + // a move behavior is also it's first keyframe + m_Origin = GetLocalOrigin(); + m_Angles = GetLocalAngles(); + + BaseClass::Spawn(); +} + +void CBaseMoveBehavior::Activate( void ) +{ + BaseClass::Activate(); + + SetMoveDoneTime( 0.5 ); // start moving in 0.2 seconds time + + // if we are just the basic keyframed entity, cycle our animation + if ( !stricmp(GetClassname(), "move_keyframed") ) + { + StartMoving( 1 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Checks to see if the we're at the start of the keyframe sequence +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseMoveBehavior::IsAtSequenceStart( void ) +{ + if ( !m_pCurrentKeyFrame ) + return true; + + if ( m_flAnimStartTime && m_flAnimStartTime >= GetLocalTime() ) + { + if ( !m_pCurrentKeyFrame->PrevKey(1) && !m_pTargetKeyFrame ) + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Checks to see if we're at the end of the keyframe sequence +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseMoveBehavior::IsAtSequenceEnd( void ) +{ + if ( !m_pCurrentKeyFrame ) + return false; + + if ( !m_pCurrentKeyFrame->NextKey(1) && !m_pTargetKeyFrame ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseMoveBehavior::IsMoving( void ) +{ + if ( m_iDirection != 0 ) + return true; + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the object moving from it's current position, in the direction indicated +// Input : direction - 1 is forward through the sequence, -1 is backwards, and 0 is stop +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseMoveBehavior::StartMoving( int direction ) +{ + // 0 direction is to stop moving + if ( direction == 0 ) + { + StopMoving(); + return false; + } + + // check to see if we should keep moving in the current direction + if ( m_iDirection == direction ) + { + // if we're at the end of the current anim key, move to the next one + if ( GetLocalTime() >= m_flAnimEndTime ) + { + m_pCurrentKeyFrame = m_pTargetKeyFrame; + m_flTimeIntoFrame = 0; + + if ( !m_pTargetKeyFrame->NextKey(direction) ) + { + // we've hit the end of the sequence + m_flAnimEndTime = 0; + m_flAnimStartTime = 0; + StopMoving(); + return false; + } + + // advance the target keyframe + m_pTargetKeyFrame = m_pTargetKeyFrame->NextKey(direction); + } + } + else + { + // we're changing direction + + // need to calculate current position in the frame + // stop first, then start again + if ( m_iDirection != 0 ) + { + StopMoving(); + } + + m_iDirection = direction; + + // if we're going in reverse, swap the currentkey and targetkey (since we're going opposite dir) + if ( direction == 1 ) + { + m_pTargetKeyFrame = m_pCurrentKeyFrame->NextKey( direction ); + } + else if ( direction == -1 ) + { + if ( m_flTimeIntoFrame > 0 ) + { + m_pTargetKeyFrame = m_pCurrentKeyFrame; + m_pCurrentKeyFrame = m_pCurrentKeyFrame->NextKey( 1 ); + } + else + { + m_pTargetKeyFrame = m_pCurrentKeyFrame->PrevKey( 1 ); + } + } + + // recalculate our movement from the stored data + if ( !m_pTargetKeyFrame ) + { + StopMoving(); + return false; + } + + // calculate the keyframes before and after the keyframes we're interpolating between + m_pPostKeyFrame = m_pTargetKeyFrame->NextKey( direction ); + if ( !m_pPostKeyFrame ) + { + m_pPostKeyFrame = m_pTargetKeyFrame; + } + m_pPreKeyFrame = m_pCurrentKeyFrame->PrevKey( direction ); + if ( !m_pPreKeyFrame ) + { + m_pPreKeyFrame = m_pCurrentKeyFrame; + } + } + + // no target, can't move + if ( !m_pTargetKeyFrame ) + return false; + + // calculate start/end time + // ->m_flNextTime is the time to traverse to the NEXT key, so we need the opposite if travelling backwards + if ( m_iDirection == 1 ) + { + m_flAnimStartTime = GetLocalTime() - m_flTimeIntoFrame; + m_flAnimEndTime = GetLocalTime() + m_pCurrentKeyFrame->m_flNextTime - m_flTimeIntoFrame; + } + else + { + // flip the timing, since we're in reverse + if ( m_flTimeIntoFrame ) + m_flTimeIntoFrame = m_pTargetKeyFrame->m_flNextTime - m_flTimeIntoFrame; + + m_flAnimStartTime = GetLocalTime() - m_flTimeIntoFrame; + m_flAnimEndTime = GetLocalTime() + m_pTargetKeyFrame->m_flNextTime - m_flTimeIntoFrame; + } + + // calculate the average speed at which we cross + float animDuration = (m_flAnimEndTime - m_flAnimStartTime); + float dist = (m_pCurrentKeyFrame->m_Origin - m_pTargetKeyFrame->m_Origin).Length(); + m_flAverageSpeedAcrossFrame = animDuration / dist; + + SetMoveDoneTime( m_flAnimEndTime - GetLocalTime() ); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: stops the object from moving +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +void CBaseMoveBehavior::StopMoving( void ) +{ + // remember exactly where we are in the frame + m_flTimeIntoFrame = 0; + + if ( m_iDirection == 1 ) + { + // record the time if we're not at the end of the frame + if ( GetLocalTime() < m_flAnimEndTime ) + { + m_flTimeIntoFrame = GetLocalTime() - m_flAnimStartTime; + } + else + { + // we're actually at the end + if ( m_pTargetKeyFrame ) + { + m_pCurrentKeyFrame = m_pTargetKeyFrame; + } + } + } + else if ( m_iDirection == -1 ) + { + // store it only as a forward movement + m_pCurrentKeyFrame = m_pTargetKeyFrame; + + if ( GetLocalTime() < m_flAnimEndTime ) + { + m_flTimeIntoFrame = m_flAnimEndTime - GetLocalTime(); + } + } + + // stop moving totally + SetMoveDoneTime( -1 ); + m_iDirection = 0; + m_flAnimStartTime = 0; + m_flAnimEndTime = 0; + m_pTargetKeyFrame = NULL; + SetAbsVelocity(vec3_origin); + SetLocalAngularVelocity( vec3_angle ); +} + + +//----------------------------------------------------------------------------- +// Purpose: We have just arrived at a key, move onto the next keyframe +//----------------------------------------------------------------------------- +void CBaseMoveBehavior::MoveDone( void ) +{ + // if we're just a base then keep playing the anim + if ( !stricmp(STRING(m_iClassname), "move_keyframed") ) + { + int direction = m_iDirection; + // start moving from the keyframe we've just reached + if ( !StartMoving(direction) ) + { + // try moving in the other direction + StartMoving( -direction ); + } + } + + BaseClass::MoveDone(); +} + +//----------------------------------------------------------------------------- +// Purpose: Calculates a new moveTime based on the speed and the current point +// in the animation. +// used to advance keyframed objects that have dynamic speeds. +// Input : moveTime - +// Output : float - the new time in the keyframing sequence +//----------------------------------------------------------------------------- +float CBaseMoveBehavior::CalculateTimeAdvancementForSpeed( float moveTime, float speed ) +{ + return (moveTime * speed * m_flAverageSpeedAcrossFrame); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// GetLocalTime() is the objects local current time +// Input : destTime - new time that is being moved to +// moveTime - amount of time to be advanced this frame +// Output : float - the actual amount of time to move (usually moveTime) +//----------------------------------------------------------------------------- +float CBaseMoveBehavior::SetObjectPhysicsVelocity( float moveTime ) +{ + // make sure we have a valid set up + if ( !m_pCurrentKeyFrame || !m_pTargetKeyFrame ) + return moveTime; + + // if we're not moving, we're not moving + if ( !IsMoving() ) + return moveTime; + + float destTime = moveTime + GetLocalTime(); + + // work out where we want to be, using destTime + m_flTimeIntoFrame = destTime - m_flAnimStartTime; + float newTime = (destTime - m_flAnimStartTime) / (m_flAnimEndTime - m_flAnimStartTime); + Vector newPos; + QAngle newAngles; + + IPositionInterpolator *pInterp = GetPositionInterpolator( m_iPositionInterpolator ); + if( pInterp ) + { + // setup key frames + pInterp->SetKeyPosition( -1, m_pPreKeyFrame->m_Origin ); + Motion_SetKeyAngles( -1, m_pPreKeyFrame->m_qAngle ); + + pInterp->SetKeyPosition( 0, m_pCurrentKeyFrame->m_Origin ); + Motion_SetKeyAngles( 0, m_pCurrentKeyFrame->m_qAngle ); + + pInterp->SetKeyPosition( 1, m_pTargetKeyFrame->m_Origin ); + Motion_SetKeyAngles( 1, m_pTargetKeyFrame->m_qAngle ); + + pInterp->SetKeyPosition( 2, m_pPostKeyFrame->m_Origin ); + Motion_SetKeyAngles( 2, m_pPostKeyFrame->m_qAngle ); + + // find new interpolated position & rotation + pInterp->InterpolatePosition( newTime, newPos ); + } + else + { + newPos.Init(); + } + + Quaternion qRot; + Motion_InterpolateRotation( newTime, m_iRotationInterpolator, qRot ); + QuaternionAngles( qRot, newAngles ); + + // find our velocity vector (newPos - currentPos) and scale velocity vector according to the movetime + float oneOnMoveTime = 1 / moveTime; + SetAbsVelocity( (newPos - GetLocalOrigin()) * oneOnMoveTime ); + SetLocalAngularVelocity( (newAngles - GetLocalAngles()) * oneOnMoveTime ); + + return moveTime; +} + diff --git a/sp/src/game/server/movie_display.cpp b/sp/src/game/server/movie_display.cpp new file mode 100644 index 00000000..5c31fb23 --- /dev/null +++ b/sp/src/game/server/movie_display.cpp @@ -0,0 +1,372 @@ +//========= Copyright © 1996-2009, Valve Corporation, All rights reserved. ============// +// +// Purpose: Allows movies to be played as a VGUI screen in the world +// +//=====================================================================================// + +#include "cbase.h" +#include "EnvMessage.h" +#include "fmtstr.h" +#include "vguiscreen.h" +#include "filesystem.h" + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + +class CMovieDisplay : public CBaseEntity +{ +public: + + DECLARE_CLASS( CMovieDisplay, CBaseEntity ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + virtual ~CMovieDisplay(); + + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + + virtual int UpdateTransmitState(); + virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + + virtual void Spawn( void ); + virtual void Precache( void ); + virtual void OnRestore( void ); + + void ScreenVisible( bool bVisible ); + + void Disable( void ); + void Enable( void ); + + void InputDisable( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + + void InputSetDisplayText( inputdata_t &inputdata ); + +private: + + // Control panel + void GetControlPanelInfo( int nPanelIndex, const char *&pPanelName ); + void GetControlPanelClassName( int nPanelIndex, const char *&pPanelName ); + void SpawnControlPanels( void ); + void RestoreControlPanels( void ); + +private: + CNetworkVar( bool, m_bEnabled ); + CNetworkVar( bool, m_bLooping ); + + CNetworkString( m_szDisplayText, 128 ); + + // Filename of the movie to play + CNetworkString( m_szMovieFilename, 128 ); + string_t m_strMovieFilename; + + // "Group" name. Screens of the same group name will play the same movie at the same time + // Effectively this lets multiple screens tune to the same "channel" in the world + CNetworkString( m_szGroupName, 128 ); + string_t m_strGroupName; + + int m_iScreenWidth; + int m_iScreenHeight; + + bool m_bDoFullTransmit; + + CHandle m_hScreen; +}; + +LINK_ENTITY_TO_CLASS( vgui_movie_display, CMovieDisplay ); + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CMovieDisplay ) + + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + + DEFINE_AUTO_ARRAY_KEYFIELD( m_szDisplayText, FIELD_CHARACTER, "displaytext" ), + + DEFINE_AUTO_ARRAY( m_szMovieFilename, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_strMovieFilename, FIELD_STRING, "moviefilename" ), + + DEFINE_AUTO_ARRAY( m_szGroupName, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_strGroupName, FIELD_STRING, "groupname" ), + + DEFINE_KEYFIELD( m_iScreenWidth, FIELD_INTEGER, "width" ), + DEFINE_KEYFIELD( m_iScreenHeight, FIELD_INTEGER, "height" ), + DEFINE_KEYFIELD( m_bLooping, FIELD_BOOLEAN, "looping" ), + + DEFINE_FIELD( m_bDoFullTransmit, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_hScreen, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetDisplayText", InputSetDisplayText ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CMovieDisplay, DT_MovieDisplay ) + SendPropBool( SENDINFO( m_bEnabled ) ), + SendPropBool( SENDINFO( m_bLooping ) ), + SendPropString( SENDINFO( m_szMovieFilename ) ), + SendPropString( SENDINFO( m_szGroupName ) ), +END_SEND_TABLE() + +CMovieDisplay::~CMovieDisplay() +{ + DestroyVGuiScreen( m_hScreen.Get() ); +} + +//----------------------------------------------------------------------------- +// Read in Hammer data +//----------------------------------------------------------------------------- +bool CMovieDisplay::KeyValue( const char *szKeyName, const char *szValue ) +{ + // NOTE: Have to do these separate because they set two values instead of one + if( FStrEq( szKeyName, "angles" ) ) + { + Assert( GetMoveParent() == NULL ); + QAngle angles; + UTIL_StringToVector( angles.Base(), szValue ); + + // Because the vgui screen basis is strange (z is front, y is up, x is right) + // we need to rotate the typical basis before applying it + VMatrix mat, rotation, tmp; + MatrixFromAngles( angles, mat ); + MatrixBuildRotationAboutAxis( rotation, Vector( 0, 1, 0 ), 90 ); + MatrixMultiply( mat, rotation, tmp ); + MatrixBuildRotateZ( rotation, 90 ); + MatrixMultiply( tmp, rotation, mat ); + MatrixToAngles( mat, angles ); + SetAbsAngles( angles ); + + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +int CMovieDisplay::UpdateTransmitState() +{ + if ( m_bDoFullTransmit ) + { + m_bDoFullTransmit = false; + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + return SetTransmitState( FL_EDICT_FULLCHECK ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + // Are we already marked for transmission? + if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) + return; + + BaseClass::SetTransmit( pInfo, bAlways ); + + // Force our screen to be sent too. + m_hScreen->SetTransmit( pInfo, bAlways ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::Spawn( void ) +{ + // Move the strings into a networkable form + Q_strcpy( m_szMovieFilename.GetForModify(), m_strMovieFilename.ToCStr() ); + Q_strcpy( m_szGroupName.GetForModify(), m_strGroupName.ToCStr() ); + + Precache(); + + BaseClass::Spawn(); + + m_bEnabled = false; + + SpawnControlPanels(); + + ScreenVisible( m_bEnabled ); + + m_bDoFullTransmit = true; +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::Precache( void ) +{ + BaseClass::Precache(); + + PrecacheVGuiScreen( "video_display_screen" ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::OnRestore( void ) +{ + BaseClass::OnRestore(); + + RestoreControlPanels(); + + ScreenVisible( m_bEnabled ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::ScreenVisible( bool bVisible ) +{ + // Set its active state + m_hScreen->SetActive( bVisible ); + + if ( bVisible ) + { + m_hScreen->RemoveEffects( EF_NODRAW ); + } + else + { + m_hScreen->AddEffects( EF_NODRAW ); + } +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::Disable( void ) +{ + if ( !m_bEnabled ) + return; + + m_bEnabled = false; + + ScreenVisible( false ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::Enable( void ) +{ + if ( m_bEnabled ) + return; + + m_bEnabled = true; + + ScreenVisible( true ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::InputSetDisplayText( inputdata_t &inputdata ) +{ + Q_strcpy( m_szDisplayText.GetForModify(), inputdata.value.String() ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::GetControlPanelInfo( int nPanelIndex, const char *&pPanelName ) +{ + pPanelName = "movie_display_screen"; +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::GetControlPanelClassName( int nPanelIndex, const char *&pPanelName ) +{ + pPanelName = "vgui_screen"; +} + +//----------------------------------------------------------------------------- +// This is called by the base object when it's time to spawn the control panels +//----------------------------------------------------------------------------- +void CMovieDisplay::SpawnControlPanels() +{ + int nPanel; + for ( nPanel = 0; true; ++nPanel ) + { + const char *pScreenName; + GetControlPanelInfo( nPanel, pScreenName ); + if (!pScreenName) + continue; + + const char *pScreenClassname; + GetControlPanelClassName( nPanel, pScreenClassname ); + if ( !pScreenClassname ) + continue; + + float flWidth = m_iScreenWidth; + float flHeight = m_iScreenHeight; + + CVGuiScreen *pScreen = CreateVGuiScreen( pScreenClassname, pScreenName, this, this, 0 ); + pScreen->ChangeTeam( GetTeamNumber() ); + pScreen->SetActualSize( flWidth, flHeight ); + pScreen->SetActive( true ); + pScreen->MakeVisibleOnlyToTeammates( false ); + pScreen->SetTransparency( true ); + m_hScreen = pScreen; + + return; + } +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CMovieDisplay::RestoreControlPanels( void ) +{ + int nPanel; + for ( nPanel = 0; true; ++nPanel ) + { + const char *pScreenName; + GetControlPanelInfo( nPanel, pScreenName ); + if (!pScreenName) + continue; + + const char *pScreenClassname; + GetControlPanelClassName( nPanel, pScreenClassname ); + if ( !pScreenClassname ) + continue; + + CVGuiScreen *pScreen = (CVGuiScreen *)gEntList.FindEntityByClassname( NULL, pScreenClassname ); + + while ( ( pScreen && pScreen->GetOwnerEntity() != this ) || Q_strcmp( pScreen->GetPanelName(), pScreenName ) != 0 ) + { + pScreen = (CVGuiScreen *)gEntList.FindEntityByClassname( pScreen, pScreenClassname ); + } + + if ( pScreen ) + { + m_hScreen = pScreen; + m_hScreen->SetActive( true ); + } + + return; + } +} diff --git a/sp/src/game/server/movie_explosion.cpp b/sp/src/game/server/movie_explosion.cpp new file mode 100644 index 00000000..d3a86d5c --- /dev/null +++ b/sp/src/game/server/movie_explosion.cpp @@ -0,0 +1,44 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "movie_explosion.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define MOVIEEXPLOSION_ENTITYNAME "env_movieexplosion" + + +IMPLEMENT_SERVERCLASS_ST(MovieExplosion, DT_MovieExplosion) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS(env_movieexplosion, MovieExplosion); + + +MovieExplosion* MovieExplosion::CreateMovieExplosion(const Vector &pos) +{ + CBaseEntity *pEnt = CreateEntityByName(MOVIEEXPLOSION_ENTITYNAME); + if(pEnt) + { + MovieExplosion *pEffect = dynamic_cast(pEnt); + if(pEffect && pEffect->edict()) + { + pEffect->SetLocalOrigin( pos ); + pEffect->Activate(); + return pEffect; + } + else + { + UTIL_Remove(pEnt); + } + } + + return NULL; +} + + diff --git a/sp/src/game/server/movie_explosion.h b/sp/src/game/server/movie_explosion.h new file mode 100644 index 00000000..4cd19c0a --- /dev/null +++ b/sp/src/game/server/movie_explosion.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef MOVIE_EXPLOSION_H +#define MOVIE_EXPLOSION_H + + +#include "baseparticleentity.h" + + +class MovieExplosion : public CBaseParticleEntity +{ +public: + DECLARE_CLASS( MovieExplosion, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + static MovieExplosion* CreateMovieExplosion(const Vector &pos); +}; + + +#endif + + diff --git a/sp/src/game/server/nav.h b/sp/src/game/server/nav.h new file mode 100644 index 00000000..0319991a --- /dev/null +++ b/sp/src/game/server/nav.h @@ -0,0 +1,498 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav.h +// Data structures and constants for the Navigation Mesh system +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#ifndef _NAV_H_ +#define _NAV_H_ + +#include "modelentities.h" // for CFuncBrush +#include "doors.h" + +/** + * Below are several constants used by the navigation system. + * @todo Move these into TheNavMesh singleton. + */ +const float GenerationStepSize = 25.0f; // (30) was 20, but bots can't fit always fit +const float JumpHeight = 41.8f; // if delta Z is less than this, we can jump up on it + +#if defined(CSTRIKE_DLL) +const float JumpCrouchHeight = 58.0f; // (48) if delta Z is less than or equal to this, we can jumpcrouch up on it +#else +const float JumpCrouchHeight = 64.0f; // (48) if delta Z is less than or equal to this, we can jumpcrouch up on it +#endif + +// There are 3 different definitions of StepHeight throughout the code, waiting to produce bugs if the 18.0 is ever changed. +const float StepHeight = 18.0f; // if delta Z is greater than this, we have to jump to get up + +// TERROR: Increased DeathDrop from 200, since zombies don't take falling damage +#if defined(CSTRIKE_DLL) +const float DeathDrop = 200.0f; // (300) distance at which we will die if we fall - should be about 600, and pay attention to fall damage during pathfind +#else +const float DeathDrop = 400.0f; // (300) distance at which we will die if we fall - should be about 600, and pay attention to fall damage during pathfind +#endif + +#if defined(CSTRIKE_DLL) +const float ClimbUpHeight = JumpCrouchHeight; // CSBots assume all jump up links are reachable +#else +const float ClimbUpHeight = 200.0f; // height to check for climbing up +#endif + +const float CliffHeight = 300.0f; // height which we consider a significant cliff which we would not want to fall off of + +// TERROR: Converted these values to use the same numbers as the player bounding boxes etc +#define HalfHumanWidth 16 +#define HalfHumanHeight 35.5 +#define HumanHeight 71 +#define HumanEyeHeight 62 +#define HumanCrouchHeight 55 +#define HumanCrouchEyeHeight 37 + + +#define NAV_MAGIC_NUMBER 0xFEEDFACE // to help identify nav files + +/** + * A place is a named group of navigation areas + */ +typedef unsigned int Place; +#define UNDEFINED_PLACE 0 // ie: "no place" +#define ANY_PLACE 0xFFFF + +enum NavErrorType +{ + NAV_OK, + NAV_CANT_ACCESS_FILE, + NAV_INVALID_FILE, + NAV_BAD_FILE_VERSION, + NAV_FILE_OUT_OF_DATE, + NAV_CORRUPT_DATA, + NAV_OUT_OF_MEMORY, +}; + +enum NavAttributeType +{ + NAV_MESH_INVALID = 0, + NAV_MESH_CROUCH = 0x00000001, // must crouch to use this node/area + NAV_MESH_JUMP = 0x00000002, // must jump to traverse this area (only used during generation) + NAV_MESH_PRECISE = 0x00000004, // do not adjust for obstacles, just move along area + NAV_MESH_NO_JUMP = 0x00000008, // inhibit discontinuity jumping + NAV_MESH_STOP = 0x00000010, // must stop when entering this area + NAV_MESH_RUN = 0x00000020, // must run to traverse this area + NAV_MESH_WALK = 0x00000040, // must walk to traverse this area + NAV_MESH_AVOID = 0x00000080, // avoid this area unless alternatives are too dangerous + NAV_MESH_TRANSIENT = 0x00000100, // area may become blocked, and should be periodically checked + NAV_MESH_DONT_HIDE = 0x00000200, // area should not be considered for hiding spot generation + NAV_MESH_STAND = 0x00000400, // bots hiding in this area should stand + NAV_MESH_NO_HOSTAGES = 0x00000800, // hostages shouldn't use this area + NAV_MESH_STAIRS = 0x00001000, // this area represents stairs, do not attempt to climb or jump them - just walk up + NAV_MESH_NO_MERGE = 0x00002000, // don't merge this area with adjacent areas + NAV_MESH_OBSTACLE_TOP = 0x00004000, // this nav area is the climb point on the tip of an obstacle + NAV_MESH_CLIFF = 0x00008000, // this nav area is adjacent to a drop of at least CliffHeight + + NAV_MESH_FIRST_CUSTOM = 0x00010000, // apps may define custom app-specific bits starting with this value + NAV_MESH_LAST_CUSTOM = 0x04000000, // apps must not define custom app-specific bits higher than with this value + + NAV_MESH_FUNC_COST = 0x20000000, // area has designer specified cost controlled by func_nav_cost entities + NAV_MESH_HAS_ELEVATOR = 0x40000000, // area is in an elevator's path + NAV_MESH_NAV_BLOCKER = 0x80000000 // area is blocked by nav blocker ( Alas, needed to hijack a bit in the attributes to get within a cache line [7/24/2008 tom]) +}; + +extern NavAttributeType NameToNavAttribute( const char *name ); + +enum NavDirType +{ + NORTH = 0, + EAST = 1, + SOUTH = 2, + WEST = 3, + + NUM_DIRECTIONS +}; + +/** + * Defines possible ways to move from one area to another + */ +enum NavTraverseType +{ + // NOTE: First 4 directions MUST match NavDirType + GO_NORTH = 0, + GO_EAST, + GO_SOUTH, + GO_WEST, + + GO_LADDER_UP, + GO_LADDER_DOWN, + GO_JUMP, + GO_ELEVATOR_UP, + GO_ELEVATOR_DOWN, + + NUM_TRAVERSE_TYPES +}; + +enum NavCornerType +{ + NORTH_WEST = 0, + NORTH_EAST = 1, + SOUTH_EAST = 2, + SOUTH_WEST = 3, + + NUM_CORNERS +}; + +enum NavRelativeDirType +{ + FORWARD = 0, + RIGHT, + BACKWARD, + LEFT, + UP, + DOWN, + + NUM_RELATIVE_DIRECTIONS +}; + +struct Extent +{ + Vector lo, hi; + + void Init( void ) + { + lo.Init(); + hi.Init(); + } + + void Init( CBaseEntity *entity ) + { + entity->CollisionProp()->WorldSpaceSurroundingBounds( &lo, &hi ); + } + + float SizeX( void ) const { return hi.x - lo.x; } + float SizeY( void ) const { return hi.y - lo.y; } + float SizeZ( void ) const { return hi.z - lo.z; } + float Area( void ) const { return SizeX() * SizeY(); } + + // Increase bounds to contain the given point + void Encompass( const Vector &pos ) + { + for ( int i=0; i<3; ++i ) + { + if ( pos[i] < lo[i] ) + { + lo[i] = pos[i]; + } + else if ( pos[i] > hi[i] ) + { + hi[i] = pos[i]; + } + } + } + + // Increase bounds to contain the given extent + void Encompass( const Extent &extent ) + { + Encompass( extent.lo ); + Encompass( extent.hi ); + } + + // return true if 'pos' is inside of this extent + bool Contains( const Vector &pos ) const + { + return (pos.x >= lo.x && pos.x <= hi.x && + pos.y >= lo.y && pos.y <= hi.y && + pos.z >= lo.z && pos.z <= hi.z); + } + + // return true if this extent overlaps the given one + bool IsOverlapping( const Extent &other ) const + { + return (lo.x <= other.hi.x && hi.x >= other.lo.x && + lo.y <= other.hi.y && hi.y >= other.lo.y && + lo.z <= other.hi.z && hi.z >= other.lo.z); + } + + // return true if this extent completely contains the given one + bool IsEncompassing( const Extent &other, float tolerance = 0.0f ) const + { + return (lo.x <= other.lo.x + tolerance && hi.x >= other.hi.x - tolerance && + lo.y <= other.lo.y + tolerance && hi.y >= other.hi.y - tolerance && + lo.z <= other.lo.z + tolerance && hi.z >= other.hi.z - tolerance); + } +}; + +struct Ray +{ + Vector from, to; +}; + + +class CNavArea; +class CNavNode; + + +//-------------------------------------------------------------------------------------------------------------- +inline NavDirType OppositeDirection( NavDirType dir ) +{ + switch( dir ) + { + case NORTH: return SOUTH; + case SOUTH: return NORTH; + case EAST: return WEST; + case WEST: return EAST; + default: break; + } + + return NORTH; +} + +//-------------------------------------------------------------------------------------------------------------- +inline NavDirType DirectionLeft( NavDirType dir ) +{ + switch( dir ) + { + case NORTH: return WEST; + case SOUTH: return EAST; + case EAST: return NORTH; + case WEST: return SOUTH; + default: break; + } + + return NORTH; +} + +//-------------------------------------------------------------------------------------------------------------- +inline NavDirType DirectionRight( NavDirType dir ) +{ + switch( dir ) + { + case NORTH: return EAST; + case SOUTH: return WEST; + case EAST: return SOUTH; + case WEST: return NORTH; + default: break; + } + + return NORTH; +} + +//-------------------------------------------------------------------------------------------------------------- +inline void AddDirectionVector( Vector *v, NavDirType dir, float amount ) +{ + switch( dir ) + { + case NORTH: v->y -= amount; return; + case SOUTH: v->y += amount; return; + case EAST: v->x += amount; return; + case WEST: v->x -= amount; return; + default: break; + } +} + +//-------------------------------------------------------------------------------------------------------------- +inline float DirectionToAngle( NavDirType dir ) +{ + switch( dir ) + { + case NORTH: return 270.0f; + case SOUTH: return 90.0f; + case EAST: return 0.0f; + case WEST: return 180.0f; + default: break; + } + + return 0.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +inline NavDirType AngleToDirection( float angle ) +{ + while( angle < 0.0f ) + angle += 360.0f; + + while( angle > 360.0f ) + angle -= 360.0f; + + if (angle < 45 || angle > 315) + return EAST; + + if (angle >= 45 && angle < 135) + return SOUTH; + + if (angle >= 135 && angle < 225) + return WEST; + + return NORTH; +} + +//-------------------------------------------------------------------------------------------------------------- +inline void DirectionToVector2D( NavDirType dir, Vector2D *v ) +{ + switch( dir ) + { + case NORTH: v->x = 0.0f; v->y = -1.0f; break; + case SOUTH: v->x = 0.0f; v->y = 1.0f; break; + case EAST: v->x = 1.0f; v->y = 0.0f; break; + case WEST: v->x = -1.0f; v->y = 0.0f; break; + default: break; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +inline void CornerToVector2D( NavCornerType dir, Vector2D *v ) +{ + switch( dir ) + { + case NORTH_WEST: v->x = -1.0f; v->y = -1.0f; break; + case NORTH_EAST: v->x = 1.0f; v->y = -1.0f; break; + case SOUTH_EAST: v->x = 1.0f; v->y = 1.0f; break; + case SOUTH_WEST: v->x = -1.0f; v->y = 1.0f; break; + default: break; + } + + v->NormalizeInPlace(); +} + + +//-------------------------------------------------------------------------------------------------------------- +// Gets the corner types that surround the given direction +inline void GetCornerTypesInDirection( NavDirType dir, NavCornerType *first, NavCornerType *second ) +{ + switch ( dir ) + { + case NORTH: + *first = NORTH_WEST; + *second = NORTH_EAST; + break; + case SOUTH: + *first = SOUTH_WEST; + *second = SOUTH_EAST; + break; + case EAST: + *first = NORTH_EAST; + *second = SOUTH_EAST; + break; + case WEST: + *first = NORTH_WEST; + *second = SOUTH_WEST; + break; + default: + break; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +inline float RoundToUnits( float val, float unit ) +{ + val = val + ((val < 0.0f) ? -unit*0.5f : unit*0.5f); + return (float)( unit * ( ((int)val) / (int)unit ) ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if given entity can be ignored when moving + */ +#define WALK_THRU_PROP_DOORS 0x01 +#define WALK_THRU_FUNC_DOORS 0x02 +#define WALK_THRU_DOORS (WALK_THRU_PROP_DOORS | WALK_THRU_FUNC_DOORS) +#define WALK_THRU_BREAKABLES 0x04 +#define WALK_THRU_TOGGLE_BRUSHES 0x08 +#define WALK_THRU_EVERYTHING (WALK_THRU_DOORS | WALK_THRU_BREAKABLES | WALK_THRU_TOGGLE_BRUSHES) +extern ConVar nav_solid_props; +inline bool IsEntityWalkable( CBaseEntity *entity, unsigned int flags ) +{ + if (FClassnameIs( entity, "worldspawn" )) + return false; + + if (FClassnameIs( entity, "player" )) + return false; + + // if we hit a door, assume its walkable because it will open when we touch it + if (FClassnameIs( entity, "func_door*" )) + { +#ifdef PROBLEMATIC // cp_dustbowl doors dont open by touch - they use surrounding triggers + if ( !entity->HasSpawnFlags( SF_DOOR_PTOUCH ) ) + { + // this door is not opened by touching it, if it is closed, the area is blocked + CBaseDoor *door = (CBaseDoor *)entity; + return door->m_toggle_state == TS_AT_TOP; + } +#endif // _DEBUG + + return (flags & WALK_THRU_FUNC_DOORS) ? true : false; + } + + if (FClassnameIs( entity, "prop_door*" )) + { + return (flags & WALK_THRU_PROP_DOORS) ? true : false; + } + + // if we hit a clip brush, ignore it if it is not BRUSHSOLID_ALWAYS + if (FClassnameIs( entity, "func_brush" )) + { + CFuncBrush *brush = (CFuncBrush *)entity; + switch ( brush->m_iSolidity ) + { + case CFuncBrush::BRUSHSOLID_ALWAYS: + return false; + case CFuncBrush::BRUSHSOLID_NEVER: + return true; + case CFuncBrush::BRUSHSOLID_TOGGLE: + return (flags & WALK_THRU_TOGGLE_BRUSHES) ? true : false; + } + } + + // if we hit a breakable object, assume its walkable because we will shoot it when we touch it + if (FClassnameIs( entity, "func_breakable" ) && entity->GetHealth() && entity->m_takedamage == DAMAGE_YES) + return (flags & WALK_THRU_BREAKABLES) ? true : false; + + if (FClassnameIs( entity, "func_breakable_surf" ) && entity->m_takedamage == DAMAGE_YES) + return (flags & WALK_THRU_BREAKABLES) ? true : false; + + if ( FClassnameIs( entity, "func_playerinfected_clip" ) == true ) + return true; + + if ( nav_solid_props.GetBool() && FClassnameIs( entity, "prop_*" ) ) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Trace filter that ignores players, NPCs, and objects that can be walked through + */ +class CTraceFilterWalkableEntities : public CTraceFilterNoNPCsOrPlayer +{ +public: + CTraceFilterWalkableEntities( const IHandleEntity *passentity, int collisionGroup, unsigned int flags ) + : CTraceFilterNoNPCsOrPlayer( passentity, collisionGroup ), m_flags( flags ) + { + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + if ( CTraceFilterNoNPCsOrPlayer::ShouldHitEntity(pServerEntity, contentsMask) ) + { + CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity ); + return ( !IsEntityWalkable( pEntity, m_flags ) ); + } + return false; + } + +private: + unsigned int m_flags; +}; + + +extern bool IsWalkableTraceLineClear( const Vector &from, const Vector &to, unsigned int flags = 0 ); + +#endif // _NAV_H_ diff --git a/sp/src/game/server/nav_area.cpp b/sp/src/game/server/nav_area.cpp new file mode 100644 index 00000000..73379f15 --- /dev/null +++ b/sp/src/game/server/nav_area.cpp @@ -0,0 +1,5879 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_area.cpp +// AI Navigation areas +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#include "cbase.h" + +#include "tier0/vprof.h" +#include "tier0/tslist.h" +#include "tier1/utlhash.h" +#include "vstdlib/jobthread.h" + +#include "nav_mesh.h" +#include "nav_node.h" +#include "nav_pathfind.h" +#include "nav_colors.h" +#include "fmtstr.h" +#include "props_shared.h" +#include "func_breakablesurf.h" + +#ifdef TERROR +#include "func_elevator.h" +#include "AmbientLight.h" +#endif + +#include "Color.h" +#include "collisionutils.h" +#include "functorutils.h" +#include "team.h" +#include "nav_entities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern void HintMessageToAllPlayers( const char *message ); + +unsigned int CNavArea::m_nextID = 1; +NavAreaVector TheNavAreas; + +unsigned int CNavArea::m_masterMarker = 1; +CNavArea *CNavArea::m_openList = NULL; +CNavArea *CNavArea::m_openListTail = NULL; + +bool CNavArea::m_isReset = false; +uint32 CNavArea::s_nCurrVisTestCounter = 0; + +ConVar nav_coplanar_slope_limit( "nav_coplanar_slope_limit", "0.99", FCVAR_CHEAT ); +ConVar nav_coplanar_slope_limit_displacement( "nav_coplanar_slope_limit_displacement", "0.7", FCVAR_CHEAT ); +ConVar nav_split_place_on_ground( "nav_split_place_on_ground", "0", FCVAR_CHEAT, "If true, nav areas will be placed flush with the ground when split." ); +ConVar nav_area_bgcolor( "nav_area_bgcolor", "0 0 0 30", FCVAR_CHEAT, "RGBA color to draw as the background color for nav areas while editing." ); +ConVar nav_corner_adjust_adjacent( "nav_corner_adjust_adjacent", "18", FCVAR_CHEAT, "radius used to raise/lower corners in nearby areas when raising/lowering corners." ); +ConVar nav_show_light_intensity( "nav_show_light_intensity", "0", FCVAR_CHEAT ); +ConVar nav_debug_blocked( "nav_debug_blocked", "0", FCVAR_CHEAT ); +ConVar nav_show_contiguous( "nav_show_continguous", "0", FCVAR_CHEAT, "Highlight non-contiguous connections" ); + +const float DEF_NAV_VIEW_DISTANCE = 1500.0; +ConVar nav_max_view_distance( "nav_max_view_distance", "6000", FCVAR_CHEAT, "Maximum range for precomputed nav mesh visibility (0 = default 1500 units)" ); +ConVar nav_update_visibility_on_edit( "nav_update_visibility_on_edit", "0", FCVAR_CHEAT, "If nonzero editing the mesh will incrementally recompue visibility" ); +ConVar nav_potentially_visible_dot_tolerance( "nav_potentially_visible_dot_tolerance", "0.98", FCVAR_CHEAT ); +ConVar nav_show_potentially_visible( "nav_show_potentially_visible", "0", FCVAR_CHEAT, "Show areas that are potentially visible from the current nav area" ); + +Color s_selectedSetColor( 255, 255, 200, 96 ); +Color s_selectedSetBorderColor( 100, 100, 0, 255 ); +Color s_dragSelectionSetBorderColor( 50, 50, 50, 255 ); +static void SelectedSetColorChaged( IConVar *var, const char *pOldValue, float flOldValue ) +{ + ConVarRef colorVar( var->GetName() ); + + Color *color = &s_selectedSetColor; + if ( FStrEq( var->GetName(), "nav_selected_set_border_color" ) ) + { + color = &s_selectedSetBorderColor; + } + + // Xbox compiler needs these to be in this explicit form + // likely due to sscanf expecting word aligned boundaries + int r = color->r(); + int g = color->r(); + int b = color->b(); + int a = color->a(); + int numFound = sscanf( colorVar.GetString(), "%d %d %d %d", &r, &g, &b, &a ); + + (*color)[0] = r; + (*color)[1] = g; + (*color)[2] = b; + if ( numFound > 3 ) + { + (*color)[3] = a; + } +} +ConVar nav_selected_set_color( "nav_selected_set_color", "255 255 200 96", FCVAR_CHEAT, "Color used to draw the selected set background while editing.", false, 0.0f, false, 0.0f, SelectedSetColorChaged ); +ConVar nav_selected_set_border_color( "nav_selected_set_border_color", "100 100 0 255", FCVAR_CHEAT, "Color used to draw the selected set borders while editing.", false, 0.0f, false, 0.0f, SelectedSetColorChaged ); + +//-------------------------------------------------------------------------------------------------------------- + +CMemoryStack CNavVectorNoEditAllocator::m_memory; +void *CNavVectorNoEditAllocator::m_pCurrent; +int CNavVectorNoEditAllocator::m_nBytesCurrent; + +CNavVectorNoEditAllocator::CNavVectorNoEditAllocator() +{ + m_pCurrent = NULL; + m_nBytesCurrent = 0; +} + +void CNavVectorNoEditAllocator::Reset() +{ + m_memory.FreeAll(); + m_pCurrent = NULL; + m_nBytesCurrent = 0; +} + +void *CNavVectorNoEditAllocator::Alloc( size_t nSize ) +{ + if ( !m_memory.GetBase() ) + { + m_memory.Init( 1024*1024, 0, 0, 4 ); + } + m_pCurrent = (int *)m_memory.Alloc( nSize ); + m_nBytesCurrent = nSize; + return m_pCurrent; +} + +void *CNavVectorNoEditAllocator::Realloc( void *pMem, size_t nSize ) +{ + if ( pMem != m_pCurrent ) + { + Assert( 0 ); + Error( "Nav mesh cannot be mutated after load\n" ); + } + if ( nSize > (size_t)m_nBytesCurrent ) + { + m_memory.Alloc( nSize - m_nBytesCurrent ); + m_nBytesCurrent = nSize; + } + return m_pCurrent; +} + +void CNavVectorNoEditAllocator::Free( void *pMem ) +{ +} + +size_t CNavVectorNoEditAllocator::GetSize( void *pMem ) +{ + if ( pMem != m_pCurrent ) + { + Assert( 0 ); + Error( "Nav mesh cannot be mutated after load\n" ); + } + return m_nBytesCurrent; +} + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::CompressIDs( void ) +{ + m_nextID = 1; + + FOR_EACH_VEC( TheNavAreas, id ) + { + CNavArea *area = TheNavAreas[id]; + area->m_id = m_nextID++; + + // remove and re-add the area from the nav mesh to update the hashed ID + TheNavMesh->RemoveNavArea( area ); + TheNavMesh->AddNavArea( area ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor used during normal runtime. + */ +CNavArea::CNavArea( void ) +{ + m_marker = 0; + m_nearNavSearchMarker = 0; + m_damagingTickCount = 0; + m_openMarker = 0; + + m_parent = NULL; + m_parentHow = GO_NORTH; + m_attributeFlags = 0; + m_place = TheNavMesh->GetNavPlace(); + m_isUnderwater = false; + m_avoidanceObstacleHeight = 0.0f; + + m_totalCost = 0.0f; + + ResetNodes(); + + int i; + for ( i=0; i 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + } + + m_neZ = corner.z; + m_swZ = otherCorner.z; + + CalcDebugID(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Build a nav area given the positions of its four corners. + */ +void CNavArea::Build( const Vector &nwCorner, const Vector &neCorner, const Vector &seCorner, const Vector &swCorner ) +{ + m_nwCorner = nwCorner; + m_seCorner = seCorner; + + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + m_neZ = neCorner.z; + m_swZ = swCorner.z; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + } + + CalcDebugID(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Used during generation phase to build nav areas from sampled nodes. + */ +void CNavArea::Build( CNavNode *nwNode, CNavNode *neNode, CNavNode *seNode, CNavNode *swNode ) +{ + m_nwCorner = *nwNode->GetPosition(); + m_seCorner = *seNode->GetPosition(); + + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + m_neZ = neNode->GetPosition()->z; + m_swZ = swNode->GetPosition()->z; + + m_node[ NORTH_WEST ] = nwNode; + m_node[ NORTH_EAST ] = neNode; + m_node[ SOUTH_EAST ] = seNode; + m_node[ SOUTH_WEST ] = swNode; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + } + + // mark internal nodes as part of this area + AssignNodes( this ); + + CalcDebugID(); +} + + +//-------------------------------------------------------------------------------------------------------------- +// Return a computed extent (XY is in m_nwCorner and m_seCorner, Z is computed) +void CNavArea::GetExtent( Extent *extent ) const +{ + extent->lo = m_nwCorner; + extent->hi = m_seCorner; + + extent->lo.z = MIN( extent->lo.z, m_nwCorner.z ); + extent->lo.z = MIN( extent->lo.z, m_seCorner.z ); + extent->lo.z = MIN( extent->lo.z, m_neZ ); + extent->lo.z = MIN( extent->lo.z, m_swZ ); + + extent->hi.z = MAX( extent->hi.z, m_nwCorner.z ); + extent->hi.z = MAX( extent->hi.z, m_seCorner.z ); + extent->hi.z = MAX( extent->hi.z, m_neZ ); + extent->hi.z = MAX( extent->hi.z, m_swZ ); +} + + +//-------------------------------------------------------------------------------------------------------------- +// returns the closest node along the given edge to the given point +CNavNode *CNavArea::FindClosestNode( const Vector &pos, NavDirType dir ) const +{ + if ( !HasNodes() ) + return NULL; + + CUtlVector< CNavNode * > nodes; + GetNodes( dir, &nodes ); + + CNavNode *bestNode = NULL; + float bestDistanceSq = FLT_MAX; + + for ( int i=0; iGetPosition() ); + if ( distSq < bestDistanceSq ) + { + bestDistanceSq = distSq; + bestNode = nodes[i]; + } + } + + return bestNode; +} + + +//-------------------------------------------------------------------------------------------------------------- +// build a vector of nodes along the given direction +void CNavArea::GetNodes( NavDirType dir, CUtlVector< CNavNode * > *nodes ) const +{ + if ( !nodes ) + return; + + nodes->RemoveAll(); + + NavCornerType startCorner; + NavCornerType endCorner; + NavDirType traversalDirection; + + switch ( dir ) + { + case NORTH: + startCorner = NORTH_WEST; + endCorner = NORTH_EAST; + traversalDirection = EAST; + break; + + case SOUTH: + startCorner = SOUTH_WEST; + endCorner = SOUTH_EAST; + traversalDirection = EAST; + break; + + case EAST: + startCorner = NORTH_EAST; + endCorner = SOUTH_EAST; + traversalDirection = SOUTH; + break; + + case WEST: + startCorner = NORTH_WEST; + endCorner = SOUTH_WEST; + traversalDirection = SOUTH; + break; + + default: + return; + } + + CNavNode *node; + for ( node = m_node[ startCorner ]; node && node != m_node[ endCorner ]; node = node->GetConnectedNode( traversalDirection ) ) + { + nodes->AddToTail( node ); + } + if ( node && node == m_node[ endCorner ] ) + { + nodes->AddToTail( node ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +class ForgetArea +{ +public: + ForgetArea( CNavArea *area ) + { + m_area = area; + } + + bool operator() ( CBasePlayer *player ) + { + player->OnNavAreaRemoved( m_area ); + + return true; + } + + bool operator() ( CBaseCombatCharacter *player ) + { + player->OnNavAreaRemoved( m_area ); + + return true; + } + + CNavArea *m_area; +}; + + +//-------------------------------------------------------------------------------------------------------------- +class AreaDestroyNotification +{ + CNavArea *m_area; + +public: + AreaDestroyNotification( CNavArea *area ) + { + m_area = area; + } + + bool operator()( CNavLadder *ladder ) + { + ladder->OnDestroyNotify( m_area ); + return true; + } + + bool operator()( CNavArea *area ) + { + if ( area != m_area ) + { + area->OnDestroyNotify( m_area ); + } + return true; + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Destructor + */ +CNavArea::~CNavArea() +{ + // spot encounters aren't owned by anything else, so free them up here + m_spotEncounters.PurgeAndDeleteElements(); + + // if we are resetting the system, don't bother cleaning up - all areas are being destroyed + if (m_isReset) + return; + + // tell the other areas and ladders we are going away + AreaDestroyNotification notification( this ); + TheNavMesh->ForAllAreas( notification ); + TheNavMesh->ForAllLadders( notification ); + + // remove the area from the grid + TheNavMesh->RemoveNavArea( this ); + + // make sure no players keep a pointer to this area + ForgetArea forget( this ); + ForEachActor( forget ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find elevator connections between areas + */ +void CNavArea::ConnectElevators( void ) +{ + m_elevator = NULL; + m_attributeFlags &= ~NAV_MESH_HAS_ELEVATOR; + m_elevatorAreas.RemoveAll(); + +#ifdef TERROR + // connect elevators + CFuncElevator *elevator = NULL; + while( ( elevator = (CFuncElevator *)gEntList.FindEntityByClassname( elevator, "func_elevator" ) ) != NULL ) + { + if ( elevator->GetNumFloors() < 2 ) + { + // broken elevator + continue; + } + + Extent elevatorExtent; + elevator->CollisionProp()->WorldSpaceSurroundingBounds( &elevatorExtent.lo, &elevatorExtent.hi ); + + if ( IsOverlapping( elevatorExtent ) ) + { + // overlaps in 2D - check that this area is within the shaft of the elevator + const Vector ¢er = GetCenter(); + + for( int f=0; fGetNumFloors(); ++f ) + { + const FloorInfo *floor = elevator->GetFloor( f ); + const float tolerance = 30.0f; + + if ( center.z <= floor->height + tolerance && center.z >= floor->height - tolerance ) + { + if ( m_elevator ) + { + Warning( "Multiple elevators overlap navigation area #%d\n", GetID() ); + break; + } + + // this area is part of an elevator system + m_elevator = elevator; + m_attributeFlags |= NAV_MESH_HAS_ELEVATOR; + + // find the largest area overlapping this elevator on each other floor + for( int of=0; ofGetNumFloors(); ++of ) + { + if ( of == f ) + { + // we are on this floor + continue; + } + + const FloorInfo *otherFloor = elevator->GetFloor( of ); + + // find the largest area at this floor + CNavArea *floorArea = NULL; + float floorAreaSize = 0.0f; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area->IsOverlapping( elevatorExtent ) ) + { + if ( area->GetCenter().z <= otherFloor->height + tolerance && area->GetCenter().z >= otherFloor->height - tolerance ) + { + float size = area->GetSizeX() * area->GetSizeY(); + if ( size > floorAreaSize ) + { + floorArea = area; + floorAreaSize = size; + } + } + } + } + + if ( floorArea ) + { + // add this area to the set of areas reachable via elevator + NavConnect con; + con.area = floorArea; + con.length = ( floorArea->GetCenter() - GetCenter() ).Length(); + m_elevatorAreas.AddToTail( con ); + } + else + { + Warning( "Floor %d ('%s') of elevator at ( %3.2f, %3.2f, %3.2f ) has no matching navigation areas\n", + of, + otherFloor->name.ToCStr(), + elevator->GetAbsOrigin().x, elevator->GetAbsOrigin().y, elevator->GetAbsOrigin().z ); + } + } + + // we found our floor + break; + } + } + } + } +#endif // TERROR +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when map is initially loaded + */ +void CNavArea::OnServerActivate( void ) +{ + ConnectElevators(); + m_damagingTickCount = 0; + ClearAllNavCostEntities(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked for each area when the round restarts + */ +void CNavArea::OnRoundRestart( void ) +{ + // need to redo this here since func_elevators are deleted and recreated at round restart + ConnectElevators(); + m_damagingTickCount = 0; + ClearAllNavCostEntities(); +} + + +#ifdef DEBUG_AREA_PLAYERCOUNTS +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::IncrementPlayerCount( int teamID, int entIndex ) +{ + ConColorMsg( Color( 128, 255, 128, 255 ), "%f: Adding ent %d (team %d) to area %d\n", gpGlobals->curtime, entIndex, teamID, GetID() ); + teamID = teamID % MAX_NAV_TEAMS; + Assert( !m_playerEntIndices[teamID].HasElement( entIndex ) ); + if ( !m_playerEntIndices[teamID].HasElement( entIndex ) ) + { + m_playerEntIndices[teamID].AddToTail( entIndex ); + } + + if (m_playerCount[ teamID ] == 255) + { + Warning( "CNavArea::IncrementPlayerCount: Overflow\n" ); + return; + } + + ++m_playerCount[ teamID ]; +} + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::DecrementPlayerCount( int teamID, int entIndex ) +{ + ConColorMsg( Color( 128, 128, 255, 255 ), "%f: Removing ent %d (team %d) from area %d\n", gpGlobals->curtime, entIndex, teamID, GetID() ); + teamID = teamID % MAX_NAV_TEAMS; + Assert( m_playerEntIndices[teamID].HasElement( entIndex ) ); + m_playerEntIndices[teamID].FindAndFastRemove( entIndex ); + + if (m_playerCount[ teamID ] == 0) + { + Warning( "CNavArea::IncrementPlayerCount: Underflow\n" ); + return; + } + + --m_playerCount[ teamID ]; +} +#endif // DEBUG_AREA_PLAYERCOUNTS + + +//-------------------------------------------------------------------------------------------------------------- +/** + * This is invoked at the start of an incremental nav generation on pre-existing areas. + */ +void CNavArea::ResetNodes( void ) +{ + for ( int i=0; iGetCenter() - GetCenter() ).Length(); + m_connect[ dir ].AddToTail( con ); + m_incomingConnect[ dir ].FindAndRemove( con ); + + NavDirType dirOpposite = OppositeDirection( dir ); + con.area = this; + if ( area->m_connect[ dirOpposite ].Find( con ) == area->m_connect[ dirOpposite ].InvalidIndex() ) + { + area->AddIncomingConnection( this, dirOpposite ); + } + + //static char *dirName[] = { "NORTH", "EAST", "SOUTH", "WEST" }; + //CONSOLE_ECHO( " Connected area #%d to #%d, %s\n", m_id, area->m_id, dirName[ dir ] ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Connect this area to given ladder + */ +void CNavArea::ConnectTo( CNavLadder *ladder ) +{ + float center = (ladder->m_top.z + ladder->m_bottom.z) * 0.5f; + + Disconnect( ladder ); // just in case + + if ( GetCenter().z > center ) + { + AddLadderDown( ladder ); + } + else + { + AddLadderUp( ladder ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Disconnect this area from given area + */ +void CNavArea::Disconnect( CNavArea *area ) +{ + NavConnect connect; + connect.area = area; + + for( int i = 0; iIsConnected( this, dirOpposite ) ) + { + AddIncomingConnection( area, dir ); + } + else + { + connect.area = this; + area->m_incomingConnect[ dirOpposite ].FindAndRemove( connect ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Disconnect this area from given ladder + */ +void CNavArea::Disconnect( CNavLadder *ladder ) +{ + NavLadderConnect con; + con.ladder = ladder; + + for( int i=0; iGetPosition(); + m_seCorner = *m_node[ SOUTH_EAST ]->GetPosition(); + + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + m_neZ = m_node[ NORTH_EAST ]->GetPosition()->z; + m_swZ = m_node[ SOUTH_WEST ]->GetPosition()->z; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + } + + // reassign the adjacent area's internal nodes to the final area + adjArea->AssignNodes( this ); + + // merge adjacency links - we gain all the connections that adjArea had + MergeAdjacentConnections( adjArea ); + + // remove subsumed adjacent area + TheNavAreas.FindAndRemove( adjArea ); + TheNavMesh->OnEditDestroyNotify( adjArea ); + TheNavMesh->DestroyArea( adjArea ); +} + + +//-------------------------------------------------------------------------------------------------------------- +class LadderConnectionReplacement +{ + CNavArea *m_originalArea; + CNavArea *m_replacementArea; + +public: + LadderConnectionReplacement( CNavArea *originalArea, CNavArea *replacementArea ) + { + m_originalArea = originalArea; + m_replacementArea = replacementArea; + } + + bool operator()( CNavLadder *ladder ) + { + if ( ladder->m_topForwardArea == m_originalArea ) + ladder->m_topForwardArea = m_replacementArea; + + if ( ladder->m_topRightArea == m_originalArea ) + ladder->m_topRightArea = m_replacementArea; + + if ( ladder->m_topLeftArea == m_originalArea ) + ladder->m_topLeftArea = m_replacementArea; + + if ( ladder->m_topBehindArea == m_originalArea ) + ladder->m_topBehindArea = m_replacementArea; + + if ( ladder->m_bottomArea == m_originalArea ) + ladder->m_bottomArea = m_replacementArea; + + return true; + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * For merging with "adjArea" - pick up all of "adjArea"s connections + */ +void CNavArea::MergeAdjacentConnections( CNavArea *adjArea ) +{ + // merge adjacency links - we gain all the connections that adjArea had + int dir; + for( dir = 0; dirm_connect[ dir ], it ) + { + NavConnect connect = adjArea->m_connect[ dir ][ it ]; + + if (connect.area != adjArea && connect.area != this) + ConnectTo( connect.area, (NavDirType)dir ); + } + } + + // remove any references from this area to the adjacent area, since it is now part of us + Disconnect( adjArea ); + + // Change other references to adjArea to refer instead to us + // We can't just replace existing connections, as several adjacent areas may have been merged into one, + // resulting in a large area adjacent to all of them ending up with multiple redunandant connections + // into the merged area, one for each of the adjacent subsumed smaller ones. + // If an area has a connection to the merged area, we must remove all references to adjArea, and add + // a single connection to us. + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if (area == this || area == adjArea) + continue; + + for( dir = 0; dirm_connect[ dir ], cit ) + { + NavConnect connect = area->m_connect[ dir ][ cit ]; + + if (connect.area == adjArea) + { + connected = true; + break; + } + } + + if (connected) + { + // remove all references to adjArea + area->Disconnect( adjArea ); + + // remove all references to the new area + area->Disconnect( this ); + + // add a single connection to the new area + area->ConnectTo( this, (NavDirType) dir ); + } + } + } + + // We gain all ladder connections adjArea had + for( dir=0; dirm_ladder[ dir ], it ) + { + ConnectTo( adjArea->m_ladder[ dir ][ it ].ladder ); + } + } + + // All ladders that point to adjArea should point to us now + LadderConnectionReplacement replacement( adjArea, this ); + TheNavMesh->ForAllLadders( replacement ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Assign internal nodes to the given area + * NOTE: "internal" nodes do not include the east or south border nodes + */ +void CNavArea::AssignNodes( CNavArea *area ) +{ + CNavNode *horizLast = m_node[ NORTH_EAST ]; + + for( CNavNode *vertNode = m_node[ NORTH_WEST ]; vertNode != m_node[ SOUTH_WEST ]; vertNode = vertNode->GetConnectedNode( SOUTH ) ) + { + for( CNavNode *horizNode = vertNode; horizNode != horizLast; horizNode = horizNode->GetConnectedNode( EAST ) ) + { + horizNode->AssignArea( area ); + } + + horizLast = horizLast->GetConnectedNode( SOUTH ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +class SplitNotification +{ + CNavArea *m_originalArea; + CNavArea *m_alphaArea; + CNavArea *m_betaArea; + +public: + SplitNotification( CNavArea *originalArea, CNavArea *alphaArea, CNavArea *betaArea ) + { + m_originalArea = originalArea; + m_alphaArea = alphaArea; + m_betaArea = betaArea; + } + + bool operator()( CNavLadder *ladder ) + { + ladder->OnSplit( m_originalArea, m_alphaArea, m_betaArea ); + return true; + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Split this area into two areas at the given edge. + * Preserve all adjacency connections. + * NOTE: This does not update node connections, only areas. + */ +bool CNavArea::SplitEdit( bool splitAlongX, float splitEdge, CNavArea **outAlpha, CNavArea **outBeta ) +{ + CNavArea *alpha = NULL; + CNavArea *beta = NULL; + + if (splitAlongX) + { + // +-----+->X + // | A | + // +-----+ + // | B | + // +-----+ + // | + // Y + + // don't do split if at edge of area + if (splitEdge <= m_nwCorner.y + 1.0f) + return false; + + if (splitEdge >= m_seCorner.y - 1.0f) + return false; + + alpha = TheNavMesh->CreateArea(); + alpha->m_nwCorner = m_nwCorner; + + alpha->m_seCorner.x = m_seCorner.x; + alpha->m_seCorner.y = splitEdge; + alpha->m_seCorner.z = GetZ( alpha->m_seCorner ); + + beta = TheNavMesh->CreateArea(); + beta->m_nwCorner.x = m_nwCorner.x; + beta->m_nwCorner.y = splitEdge; + beta->m_nwCorner.z = GetZ( beta->m_nwCorner ); + + beta->m_seCorner = m_seCorner; + + alpha->ConnectTo( beta, SOUTH ); + beta->ConnectTo( alpha, NORTH ); + + FinishSplitEdit( alpha, SOUTH ); + FinishSplitEdit( beta, NORTH ); + } + else + { + // +--+--+->X + // | | | + // | A|B | + // | | | + // +--+--+ + // | + // Y + + // don't do split if at edge of area + if (splitEdge <= m_nwCorner.x + 1.0f) + return false; + + if (splitEdge >= m_seCorner.x - 1.0f) + return false; + + alpha = TheNavMesh->CreateArea(); + alpha->m_nwCorner = m_nwCorner; + + alpha->m_seCorner.x = splitEdge; + alpha->m_seCorner.y = m_seCorner.y; + alpha->m_seCorner.z = GetZ( alpha->m_seCorner ); + + beta = TheNavMesh->CreateArea(); + beta->m_nwCorner.x = splitEdge; + beta->m_nwCorner.y = m_nwCorner.y; + beta->m_nwCorner.z = GetZ( beta->m_nwCorner ); + + beta->m_seCorner = m_seCorner; + + alpha->ConnectTo( beta, EAST ); + beta->ConnectTo( alpha, WEST ); + + FinishSplitEdit( alpha, EAST ); + FinishSplitEdit( beta, WEST ); + } + + if ( !TheNavMesh->IsGenerating() && nav_split_place_on_ground.GetBool() ) + { + alpha->PlaceOnGround( NUM_CORNERS ); + beta->PlaceOnGround( NUM_CORNERS ); + } + + // For every ladder we pointed to, alpha or beta should point to it, based on + // their distance to the ladder + int dir; + for( dir=0; dirm_top; // doesn't matter if we choose top or bottom + + float alphaDistance = alpha->GetDistanceSquaredToPoint( ladderPos ); + float betaDistance = beta->GetDistanceSquaredToPoint( ladderPos ); + + if ( alphaDistance < betaDistance ) + { + alpha->ConnectTo( ladder ); + } + else + { + beta->ConnectTo( ladder ); + } + } + } + + // For every ladder that pointed to us, connect that ladder to the closer of alpha and beta + SplitNotification notify( this, alpha, beta ); + TheNavMesh->ForAllLadders( notify ); + + // return new areas + if (outAlpha) + *outAlpha = alpha; + + if (outBeta) + *outBeta = beta; + + TheNavMesh->OnEditCreateNotify( alpha ); + TheNavMesh->OnEditCreateNotify( beta ); + if ( TheNavMesh->IsInSelectedSet( this ) ) + { + TheNavMesh->AddToSelectedSet( alpha ); + TheNavMesh->AddToSelectedSet( beta ); + } + + // remove original area + TheNavMesh->OnEditDestroyNotify( this ); + TheNavAreas.FindAndRemove( this ); + TheNavMesh->RemoveFromSelectedSet( this ); + TheNavMesh->DestroyArea( this ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if given ladder is connected in given direction + * @todo Formalize "asymmetric" flag on connections + */ +bool CNavArea::IsConnected( const CNavLadder *ladder, CNavLadder::LadderDirectionType dir ) const +{ + FOR_EACH_VEC( m_ladder[ dir ], it ) + { + if ( ladder == m_ladder[ dir ][ it ].ladder ) + { + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if given area is connected in given direction + * if dir == NUM_DIRECTIONS, check all directions (direction is unknown) + * @todo Formalize "asymmetric" flag on connections + */ +bool CNavArea::IsConnected( const CNavArea *area, NavDirType dir ) const +{ + // we are connected to ourself + if (area == this) + return true; + + if (dir == NUM_DIRECTIONS) + { + // search all directions + for( int d=0; dm_topBehindArea == area || + ladder->m_topForwardArea == area || + ladder->m_topLeftArea == area || + ladder->m_topRightArea == area) + return true; + } + + FOR_EACH_VEC( m_ladder[ CNavLadder::LADDER_DOWN ], dit ) + { + CNavLadder *ladder = m_ladder[ CNavLadder::LADDER_DOWN ][ dit ].ladder; + + if (ladder->m_bottomArea == area) + return true; + } + } + else + { + // check specific direction + FOR_EACH_VEC( m_connect[ dir ], it ) + { + if (area == m_connect[ dir ][ it ].area) + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute change in actual ground height from this area to given area + */ +float CNavArea::ComputeGroundHeightChange( const CNavArea *area ) +{ + VPROF_BUDGET( "CNavArea::ComputeHeightChange", "NextBot" ); + + Vector closeFrom, closeTo; + area->GetClosestPointOnArea( GetCenter(), &closeTo ); + GetClosestPointOnArea( area->GetCenter(), &closeFrom ); + + // find actual ground height at each point in case + // areas are below/above actual terrain + float toZ, fromZ; + if ( TheNavMesh->GetSimpleGroundHeight( closeTo + Vector( 0, 0, StepHeight ), &toZ ) == false ) + { + return 0.0f; + } + + if ( TheNavMesh->GetSimpleGroundHeight( closeFrom + Vector( 0, 0, StepHeight ), &fromZ ) == false ) + { + return 0.0f; + } + + return toZ - fromZ; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * The area 'source' is connected to us along our 'incomingEdgeDir' edge + */ +void CNavArea::AddIncomingConnection( CNavArea *source, NavDirType incomingEdgeDir ) +{ + NavConnect con; + con.area = source; + if ( m_incomingConnect[ incomingEdgeDir ].Find( con ) == m_incomingConnect[ incomingEdgeDir ].InvalidIndex() ) + { + con.length = ( source->GetCenter() - GetCenter() ).Length(); + m_incomingConnect[ incomingEdgeDir ].AddToTail( con ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given the portion of the original area, update its internal data + * The "ignoreEdge" direction defines the side of the original area that the new area does not include + */ +void CNavArea::FinishSplitEdit( CNavArea *newArea, NavDirType ignoreEdge ) +{ + newArea->InheritAttributes( this ); + + newArea->m_center.x = (newArea->m_nwCorner.x + newArea->m_seCorner.x)/2.0f; + newArea->m_center.y = (newArea->m_nwCorner.y + newArea->m_seCorner.y)/2.0f; + newArea->m_center.z = (newArea->m_nwCorner.z + newArea->m_seCorner.z)/2.0f; + + newArea->m_neZ = GetZ( newArea->m_seCorner.x, newArea->m_nwCorner.y ); + newArea->m_swZ = GetZ( newArea->m_nwCorner.x, newArea->m_seCorner.y ); + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + newArea->m_invDxCorners = 1.0f / ( newArea->m_seCorner.x - newArea->m_nwCorner.x ); + newArea->m_invDyCorners = 1.0f / ( newArea->m_seCorner.y - newArea->m_nwCorner.y ); + } + else + { + newArea->m_invDxCorners = newArea->m_invDyCorners = 0; + } + + // connect to adjacent areas + for( int d=0; dIsOverlappingX( adj )) + { + newArea->ConnectTo( adj, (NavDirType)d ); + + // add reciprocal connection if needed + if (adj->IsConnected( this, OppositeDirection( (NavDirType)d ))) + adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); + } + break; + + case EAST: + case WEST: + if (newArea->IsOverlappingY( adj )) + { + newArea->ConnectTo( adj, (NavDirType)d ); + + // add reciprocal connection if needed + if (adj->IsConnected( this, OppositeDirection( (NavDirType)d ))) + adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); + } + break; + } + + for ( int a = 0; a < m_incomingConnect[d].Count(); a++ ) + { + CNavArea *adj = m_incomingConnect[d][a].area; + + switch( d ) + { + case NORTH: + case SOUTH: + if (newArea->IsOverlappingX( adj )) + { + adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); + } + break; + + case EAST: + case WEST: + if (newArea->IsOverlappingY( adj )) + { + adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); + } + break; + } + } + } + } + + TheNavAreas.AddToTail( newArea ); + TheNavMesh->AddNavArea( newArea ); + + // Assign nodes + if ( HasNodes() ) + { + // first give it all our nodes... + newArea->m_node[ NORTH_WEST ] = m_node[ NORTH_WEST ]; + newArea->m_node[ NORTH_EAST ] = m_node[ NORTH_EAST ]; + newArea->m_node[ SOUTH_EAST ] = m_node[ SOUTH_EAST ]; + newArea->m_node[ SOUTH_WEST ] = m_node[ SOUTH_WEST ]; + + // ... then pull in one edge... + NavDirType dir = NUM_DIRECTIONS; + NavCornerType corner[2] = { NUM_CORNERS, NUM_CORNERS }; + + switch ( ignoreEdge ) + { + case NORTH: + dir = SOUTH; + corner[0] = NORTH_WEST; + corner[1] = NORTH_EAST; + break; + case SOUTH: + dir = NORTH; + corner[0] = SOUTH_WEST; + corner[1] = SOUTH_EAST; + break; + case EAST: + dir = WEST; + corner[0] = NORTH_EAST; + corner[1] = SOUTH_EAST; + break; + case WEST: + dir = EAST; + corner[0] = NORTH_WEST; + corner[1] = SOUTH_WEST; + break; + } + + while ( !newArea->IsOverlapping( *newArea->m_node[ corner[0] ]->GetPosition(), GenerationStepSize/2 ) ) + { + for ( int i=0; i<2; ++i ) + { + Assert( newArea->m_node[ corner[i] ] ); + Assert( newArea->m_node[ corner[i] ]->GetConnectedNode( dir ) ); + newArea->m_node[ corner[i] ] = newArea->m_node[ corner[i] ]->GetConnectedNode( dir ); + } + } + + // assign internal nodes... + newArea->AssignNodes( newArea ); + + // ... and grab the node heights for our corner heights. + newArea->m_neZ = newArea->m_node[ NORTH_EAST ]->GetPosition()->z; + newArea->m_nwCorner.z = newArea->m_node[ NORTH_WEST ]->GetPosition()->z; + newArea->m_swZ = newArea->m_node[ SOUTH_WEST ]->GetPosition()->z; + newArea->m_seCorner.z = newArea->m_node[ SOUTH_EAST ]->GetPosition()->z; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Create a new area between this area and given area + */ +bool CNavArea::SpliceEdit( CNavArea *other ) +{ + CNavArea *newArea = NULL; + Vector nw, ne, se, sw; + + if (m_nwCorner.x > other->m_seCorner.x) + { + // 'this' is east of 'other' + float top = MAX( m_nwCorner.y, other->m_nwCorner.y ); + float bottom = MIN( m_seCorner.y, other->m_seCorner.y ); + + nw.x = other->m_seCorner.x; + nw.y = top; + nw.z = other->GetZ( nw ); + + se.x = m_nwCorner.x; + se.y = bottom; + se.z = GetZ( se ); + + ne.x = se.x; + ne.y = nw.y; + ne.z = GetZ( ne ); + + sw.x = nw.x; + sw.y = se.y; + sw.z = other->GetZ( sw ); + + newArea = TheNavMesh->CreateArea(); + if (newArea == NULL) + { + Warning( "SpliceEdit: Out of memory.\n" ); + return false; + } + + newArea->Build( nw, ne, se, sw ); + + this->ConnectTo( newArea, WEST ); + newArea->ConnectTo( this, EAST ); + + other->ConnectTo( newArea, EAST ); + newArea->ConnectTo( other, WEST ); + } + else if (m_seCorner.x < other->m_nwCorner.x) + { + // 'this' is west of 'other' + float top = MAX( m_nwCorner.y, other->m_nwCorner.y ); + float bottom = MIN( m_seCorner.y, other->m_seCorner.y ); + + nw.x = m_seCorner.x; + nw.y = top; + nw.z = GetZ( nw ); + + se.x = other->m_nwCorner.x; + se.y = bottom; + se.z = other->GetZ( se ); + + ne.x = se.x; + ne.y = nw.y; + ne.z = other->GetZ( ne ); + + sw.x = nw.x; + sw.y = se.y; + sw.z = GetZ( sw ); + + newArea = TheNavMesh->CreateArea(); + if (newArea == NULL) + { + Warning( "SpliceEdit: Out of memory.\n" ); + return false; + } + + newArea->Build( nw, ne, se, sw ); + + this->ConnectTo( newArea, EAST ); + newArea->ConnectTo( this, WEST ); + + other->ConnectTo( newArea, WEST ); + newArea->ConnectTo( other, EAST ); + } + else // 'this' overlaps in X + { + if (m_nwCorner.y > other->m_seCorner.y) + { + // 'this' is south of 'other' + float left = MAX( m_nwCorner.x, other->m_nwCorner.x ); + float right = MIN( m_seCorner.x, other->m_seCorner.x ); + + nw.x = left; + nw.y = other->m_seCorner.y; + nw.z = other->GetZ( nw ); + + se.x = right; + se.y = m_nwCorner.y; + se.z = GetZ( se ); + + ne.x = se.x; + ne.y = nw.y; + ne.z = other->GetZ( ne ); + + sw.x = nw.x; + sw.y = se.y; + sw.z = GetZ( sw ); + + newArea = TheNavMesh->CreateArea(); + if (newArea == NULL) + { + Warning( "SpliceEdit: Out of memory.\n" ); + return false; + } + + newArea->Build( nw, ne, se, sw ); + + this->ConnectTo( newArea, NORTH ); + newArea->ConnectTo( this, SOUTH ); + + other->ConnectTo( newArea, SOUTH ); + newArea->ConnectTo( other, NORTH ); + } + else if (m_seCorner.y < other->m_nwCorner.y) + { + // 'this' is north of 'other' + float left = MAX( m_nwCorner.x, other->m_nwCorner.x ); + float right = MIN( m_seCorner.x, other->m_seCorner.x ); + + nw.x = left; + nw.y = m_seCorner.y; + nw.z = GetZ( nw ); + + se.x = right; + se.y = other->m_nwCorner.y; + se.z = other->GetZ( se ); + + ne.x = se.x; + ne.y = nw.y; + ne.z = GetZ( ne ); + + sw.x = nw.x; + sw.y = se.y; + sw.z = other->GetZ( sw ); + + newArea = TheNavMesh->CreateArea(); + if (newArea == NULL) + { + Warning( "SpliceEdit: Out of memory.\n" ); + return false; + } + + newArea->Build( nw, ne, se, sw ); + + this->ConnectTo( newArea, SOUTH ); + newArea->ConnectTo( this, NORTH ); + + other->ConnectTo( newArea, NORTH ); + newArea->ConnectTo( other, SOUTH ); + } + else + { + // areas overlap + return false; + } + } + + newArea->InheritAttributes( this, other ); + + TheNavAreas.AddToTail( newArea ); + TheNavMesh->AddNavArea( newArea ); + + TheNavMesh->OnEditCreateNotify( newArea ); + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Calculates a constant ID for an area at this location, for debugging +*/ +void CNavArea::CalcDebugID() +{ + if ( m_debugid == 0 ) + { + // calculate a debug ID which will be constant for this nav area across generation runs + int coord[6] = { (int) m_nwCorner.x, (int) m_nwCorner.x, (int) m_nwCorner.z, (int) m_seCorner.x, (int) m_seCorner.y, (int) m_seCorner.z }; + m_debugid = CRC32_ProcessSingleBuffer( &coord, sizeof( coord ) ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Merge this area and given adjacent area + */ +bool CNavArea::MergeEdit( CNavArea *adj ) +{ + // can only merge if attributes of both areas match + + + // check that these areas can be merged + const float tolerance = 1.0f; + bool merge = false; + if (fabs( m_nwCorner.x - adj->m_nwCorner.x ) < tolerance && + fabs( m_seCorner.x - adj->m_seCorner.x ) < tolerance) + merge = true; + + if (fabs( m_nwCorner.y - adj->m_nwCorner.y ) < tolerance && + fabs( m_seCorner.y - adj->m_seCorner.y ) < tolerance) + merge = true; + + if (merge == false) + return false; + + Vector originalNWCorner = m_nwCorner; + Vector originalSECorner = m_seCorner; + + // update extent + if (m_nwCorner.x > adj->m_nwCorner.x || m_nwCorner.y > adj->m_nwCorner.y) + m_nwCorner = adj->m_nwCorner; + + if (m_seCorner.x < adj->m_seCorner.x || m_seCorner.y < adj->m_seCorner.y) + m_seCorner = adj->m_seCorner; + + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + } + + if (m_seCorner.x > originalSECorner.x || m_nwCorner.y < originalNWCorner.y) + m_neZ = adj->GetZ( m_seCorner.x, m_nwCorner.y ); + else + m_neZ = GetZ( m_seCorner.x, m_nwCorner.y ); + + if (m_nwCorner.x < originalNWCorner.x || m_seCorner.y > originalSECorner.y) + m_swZ = adj->GetZ( m_nwCorner.x, m_seCorner.y ); + else + m_swZ = GetZ( m_nwCorner.x, m_seCorner.y ); + + // merge adjacency links - we gain all the connections that adjArea had + MergeAdjacentConnections( adj ); + + InheritAttributes( adj ); + + // remove subsumed adjacent area + TheNavAreas.FindAndRemove( adj ); + TheNavMesh->OnEditDestroyNotify( adj ); + TheNavMesh->DestroyArea( adj ); + + TheNavMesh->OnEditCreateNotify( this ); + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::InheritAttributes( CNavArea *first, CNavArea *second ) +{ + if ( first && second ) + { + SetAttributes( first->GetAttributes() | second->GetAttributes() ); + + // if both areas have the same place, the new area inherits it + if ( first->GetPlace() == second->GetPlace() ) + { + SetPlace( first->GetPlace() ); + } + else if ( first->GetPlace() == UNDEFINED_PLACE ) + { + SetPlace( second->GetPlace() ); + } + else if ( second->GetPlace() == UNDEFINED_PLACE ) + { + SetPlace( first->GetPlace() ); + } + else + { + // both have valid, but different places - pick on at random + if ( RandomInt( 0, 100 ) < 50 ) + SetPlace( first->GetPlace() ); + else + SetPlace( second->GetPlace() ); + } + } + else if ( first ) + { + SetAttributes( GetAttributes() | first->GetAttributes() ); + if ( GetPlace() == UNDEFINED_PLACE ) + { + SetPlace( first->GetPlace() ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void ApproachAreaAnalysisPrep( void ) +{ +} + +//-------------------------------------------------------------------------------------------------------------- +void CleanupApproachAreaAnalysisPrep( void ) +{ +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Remove "analyzed" data from nav area + */ +void CNavArea::Strip( void ) +{ + m_spotEncounters.PurgeAndDeleteElements(); // this calls delete on each element +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if area is more or less square. + * This is used when merging to prevent long, thin, areas being created. + */ +bool CNavArea::IsRoughlySquare( void ) const +{ + float aspect = GetSizeX() / GetSizeY(); + + const float maxAspect = 3.01; + const float minAspect = 1.0f / maxAspect; + if (aspect < minAspect || aspect > maxAspect) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if 'pos' is within 2D extents of area. + */ +bool CNavArea::IsOverlapping( const Vector &pos, float tolerance ) const +{ + if (pos.x + tolerance >= m_nwCorner.x && pos.x - tolerance <= m_seCorner.x && + pos.y + tolerance >= m_nwCorner.y && pos.y - tolerance <= m_seCorner.y) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if 'area' overlaps our 2D extents + */ +bool CNavArea::IsOverlapping( const CNavArea *area ) const +{ + if (area->m_nwCorner.x < m_seCorner.x && area->m_seCorner.x > m_nwCorner.x && + area->m_nwCorner.y < m_seCorner.y && area->m_seCorner.y > m_nwCorner.y) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if 'extent' overlaps our 2D extents + */ +bool CNavArea::IsOverlapping( const Extent &extent ) const +{ + return ( extent.lo.x < m_seCorner.x && extent.hi.x > m_nwCorner.x && + extent.lo.y < m_seCorner.y && extent.hi.y > m_nwCorner.y ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if 'area' overlaps our X extent + */ +bool CNavArea::IsOverlappingX( const CNavArea *area ) const +{ + if (area->m_nwCorner.x < m_seCorner.x && area->m_seCorner.x > m_nwCorner.x) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if 'area' overlaps our Y extent + */ +bool CNavArea::IsOverlappingY( const CNavArea *area ) const +{ + if (area->m_nwCorner.y < m_seCorner.y && area->m_seCorner.y > m_nwCorner.y) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +class COverlapCheck +{ +public: + COverlapCheck( const CNavArea *me, const Vector &pos ) : m_pos( pos ) + { + m_me = me; + m_myZ = me->GetZ( pos ); + } + + bool operator() ( CNavArea *area ) + { + // skip self + if ( area == m_me ) + return true; + + // check 2D overlap + if ( !area->IsOverlapping( m_pos ) ) + return true; + + float theirZ = area->GetZ( m_pos ); + if ( theirZ > m_pos.z ) + { + // they are above the point + return true; + } + + if ( theirZ > m_myZ ) + { + // we are below an area that is beneath the given position + return false; + } + + return true; + } + + const CNavArea *m_me; + float m_myZ; + const Vector &m_pos; +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if given point is on or above this area, but no others + */ +bool CNavArea::Contains( const Vector &pos ) const +{ + // check 2D overlap + if (!IsOverlapping( pos )) + return false; + + // the point overlaps us, check that it is above us, but not above any areas that overlap us + float myZ = GetZ( pos ); + + // if the nav area is above the given position, fail + // allow nav area to be as much as a step height above the given position + if (myZ - StepHeight > pos.z) + return false; + + Extent areaExtent; + GetExtent( &areaExtent ); + + COverlapCheck overlap( this, pos ); + return TheNavMesh->ForAllAreasOverlappingExtent( overlap, areaExtent ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Returns true if area completely contains other area +*/ +bool CNavArea::Contains( const CNavArea *area ) const +{ + return ( ( m_nwCorner.x <= area->m_nwCorner.x ) && ( m_seCorner.x >= area->m_seCorner.x ) && + ( m_nwCorner.y <= area->m_nwCorner.y ) && ( m_seCorner.y >= area->m_seCorner.y ) && + ( m_nwCorner.z <= area->m_nwCorner.z ) && ( m_seCorner.z >= area->m_seCorner.z ) ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::ComputeNormal( Vector *normal, bool alternate ) const +{ + if ( !normal ) + return; + + Vector u, v; + + if ( !alternate ) + { + u.x = m_seCorner.x - m_nwCorner.x; + u.y = 0.0f; + u.z = m_neZ - m_nwCorner.z; + + v.x = 0.0f; + v.y = m_seCorner.y - m_nwCorner.y; + v.z = m_swZ - m_nwCorner.z; + } + else + { + u.x = m_nwCorner.x - m_seCorner.x; + u.y = 0.0f; + u.z = m_swZ - m_seCorner.z; + + v.x = 0.0f; + v.y = m_nwCorner.y - m_seCorner.y; + v.z = m_neZ - m_seCorner.z; + } + + *normal = CrossProduct( u, v ); + normal->NormalizeInPlace(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Removes all connections in directions to left and right of specified direction +*/ +void CNavArea::RemoveOrthogonalConnections( NavDirType dir ) +{ + NavDirType dirToRemove[2]; + dirToRemove[0] = DirectionLeft( dir ); + dirToRemove[1] = DirectionRight( dir ); + for ( int i = 0; i < 2; i++ ) + { + dir = dirToRemove[i]; + while ( GetAdjacentCount( dir ) > 0 ) + { + CNavArea *adj = GetAdjacentArea( dir, 0 ); + Disconnect( adj ); + adj->Disconnect( this ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the area is approximately flat, using normals computed from opposite corners + */ +bool CNavArea::IsFlat( void ) const +{ + Vector normal, otherNormal; + ComputeNormal( &normal ); + ComputeNormal( &otherNormal, true ); + + float tolerance = nav_coplanar_slope_limit.GetFloat(); + if ( ( m_node[ NORTH_WEST ] && m_node[ NORTH_WEST ]->IsOnDisplacement() ) || + ( m_node[ NORTH_EAST ] && m_node[ NORTH_EAST ]->IsOnDisplacement() ) || + ( m_node[ SOUTH_EAST ] && m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || + ( m_node[ SOUTH_WEST ] && m_node[ SOUTH_WEST ]->IsOnDisplacement() ) ) + { + tolerance = nav_coplanar_slope_limit_displacement.GetFloat(); + } + + if (DotProduct( normal, otherNormal ) > tolerance) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this area and given area are approximately co-planar + */ +bool CNavArea::IsCoplanar( const CNavArea *area ) const +{ + Vector u, v; + + bool isOnDisplacement = ( m_node[ NORTH_WEST ] && m_node[ NORTH_WEST ]->IsOnDisplacement() ) || + ( m_node[ NORTH_EAST ] && m_node[ NORTH_EAST ]->IsOnDisplacement() ) || + ( m_node[ SOUTH_EAST ] && m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || + ( m_node[ SOUTH_WEST ] && m_node[ SOUTH_WEST ]->IsOnDisplacement() ); + + if ( !isOnDisplacement && !IsFlat() ) + return false; + + bool areaIsOnDisplacement = ( area->m_node[ NORTH_WEST ] && area->m_node[ NORTH_WEST ]->IsOnDisplacement() ) || + ( area->m_node[ NORTH_EAST ] && area->m_node[ NORTH_EAST ]->IsOnDisplacement() ) || + ( area->m_node[ SOUTH_EAST ] && area->m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || + ( area->m_node[ SOUTH_WEST ] && area->m_node[ SOUTH_WEST ]->IsOnDisplacement() ); + + if ( !areaIsOnDisplacement && !area->IsFlat() ) + return false; + + // compute our unit surface normal + Vector normal, otherNormal; + ComputeNormal( &normal ); + area->ComputeNormal( &otherNormal ); + + // can only merge areas that are nearly planar, to ensure areas do not differ from underlying geometry much + float tolerance = nav_coplanar_slope_limit.GetFloat(); + if ( ( m_node[ NORTH_WEST ] && m_node[ NORTH_WEST ]->IsOnDisplacement() ) || + ( m_node[ NORTH_EAST ] && m_node[ NORTH_EAST ]->IsOnDisplacement() ) || + ( m_node[ SOUTH_EAST ] && m_node[ SOUTH_EAST ]->IsOnDisplacement() ) || + ( m_node[ SOUTH_WEST ] && m_node[ SOUTH_WEST ]->IsOnDisplacement() ) ) + { + tolerance = nav_coplanar_slope_limit_displacement.GetFloat(); + } + + if (DotProduct( normal, otherNormal ) > tolerance) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return Z of area at (x,y) of 'pos' + * Trilinear interpolation of Z values at quad edges. + * NOTE: pos->z is not used. + */ + +float CNavArea::GetZ( float x, float y ) const RESTRICT +{ + // guard against division by zero due to degenerate areas +#ifdef _X360 + // do the compare-against-zero on the integer unit to avoid a fcmp + // IEEE754 float positive zero is simply 0x00. There is also a + // floating-point negative zero (-0.0f == 0x80000000), but given + // how m_inv is computed earlier, that's not a possible value for + // it here, so we don't have to check for that. + // + // oddly, the compiler isn't smart enough to do this on its own + if ( *reinterpret_cast(&m_invDxCorners) == 0 || + *reinterpret_cast(&m_invDyCorners) == 0 ) + return m_neZ; +#else + if (m_invDxCorners == 0.0f || m_invDyCorners == 0.0f) + return m_neZ; +#endif + + float u = (x - m_nwCorner.x) * m_invDxCorners; + float v = (y - m_nwCorner.y) * m_invDyCorners; + + // clamp Z values to (x,y) volume + + u = fsel( u, u, 0 ); // u >= 0 ? u : 0 + u = fsel( u - 1.0f, 1.0f, u ); // u >= 1 ? 1 : u + + v = fsel( v, v, 0 ); // v >= 0 ? v : 0 + v = fsel( v - 1.0f, 1.0f, v ); // v >= 1 ? 1 : v + + float northZ = m_nwCorner.z + u * (m_neZ - m_nwCorner.z); + float southZ = m_swZ + u * (m_seCorner.z - m_swZ); + + return northZ + v * (southZ - northZ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return closest point to 'pos' on 'area'. + * Returned point is in 'close'. + */ +void CNavArea::GetClosestPointOnArea( const Vector * RESTRICT pPos, Vector *close ) const RESTRICT +{ + float x, y, z; + + // Using fsel rather than compares, as much faster on 360 [7/28/2008 tom] + x = fsel( pPos->x - m_nwCorner.x, pPos->x, m_nwCorner.x ); + x = fsel( x - m_seCorner.x, m_seCorner.x, x ); + + y = fsel( pPos->y - m_nwCorner.y, pPos->y, m_nwCorner.y ); + y = fsel( y - m_seCorner.y, m_seCorner.y, y ); + + z = GetZ( x, y ); + + close->Init( x, y, z ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return shortest distance squared between point and this area + */ +float CNavArea::GetDistanceSquaredToPoint( const Vector &pos ) const +{ + if (pos.x < m_nwCorner.x) + { + if (pos.y < m_nwCorner.y) + { + // position is north-west of area + return (m_nwCorner - pos).LengthSqr(); + } + else if (pos.y > m_seCorner.y) + { + // position is south-west of area + Vector d; + d.x = m_nwCorner.x - pos.x; + d.y = m_seCorner.y - pos.y; + d.z = m_swZ - pos.z; + return d.LengthSqr(); + } + else + { + // position is west of area + float d = m_nwCorner.x - pos.x; + return d * d; + } + } + else if (pos.x > m_seCorner.x) + { + if (pos.y < m_nwCorner.y) + { + // position is north-east of area + Vector d; + d.x = m_seCorner.x - pos.x; + d.y = m_nwCorner.y - pos.y; + d.z = m_neZ - pos.z; + return d.LengthSqr(); + } + else if (pos.y > m_seCorner.y) + { + // position is south-east of area + return (m_seCorner - pos).LengthSqr(); + } + else + { + // position is east of area + float d = pos.x - m_seCorner.x; + return d * d; + } + } + else if (pos.y < m_nwCorner.y) + { + // position is north of area + float d = m_nwCorner.y - pos.y; + return d * d; + } + else if (pos.y > m_seCorner.y) + { + // position is south of area + float d = pos.y - m_seCorner.y; + return d * d; + } + else + { + // position is inside of 2D extent of area - find delta Z + float z = GetZ( pos ); + float d = z - pos.z; + return d * d; + } +} + + + +//-------------------------------------------------------------------------------------------------------------- +CNavArea *CNavArea::GetRandomAdjacentArea( NavDirType dir ) const +{ + int count = m_connect[ dir ].Count(); + int which = RandomInt( 0, count-1 ); + + int i = 0; + FOR_EACH_VEC( m_connect[ dir ], it ) + { + if (i == which) + return m_connect[ dir ][ it ].area; + + ++i; + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +// Build a vector of all adjacent areas +void CNavArea::CollectAdjacentAreas( CUtlVector< CNavArea * > *adjVector ) const +{ + for( int d=0; dAddToTail( m_connect[d].Element(i).area ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute "portal" between two adjacent areas. + * Return center of portal opening, and half-width defining sides of portal from center. + * NOTE: center->z is unset. + */ +void CNavArea::ComputePortal( const CNavArea *to, NavDirType dir, Vector *center, float *halfWidth ) const +{ + if ( dir == NORTH || dir == SOUTH ) + { + if ( dir == NORTH ) + { + center->y = m_nwCorner.y; + } + else + { + center->y = m_seCorner.y; + } + + float left = MAX( m_nwCorner.x, to->m_nwCorner.x ); + float right = MIN( m_seCorner.x, to->m_seCorner.x ); + + // clamp to our extent in case areas are disjoint + if ( left < m_nwCorner.x ) + { + left = m_nwCorner.x; + } + else if ( left > m_seCorner.x ) + { + left = m_seCorner.x; + } + + if ( right < m_nwCorner.x ) + { + right = m_nwCorner.x; + } + else if ( right > m_seCorner.x ) + { + right = m_seCorner.x; + } + + center->x = ( left + right )/2.0f; + *halfWidth = ( right - left )/2.0f; + } + else // EAST or WEST + { + if ( dir == WEST ) + { + center->x = m_nwCorner.x; + } + else + { + center->x = m_seCorner.x; + } + + float top = MAX( m_nwCorner.y, to->m_nwCorner.y ); + float bottom = MIN( m_seCorner.y, to->m_seCorner.y ); + + // clamp to our extent in case areas are disjoint + if ( top < m_nwCorner.y ) + { + top = m_nwCorner.y; + } + else if ( top > m_seCorner.y ) + { + top = m_seCorner.y; + } + + if ( bottom < m_nwCorner.y ) + { + bottom = m_nwCorner.y; + } + else if ( bottom > m_seCorner.y ) + { + bottom = m_seCorner.y; + } + + center->y = (top + bottom)/2.0f; + *halfWidth = (bottom - top)/2.0f; + } + + center->z = GetZ( center->x, center->y ); +} + + +//-------------------------------------------------------------------------------------------------------------- +// compute largest portal to adjacent area, returning direction +NavDirType CNavArea::ComputeLargestPortal( const CNavArea *to, Vector *center, float *halfWidth ) const +{ + NavDirType bestDir = NUM_DIRECTIONS; + Vector bestCenter( vec3_origin ); + float bestHalfWidth = 0.0f; + + Vector centerDir = to->GetCenter() - GetCenter(); + + for ( int i=0; i= 0.0f ) + continue; + break; + case SOUTH: // +y + if ( centerDir.y <= 0.0f ) + continue; + break; + case WEST: // -x + if ( centerDir.x >= 0.0f ) + continue; + break; + case EAST: // +x + if ( centerDir.x <= 0.0f ) + continue; + break; + } + + ComputePortal( to, testDir, &testCenter, &testHalfWidth ); + if ( testHalfWidth > bestHalfWidth ) + { + bestDir = testDir; + bestCenter = testCenter; + bestHalfWidth = testHalfWidth; + } + } + + *center = bestCenter; + *halfWidth = bestHalfWidth; + return bestDir; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute closest point within the "portal" between to adjacent areas. + */ +void CNavArea::ComputeClosestPointInPortal( const CNavArea *to, NavDirType dir, const Vector &fromPos, Vector *closePos ) const +{ +// const float margin = 0.0f; //GenerationStepSize/2.0f; // causes trouble with very small/narrow nav areas + const float margin = GenerationStepSize; + + if ( dir == NORTH || dir == SOUTH ) + { + if ( dir == NORTH ) + { + closePos->y = m_nwCorner.y; + } + else + { + closePos->y = m_seCorner.y; + } + + float left = MAX( m_nwCorner.x, to->m_nwCorner.x ); + float right = MIN( m_seCorner.x, to->m_seCorner.x ); + + // clamp to our extent in case areas are disjoint + // no good - need to push into to area for margins + /* + if (left < m_nwCorner.x) + left = m_nwCorner.x; + else if (left > m_seCorner.x) + left = m_seCorner.x; + + if (right < m_nwCorner.x) + right = m_nwCorner.x; + else if (right > m_seCorner.x) + right = m_seCorner.x; + */ + + // keep margin if against edge + /// @todo Need better check whether edge is outer edge or not - partial overlap is missed + float leftMargin = ( to->IsEdge( WEST ) ) ? ( left + margin ) : left; + float rightMargin = ( to->IsEdge( EAST ) ) ? ( right - margin ) : right; + + // if area is narrow, margins may have crossed + if ( leftMargin > rightMargin ) + { + // use midline + float mid = ( left + right )/2.0f; + leftMargin = mid; + rightMargin = mid; + } + + // limit x to within portal + if ( fromPos.x < leftMargin ) + { + closePos->x = leftMargin; + } + else if ( fromPos.x > rightMargin ) + { + closePos->x = rightMargin; + } + else + { + closePos->x = fromPos.x; + } + } + else // EAST or WEST + { + if ( dir == WEST ) + { + closePos->x = m_nwCorner.x; + } + else + { + closePos->x = m_seCorner.x; + } + + float top = MAX( m_nwCorner.y, to->m_nwCorner.y ); + float bottom = MIN( m_seCorner.y, to->m_seCorner.y ); + + // clamp to our extent in case areas are disjoint + // no good - need to push into to area for margins + /* + if (top < m_nwCorner.y) + top = m_nwCorner.y; + else if (top > m_seCorner.y) + top = m_seCorner.y; + + if (bottom < m_nwCorner.y) + bottom = m_nwCorner.y; + else if (bottom > m_seCorner.y) + bottom = m_seCorner.y; + */ + + // keep margin if against edge + float topMargin = ( to->IsEdge( NORTH ) ) ? ( top + margin ) : top; + float bottomMargin = ( to->IsEdge( SOUTH ) ) ? ( bottom - margin ) : bottom; + + // if area is narrow, margins may have crossed + if ( topMargin > bottomMargin ) + { + // use midline + float mid = ( top + bottom )/2.0f; + topMargin = mid; + bottomMargin = mid; + } + + // limit y to within portal + if ( fromPos.y < topMargin ) + { + closePos->y = topMargin; + } + else if ( fromPos.y > bottomMargin ) + { + closePos->y = bottomMargin; + } + else + { + closePos->y = fromPos.y; + } + } + + closePos->z = GetZ( closePos->x, closePos->y ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the given area and 'other' share a colinear edge (ie: no drop-down or step/jump/climb) + */ +bool CNavArea::IsContiguous( const CNavArea *other ) const +{ + VPROF_BUDGET( "CNavArea::IsContiguous", "NextBot" ); + + // find which side it is connected on + int dir; + for( dir=0; dirComputePortal( this, OppositeDirection( (NavDirType)dir ), &otherEdge, &halfWidth ); + + // must use stepheight because rough terrain can have gaps/cracks between adjacent nav areas + return ( myEdge - otherEdge ).IsLengthLessThan( StepHeight ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return height change between edges of adjacent nav areas (not actual underlying ground) + */ +float CNavArea::ComputeAdjacentConnectionHeightChange( const CNavArea *destinationArea ) const +{ + VPROF_BUDGET( "CNavArea::ComputeAdjacentConnectionHeightChange", "NextBot" ); + + // find which side it is connected on + int dir; + for( dir=0; dirComputePortal( this, OppositeDirection( (NavDirType)dir ), &otherEdge, &halfWidth ); + + return otherEdge.z - myEdge.z; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if there are no bi-directional links on the given side + */ +bool CNavArea::IsEdge( NavDirType dir ) const +{ + FOR_EACH_VEC( m_connect[ dir ], it ) + { + const NavConnect connect = m_connect[ dir ][ it ]; + + if (connect.area->IsConnected( this, OppositeDirection( dir ) )) + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return direction from this area to the given point + */ +NavDirType CNavArea::ComputeDirection( Vector *point ) const +{ + if (point->x >= m_nwCorner.x && point->x <= m_seCorner.x) + { + if (point->y < m_nwCorner.y) + return NORTH; + else if (point->y > m_seCorner.y) + return SOUTH; + } + else if (point->y >= m_nwCorner.y && point->y <= m_seCorner.y) + { + if (point->x < m_nwCorner.x) + return WEST; + else if (point->x > m_seCorner.x) + return EAST; + } + + // find closest direction + Vector to = *point - m_center; + + if (fabs(to.x) > fabs(to.y)) + { + if (to.x > 0.0f) + return EAST; + return WEST; + } + else + { + if (to.y > 0.0f) + return SOUTH; + return NORTH; + } + + return NUM_DIRECTIONS; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CNavArea::GetCornerHotspot( NavCornerType corner, Vector hotspot[NUM_CORNERS] ) const +{ + Vector nw = GetCorner( NORTH_WEST ); + Vector ne = GetCorner( NORTH_EAST ); + Vector sw = GetCorner( SOUTH_WEST ); + Vector se = GetCorner( SOUTH_EAST ); + + float size = 9.0f; + size = MIN( size, GetSizeX()/3 ); // make sure the hotspot doesn't extend outside small areas + size = MIN( size, GetSizeY()/3 ); + + switch ( corner ) + { + case NORTH_WEST: + hotspot[0] = nw; + hotspot[1] = hotspot[0] + Vector( size, 0, 0 ); + hotspot[2] = hotspot[0] + Vector( size, size, 0 ); + hotspot[3] = hotspot[0] + Vector( 0, size, 0 ); + break; + case NORTH_EAST: + hotspot[0] = ne; + hotspot[1] = hotspot[0] + Vector( -size, 0, 0 ); + hotspot[2] = hotspot[0] + Vector( -size, size, 0 ); + hotspot[3] = hotspot[0] + Vector( 0, size, 0 ); + break; + case SOUTH_WEST: + hotspot[0] = sw; + hotspot[1] = hotspot[0] + Vector( size, 0, 0 ); + hotspot[2] = hotspot[0] + Vector( size, -size, 0 ); + hotspot[3] = hotspot[0] + Vector( 0, -size, 0 ); + break; + case SOUTH_EAST: + hotspot[0] = se; + hotspot[1] = hotspot[0] + Vector( -size, 0, 0 ); + hotspot[2] = hotspot[0] + Vector( -size, -size, 0 ); + hotspot[3] = hotspot[0] + Vector( 0, -size, 0 ); + break; + default: + return false; + } + + for ( int i=1; iGetEditVectors( &eyePos, &eyeForward ); + + Ray_t ray; + ray.Init( eyePos, eyePos + 10000.0f * eyeForward, vec3_origin, vec3_origin ); + + float dist = IntersectRayWithTriangle( ray, hotspot[0], hotspot[1], hotspot[2], false ); + if ( dist > 0 ) + { + return true; + } + + dist = IntersectRayWithTriangle( ray, hotspot[2], hotspot[3], hotspot[0], false ); + if ( dist > 0 ) + { + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +NavCornerType CNavArea::GetCornerUnderCursor( void ) const +{ + Vector eyePos, eyeForward; + TheNavMesh->GetEditVectors( &eyePos, &eyeForward ); + + for ( int i=0; iIsEditMode( CNavMesh::PLACE_PAINTING ) ) + { + useAttributeColors = false; + + if ( m_place == UNDEFINED_PLACE ) + { + color = NavNoPlaceColor; + } + else if ( TheNavMesh->GetNavPlace() == m_place ) + { + color = NavSamePlaceColor; + } + else + { + color = NavDifferentPlaceColor; + } + } + else + { + // normal edit mode + if ( this == TheNavMesh->GetMarkedArea() ) + { + useAttributeColors = false; + color = NavMarkedColor; + } + else if ( this == TheNavMesh->GetSelectedArea() ) + { + color = NavSelectedColor; + } + else + { + color = NavNormalColor; + } + } + + if ( IsDegenerate() ) + { + static IntervalTimer blink; + static bool blinkOn = false; + + if (blink.GetElapsedTime() > 1.0f) + { + blink.Reset(); + blinkOn = !blinkOn; + } + + useAttributeColors = false; + + if (blinkOn) + color = NavDegenerateFirstColor; + else + color = NavDegenerateSecondColor; + + NDebugOverlay::Text( GetCenter(), UTIL_VarArgs( "Degenerate area %d", GetID() ), true, DebugDuration ); + } + + Vector nw, ne, sw, se; + + nw = m_nwCorner; + se = m_seCorner; + ne.x = se.x; + ne.y = nw.y; + ne.z = m_neZ; + sw.x = nw.x; + sw.y = se.y; + sw.z = m_swZ; + + if ( nav_show_light_intensity.GetBool() ) + { + for ( int i=0; i 0 ) + { + const Vector offset( 0, 0, 0.8f ); + NDebugOverlay::Triangle( nw+offset, se+offset, ne+offset, bgcolor[0], bgcolor[1], bgcolor[2], bgcolor[3], true, DebugDuration ); + NDebugOverlay::Triangle( se+offset, nw+offset, sw+offset, bgcolor[0], bgcolor[1], bgcolor[2], bgcolor[3], true, DebugDuration ); + } + } + + const float inset = 0.2f; + nw.x += inset; + nw.y += inset; + ne.x -= inset; + ne.y += inset; + sw.x += inset; + sw.y -= inset; + se.x -= inset; + se.y -= inset; + + if ( GetAttributes() & NAV_MESH_TRANSIENT ) + { + NavDrawDashedLine( nw, ne, color ); + NavDrawDashedLine( ne, se, color ); + NavDrawDashedLine( se, sw, color ); + NavDrawDashedLine( sw, nw, color ); + } + else + { + NavDrawLine( nw, ne, color ); + NavDrawLine( ne, se, color ); + NavDrawLine( se, sw, color ); + NavDrawLine( sw, nw, color ); + } + + if ( this == TheNavMesh->GetMarkedArea() && TheNavMesh->m_markedCorner != NUM_CORNERS ) + { + Vector p[NUM_CORNERS]; + GetCornerHotspot( TheNavMesh->m_markedCorner, p ); + + NavDrawLine( p[1], p[2], NavMarkedColor ); + NavDrawLine( p[2], p[3], NavMarkedColor ); + } + if ( this != TheNavMesh->GetMarkedArea() && this == TheNavMesh->GetSelectedArea() && TheNavMesh->IsEditMode( CNavMesh::NORMAL ) ) + { + NavCornerType bestCorner = GetCornerUnderCursor(); + + Vector p[NUM_CORNERS]; + if ( GetCornerHotspot( bestCorner, p ) ) + { + NavDrawLine( p[1], p[2], NavSelectedColor ); + NavDrawLine( p[2], p[3], NavSelectedColor ); + } + } + + if (GetAttributes() & NAV_MESH_CROUCH) + { + if ( useAttributeColors ) + color = NavAttributeCrouchColor; + + NavDrawLine( nw, se, color ); + } + + if (GetAttributes() & NAV_MESH_JUMP) + { + if ( useAttributeColors ) + color = NavAttributeJumpColor; + + if ( !(GetAttributes() & NAV_MESH_CROUCH) ) + { + NavDrawLine( nw, se, color ); + } + NavDrawLine( ne, sw, color ); + } + + if (GetAttributes() & NAV_MESH_PRECISE) + { + if ( useAttributeColors ) + color = NavAttributePreciseColor; + + float size = 8.0f; + Vector up( m_center.x, m_center.y - size, m_center.z ); + Vector down( m_center.x, m_center.y + size, m_center.z ); + NavDrawLine( up, down, color ); + + Vector left( m_center.x - size, m_center.y, m_center.z ); + Vector right( m_center.x + size, m_center.y, m_center.z ); + NavDrawLine( left, right, color ); + } + + if (GetAttributes() & NAV_MESH_NO_JUMP) + { + if ( useAttributeColors ) + color = NavAttributeNoJumpColor; + + float size = 8.0f; + Vector up( m_center.x, m_center.y - size, m_center.z ); + Vector down( m_center.x, m_center.y + size, m_center.z ); + Vector left( m_center.x - size, m_center.y, m_center.z ); + Vector right( m_center.x + size, m_center.y, m_center.z ); + NavDrawLine( up, right, color ); + NavDrawLine( right, down, color ); + NavDrawLine( down, left, color ); + NavDrawLine( left, up, color ); + } + + if (GetAttributes() & NAV_MESH_STAIRS) + { + if ( useAttributeColors ) + color = NavAttributeStairColor; + + float northZ = ( GetCorner( NORTH_WEST ).z + GetCorner( NORTH_EAST ).z ) / 2.0f; + float southZ = ( GetCorner( SOUTH_WEST ).z + GetCorner( SOUTH_EAST ).z ) / 2.0f; + float westZ = ( GetCorner( NORTH_WEST ).z + GetCorner( SOUTH_WEST ).z ) / 2.0f; + float eastZ = ( GetCorner( NORTH_EAST ).z + GetCorner( SOUTH_EAST ).z ) / 2.0f; + + float deltaEastWest = abs( westZ - eastZ ); + float deltaNorthSouth = abs( northZ - southZ ); + + float stepSize = StepHeight / 2.0f; + float t; + + if ( deltaEastWest > deltaNorthSouth ) + { + float inc = stepSize / GetSizeX(); + + for( t = 0.0f; t <= 1.0f; t += inc ) + { + float x = m_nwCorner.x + t * GetSizeX(); + + NavDrawLine( Vector( x, m_nwCorner.y, GetZ( x, m_nwCorner.y ) ), + Vector( x, m_seCorner.y, GetZ( x, m_seCorner.y ) ), + color ); + } + } + else + { + float inc = stepSize / GetSizeY(); + + for( t = 0.0f; t <= 1.0f; t += inc ) + { + float y = m_nwCorner.y + t * GetSizeY(); + + NavDrawLine( Vector( m_nwCorner.x, y, GetZ( m_nwCorner.x, y ) ), + Vector( m_seCorner.x, y, GetZ( m_seCorner.x, y ) ), + color ); + } + } + } + + // Stop is represented by an octagon + if (GetAttributes() & NAV_MESH_STOP) + { + if ( useAttributeColors ) + color = NavAttributeStopColor; + + float dist = 8.0f; + float length = dist/2.5f; + Vector start, end; + + start = m_center + Vector( dist, -length, 0 ); + end = m_center + Vector( dist, length, 0 ); + NavDrawLine( start, end, color ); + + start = m_center + Vector( dist, length, 0 ); + end = m_center + Vector( length, dist, 0 ); + NavDrawLine( start, end, color ); + + start = m_center + Vector( -dist, -length, 0 ); + end = m_center + Vector( -dist, length, 0 ); + NavDrawLine( start, end, color ); + + start = m_center + Vector( -dist, length, 0 ); + end = m_center + Vector( -length, dist, 0 ); + NavDrawLine( start, end, color ); + + start = m_center + Vector( -length, dist, 0 ); + end = m_center + Vector( length, dist, 0 ); + NavDrawLine( start, end, color ); + + start = m_center + Vector( -dist, -length, 0 ); + end = m_center + Vector( -length, -dist, 0 ); + NavDrawLine( start, end, color ); + + start = m_center + Vector( -length, -dist, 0 ); + end = m_center + Vector( length, -dist, 0 ); + NavDrawLine( start, end, color ); + + start = m_center + Vector( length, -dist, 0 ); + end = m_center + Vector( dist, -length, 0 ); + NavDrawLine( start, end, color ); + } + + // Walk is represented by an arrow + if (GetAttributes() & NAV_MESH_WALK) + { + if ( useAttributeColors ) + color = NavAttributeWalkColor; + + float size = 8.0f; + NavDrawHorizontalArrow( m_center + Vector( -size, 0, 0 ), m_center + Vector( size, 0, 0 ), 4, color ); + } + + // Walk is represented by a double arrow + if (GetAttributes() & NAV_MESH_RUN) + { + if ( useAttributeColors ) + color = NavAttributeRunColor; + + float size = 8.0f; + float dist = 4.0f; + NavDrawHorizontalArrow( m_center + Vector( -size, dist, 0 ), m_center + Vector( size, dist, 0 ), 4, color ); + NavDrawHorizontalArrow( m_center + Vector( -size, -dist, 0 ), m_center + Vector( size, -dist, 0 ), 4, color ); + } + + // Avoid is represented by an exclamation point + if (GetAttributes() & NAV_MESH_AVOID) + { + if ( useAttributeColors ) + color = NavAttributeAvoidColor; + + float topHeight = 8.0f; + float topWidth = 3.0f; + float bottomHeight = 3.0f; + float bottomWidth = 2.0f; + NavDrawTriangle( m_center, m_center + Vector( -topWidth, topHeight, 0 ), m_center + Vector( +topWidth, topHeight, 0 ), color ); + NavDrawTriangle( m_center + Vector( 0, -bottomHeight, 0 ), m_center + Vector( -bottomWidth, -bottomHeight*2, 0 ), m_center + Vector( bottomWidth, -bottomHeight*2, 0 ), color ); + } + + if ( IsBlocked( TEAM_ANY ) || HasAvoidanceObstacle() || IsDamaging() ) + { + NavEditColor color = (IsBlocked( TEAM_ANY ) && ( m_attributeFlags & NAV_MESH_NAV_BLOCKER ) ) ? NavBlockedByFuncNavBlockerColor : NavBlockedByDoorColor; + const float blockedInset = 4.0f; + nw.x += blockedInset; + nw.y += blockedInset; + ne.x -= blockedInset; + ne.y += blockedInset; + sw.x += blockedInset; + sw.y -= blockedInset; + se.x -= blockedInset; + se.y -= blockedInset; + NavDrawLine( nw, ne, color ); + NavDrawLine( ne, se, color ); + NavDrawLine( se, sw, color ); + NavDrawLine( sw, nw, color ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Draw area as a filled rect of the given color + */ +void CNavArea::DrawFilled( int r, int g, int b, int a, float deltaT, bool noDepthTest, float margin ) const +{ + Vector nw = GetCorner( NORTH_WEST ) + Vector( margin, margin, 0.0f ); + Vector ne = GetCorner( NORTH_EAST ) + Vector( -margin, margin, 0.0f ); + Vector sw = GetCorner( SOUTH_WEST ) + Vector( margin, -margin, 0.0f ); + Vector se = GetCorner( SOUTH_EAST ) + Vector( -margin, -margin, 0.0f ); + + if ( a == 0 ) + { + NDebugOverlay::Line( nw, ne, r, g, b, true, deltaT ); + NDebugOverlay::Line( nw, sw, r, g, b, true, deltaT ); + NDebugOverlay::Line( sw, se, r, g, b, true, deltaT ); + NDebugOverlay::Line( se, ne, r, g, b, true, deltaT ); + } + else + { + NDebugOverlay::Triangle( nw, se, ne, r, g, b, a, noDepthTest, deltaT ); + NDebugOverlay::Triangle( se, nw, sw, r, g, b, a, noDepthTest, deltaT ); + } + + // backside +// NDebugOverlay::Triangle( nw, ne, se, r, g, b, a, noDepthTest, deltaT ); +// NDebugOverlay::Triangle( se, sw, nw, r, g, b, a, noDepthTest, deltaT ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavArea::DrawSelectedSet( const Vector &shift ) const +{ + const float deltaT = NDEBUG_PERSIST_TILL_NEXT_SERVER; + int r = s_selectedSetColor.r(); + int g = s_selectedSetColor.g(); + int b = s_selectedSetColor.b(); + int a = s_selectedSetColor.a(); + + Vector nw = GetCorner( NORTH_WEST ) + shift; + Vector ne = GetCorner( NORTH_EAST ) + shift; + Vector sw = GetCorner( SOUTH_WEST ) + shift; + Vector se = GetCorner( SOUTH_EAST ) + shift; + + NDebugOverlay::Triangle( nw, se, ne, r, g, b, a, true, deltaT ); + NDebugOverlay::Triangle( se, nw, sw, r, g, b, a, true, deltaT ); + + r = s_selectedSetBorderColor.r(); + g = s_selectedSetBorderColor.g(); + b = s_selectedSetBorderColor.b(); + NDebugOverlay::Line( nw, ne, r, g, b, true, deltaT ); + NDebugOverlay::Line( nw, sw, r, g, b, true, deltaT ); + NDebugOverlay::Line( sw, se, r, g, b, true, deltaT ); + NDebugOverlay::Line( se, ne, r, g, b, true, deltaT ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavArea::DrawDragSelectionSet( Color &dragSelectionSetColor ) const +{ + const float deltaT = NDEBUG_PERSIST_TILL_NEXT_SERVER; + int r = dragSelectionSetColor.r(); + int g = dragSelectionSetColor.g(); + int b = dragSelectionSetColor.b(); + int a = dragSelectionSetColor.a(); + + Vector nw = GetCorner( NORTH_WEST ); + Vector ne = GetCorner( NORTH_EAST ); + Vector sw = GetCorner( SOUTH_WEST ); + Vector se = GetCorner( SOUTH_EAST ); + + NDebugOverlay::Triangle( nw, se, ne, r, g, b, a, true, deltaT ); + NDebugOverlay::Triangle( se, nw, sw, r, g, b, a, true, deltaT ); + + r = s_dragSelectionSetBorderColor.r(); + g = s_dragSelectionSetBorderColor.g(); + b = s_dragSelectionSetBorderColor.b(); + NDebugOverlay::Line( nw, ne, r, g, b, true, deltaT ); + NDebugOverlay::Line( nw, sw, r, g, b, true, deltaT ); + NDebugOverlay::Line( sw, se, r, g, b, true, deltaT ); + NDebugOverlay::Line( se, ne, r, g, b, true, deltaT ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw navigation areas and edit them + */ +void CNavArea::DrawHidingSpots( void ) const +{ + const HidingSpotVector *hidingSpots = GetHidingSpots(); + + FOR_EACH_VEC( (*hidingSpots), it ) + { + const HidingSpot *spot = (*hidingSpots)[ it ]; + + NavEditColor color; + + if (spot->IsIdealSniperSpot()) + { + color = NavIdealSniperColor; + } + else if (spot->IsGoodSniperSpot()) + { + color = NavGoodSniperColor; + } + else if (spot->HasGoodCover()) + { + color = NavGoodCoverColor; + } + else + { + color = NavExposedColor; + } + + NavDrawLine( spot->GetPosition(), spot->GetPosition() + Vector( 0, 0, 50 ), color ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw ourselves and adjacent areas + */ +void CNavArea::DrawConnectedAreas( void ) const +{ + int i; + + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + // draw self + if (TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING )) + { + Draw(); + } + else + { + Draw(); + DrawHidingSpots(); + } + + // draw connected ladders + { + FOR_EACH_VEC( m_ladder[ CNavLadder::LADDER_UP ], it ) + { + CNavLadder *ladder = m_ladder[ CNavLadder::LADDER_UP ][ it ].ladder; + + ladder->DrawLadder(); + + if ( !ladder->IsConnected( this, CNavLadder::LADDER_DOWN ) ) + { + NavDrawLine( m_center, ladder->m_bottom + Vector( 0, 0, GenerationStepSize ), NavConnectedOneWayColor ); + } + } + } + { + FOR_EACH_VEC( m_ladder[ CNavLadder::LADDER_DOWN ], it ) + { + CNavLadder *ladder = m_ladder[ CNavLadder::LADDER_DOWN ][ it ].ladder; + + ladder->DrawLadder(); + + if ( !ladder->IsConnected( this, CNavLadder::LADDER_UP ) ) + { + NavDrawLine( m_center, ladder->m_top, NavConnectedOneWayColor ); + } + } + } + + // draw connected areas + for( i=0; iDraw(); + + if ( !TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING ) ) + { + adj->DrawHidingSpots(); + + Vector from, to; + Vector hookPos; + float halfWidth; + float size = 5.0f; + ComputePortal( adj, dir, &hookPos, &halfWidth ); + + switch( dir ) + { + case NORTH: + from = hookPos + Vector( 0.0f, size, 0.0f ); + to = hookPos + Vector( 0.0f, -size, 0.0f ); + break; + case SOUTH: + from = hookPos + Vector( 0.0f, -size, 0.0f ); + to = hookPos + Vector( 0.0f, size, 0.0f ); + break; + case EAST: + from = hookPos + Vector( -size, 0.0f, 0.0f ); + to = hookPos + Vector( +size, 0.0f, 0.0f ); + break; + case WEST: + from = hookPos + Vector( size, 0.0f, 0.0f ); + to = hookPos + Vector( -size, 0.0f, 0.0f ); + break; + } + + from.z = GetZ( from ); + to.z = adj->GetZ( to ); + + Vector drawTo; + adj->GetClosestPointOnArea( to, &drawTo ); + + if ( nav_show_contiguous.GetBool() ) + { + if ( IsContiguous( adj ) ) + NavDrawLine( from, drawTo, NavConnectedContiguous ); + else + NavDrawLine( from, drawTo, NavConnectedNonContiguous ); + } + else + { + if ( adj->IsConnected( this, OppositeDirection( dir ) ) ) + NavDrawLine( from, drawTo, NavConnectedTwoWaysColor ); + else + NavDrawLine( from, drawTo, NavConnectedOneWayColor ); + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add to open list in decreasing value order + */ +void CNavArea::AddToOpenList( void ) +{ + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); + + if ( IsOpen() ) + { + // already on list + return; + } + + // mark as being on open list for quick check + m_openMarker = m_masterMarker; + + // if list is empty, add and return + if ( m_openList == NULL ) + { + m_openList = this; + m_openListTail = this; + this->m_prevOpen = NULL; + this->m_nextOpen = NULL; + return; + } + + // insert self in ascending cost order + // Since costs are positive, IEEE754 let's us compare as integers (see http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm) + CNavArea *area, *last = NULL; + int thisCostBits = *reinterpret_cast(&m_totalCost); + + Assert ( m_totalCost >= 0.0f ); + for( area = m_openList; area; area = area->m_nextOpen ) + { + Assert ( area->GetTotalCost() >= 0.0f ); + int thoseCostBits = *reinterpret_cast(&area->m_totalCost); + if ( thisCostBits < thoseCostBits ) + { + break; + } + last = area; + } + + if ( area ) + { + // insert before this area + this->m_prevOpen = area->m_prevOpen; + + if ( this->m_prevOpen ) + { + this->m_prevOpen->m_nextOpen = this; + } + else + { + m_openList = this; + } + + this->m_nextOpen = area; + area->m_prevOpen = this; + } + else + { + // append to end of list + last->m_nextOpen = this; + this->m_prevOpen = last; + + this->m_nextOpen = NULL; + + m_openListTail = this; + } + + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add to tail of the open list + */ +void CNavArea::AddToOpenListTail( void ) +{ + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); + + if ( IsOpen() ) + { + // already on list + return; + } + + // mark as being on open list for quick check + m_openMarker = m_masterMarker; + + // if list is empty, add and return + if ( m_openList == NULL ) + { + m_openList = this; + m_openListTail = this; + this->m_prevOpen = NULL; + this->m_nextOpen = NULL; + + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); + return; + } + + // append to end of list + m_openListTail->m_nextOpen = this; + + this->m_prevOpen = m_openListTail; + this->m_nextOpen = NULL; + + m_openListTail = this; + + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * A smaller value has been found, update this area on the open list + * @todo "bubbling" does unnecessary work, since the order of all other nodes will be unchanged - only this node is altered + */ +void CNavArea::UpdateOnOpenList( void ) +{ + // since value can only decrease, bubble this area up from current spot + while( m_prevOpen && this->GetTotalCost() < m_prevOpen->GetTotalCost() ) + { + // swap position with predecessor + CNavArea *other = m_prevOpen; + CNavArea *before = other->m_prevOpen; + CNavArea *after = this->m_nextOpen; + + this->m_nextOpen = other; + this->m_prevOpen = before; + + other->m_prevOpen = this; + other->m_nextOpen = after; + + if ( before ) + { + before->m_nextOpen = this; + } + else + { + m_openList = this; + } + + if ( after ) + { + after->m_prevOpen = other; + } + else + { + m_openListTail = this; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::RemoveFromOpenList( void ) +{ + if ( m_openMarker == 0 ) + { + // not on the list + return; + } + + if ( m_prevOpen ) + { + m_prevOpen->m_nextOpen = m_nextOpen; + } + else + { + m_openList = m_nextOpen; + } + + if ( m_nextOpen ) + { + m_nextOpen->m_prevOpen = m_prevOpen; + } + else + { + m_openListTail = m_prevOpen; + } + + // zero is an invalid marker + m_openMarker = 0; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Clears the open and closed lists for a new search + */ +void CNavArea::ClearSearchLists( void ) +{ + // effectively clears all open list pointers and closed flags + CNavArea::MakeNewMarker(); + + m_openList = NULL; + m_openListTail = NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::SetCorner( NavCornerType corner, const Vector& newPosition ) +{ + switch( corner ) + { + case NORTH_WEST: + m_nwCorner = newPosition; + break; + + case NORTH_EAST: + m_seCorner.x = newPosition.x; + m_nwCorner.y = newPosition.y; + m_neZ = newPosition.z; + break; + + case SOUTH_WEST: + m_nwCorner.x = newPosition.x; + m_seCorner.y = newPosition.y; + m_swZ = newPosition.z; + break; + + case SOUTH_EAST: + m_seCorner = newPosition; + break; + + default: + { + Vector oldPosition = GetCenter(); + Vector delta = newPosition - oldPosition; + m_nwCorner += delta; + m_seCorner += delta; + m_neZ += delta.z; + m_swZ += delta.z; + } + } + + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + } + + CalcDebugID(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if an existing hiding spot is too close to given position + */ +bool CNavArea::IsHidingSpotCollision( const Vector &pos ) const +{ + const float collisionRange = 30.0f; + + FOR_EACH_VEC( m_hidingSpots, it ) + { + const HidingSpot *spot = m_hidingSpots[ it ]; + + if ((spot->GetPosition() - pos).IsLengthLessThan( collisionRange )) + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +bool IsHidingSpotInCover( const Vector &spot ) +{ + int coverCount = 0; + trace_t result; + + Vector from = spot; + from.z += HalfHumanHeight; + + Vector to; + + // if we are crouched underneath something, that counts as good cover + to = from + Vector( 0, 0, 20.0f ); + UTIL_TraceLine( from, to, MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction != 1.0f) + return true; + + const float coverRange = 100.0f; + const float inc = M_PI / 8.0f; + + for( float angle = 0.0f; angle < 2.0f * M_PI; angle += inc ) + { + to = from + Vector( coverRange * (float)cos(angle), coverRange * (float)sin(angle), HalfHumanHeight ); + + UTIL_TraceLine( from, to, MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + + // if traceline hit something, it hit "cover" + if (result.fraction != 1.0f) + ++coverCount; + } + + // if more than half of the circle has no cover, the spot is not "in cover" + const int halfCover = 8; + if (coverCount < halfCover) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Finds the hiding spot position in a corner's area. If the typical inset is off the nav area (small + * hand-constructed areas), it tries to fit the position inside the area. + */ +static Vector FindPositionInArea( CNavArea *area, NavCornerType corner ) +{ + int multX = 1, multY = 1; + switch ( corner ) + { + case NORTH_WEST: + break; + case NORTH_EAST: + multX = -1; + break; + case SOUTH_WEST: + multY = -1; + break; + case SOUTH_EAST: + multX = -1; + multY = -1; + break; + } + + const float offset = 12.5f; + Vector cornerPos = area->GetCorner( corner ); + + // Try the basic inset + Vector pos = cornerPos + Vector( offset*multX, offset*multY, 0.0f ); + if ( !area->IsOverlapping( pos ) ) + { + // Try pulling the Y offset to the area's center + pos = cornerPos + Vector( offset*multX, area->GetSizeY()*0.5f*multY, 0.0f ); + if ( !area->IsOverlapping( pos ) ) + { + // Try pulling the X offset to the area's center + pos = cornerPos + Vector( area->GetSizeX()*0.5f*multX, offset*multY, 0.0f ); + if ( !area->IsOverlapping( pos ) ) + { + // Try pulling the X and Y offsets to the area's center + pos = cornerPos + Vector( area->GetSizeX()*0.5f*multX, area->GetSizeY()*0.5f*multY, 0.0f ); + if ( !area->IsOverlapping( pos ) ) + { + AssertMsg( false, UTIL_VarArgs( "A Hiding Spot can't be placed on its area at (%.0f %.0f %.0f)", cornerPos.x, cornerPos.y, cornerPos.z) ); + + // Just pull the position to a small offset + pos = cornerPos + Vector( 1.0f*multX, 1.0f*multY, 0.0f ); + if ( !area->IsOverlapping( pos ) ) + { + // Nothing is working (degenerate area?), so just put it directly on the corner + pos = cornerPos; + } + } + } + } + } + + return pos; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Analyze local area neighborhood to find "hiding spots" for this area + */ +void CNavArea::ComputeHidingSpots( void ) +{ + struct + { + float lo, hi; + } + extent; + + m_hidingSpots.PurgeAndDeleteElements(); + + + // "jump areas" cannot have hiding spots + if ( GetAttributes() & NAV_MESH_JUMP ) + return; + + // "don't hide areas" cannot have hiding spots + if ( GetAttributes() & NAV_MESH_DONT_HIDE ) + return; + + int cornerCount[NUM_CORNERS]; + for( int i=0; iIsConnected( this, OppositeDirection( static_cast( d ) ) ) == false) + continue; + + // ignore jump areas + if (connect.area->GetAttributes() & NAV_MESH_JUMP) + continue; + + if (isHoriz) + { + if (connect.area->m_nwCorner.x < extent.lo) + extent.lo = connect.area->m_nwCorner.x; + + if (connect.area->m_seCorner.x > extent.hi) + extent.hi = connect.area->m_seCorner.x; + } + else + { + if (connect.area->m_nwCorner.y < extent.lo) + extent.lo = connect.area->m_nwCorner.y; + + if (connect.area->m_seCorner.y > extent.hi) + extent.hi = connect.area->m_seCorner.y; + } + } + + switch( d ) + { + case NORTH: + if (extent.lo - m_nwCorner.x >= cornerSize) + ++cornerCount[ NORTH_WEST ]; + + if (m_seCorner.x - extent.hi >= cornerSize) + ++cornerCount[ NORTH_EAST ]; + break; + + case SOUTH: + if (extent.lo - m_nwCorner.x >= cornerSize) + ++cornerCount[ SOUTH_WEST ]; + + if (m_seCorner.x - extent.hi >= cornerSize) + ++cornerCount[ SOUTH_EAST ]; + break; + + case EAST: + if (extent.lo - m_nwCorner.y >= cornerSize) + ++cornerCount[ NORTH_EAST ]; + + if (m_seCorner.y - extent.hi >= cornerSize) + ++cornerCount[ SOUTH_EAST ]; + break; + + case WEST: + if (extent.lo - m_nwCorner.y >= cornerSize) + ++cornerCount[ NORTH_WEST ]; + + if (m_seCorner.y - extent.hi >= cornerSize) + ++cornerCount[ SOUTH_WEST ]; + break; + } + } + + for ( int c=0; cCreateHidingSpot(); + spot->SetPosition( pos ); + spot->SetFlags( IsHidingSpotInCover( pos ) ? HidingSpot::IN_COVER : HidingSpot::EXPOSED ); + m_hidingSpots.AddToTail( spot ); + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine how much walkable area we can see from the spot, and how far away we can see. + */ +void ClassifySniperSpot( HidingSpot *spot ) +{ + Vector eye = spot->GetPosition(); + + CNavArea *hidingArea = TheNavMesh->GetNavArea( spot->GetPosition() ); + if (hidingArea && (hidingArea->GetAttributes() & NAV_MESH_STAND)) + { + // we will be standing at this hiding spot + eye.z += HumanEyeHeight; + } + else + { + // we are crouching when at this hiding spot + eye.z += HumanCrouchEyeHeight; + } + + Vector walkable; + trace_t result; + + Extent sniperExtent; + float farthestRangeSq = 0.0f; + const float minSniperRangeSq = 1000.0f * 1000.0f; + bool found = false; + + // to make compiler stop warning me + sniperExtent.lo = Vector( 0.0f, 0.0f, 0.0f ); + sniperExtent.hi = Vector( 0.0f, 0.0f, 0.0f ); + + Extent areaExtent; + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + area->GetExtent( &areaExtent ); + + // scan this area + for( walkable.y = areaExtent.lo.y + GenerationStepSize/2.0f; walkable.y < areaExtent.hi.y; walkable.y += GenerationStepSize ) + { + for( walkable.x = areaExtent.lo.x + GenerationStepSize/2.0f; walkable.x < areaExtent.hi.x; walkable.x += GenerationStepSize ) + { + walkable.z = area->GetZ( walkable ) + HalfHumanHeight; + + // check line of sight + UTIL_TraceLine( eye, walkable, CONTENTS_SOLID|CONTENTS_MOVEABLE|CONTENTS_PLAYERCLIP, NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f && !result.startsolid) + { + // can see this spot + + // keep track of how far we can see + float rangeSq = (eye - walkable).LengthSqr(); + if (rangeSq > farthestRangeSq) + { + farthestRangeSq = rangeSq; + + if (rangeSq >= minSniperRangeSq) + { + // this is a sniper spot + // determine how good of a sniper spot it is by keeping track of the snipable area + if (found) + { + if (walkable.x < sniperExtent.lo.x) + sniperExtent.lo.x = walkable.x; + if (walkable.x > sniperExtent.hi.x) + sniperExtent.hi.x = walkable.x; + + if (walkable.y < sniperExtent.lo.y) + sniperExtent.lo.y = walkable.y; + if (walkable.y > sniperExtent.hi.y) + sniperExtent.hi.y = walkable.y; + } + else + { + sniperExtent.lo = walkable; + sniperExtent.hi = walkable; + found = true; + } + } + } + } + } + } + } + + if (found) + { + // if we can see a large snipable area, it is an "ideal" spot + float snipableArea = sniperExtent.Area(); + + const float minIdealSniperArea = 200.0f * 200.0f; + const float longSniperRangeSq = 1500.0f * 1500.0f; + + if (snipableArea >= minIdealSniperArea || farthestRangeSq >= longSniperRangeSq) + spot->m_flags |= HidingSpot::IDEAL_SNIPER_SPOT; + else + spot->m_flags |= HidingSpot::GOOD_SNIPER_SPOT; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Analyze local area neighborhood to find "sniper spots" for this area + */ +void CNavArea::ComputeSniperSpots( void ) +{ + if (nav_quicksave.GetBool()) + return; + + FOR_EACH_VEC( m_hidingSpots, it ) + { + HidingSpot *spot = m_hidingSpots[ it ]; + + ClassifySniperSpot( spot ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given the areas we are moving between, return the spots we will encounter + */ +SpotEncounter *CNavArea::GetSpotEncounter( const CNavArea *from, const CNavArea *to ) +{ + if (from && to) + { + SpotEncounter *e; + + FOR_EACH_VEC( m_spotEncounters, it ) + { + e = m_spotEncounters[ it ]; + + if (e->from.area == from && e->to.area == to) + return e; + } + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add spot encounter data when moving from area to area + */ +void CNavArea::AddSpotEncounters( const CNavArea *from, NavDirType fromDir, const CNavArea *to, NavDirType toDir ) +{ + SpotEncounter *e = new SpotEncounter; + + e->from.area = const_cast( from ); + e->fromDir = fromDir; + + e->to.area = const_cast( to ); + e->toDir = toDir; + + float halfWidth; + ComputePortal( to, toDir, &e->path.to, &halfWidth ); + ComputePortal( from, fromDir, &e->path.from, &halfWidth ); + + const float eyeHeight = HumanEyeHeight; + e->path.from.z = from->GetZ( e->path.from ) + eyeHeight; + e->path.to.z = to->GetZ( e->path.to ) + eyeHeight; + + // step along ray and track which spots can be seen + Vector dir = e->path.to - e->path.from; + float length = dir.NormalizeInPlace(); + + // create unique marker to flag used spots + HidingSpot::ChangeMasterMarker(); + + const float stepSize = 25.0f; // 50 + const float seeSpotRange = 2000.0f; // 3000 + trace_t result; + + Vector eye, delta; + HidingSpot *spot; + SpotOrder spotOrder; + + // step along path thru this area + bool done = false; + for( float along = 0.0f; !done; along += stepSize ) + { + // make sure we check the endpoint of the path segment + if (along >= length) + { + along = length; + done = true; + } + + // move the eyepoint along the path segment + eye = e->path.from + along * dir; + + // check each hiding spot for visibility + FOR_EACH_VEC( TheHidingSpots, it ) + { + spot = TheHidingSpots[ it ]; + + // only look at spots with cover (others are out in the open and easily seen) + if (!spot->HasGoodCover()) + continue; + + if (spot->IsMarked()) + continue; + + const Vector &spotPos = spot->GetPosition(); + + delta.x = spotPos.x - eye.x; + delta.y = spotPos.y - eye.y; + delta.z = (spotPos.z + eyeHeight) - eye.z; + + // check if in range + if (delta.IsLengthGreaterThan( seeSpotRange )) + continue; + + // check if we have LOS + // BOTPORT: ignore glass here + UTIL_TraceLine( eye, Vector( spotPos.x, spotPos.y, spotPos.z + HalfHumanHeight ), MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction != 1.0f) + continue; + + // if spot is in front of us along our path, ignore it + delta.NormalizeInPlace(); + float dot = DotProduct( dir, delta ); + if (dot < 0.7071f && dot > -0.7071f) + { + // we only want to keep spots that BECOME visible as we walk past them + // therefore, skip ALL visible spots at the start of the path segment + if (along > 0.0f) + { + // add spot to encounter + spotOrder.spot = spot; + spotOrder.t = along/length; + e->spots.AddToTail( spotOrder ); + } + } + + // mark spot as encountered + spot->Mark(); + } + } + + // add encounter to list + m_spotEncounters.AddToTail( e ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute "spot encounter" data. This is an ordered list of spots to look at + * for each possible path thru a nav area. + */ +void CNavArea::ComputeSpotEncounters( void ) +{ + m_spotEncounters.RemoveAll(); + + if (nav_quicksave.GetBool()) + return; + + // for each adjacent area + for( int fromDir=0; fromDirarea, (NavDirType)fromDir, toCon->area, (NavDirType)toDir ); + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Decay the danger values + */ +void CNavArea::DecayDanger( void ) +{ + for( int i=0; icurtime - m_dangerTimestamp[i]; + float decayAmount = GetDangerDecayRate() * deltaT; + + m_danger[i] -= decayAmount; + if (m_danger[i] < 0.0f) + m_danger[i] = 0.0f; + + // update timestamp + m_dangerTimestamp[i] = gpGlobals->curtime; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Increase the danger of this area for the given team + */ +void CNavArea::IncreaseDanger( int teamID, float amount ) +{ + // before we add the new value, decay what's there + DecayDanger(); + + int teamIdx = teamID % MAX_NAV_TEAMS; + + m_danger[ teamIdx ] += amount; + m_dangerTimestamp[ teamIdx ] = gpGlobals->curtime; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the danger of this area (decays over time) + */ +float CNavArea::GetDanger( int teamID ) +{ + DecayDanger(); + + int teamIdx = teamID % MAX_NAV_TEAMS; + return m_danger[ teamIdx ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns a 0..1 light intensity for the given point + */ +float CNavArea::GetLightIntensity( const Vector &pos ) const +{ + Vector testPos; + testPos.x = clamp( pos.x, m_nwCorner.x, m_seCorner.x ); + testPos.y = clamp( pos.y, m_nwCorner.y, m_seCorner.y ); + testPos.z = pos.z; + + float dX = (testPos.x - m_nwCorner.x) / (m_seCorner.x - m_nwCorner.x); + float dY = (testPos.y - m_nwCorner.y) / (m_seCorner.y - m_nwCorner.y); + + float northLight = m_lightIntensity[ NORTH_WEST ] * ( 1 - dX ) + m_lightIntensity[ NORTH_EAST ] * dX; + float southLight = m_lightIntensity[ SOUTH_WEST ] * ( 1 - dX ) + m_lightIntensity[ SOUTH_EAST ] * dX; + float light = northLight * ( 1 - dY ) + southLight * dY; + + return light; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns a 0..1 light intensity for the given point + */ +float CNavArea::GetLightIntensity( float x, float y ) const +{ + return GetLightIntensity( Vector( x, y, 0 ) ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns a 0..1 light intensity averaged over the whole area + */ +float CNavArea::GetLightIntensity( void ) const +{ + float light = m_lightIntensity[ NORTH_WEST ]; + light += m_lightIntensity[ NORTH_EAST ]; + light += m_lightIntensity[ SOUTH_WEST]; + light += m_lightIntensity[ SOUTH_EAST ]; + return light / 4.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute light intensity at corners and center (requires client via listenserver) + */ +bool CNavArea::ComputeLighting( void ) +{ + if ( engine->IsDedicatedServer() ) + { + for ( int i=0; iGetGroundHeight( pos, &height ) ) + { + pos.z = height + HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings + } + + Vector light( 0, 0, 0 ); + // FIXMEL4DTOMAINMERGE + //if ( !engine->GetLightForPointListenServerOnly( pos, false, &light ) ) + //{ + //NDebugOverlay::Line( pos, pos + Vector( 0, 0, -100 ), 255, 0, 0, false, 100.0f ); + // return false; + //} + + Vector ambientColor; + // FIXMEL4DTOMAINMERGE + //if ( !GetTerrainAmbientLightAtPoint( pos, &ambientColor ) ) + { + //NDebugOverlay::Line( pos, pos + Vector( 0, 0, -100 ), 255, 127, 0, false, 100.0f ); + return false; + } + + //NDebugOverlay::Line( pos, pos + Vector( 0, 0, -100 ), 0, 255, 127, false, 100.0f ); + + float ambientIntensity = ambientColor.x + ambientColor.y + ambientColor.z; + float lightIntensity = light.x + light.y + light.z; + lightIntensity = clamp( lightIntensity, 0.f, 1.f ); // sum can go well over 1.0, but it's the lower region we care about. if it's bright, we don't need to know *how* bright. + + lightIntensity = MAX( lightIntensity, ambientIntensity ); + + m_lightIntensity[i] = lightIntensity; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_update_lighting, "Recomputes lighting values", FCVAR_CHEAT ) +{ + int numComputed = 0; + if ( args.ArgC() == 2 ) + { + int areaID = atoi( args[1] ); + CNavArea *area = TheNavMesh->GetNavAreaByID( areaID ); + if ( area ) + { + if ( area->ComputeLighting() ) + { + ++numComputed; + } + } + } + else + { + FOR_EACH_VEC( TheNavAreas, index ) + { + CNavArea *area = TheNavAreas[ index ]; + if ( area->ComputeLighting() ) + { + ++numComputed; + } + } + } + DevMsg( "Computed lighting for %d/%d areas\n", numComputed, TheNavAreas.Count() ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Raise/lower a corner + */ +void CNavArea::RaiseCorner( NavCornerType corner, int amount, bool raiseAdjacentCorners ) +{ + if ( corner == NUM_CORNERS ) + { + RaiseCorner( NORTH_WEST, amount, raiseAdjacentCorners ); + RaiseCorner( NORTH_EAST, amount, raiseAdjacentCorners ); + RaiseCorner( SOUTH_WEST, amount, raiseAdjacentCorners ); + RaiseCorner( SOUTH_EAST, amount, raiseAdjacentCorners ); + return; + } + + // Move the corner + switch (corner) + { + case NORTH_WEST: + m_nwCorner.z += amount; + break; + case NORTH_EAST: + m_neZ += amount; + break; + case SOUTH_WEST: + m_swZ += amount; + break; + case SOUTH_EAST: + m_seCorner.z += amount; + break; + } + + // Recompute the center + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + } + + if ( !raiseAdjacentCorners || nav_corner_adjust_adjacent.GetFloat() <= 0.0f ) + { + return; + } + + // Find nearby areas that share the corner + CNavArea::MakeNewMarker(); + Mark(); + + const float tolerance = nav_corner_adjust_adjacent.GetFloat(); + + Vector cornerPos = GetCorner( corner ); + cornerPos.z -= amount; // use the pre-adjustment corner for adjacency checks + + int gridX = TheNavMesh->WorldToGridX( cornerPos.x ); + int gridY = TheNavMesh->WorldToGridY( cornerPos.y ); + + const int shift = 1; // try a 3x3 set of grids in case we're on the edge + + for( int x = gridX - shift; x <= gridX + shift; ++x ) + { + if (x < 0 || x >= TheNavMesh->m_gridSizeX) + continue; + + for( int y = gridY - shift; y <= gridY + shift; ++y ) + { + if (y < 0 || y >= TheNavMesh->m_gridSizeY) + continue; + + NavAreaVector *areas = &TheNavMesh->m_grid[ x + y*TheNavMesh->m_gridSizeX ]; + + // find closest area in this cell + FOR_EACH_VEC( (*areas), it ) + { + CNavArea *area = (*areas)[ it ]; + + // skip if we've already visited this area + if (area->IsMarked()) + continue; + + area->Mark(); + + Vector areaPos; + for ( int i=0; iGetCorner( NavCornerType(i) ); + if ( areaPos.DistTo( cornerPos ) < tolerance ) + { + float heightDiff = (cornerPos.z + amount ) - areaPos.z; + area->RaiseCorner( NavCornerType(i), heightDiff, false ); + } + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * FindGroundZFromPoint walks from the start position to the end position in GenerationStepSize increments, + * checking the ground height along the way. + */ +float FindGroundZFromPoint( const Vector& end, const Vector& start ) +{ + Vector step( 0, 0, StepHeight ); + if ( fabs( end.x - start.x ) > fabs( end.y - start.y ) ) + { + step.x = GenerationStepSize; + if ( end.x < start.x ) + { + step.x = -step.x; + } + } + else + { + step.y = GenerationStepSize; + if ( end.y < start.y ) + { + step.y = -step.y; + } + } + + // step towards our end point + Vector point = start; + float z; + while ( point.AsVector2D().DistTo( end.AsVector2D() ) > GenerationStepSize ) + { + point = point + step; + z = point.z; + if ( TheNavMesh->GetGroundHeight( point, &z ) ) + { + point.z = z; + } + else + { + point.z -= step.z; + } + } + + // now do the exact one once we're within GenerationStepSize of it + z = point.z + step.z; + point = end; + point.z = z; + if ( TheNavMesh->GetGroundHeight( point, &z ) ) + { + point.z = z; + } + else + { + point.z -= step.z; + } + + return point.z; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Finds the Z value for a corner given two other corner points. This walks along the edges of the nav area + * in GenerationStepSize increments, to increase accuracy. + */ +float FindGroundZ( const Vector& original, const Vector& corner1, const Vector& corner2 ) +{ + float first = FindGroundZFromPoint( original, corner1 ); + float second = FindGroundZFromPoint( original, corner2 ); + + if ( fabs( first - second ) > StepHeight ) + { + // approaching the point from the two directions didn't agree. Take the one closest to the original z. + if ( fabs( original.z - first ) > fabs( original.z - second ) ) + { + return second; + } + else + { + return first; + } + } + + return first; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Places a corner (or all corners if corner == NUM_CORNERS) on the ground + */ +void CNavArea::PlaceOnGround( NavCornerType corner, float inset ) +{ + trace_t result; + Vector from, to; + + Vector nw = m_nwCorner + Vector ( inset, inset, 0 ); + Vector se = m_seCorner + Vector ( -inset, -inset, 0 ); + Vector ne, sw; + ne.x = se.x; + ne.y = nw.y; + ne.z = m_neZ; + sw.x = nw.x; + sw.y = se.y; + sw.z = m_swZ; + + if ( corner == NORTH_WEST || corner == NUM_CORNERS ) + { + float newZ = FindGroundZ( nw, ne, sw ); + RaiseCorner( NORTH_WEST, newZ - nw.z ); + } + + if ( corner == NORTH_EAST || corner == NUM_CORNERS ) + { + float newZ = FindGroundZ( ne, nw, se ); + RaiseCorner( NORTH_EAST, newZ - ne.z ); + } + + if ( corner == SOUTH_WEST || corner == NUM_CORNERS ) + { + float newZ = FindGroundZ( sw, nw, se ); + RaiseCorner( SOUTH_WEST, newZ - sw.z ); + } + + if ( corner == SOUTH_EAST || corner == NUM_CORNERS ) + { + float newZ = FindGroundZ( se, ne, sw ); + RaiseCorner( SOUTH_EAST, newZ - se.z ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Shift the nav area + */ +void CNavArea::Shift( const Vector &shift ) +{ + m_nwCorner += shift; + m_seCorner += shift; + + m_center += shift; +} + + +//-------------------------------------------------------------------------------------------------------------- +static void CommandNavUpdateBlocked( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( TheNavMesh->GetMarkedArea() ) + { + CNavArea *area = TheNavMesh->GetMarkedArea(); + area->UpdateBlocked( true ); + if ( area->IsBlocked( TEAM_ANY ) ) + { + DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); + } + } + else + { + float start = Plat_FloatTime(); + CNavArea *blockedArea = NULL; + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + area->UpdateBlocked( true ); + if ( area->IsBlocked( TEAM_ANY ) ) + { + DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); + if ( !blockedArea ) + { + blockedArea = area; + } + } + } + + float end = Plat_FloatTime(); + float time = (end - start) * 1000.0f; + DevMsg( "nav_update_blocked took %2.2f ms\n", time ); + + if ( blockedArea ) + { + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player ) + { + if ( ( player->IsDead() || player->IsObserver() ) && player->GetObserverMode() == OBS_MODE_ROAMING ) + { + Vector origin = blockedArea->GetCenter() + Vector( 0, 0, 0.75f * HumanHeight ); + UTIL_SetOrigin( player, origin ); + } + } + } + } +} +static ConCommand nav_update_blocked( "nav_update_blocked", CommandNavUpdateBlocked, "Updates the blocked/unblocked status for every nav area.", FCVAR_GAMEDLL ); + + +//-------------------------------------------------------------------------------------------------------- +bool CNavArea::IsBlocked( int teamID, bool ignoreNavBlockers ) const +{ + if ( ignoreNavBlockers && ( m_attributeFlags & NAV_MESH_NAV_BLOCKER ) ) + { + return false; + } + +#ifdef TERROR + if ( ( teamID == TEAM_SURVIVOR ) && ( m_attributeFlags & CNavArea::NAV_PLAYERCLIP ) ) + return true; +#endif + + if ( teamID == TEAM_ANY ) + { + bool isBlocked = false; + for ( int i=0; iClassMatches( "func_nav_blocker" ) ) + { + m_attributeFlags |= NAV_MESH_NAV_BLOCKER; + } + + bool wasBlocked = false; + if ( teamID == TEAM_ANY ) + { + for ( int i=0; iCreateEvent( "nav_blocked" ); + if ( event ) + { + event->SetInt( "area", m_id ); + event->SetInt( "blocked", 1 ); + gameeventmanager->FireEvent( event ); + } + } + + if ( nav_debug_blocked.GetBool() ) + { + if ( blocker ) + { + ConColorMsg( Color( 0, 255, 128, 255 ), "%s %d blocked area %d\n", blocker->GetDebugName(), blocker->entindex(), GetID() ); + } + else + { + ConColorMsg( Color( 0, 255, 128, 255 ), "non-entity blocked area %d\n", GetID() ); + } + } + TheNavMesh->OnAreaBlocked( this ); + } + else + { + if ( nav_debug_blocked.GetBool() ) + { + if ( blocker ) + { + ConColorMsg( Color( 0, 255, 128, 255 ), "DUPE: %s %d blocked area %d\n", blocker->GetDebugName(), blocker->entindex(), GetID() ); + } + else + { + ConColorMsg( Color( 0, 255, 128, 255 ), "DUPE: non-entity blocked area %d\n", GetID() ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +// checks if any func_nav_blockers are still blocking the area +void CNavArea::UpdateBlockedFromNavBlockers( void ) +{ + VPROF( "CNavArea::UpdateBlockedFromNavBlockers" ); + Extent bounds; + GetExtent( &bounds ); + + // Save off old values, reset to not blocked state + m_attributeFlags &= ~NAV_MESH_NAV_BLOCKER; + bool oldBlocked[MAX_NAV_TEAMS]; + bool wasBlocked = false; + for ( int i=0; iCreateEvent( "nav_blocked" ); + if ( event ) + { + event->SetInt( "area", m_id ); + event->SetInt( "blocked", isBlocked ); + gameeventmanager->FireEvent( event ); + } + + if ( isBlocked ) + { + if ( nav_debug_blocked.GetBool() ) + { + ConColorMsg( Color( 0, 255, 128, 255 ), "area %d is blocked by a nav blocker\n", GetID() ); + } + TheNavMesh->OnAreaBlocked( this ); + } + else + { + if ( nav_debug_blocked.GetBool() ) + { + ConColorMsg( Color( 0, 128, 255, 255 ), "area %d is unblocked by a nav blocker\n", GetID() ); + } + TheNavMesh->OnAreaUnblocked( this ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::UnblockArea( int teamID ) +{ + bool wasBlocked = IsBlocked( teamID ); + + if ( teamID == TEAM_ANY ) + { + for ( int i=0; iCreateEvent( "nav_blocked" ); + if ( event ) + { + event->SetInt( "area", m_id ); + event->SetInt( "blocked", false ); + gameeventmanager->FireEvent( event ); + } + + if ( nav_debug_blocked.GetBool() ) + { + ConColorMsg( Color( 255, 0, 128, 255 ), "area %d is unblocked by UnblockArea\n", GetID() ); + } + TheNavMesh->OnAreaUnblocked( this ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Updates the (un)blocked status of the nav area + * The semantics of this method have gotten very muddled - needs refactoring (MSB 5/7/09) + */ +void CNavArea::UpdateBlocked( bool force, int teamID ) +{ + VPROF( "CNavArea::UpdateBlocked" ); + if ( !force && !m_blockedTimer.IsElapsed() ) + { + return; + } + + const float MaxBlockedCheckInterval = 5; + float interval = m_blockedTimer.GetCountdownDuration() + 1; + if ( interval > MaxBlockedCheckInterval ) + { + interval = MaxBlockedCheckInterval; + } + m_blockedTimer.Start( interval ); + + if ( ( m_attributeFlags & NAV_MESH_NAV_BLOCKER ) ) + { + if ( force ) + { + UpdateBlockedFromNavBlockers(); + } + return; + } + + Vector origin = GetCenter(); + origin.z += HalfHumanHeight; + + const float sizeX = MAX( 1, MIN( GetSizeX()/2 - 5, HalfHumanWidth ) ); + const float sizeY = MAX( 1, MIN( GetSizeY()/2 - 5, HalfHumanWidth ) ); + Extent bounds; + bounds.lo.Init( -sizeX, -sizeY, 0 ); + bounds.hi.Init( sizeX, sizeY, VEC_DUCK_HULL_MAX.z - HalfHumanHeight ); + + bool wasBlocked = IsBlocked( TEAM_ANY ); + + // See if spot is valid +#ifdef TERROR + // don't unblock func_doors + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_PROP_DOORS | WALK_THRU_BREAKABLES ); +#else + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_DOORS | WALK_THRU_BREAKABLES ); +#endif + trace_t tr; + { + VPROF( "CNavArea::UpdateBlocked-Trace" ); + UTIL_TraceHull( + origin, + origin, + bounds.lo, + bounds.hi, + MASK_NPCSOLID_BRUSHONLY, + &filter, + &tr ); + + } + + if ( !tr.startsolid ) + { + // unblock ourself +#ifdef TERROR + extern ConVar DebugZombieBreakables; + if ( DebugZombieBreakables.GetBool() ) +#else + if ( false ) +#endif + + { + NDebugOverlay::Box( origin, bounds.lo, bounds.hi, 0, 255, 0, 10, 5.0f ); + } + else + { + for ( int i=0; iCreateEvent( "nav_blocked" ); + if ( event ) + { + event->SetInt( "area", m_id ); + event->SetInt( "blocked", isBlocked ); + gameeventmanager->FireEvent( event ); + } + + if ( isBlocked ) + { + TheNavMesh->OnAreaBlocked( this ); + } + else + { + TheNavMesh->OnAreaUnblocked( this ); + } + } + + if ( TheNavMesh->GetMarkedArea() == this ) + { + if ( IsBlocked( teamID ) ) + { + NDebugOverlay::Box( origin, bounds.lo, bounds.hi, 255, 0, 0, 64, 3.0f ); + } + else + { + NDebugOverlay::Box( origin, bounds.lo, bounds.hi, 0, 255, 0, 64, 3.0f ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Checks if there is a floor under the nav area, in case a breakable floor is gone + */ +void CNavArea::CheckFloor( CBaseEntity *ignore ) +{ + if ( IsBlocked( TEAM_ANY ) ) + return; + + Vector origin = GetCenter(); + origin.z -= JumpCrouchHeight; + + const float size = GenerationStepSize * 0.5f; + Vector mins = Vector( -size, -size, 0 ); + Vector maxs = Vector( size, size, JumpCrouchHeight + 10.0f ); + + // See if spot is valid + trace_t tr; + UTIL_TraceHull( + origin, + origin, + mins, + maxs, + MASK_NPCSOLID_BRUSHONLY, + ignore, + COLLISION_GROUP_PLAYER_MOVEMENT, + &tr ); + + // If the center is open space, we're effectively blocked + if ( !tr.startsolid ) + { + MarkAsBlocked( TEAM_ANY, NULL ); + } + + /* + if ( IsBlocked( TEAM_ANY ) ) + { + NDebugOverlay::Box( origin, mins, maxs, 255, 0, 0, 64, 3.0f ); + } + else + { + NDebugOverlay::Box( origin, mins, maxs, 0, 255, 0, 64, 3.0f ); + } + */ +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavArea::MarkObstacleToAvoid( float obstructionHeight ) +{ + if ( m_avoidanceObstacleHeight < obstructionHeight ) + { + if ( m_avoidanceObstacleHeight == 0 ) + { + TheNavMesh->OnAvoidanceObstacleEnteredArea( this ); + } + + m_avoidanceObstacleHeight = obstructionHeight; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Updates the (un)obstructed status of the nav area + */ +void CNavArea::UpdateAvoidanceObstacles( void ) +{ + if ( !m_avoidanceObstacleTimer.IsElapsed() ) + { + return; + } + + const float MaxBlockedCheckInterval = 5; + float interval = m_blockedTimer.GetCountdownDuration() + 1; + if ( interval > MaxBlockedCheckInterval ) + { + interval = MaxBlockedCheckInterval; + } + m_avoidanceObstacleTimer.Start( interval ); + + Vector mins = m_nwCorner; + Vector maxs = m_seCorner; + + mins.z = MIN( m_nwCorner.z, m_seCorner.z ); + maxs.z = MAX( m_nwCorner.z, m_seCorner.z ) + HumanCrouchHeight; + + float obstructionHeight = 0.0f; + for ( int i=0; iGetObstructions().Count(); ++i ) + { + INavAvoidanceObstacle *obstruction = TheNavMesh->GetObstructions()[i]; + CBaseEntity *obstructingEntity = obstruction->GetObstructingEntity(); + if ( !obstructingEntity ) + continue; + + // check if the aabb intersects the search aabb. + Vector vecSurroundMins, vecSurroundMaxs; + obstructingEntity->CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); + if ( !IsBoxIntersectingBox( mins, maxs, vecSurroundMins, vecSurroundMaxs ) ) + continue; + + if ( !obstruction->CanObstructNavAreas() ) + continue; + + float propHeight = obstruction->GetNavObstructionHeight(); + + obstructionHeight = MAX( obstructionHeight, propHeight ); + } + + m_avoidanceObstacleHeight = obstructionHeight; + + if ( m_avoidanceObstacleHeight == 0.0f ) + { + TheNavMesh->OnAvoidanceObstacleLeftArea( this ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +// Clear set of func_nav_cost entities that affect this area +void CNavArea::ClearAllNavCostEntities( void ) +{ + RemoveAttributes( NAV_MESH_FUNC_COST ); + m_funcNavCostVector.RemoveAll(); +} + + +//-------------------------------------------------------------------------------------------------------------- +// Add the given func_nav_cost entity to the cost of this area +void CNavArea::AddFuncNavCostEntity( CFuncNavCost *cost ) +{ + SetAttributes( NAV_MESH_FUNC_COST ); + m_funcNavCostVector.AddToTail( cost ); +} + + +//-------------------------------------------------------------------------------------------------------------- +// Return the cost multiplier of this area's func_nav_cost entities for the given actor +float CNavArea::ComputeFuncNavCost( CBaseCombatCharacter *who ) const +{ + float funcCost = 1.0f; + + for( int i=0; iGetCostMultiplier( who ); + } + } + + return funcCost; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CNavArea::HasFuncNavAvoid( void ) const +{ + for( int i=0; i( m_funcNavCostVector[i].Get() ); + if ( avoid ) + { + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CNavArea::HasFuncNavPrefer( void ) const +{ + for( int i=0; i( m_funcNavCostVector[i].Get() ); + if ( prefer ) + { + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavArea::CheckWaterLevel( void ) +{ + Vector pos( GetCenter() ); + if ( !TheNavMesh->GetGroundHeight( pos, &pos.z ) ) + { + m_isUnderwater = false; + return; + } + + pos.z += 1; + m_isUnderwater = (enginetrace->GetPointContents( pos ) & MASK_WATER ) != 0; +} + + +//-------------------------------------------------------------------------------------------------------------- +static void CommandNavCheckFloor( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( TheNavMesh->GetMarkedArea() ) + { + CNavArea *area = TheNavMesh->GetMarkedArea(); + area->CheckFloor( NULL ); + if ( area->IsBlocked( TEAM_ANY ) ) + { + DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); + } + } + else + { + float start = Plat_FloatTime(); + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + area->CheckFloor( NULL ); + if ( area->IsBlocked( TEAM_ANY ) ) + { + DevMsg( "Area #%d %s is blocked\n", area->GetID(), VecToString( area->GetCenter() + Vector( 0, 0, HalfHumanHeight ) ) ); + } + } + + float end = Plat_FloatTime(); + float time = (end - start) * 1000.0f; + DevMsg( "nav_check_floor took %2.2f ms\n", time ); + } +} +static ConCommand nav_check_floor( "nav_check_floor", CommandNavCheckFloor, "Updates the blocked/unblocked status for every nav area.", FCVAR_GAMEDLL ); + + +//-------------------------------------------------------------------------------------------------------------- +bool SelectOverlappingAreas::operator()( CNavArea *area ) +{ + CNavArea *overlappingArea = NULL; + CNavLadder *overlappingLadder = NULL; + + Vector nw = area->GetCorner( NORTH_WEST ); + Vector se = area->GetCorner( SOUTH_EAST ); + Vector start = nw; + start.x += GenerationStepSize/2; + start.y += GenerationStepSize/2; + + while ( start.x < se.x ) + { + start.y = nw.y + GenerationStepSize/2; + while ( start.y < se.y ) + { + start.z = area->GetZ( start.x, start.y ); + Vector end = start; + start.z -= StepHeight; + end.z += HalfHumanHeight; + + if ( TheNavMesh->FindNavAreaOrLadderAlongRay( start, end, &overlappingArea, &overlappingLadder, area ) ) + { + if ( overlappingArea ) + { + TheNavMesh->AddToSelectedSet( overlappingArea ); + TheNavMesh->AddToSelectedSet( area ); + } + } + + start.y += GenerationStepSize; + } + start.x += GenerationStepSize; + } + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +static void CommandNavSelectOverlapping( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->ClearSelectedSet(); + + SelectOverlappingAreas overlapCheck; + TheNavMesh->ForAllAreas( overlapCheck ); + + Msg( "%d overlapping areas selected\n", TheNavMesh->GetSelecteSetSize() ); +} +static ConCommand nav_select_overlapping( "nav_select_overlapping", CommandNavSelectOverlapping, "Selects nav areas that are overlapping others.", FCVAR_GAMEDLL ); + + +//-------------------------------------------------------------------------------------------------------- +static byte m_PVS[PAD_NUMBER( MAX_MAP_CLUSTERS,8 ) / 8]; +static int m_nPVSSize; // PVS size in bytes + +CUtlHash< NavVisPair_t, CVisPairHashFuncs, CVisPairHashFuncs > *g_pNavVisPairHash; + +#define MASK_NAV_VISION (MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE) + + +//-------------------------------------------------------------------------------------------------------- +/** + * Set PVS to only include the Potentially Visible Set as seen from anywhere + * within this nav area + */ +void CNavArea::SetupPVS( void ) const +{ + m_nPVSSize = sizeof( m_PVS ); + engine->ResetPVS( m_PVS, m_nPVSSize ); + + const float margin = GenerationStepSize/2.0f; + Vector eye( 0, 0, 0.75f * HumanHeight ); + + // step across area checking visibility to given area + Vector shift( eye ); + for( shift.y = margin; shift.y <= GetSizeY() - margin; shift.y += GenerationStepSize ) + { + for( shift.x = margin; shift.x <= GetSizeX() - margin; shift.x += GenerationStepSize ) + { + // Optimization: + // If we are already POTENTIALLY_VISIBLE, and no longer COMPLETELY_VISIBLE, there's + // no way for vis to change again. + Vector testPos( GetCorner( NORTH_WEST ) + shift ); + testPos.z = GetZ( testPos ) + eye.z; + + engine->AddOriginToPVS( testPos ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Return true if this area is within the current PVS + */ +bool CNavArea::IsInPVS( void ) const +{ + Vector eye( 0, 0, 0.75f * HumanHeight ); + + Extent areaExtent; + + areaExtent.lo = GetCenter() + eye; + areaExtent.hi = areaExtent.lo; + + areaExtent.Encompass( GetCorner( NORTH_WEST ) + eye ); + areaExtent.Encompass( GetCorner( NORTH_EAST ) + eye ); + areaExtent.Encompass( GetCorner( SOUTH_WEST ) + eye ); + areaExtent.Encompass( GetCorner( SOUTH_EAST ) + eye ); + + return engine->CheckBoxInPVS( areaExtent.lo, areaExtent.hi, m_PVS, m_nPVSSize ); +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Do actual line-of-sight traces to determine if any part of given area is visible from this area + */ +CNavArea::VisibilityType CNavArea::ComputeVisibility( const CNavArea *area, bool isPVSValid, bool bCheckPVS, bool *pOutsidePVS ) const +{ + float distanceSq = area->GetCenter().DistToSqr( GetCenter() ); + + if ( nav_max_view_distance.GetFloat() > 0.00001f ) + { + // limit range of visibility check + if ( distanceSq > Sqr( nav_max_view_distance.GetFloat() ) ) + { + // too far to be visible + return NOT_VISIBLE; + } + } + + if ( !isPVSValid ) + { + SetupPVS(); + } + + Vector eye( 0, 0, 0.75f * HumanHeight ); + + if ( bCheckPVS ) + { + Extent areaExtent; + areaExtent.lo = areaExtent.hi = area->GetCenter() + eye; + areaExtent.Encompass( area->GetCorner( NORTH_WEST ) + eye ); + areaExtent.Encompass( area->GetCorner( NORTH_EAST ) + eye ); + areaExtent.Encompass( area->GetCorner( SOUTH_WEST ) + eye ); + areaExtent.Encompass( area->GetCorner( SOUTH_EAST ) + eye ); + if ( !engine->CheckBoxInPVS( areaExtent.lo, areaExtent.hi, m_PVS, m_nPVSSize ) ) + { + if ( pOutsidePVS ) + *pOutsidePVS = true; + return NOT_VISIBLE; + } + + if ( pOutsidePVS ) + *pOutsidePVS = false; + } + + //------------------------------------ + Vector vThisNW = GetCorner( NORTH_WEST ) + eye; + Vector vThisNE = GetCorner( NORTH_EAST ) + eye; + Vector vThisSW = GetCorner( SOUTH_WEST ) + eye; + Vector vThisSE = GetCorner( SOUTH_EAST ) + eye; + Vector vThisCenter = GetCenter() + eye; + + Vector vTraceMins( vThisNW ); + Vector vTraceMaxs( vThisSE ); + + vTraceMins.z = MIN( MIN( MIN( vThisNW.z, vThisNE.z ), vThisSE.z ), vThisSW.z ); + vTraceMaxs.z = MAX( MAX( MAX( vThisNW.z, vThisNE.z ), vThisSE.z ), vThisSW.z ) + 0.1; + + vTraceMins -= vThisCenter; + vTraceMaxs -= vThisCenter; + + Vector vOtherMins( area->GetCorner( NORTH_WEST) ); + Vector vOtherMaxs( area->GetCorner( SOUTH_EAST) ); + + Vector vTarget; + CalcClosestPointOnAABB( vOtherMins, vOtherMaxs, vThisCenter, vTarget ); + vTarget.z = area->GetZ( vTarget ) + eye.z; + + trace_t tr; + CTraceFilterNoNPCsOrPlayer traceFilter( NULL, COLLISION_GROUP_NONE ); + + UTIL_TraceHull( vThisCenter, vTarget, vTraceMins, vTraceMaxs, MASK_NAV_VISION, &traceFilter, &tr ); + + if ( tr.fraction == 1.0 || ( tr.endpos.x > vOtherMins.x && tr.endpos.x < vOtherMaxs.x && tr.endpos.y > vOtherMins.y && tr.endpos.y < vOtherMaxs.y ) ) + { + return COMPLETELY_VISIBLE; // Counter-intuitive: the way this function was written, "COMPLETELY_VISIBLE" actually means "I am completely visible to the other" + } + + //------------------------------------ + // check line of sight between areas + unsigned char vis = COMPLETELY_VISIBLE; + + const float margin = GenerationStepSize/2.0f; + + Vector shift( 0, 0, 0.75f * HumanHeight ); + + // always check center to catch very small areas + if ( area->IsPartiallyVisible( GetCenter() + eye ) ) + { + vis |= POTENTIALLY_VISIBLE; + } + else + { + vis &= ~COMPLETELY_VISIBLE; + } + + Vector eyeToCenter( GetCenter() - area->GetCenter() ); + eyeToCenter.NormalizeInPlace(); + float angleTolerance = nav_potentially_visible_dot_tolerance.GetFloat(); // if corner-to-eye angles are this close to center-to-eye angles, assume the same result and skip the trace + + // step across area checking visibility to given area + for( shift.y = margin; shift.y <= GetSizeY() - margin; shift.y += GenerationStepSize ) + { + for( shift.x = margin; shift.x <= GetSizeX() - margin; shift.x += GenerationStepSize ) + { + // Optimization: + // If we are already POTENTIALLY_VISIBLE, and no longer COMPLETELY_VISIBLE, there's + // no way for vis to change again. + if ( vis == POTENTIALLY_VISIBLE ) + return POTENTIALLY_VISIBLE; + + Vector testPos( GetCorner( NORTH_WEST ) + shift ); + testPos.z = GetZ( testPos ) + eye.z; + + // Optimization - treat long-distance traces that are effectively collinear as the same + if ( distanceSq > Sqr( 1000 ) ) + { + Vector eyeToCorner( testPos - (GetCenter() + eye) ); + eyeToCorner.NormalizeInPlace(); + if ( eyeToCorner.Dot( eyeToCenter ) >= angleTolerance ) + { + continue; + } + } + + if ( area->IsPartiallyVisible( testPos ) ) + { + vis |= POTENTIALLY_VISIBLE; + } + else + { + vis &= ~COMPLETELY_VISIBLE; + } + } + } + + return (VisibilityType)vis; +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Return a list of the delta between our visibility list and the given adjacent area + */ +const CNavArea::CAreaBindInfoArray &CNavArea::ComputeVisibilityDelta( const CNavArea *other ) const +{ + static CAreaBindInfoArray delta; + + delta.RemoveAll(); + + // do not delta from a delta - if 'other' is already inheriting, use its inherited source directly + if ( other->m_inheritVisibilityFrom.area != NULL ) + { + Assert( false && "Visibility inheriting from inherited area" ); + + delta = m_potentiallyVisibleAreas; + return delta; + } + + // add any visible areas in my list that are not in 'others' list into the delta + int i, j; + for( i=0; im_potentiallyVisibleAreas.Count(); ++j ) + { + if ( m_potentiallyVisibleAreas[i].area == other->m_potentiallyVisibleAreas[j].area && + m_potentiallyVisibleAreas[i].attributes == other->m_potentiallyVisibleAreas[j].attributes ) + { + // mutually identically visible + break; + } + } + + if ( j == other->m_potentiallyVisibleAreas.Count() ) + { + // my vis area not in adjacent area's vis list or has different visibility attributes - add to delta + delta.AddToTail( m_potentiallyVisibleAreas[i] ); + } + } + } + + // add explicit NOT_VISIBLE references to areas in 'others' list that are NOT in mine + for( j=0; jm_potentiallyVisibleAreas.Count(); ++j ) + { + if ( other->m_potentiallyVisibleAreas[j].area ) + { + for( i=0; im_potentiallyVisibleAreas[j].area ) + { + // area in both lists - already handled in delta above + break; + } + } + + if ( i == m_potentiallyVisibleAreas.Count() ) + { + // 'other' has area in their list that we don't - mark it explicitly NOT_VISIBLE + AreaBindInfo info; + info.area = other->m_potentiallyVisibleAreas[j].area; + info.attributes = NOT_VISIBLE; + + delta.AddToTail( info ); + } + } + } + + return delta; +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavArea::ResetPotentiallyVisibleAreas() +{ + m_potentiallyVisibleAreas.RemoveAll(); +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Determine visibility between areas. + * Compute full list of all areas visible for each area. This list will be compressed into deltas + * in the PostCustomAnalysis() step. + */ + +CNavArea *g_pCurVisArea; +CTSListWithFreeList< CNavArea::AreaBindInfo > g_ComputedVis; + +void CNavArea::ComputeVisToArea( CNavArea *&pOtherArea ) +{ + CNavArea *area = assert_cast< CNavArea * >( pOtherArea ); + VisibilityType visThisToOther = ( area == g_pCurVisArea ) ? COMPLETELY_VISIBLE : NOT_VISIBLE; + VisibilityType visOtherToThis = NOT_VISIBLE; + + if ( area != g_pCurVisArea ) + { + bool bOutsidePVS; + + visOtherToThis = g_pCurVisArea->ComputeVisibility( area, true, true, &bOutsidePVS ); // TODO: Hacky right now. Compute visibility for the "complete" case actually returns how completely visible the area is to the other. Should fix it to be more clear [1/30/2009 tom] + + if ( !bOutsidePVS && ( visOtherToThis || ( g_pCurVisArea->GetCenter() - area->GetCenter() ).LengthSqr() < Sqr( nav_max_view_distance.GetFloat() ) ) ) + { + visThisToOther = area->ComputeVisibility( g_pCurVisArea, true, false ); + } + + if ( !visOtherToThis && visThisToOther ) + { + visOtherToThis = POTENTIALLY_VISIBLE; + } + + if ( !visThisToOther && visOtherToThis ) + { + visThisToOther = POTENTIALLY_VISIBLE; + } + } + + CNavArea::AreaBindInfo info; + if ( visThisToOther != NOT_VISIBLE ) + { + info.area = area; + info.attributes = visThisToOther; + g_ComputedVis.PushItem( info ); + } + + if ( visOtherToThis != NOT_VISIBLE ) + { + info.area = g_pCurVisArea; + info.attributes = visOtherToThis; + area->m_potentiallyVisibleAreas.AddToTail( info ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Determine visibility from this area to all potentially/completely visible areas in the mesh + */ +void CNavArea::ComputeVisibilityToMesh( void ) +{ + m_inheritVisibilityFrom.area = NULL; + m_isInheritedFrom = false; + + // collect all possible nav areas that could be visible from this area + NavAreaCollector collector; + float radius = nav_max_view_distance.GetFloat(); + if ( radius == 0.0f ) + { + radius = DEF_NAV_VIEW_DISTANCE; + } + collector.m_area.EnsureCapacity( 1000 ); + TheNavMesh->ForAllAreasInRadius( collector, GetCenter(), radius ); + + NavVisPair_t visPair; + UtlHashHandle_t hHash; + + // First eliminate the ones already calculated + for ( int i = collector.m_area.Count() - 1; i >= 0; --i ) + { + visPair.SetPair( this, collector.m_area[i] ); + + hHash = g_pNavVisPairHash->Find( visPair ); + if ( hHash != g_pNavVisPairHash->InvalidHandle() ) + { + collector.m_area.FastRemove( i ); + } + } + + SetupPVS(); + + g_pCurVisArea = this; + ParallelProcess( "CNavArea::ComputeVisibilityToMesh", collector.m_area.Base(), collector.m_area.Count(), &ComputeVisToArea ); + + m_potentiallyVisibleAreas.EnsureCapacity( g_ComputedVis.Count() ); + while ( g_ComputedVis.Count() ) + { + g_ComputedVis.PopItem( &m_potentiallyVisibleAreas[ m_potentiallyVisibleAreas.AddToTail() ] ); + } + + FOR_EACH_VEC( collector.m_area, it ) + { + visPair.SetPair( this, (CNavArea *)collector.m_area[it] ); + Assert( g_pNavVisPairHash->Find( visPair ) == g_pNavVisPairHash->InvalidHandle() ); + g_pNavVisPairHash->Insert( visPair ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * The center and all four corners must ALL be visible + */ +bool CNavArea::IsEntirelyVisible( const Vector &eye, CBaseEntity *ignore ) const +{ + Vector corner; + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( ignore, COLLISION_GROUP_NONE ); + const float offset = 0.75f * HumanHeight; + + // check center + UTIL_TraceLine( eye, GetCenter() + Vector( 0, 0, offset ), MASK_NAV_VISION, &traceFilter, &result ); + if (result.fraction < 1.0f) + { + return false; + } + + for( int c=0; c= 1.0f) + { + return true; + } + + Vector eyeToCenter( GetCenter() + Vector( 0, 0, offset ) - eye ); + eyeToCenter.NormalizeInPlace(); + float angleTolerance = nav_potentially_visible_dot_tolerance.GetFloat(); // if corner-to-eye angles are this close to center-to-eye angles, assume the same result and skip the trace + + for( int c=0; c= angleTolerance ) + { + continue; + } + + UTIL_TraceLine( eye, corner + Vector( 0, 0, offset ), MASK_NAV_VISION, &traceFilter, &result ); + if (result.fraction >= 1.0f) + { + return true; + } + } + + // nothing is visible + return false; +} + + +//-------------------------------------------------------------------------------------------------------- +bool CNavArea::IsPotentiallyVisible( const CNavArea *viewedArea ) const +{ + VPROF_BUDGET( "CNavArea::IsPotentiallyVisible", "NextBot" ); + + if ( viewedArea == NULL ) + { + return false; + } + + // can always see ourselves + if ( viewedArea == this ) + { + return true; + } + + // normal visibility check + for ( int i=0; im_potentiallyVisibleAreas; + + for ( int i=0; im_potentiallyVisibleAreas; + + for ( int i=0; iGetNumPlayers(); ++i ) + { + if ( team->GetPlayer(i)->IsAlive() ) + { + CNavArea *from = (CNavArea *)team->GetPlayer(i)->GetLastKnownArea(); + + if ( from && from->IsPotentiallyVisible( this ) ) + { + return true; + } + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Return true if given area is completely visible from somewhere in this area by someone on the team (very fast) + */ +bool CNavArea::IsCompletelyVisibleToTeam( int teamIndex ) const +{ + VPROF_BUDGET( "CNavArea::IsCompletelyVisibleToTeam", "NextBot" ); + + CTeam *team = GetGlobalTeam( teamIndex ); + + for( int i = 0; i < team->GetNumPlayers(); ++i ) + { + if ( team->GetPlayer(i)->IsAlive() ) + { + CNavArea *from = (CNavArea *)team->GetPlayer(i)->GetLastKnownArea(); + + if ( from && from->IsCompletelyVisible( this ) ) + { + return true; + } + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------- +Vector CNavArea::GetRandomPoint( void ) const +{ + Extent extent; + GetExtent( &extent ); + + Vector spot; + spot.x = RandomFloat( extent.lo.x, extent.hi.x ); + spot.y = RandomFloat( extent.lo.y, extent.hi.y ); + spot.z = GetZ( spot.x, spot.y ); + + return spot; +} + + + + + + + + + + + + + + + + diff --git a/sp/src/game/server/nav_area.h b/sp/src/game/server/nav_area.h new file mode 100644 index 00000000..7c18a5e0 --- /dev/null +++ b/sp/src/game/server/nav_area.h @@ -0,0 +1,1065 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_area.h +// Navigation areas +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#ifndef _NAV_AREA_H_ +#define _NAV_AREA_H_ + +#include "nav_ladder.h" +#include "tier1/memstack.h" + +// BOTPORT: Clean up relationship between team index and danger storage in nav areas +enum { MAX_NAV_TEAMS = 2 }; + + +class CFuncElevator; +class CFuncNavPrerequisite; +class CFuncNavCost; + +class CNavVectorNoEditAllocator +{ +public: + CNavVectorNoEditAllocator(); + + static void Reset(); + static void *Alloc( size_t nSize ); + static void *Realloc( void *pMem, size_t nSize ); + static void Free( void *pMem ); + static size_t GetSize( void *pMem ); + +private: + static CMemoryStack m_memory; + static void *m_pCurrent; + static int m_nBytesCurrent; +}; + +#if !defined(_X360) +typedef CUtlVectorUltraConservativeAllocator CNavVectorAllocator; +#else +typedef CNavVectorNoEditAllocator CNavVectorAllocator; +#endif + + +//------------------------------------------------------------------------------------------------------------------- +/** + * Functor interface for iteration + */ +class IForEachNavArea +{ +public: + virtual bool Inspect( const CNavArea *area ) = 0; // Invoked once on each area of the iterated set. Return false to stop iterating. + virtual void PostIteration( bool wasCompleteIteration ) { } // Invoked after the iteration has ended. 'wasCompleteIteration' will be true if the entire set was iterated (ie: Inspect() never returned false) +}; + + +//------------------------------------------------------------------------------------------------------------------- +/** + * The NavConnect union is used to refer to connections to areas + */ +struct NavConnect +{ + NavConnect() + { + id = 0; + length = -1; + } + + union + { + unsigned int id; + CNavArea *area; + }; + + mutable float length; + + bool operator==( const NavConnect &other ) const + { + return (area == other.area) ? true : false; + } +}; + +typedef CUtlVectorUltraConservative NavConnectVector; + + +//------------------------------------------------------------------------------------------------------------------- +/** + * The NavLadderConnect union is used to refer to connections to ladders + */ +union NavLadderConnect +{ + unsigned int id; + CNavLadder *ladder; + + bool operator==( const NavLadderConnect &other ) const + { + return (ladder == other.ladder) ? true : false; + } +}; +typedef CUtlVectorUltraConservative NavLadderConnectVector; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * A HidingSpot is a good place for a bot to crouch and wait for enemies + */ +class HidingSpot +{ +public: + virtual ~HidingSpot() { } + + enum + { + IN_COVER = 0x01, // in a corner with good hard cover nearby + GOOD_SNIPER_SPOT = 0x02, // had at least one decent sniping corridor + IDEAL_SNIPER_SPOT = 0x04, // can see either very far, or a large area, or both + EXPOSED = 0x08 // spot in the open, usually on a ledge or cliff + }; + + bool HasGoodCover( void ) const { return (m_flags & IN_COVER) ? true : false; } // return true if hiding spot in in cover + bool IsGoodSniperSpot( void ) const { return (m_flags & GOOD_SNIPER_SPOT) ? true : false; } + bool IsIdealSniperSpot( void ) const { return (m_flags & IDEAL_SNIPER_SPOT) ? true : false; } + bool IsExposed( void ) const { return (m_flags & EXPOSED) ? true : false; } + + int GetFlags( void ) const { return m_flags; } + + void Save( CUtlBuffer &fileBuffer, unsigned int version ) const; + void Load( CUtlBuffer &fileBuffer, unsigned int version ); + NavErrorType PostLoad( void ); + + const Vector &GetPosition( void ) const { return m_pos; } // get the position of the hiding spot + unsigned int GetID( void ) const { return m_id; } + const CNavArea *GetArea( void ) const { return m_area; } // return nav area this hiding spot is within + + void Mark( void ) { m_marker = m_masterMarker; } + bool IsMarked( void ) const { return (m_marker == m_masterMarker) ? true : false; } + static void ChangeMasterMarker( void ) { ++m_masterMarker; } + + +public: + void SetFlags( int flags ) { m_flags |= flags; } // FOR INTERNAL USE ONLY + void SetPosition( const Vector &pos ) { m_pos = pos; } // FOR INTERNAL USE ONLY + +private: + friend class CNavMesh; + friend void ClassifySniperSpot( HidingSpot *spot ); + + HidingSpot( void ); // must use factory to create + + Vector m_pos; // world coordinates of the spot + unsigned int m_id; // this spot's unique ID + unsigned int m_marker; // this spot's unique marker + CNavArea *m_area; // the nav area containing this hiding spot + + unsigned char m_flags; // bit flags + + static unsigned int m_nextID; // used when allocating spot ID's + static unsigned int m_masterMarker; // used to mark spots +}; +typedef CUtlVectorUltraConservative< HidingSpot * > HidingSpotVector; +extern HidingSpotVector TheHidingSpots; + +extern HidingSpot *GetHidingSpotByID( unsigned int id ); + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Stores a pointer to an interesting "spot", and a parametric distance along a path + */ +struct SpotOrder +{ + float t; // parametric distance along ray where this spot first has LOS to our path + union + { + HidingSpot *spot; // the spot to look at + unsigned int id; // spot ID for save/load + }; +}; +typedef CUtlVector< SpotOrder > SpotOrderVector; + +/** + * This struct stores possible path segments thru a CNavArea, and the dangerous spots + * to look at as we traverse that path segment. + */ +struct SpotEncounter +{ + NavConnect from; + NavDirType fromDir; + NavConnect to; + NavDirType toDir; + Ray path; // the path segment + SpotOrderVector spots; // list of spots to look at, in order of occurrence +}; +typedef CUtlVectorUltraConservative< SpotEncounter * > SpotEncounterVector; + + +//------------------------------------------------------------------------------------------------------------------- +/** + * A CNavArea is a rectangular region defining a walkable area in the environment + */ + +class CNavAreaCriticalData +{ +protected: + // --- Begin critical data, which is heavily hit during pathing operations and carefully arranged for cache performance [7/24/2008 tom] --- + + /* 0 */ Vector m_nwCorner; // north-west corner position (2D mins) + /* 12 */ Vector m_seCorner; // south-east corner position (2D maxs) + /* 24 */ float m_invDxCorners; + /* 28 */ float m_invDyCorners; + /* 32 */ float m_neZ; // height of the implicit corner defined by (m_seCorner.x, m_nwCorner.y, m_neZ) + /* 36 */ float m_swZ; // height of the implicit corner defined by (m_nwCorner.x, m_seCorner.y, m_neZ) + /* 40 */ Vector m_center; // centroid of area + + /* 52 */ unsigned char m_playerCount[ MAX_NAV_TEAMS ]; // the number of players currently in this area + + /* 54 */ bool m_isBlocked[ MAX_NAV_TEAMS ]; // if true, some part of the world is preventing movement through this nav area + + /* 56 */ unsigned int m_marker; // used to flag the area as visited + /* 60 */ float m_totalCost; // the distance so far plus an estimate of the distance left + /* 64 */ float m_costSoFar; // distance travelled so far + + /* 68 */ CNavArea *m_nextOpen, *m_prevOpen; // only valid if m_openMarker == m_masterMarker + /* 76 */ unsigned int m_openMarker; // if this equals the current marker value, we are on the open list + + /* 80 */ int m_attributeFlags; // set of attribute bit flags (see NavAttributeType) + + //- connections to adjacent areas ------------------------------------------------------------------- + /* 84 */ NavConnectVector m_connect[ NUM_DIRECTIONS ]; // a list of adjacent areas for each direction + /* 100*/ NavLadderConnectVector m_ladder[ CNavLadder::NUM_LADDER_DIRECTIONS ]; // list of ladders leading up and down from this area + /* 108*/ NavConnectVector m_elevatorAreas; // a list of areas reachable via elevator from this area + + /* 112*/ unsigned int m_nearNavSearchMarker; // used in GetNearestNavArea() + + /* 116*/ CNavArea *m_parent; // the area just prior to this on in the search path + /* 120*/ NavTraverseType m_parentHow; // how we get from parent to us + + /* 124*/ float m_pathLengthSoFar; // length of path so far, needed for limiting pathfind max path length + + /* *************** 360 cache line *************** */ + + /* 128*/ CFuncElevator *m_elevator; // if non-NULL, this area is in an elevator's path. The elevator can transport us vertically to another area. + + // --- End critical data --- +}; + + +class CNavArea : protected CNavAreaCriticalData +{ +public: + DECLARE_CLASS_NOBASE( CNavArea ) + + CNavArea( void ); + virtual ~CNavArea(); + + virtual void OnServerActivate( void ); // (EXTEND) invoked when map is initially loaded + virtual void OnRoundRestart( void ); // (EXTEND) invoked for each area when the round restarts + virtual void OnRoundRestartPreEntity( void ) { } // invoked for each area when the round restarts, but before entities are deleted and recreated + virtual void OnEnter( CBaseCombatCharacter *who, CNavArea *areaJustLeft ) { } // invoked when player enters this area + virtual void OnExit( CBaseCombatCharacter *who, CNavArea *areaJustEntered ) { } // invoked when player exits this area + + virtual void OnDestroyNotify( CNavArea *dead ); // invoked when given area is going away + virtual void OnDestroyNotify( CNavLadder *dead ); // invoked when given ladder is going away + + virtual void OnEditCreateNotify( CNavArea *newArea ) { } // invoked when given area has just been added to the mesh in edit mode + virtual void OnEditDestroyNotify( CNavArea *deadArea ) { } // invoked when given area has just been deleted from the mesh in edit mode + virtual void OnEditDestroyNotify( CNavLadder *deadLadder ) { } // invoked when given ladder has just been deleted from the mesh in edit mode + + virtual void Save( CUtlBuffer &fileBuffer, unsigned int version ) const; // (EXTEND) + virtual NavErrorType Load( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ); // (EXTEND) + virtual NavErrorType PostLoad( void ); // (EXTEND) invoked after all areas have been loaded - for pointer binding, etc + + virtual void SaveToSelectedSet( KeyValues *areaKey ) const; // (EXTEND) saves attributes for the area to a KeyValues + virtual void RestoreFromSelectedSet( KeyValues *areaKey ); // (EXTEND) restores attributes from a KeyValues + + // for interactively building or generating nav areas + void Build( CNavNode *nwNode, CNavNode *neNode, CNavNode *seNode, CNavNode *swNode ); + void Build( const Vector &corner, const Vector &otherCorner ); + void Build( const Vector &nwCorner, const Vector &neCorner, const Vector &seCorner, const Vector &swCorner ); + + void ConnectTo( CNavArea *area, NavDirType dir ); // connect this area to given area in given direction + void Disconnect( CNavArea *area ); // disconnect this area from given area + + void ConnectTo( CNavLadder *ladder ); // connect this area to given ladder + void Disconnect( CNavLadder *ladder ); // disconnect this area from given ladder + + unsigned int GetID( void ) const { return m_id; } // return this area's unique ID + static void CompressIDs( void ); // re-orders area ID's so they are continuous + unsigned int GetDebugID( void ) const { return m_debugid; } + + void SetAttributes( int bits ) { m_attributeFlags = bits; } + int GetAttributes( void ) const { return m_attributeFlags; } + bool HasAttributes( int bits ) const { return ( m_attributeFlags & bits ) ? true : false; } + void RemoveAttributes( int bits ) { m_attributeFlags &= ( ~bits ); } + + void SetPlace( Place place ) { m_place = place; } // set place descriptor + Place GetPlace( void ) const { return m_place; } // get place descriptor + + void MarkAsBlocked( int teamID, CBaseEntity *blocker, bool bGenerateEvent = true ); // An entity can force a nav area to be blocked + virtual void UpdateBlocked( bool force = false, int teamID = TEAM_ANY ); // Updates the (un)blocked status of the nav area (throttled) + virtual bool IsBlocked( int teamID, bool ignoreNavBlockers = false ) const; + void UnblockArea( int teamID = TEAM_ANY ); // clear blocked status for the given team(s) + + void CheckFloor( CBaseEntity *ignore ); // Checks if there is a floor under the nav area, in case a breakable floor is gone + + void MarkObstacleToAvoid( float obstructionHeight ); + void UpdateAvoidanceObstacles( void ); + bool HasAvoidanceObstacle( float maxObstructionHeight = StepHeight ) const; // is there a large, immobile object obstructing this area + float GetAvoidanceObstacleHeight( void ) const; // returns the maximum height of the obstruction above the ground + +#ifdef NEXT_BOT + bool HasPrerequisite( CBaseCombatCharacter *actor = NULL ) const; // return true if this area has a prerequisite that applies to the given actor + const CUtlVector< CHandle< CFuncNavPrerequisite > > &GetPrerequisiteVector( void ) const; // return vector of prerequisites that must be met before this area can be traversed + void RemoveAllPrerequisites( void ); + void AddPrerequisite( CFuncNavPrerequisite *prereq ); +#endif + + void ClearAllNavCostEntities( void ); // clear set of func_nav_cost entities that affect this area + void AddFuncNavCostEntity( CFuncNavCost *cost ); // add the given func_nav_cost entity to the cost of this area + float ComputeFuncNavCost( CBaseCombatCharacter *who ) const; // return the cost multiplier of this area's func_nav_cost entities for the given actor + bool HasFuncNavAvoid( void ) const; + bool HasFuncNavPrefer( void ) const; + + void CheckWaterLevel( void ); + bool IsUnderwater( void ) const { return m_isUnderwater; } + + bool IsOverlapping( const Vector &pos, float tolerance = 0.0f ) const; // return true if 'pos' is within 2D extents of area. + bool IsOverlapping( const CNavArea *area ) const; // return true if 'area' overlaps our 2D extents + bool IsOverlapping( const Extent &extent ) const; // return true if 'extent' overlaps our 2D extents + bool IsOverlappingX( const CNavArea *area ) const; // return true if 'area' overlaps our X extent + bool IsOverlappingY( const CNavArea *area ) const; // return true if 'area' overlaps our Y extent + inline float GetZ( const Vector * RESTRICT pPos ) const ; // return Z of area at (x,y) of 'pos' + inline float GetZ( const Vector &pos ) const; // return Z of area at (x,y) of 'pos' + float GetZ( float x, float y ) const RESTRICT; // return Z of area at (x,y) of 'pos' + bool Contains( const Vector &pos ) const; // return true if given point is on or above this area, but no others + bool Contains( const CNavArea *area ) const; + bool IsCoplanar( const CNavArea *area ) const; // return true if this area and given area are approximately co-planar + void GetClosestPointOnArea( const Vector * RESTRICT pPos, Vector *close ) const RESTRICT; // return closest point to 'pos' on this area - returned point in 'close' + void GetClosestPointOnArea( const Vector &pos, Vector *close ) const { return GetClosestPointOnArea( &pos, close ); } + float GetDistanceSquaredToPoint( const Vector &pos ) const; // return shortest distance between point and this area + bool IsDegenerate( void ) const; // return true if this area is badly formed + bool IsRoughlySquare( void ) const; // return true if this area is approximately square + bool IsFlat( void ) const; // return true if this area is approximately flat + bool HasNodes( void ) const; + void GetNodes( NavDirType dir, CUtlVector< CNavNode * > *nodes ) const; // build a vector of nodes along the given direction + CNavNode *FindClosestNode( const Vector &pos, NavDirType dir ) const; // returns the closest node along the given edge to the given point + + bool IsContiguous( const CNavArea *other ) const; // return true if the given area and 'other' share a colinear edge (ie: no drop-down or step/jump/climb) + float ComputeAdjacentConnectionHeightChange( const CNavArea *destinationArea ) const; // return height change between edges of adjacent nav areas (not actual underlying ground) + + bool IsEdge( NavDirType dir ) const; // return true if there are no bi-directional links on the given side + + bool IsDamaging( void ) const; // Return true if continuous damage (ie: fire) is in this area + void MarkAsDamaging( float duration ); // Mark this area is damaging for the next 'duration' seconds + + bool IsVisible( const Vector &eye, Vector *visSpot = NULL ) const; // return true if area is visible from the given eyepoint, return visible spot + + int GetAdjacentCount( NavDirType dir ) const { return m_connect[ dir ].Count(); } // return number of connected areas in given direction + CNavArea *GetAdjacentArea( NavDirType dir, int i ) const; // return the i'th adjacent area in the given direction + CNavArea *GetRandomAdjacentArea( NavDirType dir ) const; + void CollectAdjacentAreas( CUtlVector< CNavArea * > *adjVector ) const; // build a vector of all adjacent areas + + const NavConnectVector *GetAdjacentAreas( NavDirType dir ) const { return &m_connect[dir]; } + bool IsConnected( const CNavArea *area, NavDirType dir ) const; // return true if given area is connected in given direction + bool IsConnected( const CNavLadder *ladder, CNavLadder::LadderDirectionType dir ) const; // return true if given ladder is connected in given direction + float ComputeGroundHeightChange( const CNavArea *area ); // compute change in actual ground height from this area to given area + + const NavConnectVector *GetIncomingConnections( NavDirType dir ) const { return &m_incomingConnect[dir]; } // get areas connected TO this area by a ONE-WAY link (ie: we have no connection back to them) + void AddIncomingConnection( CNavArea *source, NavDirType incomingEdgeDir ); + + const NavLadderConnectVector *GetLadders( CNavLadder::LadderDirectionType dir ) const { return &m_ladder[dir]; } + CFuncElevator *GetElevator( void ) const { Assert( !( m_attributeFlags & NAV_MESH_HAS_ELEVATOR ) == (m_elevator == NULL) ); return ( m_attributeFlags & NAV_MESH_HAS_ELEVATOR ) ? m_elevator : NULL; } + const NavConnectVector &GetElevatorAreas( void ) const { return m_elevatorAreas; } // return collection of areas reachable via elevator from this area + + void ComputePortal( const CNavArea *to, NavDirType dir, Vector *center, float *halfWidth ) const; // compute portal to adjacent area + NavDirType ComputeLargestPortal( const CNavArea *to, Vector *center, float *halfWidth ) const; // compute largest portal to adjacent area, returning direction + void ComputeClosestPointInPortal( const CNavArea *to, NavDirType dir, const Vector &fromPos, Vector *closePos ) const; // compute closest point within the "portal" between to adjacent areas + NavDirType ComputeDirection( Vector *point ) const; // return direction from this area to the given point + + //- for hunting algorithm --------------------------------------------------------------------------- + void SetClearedTimestamp( int teamID ); // set this area's "clear" timestamp to now + float GetClearedTimestamp( int teamID ) const; // get time this area was marked "clear" + + //- hiding spots ------------------------------------------------------------------------------------ + const HidingSpotVector *GetHidingSpots( void ) const { return &m_hidingSpots; } + + SpotEncounter *GetSpotEncounter( const CNavArea *from, const CNavArea *to ); // given the areas we are moving between, return the spots we will encounter + int GetSpotEncounterCount( void ) const { return m_spotEncounters.Count(); } + + //- "danger" ---------------------------------------------------------------------------------------- + void IncreaseDanger( int teamID, float amount ); // increase the danger of this area for the given team + float GetDanger( int teamID ); // return the danger of this area (decays over time) + virtual float GetDangerDecayRate( void ) const; // return danger decay rate per second + + //- extents ----------------------------------------------------------------------------------------- + float GetSizeX( void ) const { return m_seCorner.x - m_nwCorner.x; } + float GetSizeY( void ) const { return m_seCorner.y - m_nwCorner.y; } + void GetExtent( Extent *extent ) const; // return a computed extent (XY is in m_nwCorner and m_seCorner, Z is computed) + const Vector &GetCenter( void ) const { return m_center; } + Vector GetRandomPoint( void ) const; + Vector GetCorner( NavCornerType corner ) const; + void SetCorner( NavCornerType corner, const Vector& newPosition ); + void ComputeNormal( Vector *normal, bool alternate = false ) const; // Computes the area's normal based on m_nwCorner. If 'alternate' is specified, m_seCorner is used instead. + void RemoveOrthogonalConnections( NavDirType dir ); + + //- occupy time ------------------------------------------------------------------------------------ + float GetEarliestOccupyTime( int teamID ) const; // returns the minimum time for someone of the given team to reach this spot from their spawn + bool IsBattlefront( void ) const { return m_isBattlefront; } // true if this area is a "battlefront" - where rushing teams initially meet + + //- player counting -------------------------------------------------------------------------------- + void IncrementPlayerCount( int teamID, int entIndex ); // add one player to this area's count + void DecrementPlayerCount( int teamID, int entIndex ); // subtract one player from this area's count + unsigned char GetPlayerCount( int teamID = 0 ) const; // return number of players of given team currently within this area (team of zero means any/all) + + //- lighting ---------------------------------------------------------------------------------------- + float GetLightIntensity( const Vector &pos ) const; // returns a 0..1 light intensity for the given point + float GetLightIntensity( float x, float y ) const; // returns a 0..1 light intensity for the given point + float GetLightIntensity( void ) const; // returns a 0..1 light intensity averaged over the whole area + + //- A* pathfinding algorithm ------------------------------------------------------------------------ + static void MakeNewMarker( void ) { ++m_masterMarker; if (m_masterMarker == 0) m_masterMarker = 1; } + void Mark( void ) { m_marker = m_masterMarker; } + BOOL IsMarked( void ) const { return (m_marker == m_masterMarker) ? true : false; } + + void SetParent( CNavArea *parent, NavTraverseType how = NUM_TRAVERSE_TYPES ) { m_parent = parent; m_parentHow = how; } + CNavArea *GetParent( void ) const { return m_parent; } + NavTraverseType GetParentHow( void ) const { return m_parentHow; } + + bool IsOpen( void ) const; // true if on "open list" + void AddToOpenList( void ); // add to open list in decreasing value order + void AddToOpenListTail( void ); // add to tail of the open list + void UpdateOnOpenList( void ); // a smaller value has been found, update this area on the open list + void RemoveFromOpenList( void ); + static bool IsOpenListEmpty( void ); + static CNavArea *PopOpenList( void ); // remove and return the first element of the open list + + bool IsClosed( void ) const; // true if on "closed list" + void AddToClosedList( void ); // add to the closed list + void RemoveFromClosedList( void ); + + static void ClearSearchLists( void ); // clears the open and closed lists for a new search + + void SetTotalCost( float value ) { Assert( value >= 0.0 && !IS_NAN(value) ); m_totalCost = value; } + float GetTotalCost( void ) const { return m_totalCost; } + + void SetCostSoFar( float value ) { Assert( value >= 0.0 && !IS_NAN(value) ); m_costSoFar = value; } + float GetCostSoFar( void ) const { return m_costSoFar; } + + void SetPathLengthSoFar( float value ) { Assert( value >= 0.0 && !IS_NAN(value) ); m_pathLengthSoFar = value; } + float GetPathLengthSoFar( void ) const { return m_pathLengthSoFar; } + + //- editing ----------------------------------------------------------------------------------------- + virtual void Draw( void ) const; // draw area for debugging & editing + virtual void DrawFilled( int r, int g, int b, int a, float deltaT = 0.1f, bool noDepthTest = true, float margin = 5.0f ) const; // draw area as a filled rect of the given color + virtual void DrawSelectedSet( const Vector &shift ) const; // draw this area as part of a selected set + void DrawDragSelectionSet( Color &dragSelectionSetColor ) const; + void DrawConnectedAreas( void ) const; + void DrawHidingSpots( void ) const; + bool SplitEdit( bool splitAlongX, float splitEdge, CNavArea **outAlpha = NULL, CNavArea **outBeta = NULL ); // split this area into two areas at the given edge + bool MergeEdit( CNavArea *adj ); // merge this area and given adjacent area + bool SpliceEdit( CNavArea *other ); // create a new area between this area and given area + void RaiseCorner( NavCornerType corner, int amount, bool raiseAdjacentCorners = true ); // raise/lower a corner (or all corners if corner == NUM_CORNERS) + void PlaceOnGround( NavCornerType corner, float inset = 0.0f ); // places a corner (or all corners if corner == NUM_CORNERS) on the ground + NavCornerType GetCornerUnderCursor( void ) const; + bool GetCornerHotspot( NavCornerType corner, Vector hotspot[NUM_CORNERS] ) const; // returns true if the corner is under the cursor + void Shift( const Vector &shift ); // shift the nav area + + //- ladders ----------------------------------------------------------------------------------------- + void AddLadderUp( CNavLadder *ladder ); + void AddLadderDown( CNavLadder *ladder ); + + //- generation and analysis ------------------------------------------------------------------------- + virtual void ComputeHidingSpots( void ); // analyze local area neighborhood to find "hiding spots" in this area - for map learning + virtual void ComputeSniperSpots( void ); // analyze local area neighborhood to find "sniper spots" in this area - for map learning + virtual void ComputeSpotEncounters( void ); // compute spot encounter data - for map learning + virtual void ComputeEarliestOccupyTimes( void ); + virtual void CustomAnalysis( bool isIncremental = false ) { } // for game-specific analysis + virtual bool ComputeLighting( void ); // compute 0..1 light intensity at corners and center (requires client via listenserver) + bool TestStairs( void ); // Test an area for being on stairs + virtual bool IsAbleToMergeWith( CNavArea *other ) const; + + virtual void InheritAttributes( CNavArea *first, CNavArea *second = NULL ); + + + //- visibility ------------------------------------------------------------------------------------- + enum VisibilityType + { + NOT_VISIBLE = 0x00, + POTENTIALLY_VISIBLE = 0x01, + COMPLETELY_VISIBLE = 0x02, + }; + + VisibilityType ComputeVisibility( const CNavArea *area, bool isPVSValid, bool bCheckPVS = true, bool *pOutsidePVS = NULL ) const; // do actual line-of-sight traces to determine if any part of given area is visible from this area + void SetupPVS( void ) const; + bool IsInPVS( void ) const; // return true if this area is within the current PVS + + struct AreaBindInfo // for pointer loading and binding + { + union + { + CNavArea *area; + unsigned int id; + }; + + unsigned char attributes; // VisibilityType + + bool operator==( const AreaBindInfo &other ) const + { + return ( area == other.area ); + } + }; + + virtual bool IsEntirelyVisible( const Vector &eye, CBaseEntity *ignore = NULL ) const; // return true if entire area is visible from given eyepoint (CPU intensive) + virtual bool IsPartiallyVisible( const Vector &eye, CBaseEntity *ignore = NULL ) const; // return true if any portion of the area is visible from given eyepoint (CPU intensive) + + virtual bool IsPotentiallyVisible( const CNavArea *area ) const; // return true if given area is potentially visible from somewhere in this area (very fast) + virtual bool IsPotentiallyVisibleToTeam( int team ) const; // return true if any portion of this area is visible to anyone on the given team (very fast) + + virtual bool IsCompletelyVisible( const CNavArea *area ) const; // return true if given area is completely visible from somewhere in this area (very fast) + virtual bool IsCompletelyVisibleToTeam( int team ) const; // return true if given area is completely visible from somewhere in this area by someone on the team (very fast) + + //------------------------------------------------------------------------------------- + /** + * Apply the functor to all navigation areas that are potentially + * visible from this area. + */ + template < typename Functor > + bool ForAllPotentiallyVisibleAreas( Functor &func ) + { + int i; + + ++s_nCurrVisTestCounter; + + for ( i=0; im_nVisTestCounter != s_nCurrVisTestCounter ); + area->m_nVisTestCounter = s_nCurrVisTestCounter; + + if ( m_potentiallyVisibleAreas[i].attributes == NOT_VISIBLE ) + continue; + + if ( func( area ) == false ) + return false; + } + + // for each inherited area + if ( !m_inheritVisibilityFrom.area ) + return true; + + CAreaBindInfoArray &inherited = m_inheritVisibilityFrom.area->m_potentiallyVisibleAreas; + + for ( i=0; im_nVisTestCounter == s_nCurrVisTestCounter ) + continue; + + // Theoretically, this shouldn't matter. But, just in case! + inherited[i].area->m_nVisTestCounter = s_nCurrVisTestCounter; + + if ( inherited[i].attributes == NOT_VISIBLE ) + continue; + + if ( func( inherited[i].area ) == false ) + return false; + } + + return true; + } + + //------------------------------------------------------------------------------------- + /** + * Apply the functor to all navigation areas that are + * completely visible from somewhere in this area. + */ + template < typename Functor > + bool ForAllCompletelyVisibleAreas( Functor &func ) + { + int i; + + ++s_nCurrVisTestCounter; + + for ( i=0; im_nVisTestCounter != s_nCurrVisTestCounter ); + area->m_nVisTestCounter = s_nCurrVisTestCounter; + + if ( ( m_potentiallyVisibleAreas[i].attributes & COMPLETELY_VISIBLE ) == 0 ) + continue; + + if ( func( area ) == false ) + return false; + } + + if ( !m_inheritVisibilityFrom.area ) + return true; + + // for each inherited area + CAreaBindInfoArray &inherited = m_inheritVisibilityFrom.area->m_potentiallyVisibleAreas; + + for ( i=0; im_nVisTestCounter == s_nCurrVisTestCounter ) + continue; + + // Theoretically, this shouldn't matter. But, just in case! + inherited[i].area->m_nVisTestCounter = s_nCurrVisTestCounter; + + if ( ( inherited[i].attributes & COMPLETELY_VISIBLE ) == 0 ) + continue; + + if ( func( inherited[i].area ) == false ) + return false; + } + + return true; + } + + +private: + friend class CNavMesh; + friend class CNavLadder; + friend class CCSNavArea; // allow CS load code to complete replace our default load behavior + + static bool m_isReset; // if true, don't bother cleaning up in destructor since everything is going away + + /* + m_nwCorner + nw ne + +-----------+ + | +-->x | + | | | + | v | + | y | + | | + +-----------+ + sw se + m_seCorner + */ + + static unsigned int m_nextID; // used to allocate unique IDs + unsigned int m_id; // unique area ID + unsigned int m_debugid; + + Place m_place; // place descriptor + + CountdownTimer m_blockedTimer; // Throttle checks on our blocked state while blocked + void UpdateBlockedFromNavBlockers( void ); // checks if nav blockers are still blocking the area + + bool m_isUnderwater; // true if the center of the area is underwater + + bool m_isBattlefront; + + float m_avoidanceObstacleHeight; // if nonzero, a prop is obstructing movement through this nav area + CountdownTimer m_avoidanceObstacleTimer; // Throttle checks on our obstructed state while obstructed + + //- for hunting ------------------------------------------------------------------------------------- + float m_clearedTimestamp[ MAX_NAV_TEAMS ]; // time this area was last "cleared" of enemies + + //- "danger" ---------------------------------------------------------------------------------------- + float m_danger[ MAX_NAV_TEAMS ]; // danger of this area, allowing bots to avoid areas where they died in the past - zero is no danger + float m_dangerTimestamp[ MAX_NAV_TEAMS ]; // time when danger value was set - used for decaying + void DecayDanger( void ); + + //- hiding spots ------------------------------------------------------------------------------------ + HidingSpotVector m_hidingSpots; + bool IsHidingSpotCollision( const Vector &pos ) const; // returns true if an existing hiding spot is too close to given position + + //- encounter spots --------------------------------------------------------------------------------- + SpotEncounterVector m_spotEncounters; // list of possible ways to move thru this area, and the spots to look at as we do + void AddSpotEncounters( const CNavArea *from, NavDirType fromDir, const CNavArea *to, NavDirType toDir ); // add spot encounter data when moving from area to area + + float m_earliestOccupyTime[ MAX_NAV_TEAMS ]; // min time to reach this spot from spawn + +#ifdef DEBUG_AREA_PLAYERCOUNTS + CUtlVector< int > m_playerEntIndices[ MAX_NAV_TEAMS ]; +#endif + + //- lighting ---------------------------------------------------------------------------------------- + float m_lightIntensity[ NUM_CORNERS ]; // 0..1 light intensity at corners + + //- A* pathfinding algorithm ------------------------------------------------------------------------ + static unsigned int m_masterMarker; + + static CNavArea *m_openList; + static CNavArea *m_openListTail; + + //- connections to adjacent areas ------------------------------------------------------------------- + NavConnectVector m_incomingConnect[ NUM_DIRECTIONS ]; // a list of adjacent areas for each direction that connect TO us, but we have no connection back to them + + //--------------------------------------------------------------------------------------------------- + CNavNode *m_node[ NUM_CORNERS ]; // nav nodes at each corner of the area + + void ResetNodes( void ); // nodes are going away as part of an incremental nav generation + void Strip( void ); // remove "analyzed" data from nav area + + void FinishMerge( CNavArea *adjArea ); // recompute internal data once nodes have been adjusted during merge + void MergeAdjacentConnections( CNavArea *adjArea ); // for merging with "adjArea" - pick up all of "adjArea"s connections + void AssignNodes( CNavArea *area ); // assign internal nodes to the given area + + void FinishSplitEdit( CNavArea *newArea, NavDirType ignoreEdge ); // given the portion of the original area, update its internal data + + void CalcDebugID(); + +#ifdef NEXT_BOT + CUtlVector< CHandle< CFuncNavPrerequisite > > m_prerequisiteVector; // list of prerequisites that must be met before this area can be traversed +#endif + + CNavArea *m_prevHash, *m_nextHash; // for hash table in CNavMesh + + void ConnectElevators( void ); // find elevator connections between areas + + int m_damagingTickCount; // this area is damaging through this tick count + + + //- visibility -------------------------------------------------------------------------------------- + void ComputeVisibilityToMesh( void ); // compute visibility to surrounding mesh + void ResetPotentiallyVisibleAreas(); + static void ComputeVisToArea( CNavArea *&pOtherArea ); + +#ifndef _X360 + typedef CUtlVectorConservative CAreaBindInfoArray; // shaves 8 bytes off structure caused by need to support editing +#else + typedef CUtlVector CAreaBindInfoArray; // Need to use this on 360 to support external allocation pattern +#endif + + AreaBindInfo m_inheritVisibilityFrom; // if non-NULL, m_potentiallyVisibleAreas becomes a list of additions and deletions (NOT_VISIBLE) to the list of this area + CAreaBindInfoArray m_potentiallyVisibleAreas; // list of areas potentially visible from inside this area (after PostLoad(), use area portion of union) + bool m_isInheritedFrom; // latch used during visibility inheritance computation + + const CAreaBindInfoArray &ComputeVisibilityDelta( const CNavArea *other ) const; // return a list of the delta between our visibility list and the given adjacent area + + uint32 m_nVisTestCounter; + static uint32 s_nCurrVisTestCounter; + + CUtlVector< CHandle< CFuncNavCost > > m_funcNavCostVector; // active, overlapping cost entities +}; + +typedef CUtlVector< CNavArea * > NavAreaVector; +extern NavAreaVector TheNavAreas; + + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- +// +// Inlines +// + +#ifdef NEXT_BOT + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::HasPrerequisite( CBaseCombatCharacter *actor ) const +{ + return m_prerequisiteVector.Count() > 0; +} + +//-------------------------------------------------------------------------------------------------------------- +inline const CUtlVector< CHandle< CFuncNavPrerequisite > > &CNavArea::GetPrerequisiteVector( void ) const +{ + return m_prerequisiteVector; +} + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavArea::RemoveAllPrerequisites( void ) +{ + m_prerequisiteVector.RemoveAll(); +} + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavArea::AddPrerequisite( CFuncNavPrerequisite *prereq ) +{ + if ( m_prerequisiteVector.Find( prereq ) == m_prerequisiteVector.InvalidIndex() ) + { + m_prerequisiteVector.AddToTail( prereq ); + } +} +#endif + +//-------------------------------------------------------------------------------------------------------------- +inline float CNavArea::GetDangerDecayRate( void ) const +{ + // one kill == 1.0, which we will forget about in two minutes + return 1.0f / 120.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::IsDegenerate( void ) const +{ + return (m_nwCorner.x >= m_seCorner.x || m_nwCorner.y >= m_seCorner.y); +} + +//-------------------------------------------------------------------------------------------------------------- +inline CNavArea *CNavArea::GetAdjacentArea( NavDirType dir, int i ) const +{ + for( int iter = 0; iter < m_connect[dir].Count(); ++iter ) + { + if (i == 0) + return m_connect[dir][iter].area; + --i; + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::IsOpen( void ) const +{ + return (m_openMarker == m_masterMarker) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::IsOpenListEmpty( void ) +{ + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); + return (m_openList) ? false : true; +} + +//-------------------------------------------------------------------------------------------------------------- +inline CNavArea *CNavArea::PopOpenList( void ) +{ + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); + + if ( m_openList ) + { + CNavArea *area = m_openList; + + // disconnect from list + area->RemoveFromOpenList(); + area->m_prevOpen = NULL; + area->m_nextOpen = NULL; + + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); + + return area; + } + + Assert( (m_openList && m_openList->m_prevOpen == NULL) || m_openList == NULL ); + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::IsClosed( void ) const +{ + if (IsMarked() && !IsOpen()) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavArea::AddToClosedList( void ) +{ + Mark(); +} + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavArea::RemoveFromClosedList( void ) +{ + // since "closed" is defined as visited (marked) and not on open list, do nothing +} + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavArea::SetClearedTimestamp( int teamID ) +{ + m_clearedTimestamp[ teamID % MAX_NAV_TEAMS ] = gpGlobals->curtime; +} + +//-------------------------------------------------------------------------------------------------------------- +inline float CNavArea::GetClearedTimestamp( int teamID ) const +{ + return m_clearedTimestamp[ teamID % MAX_NAV_TEAMS ]; +} + +//-------------------------------------------------------------------------------------------------------------- +inline float CNavArea::GetEarliestOccupyTime( int teamID ) const +{ + return m_earliestOccupyTime[ teamID % MAX_NAV_TEAMS ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::IsDamaging( void ) const +{ + return ( gpGlobals->tickcount <= m_damagingTickCount ); +} + + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavArea::MarkAsDamaging( float duration ) +{ + m_damagingTickCount = gpGlobals->tickcount + TIME_TO_TICKS( duration ); +} + + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::HasAvoidanceObstacle( float maxObstructionHeight ) const +{ + return m_avoidanceObstacleHeight > maxObstructionHeight; +} + + +//-------------------------------------------------------------------------------------------------------------- +inline float CNavArea::GetAvoidanceObstacleHeight( void ) const +{ + return m_avoidanceObstacleHeight; +} + + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavArea::IsVisible( const Vector &eye, Vector *visSpot ) const +{ + Vector corner; + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( NULL, COLLISION_GROUP_NONE ); + const float offset = 0.75f * HumanHeight; + + // check center first + UTIL_TraceLine( eye, GetCenter() + Vector( 0, 0, offset ), MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &traceFilter, &result ); + if (result.fraction == 1.0f) + { + // we can see this area + if (visSpot) + { + *visSpot = GetCenter(); + } + return true; + } + + for( int c=0; cz is not used. +*/ +inline float CNavArea::GetZ( const Vector * RESTRICT pos ) const RESTRICT +{ + return GetZ( pos->x, pos->y ); +} + +inline float CNavArea::GetZ( const Vector & pos ) const RESTRICT +{ + return GetZ( pos.x, pos.y ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Return the coordinates of the area's corner. +*/ +inline Vector CNavArea::GetCorner( NavCornerType corner ) const +{ + // @TODO: Confirm compiler does the "right thing" in release builds, or change this function to to take a pointer [2/4/2009 tom] + Vector pos; + + switch( corner ) + { + default: + Assert( false && "GetCorner: Invalid type" ); + case NORTH_WEST: + return m_nwCorner; + + case NORTH_EAST: + pos.x = m_seCorner.x; + pos.y = m_nwCorner.y; + pos.z = m_neZ; + return pos; + + case SOUTH_WEST: + pos.x = m_nwCorner.x; + pos.y = m_seCorner.y; + pos.z = m_swZ; + return pos; + + case SOUTH_EAST: + return m_seCorner; + } +} + + +#endif // _NAV_AREA_H_ diff --git a/sp/src/game/server/nav_colors.cpp b/sp/src/game/server/nav_colors.cpp new file mode 100644 index 00000000..b302c167 --- /dev/null +++ b/sp/src/game/server/nav_colors.cpp @@ -0,0 +1,182 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Central point for defining colors and drawing routines for Navigation Mesh edit mode +// Author: Matthew Campbell, 2004 + +#include "cbase.h" +#include "nav_colors.h" +#include "Color.h" + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +/** + * This MUST be kept in sync with the NavEditColor definition + */ +Color NavColors[] = +{ + // Degenerate area colors + Color( 255, 255, 255 ), // NavDegenerateFirstColor + Color( 255, 0, 255 ), // NavDegenerateSecondColor + + // Place painting color + Color( 0, 255, 0 ), // NavSamePlaceColor + Color( 0, 0, 255 ), // NavDifferentPlaceColor + Color( 255, 0, 0 ), // NavNoPlaceColor + + // Normal colors + Color( 255, 255, 0 ), // NavSelectedColor + Color( 0, 255, 255 ), // NavMarkedColor + Color( 255, 0, 0 ), // NavNormalColor + Color( 0, 0, 255 ), // NavCornerColor + Color( 0, 0, 255 ), // NavBlockedByDoorColor + Color( 0, 255, 255 ), // NavBlockedByFuncNavBlockerColor + + // Hiding spot colors + Color( 255, 0, 0 ), // NavIdealSniperColor + Color( 255, 0, 255 ), // NavGoodSniperColor + Color( 0, 255, 0 ), // NavGoodCoverColor + Color( 255, 0, 255 ), // NavExposedColor + Color( 255, 100, 0 ), // NavApproachPointColor + + // Connector colors + Color( 0, 255, 255 ), // NavConnectedTwoWaysColor + Color( 0, 0, 255 ), // NavConnectedOneWayColor + Color( 0, 255, 0 ), // NavConnectedContiguous + Color( 255, 0, 0 ), // NavConnectedNonContiguous + + // Editing colors + Color( 255, 255, 255 ), // NavCursorColor + Color( 255, 255, 255 ), // NavSplitLineColor + Color( 0, 255, 255 ), // NavCreationColor + Color( 255, 0, 0 ), // NavInvalidCreationColor + Color( 0, 64, 64 ), // NavGridColor + Color( 255, 255, 255 ), // NavDragSelectionColor + + // Nav attribute colors + Color( 0, 0, 255 ), // NavAttributeCrouchColor + Color( 0, 255, 0 ), // NavAttributeJumpColor + Color( 0, 255, 0 ), // NavAttributePreciseColor + Color( 255, 0, 0 ), // NavAttributeNoJumpColor + Color( 255, 0, 0 ), // NavAttributeStopColor + Color( 0, 0, 255 ), // NavAttributeRunColor + Color( 0, 255, 0 ), // NavAttributeWalkColor + Color( 255, 0, 0 ), // NavAttributeAvoidColor + Color( 0, 200, 0 ), // NavAttributeStairColor +}; + + +//-------------------------------------------------------------------------------------------------------------- +void NavDrawLine( const Vector& from, const Vector& to, NavEditColor navColor ) +{ + const Vector offset( 0, 0, 1 ); + + Color color = NavColors[navColor]; + NDebugOverlay::Line( from + offset, to + offset, color[0], color[1], color[2], false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + NDebugOverlay::Line( from + offset, to + offset, color[0]/2, color[1]/2, color[2]/2, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void NavDrawTriangle( const Vector& point1, const Vector& point2, const Vector& point3, NavEditColor navColor ) +{ + NavDrawLine( point1, point2, navColor ); + NavDrawLine( point2, point3, navColor ); + NavDrawLine( point1, point3, navColor ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void NavDrawFilledTriangle( const Vector& point1, const Vector& point2, const Vector& point3, NavEditColor navColor, bool dark ) +{ + Color color = NavColors[navColor]; + if ( dark ) + { + color[0] = color[0] / 2; + color[1] = color[1] / 2; + color[2] = color[2] / 2; + } + NDebugOverlay::Triangle( point1, point2, point3, color[0], color[1], color[2], 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void NavDrawHorizontalArrow( const Vector& from, const Vector& to, float width, NavEditColor navColor ) +{ + const Vector offset( 0, 0, 1 ); + + Color color = NavColors[navColor]; + NDebugOverlay::HorzArrow( from + offset, to + offset, width, color[0], color[1], color[2], 255, false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + NDebugOverlay::HorzArrow( from + offset, to + offset, width, color[0]/2, color[1]/2, color[2]/2, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void NavDrawDashedLine( const Vector& from, const Vector& to, NavEditColor navColor ) +{ + const Vector offset( 0, 0, 1 ); + + Color color = NavColors[navColor]; + + const float solidLen = 7.0f; + const float gapLen = 3.0f; + + Vector unit = (to - from); + const float totalDistance = unit.NormalizeInPlace(); + + float distance = 0.0f; + + while ( distance < totalDistance ) + { + Vector start = from + unit * distance; + float endDistance = distance + solidLen; + endDistance = MIN( endDistance, totalDistance ); + Vector end = from + unit * endDistance; + + distance += solidLen + gapLen; + + NDebugOverlay::Line( start + offset, end + offset, color[0], color[1], color[2], false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + NDebugOverlay::Line( start + offset, end + offset, color[0]/2, color[1]/2, color[2]/2, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void NavDrawVolume( const Vector &vMin, const Vector &vMax, int zMidline, NavEditColor navColor ) +{ + // Center rectangle + NavDrawLine( Vector( vMax.x, vMax.y, zMidline ), Vector( vMin.x, vMax.y, zMidline ), navColor ); + NavDrawLine( Vector( vMin.x, vMin.y, zMidline ), Vector( vMin.x, vMax.y, zMidline ), navColor ); + NavDrawLine( Vector( vMin.x, vMin.y, zMidline ), Vector( vMax.x, vMin.y, zMidline ), navColor ); + NavDrawLine( Vector( vMax.x, vMax.y, zMidline ), Vector( vMax.x, vMin.y, zMidline ), navColor ); + + // Bottom rectangle + NavDrawLine( Vector( vMax.x, vMax.y, vMin.z ), Vector( vMin.x, vMax.y, vMin.z ), navColor ); + NavDrawLine( Vector( vMin.x, vMin.y, vMin.z ), Vector( vMin.x, vMax.y, vMin.z ), navColor ); + NavDrawLine( Vector( vMin.x, vMin.y, vMin.z ), Vector( vMax.x, vMin.y, vMin.z ), navColor ); + NavDrawLine( Vector( vMax.x, vMax.y, vMin.z ), Vector( vMax.x, vMin.y, vMin.z ), navColor ); + + // Top rectangle + NavDrawLine( Vector( vMax.x, vMax.y, vMax.z ), Vector( vMin.x, vMax.y, vMax.z ), navColor ); + NavDrawLine( Vector( vMin.x, vMin.y, vMax.z ), Vector( vMin.x, vMax.y, vMax.z ), navColor ); + NavDrawLine( Vector( vMin.x, vMin.y, vMax.z ), Vector( vMax.x, vMin.y, vMax.z ), navColor ); + NavDrawLine( Vector( vMax.x, vMax.y, vMax.z ), Vector( vMax.x, vMin.y, vMax.z ), navColor ); + + // Edges + NavDrawLine( Vector( vMax.x, vMax.y, vMin.z ), Vector( vMax.x, vMax.y, vMax.z ), navColor ); + NavDrawLine( Vector( vMin.x, vMin.y, vMin.z ), Vector( vMin.x, vMin.y, vMax.z ), navColor ); + NavDrawLine( Vector( vMax.x, vMin.y, vMin.z ), Vector( vMax.x, vMin.y, vMax.z ), navColor ); + NavDrawLine( Vector( vMin.x, vMax.y, vMin.z ), Vector( vMin.x, vMax.y, vMax.z ), navColor ); +} + + + + +//-------------------------------------------------------------------------------------------------------------- diff --git a/sp/src/game/server/nav_colors.h b/sp/src/game/server/nav_colors.h new file mode 100644 index 00000000..1d3a8a43 --- /dev/null +++ b/sp/src/game/server/nav_colors.h @@ -0,0 +1,77 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Colors used for nav editing + +#ifndef NAV_COLORS_H +#define NAV_COLORS_H + +//-------------------------------------------------------------------------------------------------------------- +enum NavEditColor +{ + // Degenerate area colors + NavDegenerateFirstColor = 0, + NavDegenerateSecondColor, + + // Place painting color + NavSamePlaceColor, + NavDifferentPlaceColor, + NavNoPlaceColor, + + // Normal colors + NavSelectedColor, + NavMarkedColor, + NavNormalColor, + NavCornerColor, + NavBlockedByDoorColor, + NavBlockedByFuncNavBlockerColor, + + // Hiding spot colors + NavIdealSniperColor, + NavGoodSniperColor, + NavGoodCoverColor, + NavExposedColor, + NavApproachPointColor, + + // Connector colors + NavConnectedTwoWaysColor, + NavConnectedOneWayColor, + NavConnectedContiguous, + NavConnectedNonContiguous, + + // Editing colors + NavCursorColor, + NavSplitLineColor, + NavCreationColor, + NavInvalidCreationColor, + NavGridColor, + NavDragSelectionColor, + + // Nav attribute colors + NavAttributeCrouchColor, + NavAttributeJumpColor, + NavAttributePreciseColor, + NavAttributeNoJumpColor, + NavAttributeStopColor, + NavAttributeRunColor, + NavAttributeWalkColor, + NavAttributeAvoidColor, + NavAttributeStairColor, +}; + +//-------------------------------------------------------------------------------------------------------------- + +void NavDrawLine( const Vector& from, const Vector& to, NavEditColor navColor ); +void NavDrawTriangle( const Vector& point1, const Vector& point2, const Vector& point3, NavEditColor navColor ); +void NavDrawFilledTriangle( const Vector& point1, const Vector& point2, const Vector& point3, NavEditColor navColor, bool dark ); +void NavDrawHorizontalArrow( const Vector& from, const Vector& to, float width, NavEditColor navColor ); +void NavDrawDashedLine( const Vector& from, const Vector& to, NavEditColor navColor ); +void NavDrawVolume( const Vector &vMin, const Vector &vMax, int zMidline, NavEditColor navColor ); + +//-------------------------------------------------------------------------------------------------------------- + +#endif // NAV_COLORS_H diff --git a/sp/src/game/server/nav_edit.cpp b/sp/src/game/server/nav_edit.cpp new file mode 100644 index 00000000..4113438d --- /dev/null +++ b/sp/src/game/server/nav_edit.cpp @@ -0,0 +1,3964 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_edit.cpp +// Implementation of Navigation Mesh edit mode +// Author: Michael Booth, 2003-2004 + +#include "cbase.h" +#include "nav_mesh.h" +#include "nav_pathfind.h" +#include "nav_node.h" +#include "nav_colors.h" +#include "Color.h" +#include "tier0/vprof.h" +#include "collisionutils.h" +#include "world.h" +#include "functorutils.h" +#include "team.h" +#ifdef TERROR +#include "TerrorShared.h" +#endif + +#ifdef DOTA_SERVER_DLL +#include "dota_npc_base.h" +#include "dota_player.h" +#endif + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + + +ConVar nav_show_area_info( "nav_show_area_info", "0.5", FCVAR_CHEAT, "Duration in seconds to show nav area ID and attributes while editing" ); +ConVar nav_snap_to_grid( "nav_snap_to_grid", "0", FCVAR_CHEAT, "Snap to the nav generation grid when creating new nav areas" ); +ConVar nav_create_place_on_ground( "nav_create_place_on_ground", "0", FCVAR_CHEAT, "If true, nav areas will be placed flush with the ground when created by hand." ); +#ifdef DEBUG +ConVar nav_draw_limit( "nav_draw_limit", "50", FCVAR_CHEAT, "The maximum number of areas to draw in edit mode" ); +#else +ConVar nav_draw_limit( "nav_draw_limit", "500", FCVAR_CHEAT, "The maximum number of areas to draw in edit mode" ); +#endif +ConVar nav_solid_props( "nav_solid_props", "0", FCVAR_CHEAT, "Make props solid to nav generation/editing" ); +ConVar nav_create_area_at_feet( "nav_create_area_at_feet", "0", FCVAR_CHEAT, "Anchor nav_begin_area Z to editing player's feet" ); + +ConVar nav_drag_selection_volume_zmax_offset( "nav_drag_selection_volume_zmax_offset", "32", FCVAR_REPLICATED, "The offset of the nav drag volume top from center" ); +ConVar nav_drag_selection_volume_zmin_offset( "nav_drag_selection_volume_zmin_offset", "32", FCVAR_REPLICATED, "The offset of the nav drag volume bottom from center" ); + +extern void GetNavUIEditVectors( Vector *pos, Vector *forward ); + +Color s_dragSelectionSetAddColor( 100, 255, 100, 96 ); +Color s_dragSelectionSetDeleteColor( 255, 100, 100, 96 ); + +#if DEBUG_NAV_NODES +extern ConVar nav_show_nodes; +#endif // DEBUG_NAV_NODES + + +//-------------------------------------------------------------------------------------------------------------- +int GetGridSize( bool forceGrid = false ) +{ + if ( TheNavMesh->IsGenerating() ) + { + return (int)GenerationStepSize; + } + + int snapVal = nav_snap_to_grid.GetInt(); + if ( forceGrid && !snapVal ) + { + snapVal = 1; + } + + if ( snapVal == 0 ) + { + return 0; + } + + int scale = (int)GenerationStepSize; + switch ( snapVal ) + { + case 3: + scale = 1; + break; + case 2: + scale = 5; + break; + case 1: + default: + break; + } + + return scale; +} + + +//-------------------------------------------------------------------------------------------------------------- +Vector CNavMesh::SnapToGrid( const Vector& in, bool snapX, bool snapY, bool forceGrid ) const +{ + int scale = GetGridSize( forceGrid ); + if ( !scale ) + { + return in; + } + + Vector out( in ); + + if ( snapX ) + { + out.x = RoundToUnits( in.x, scale ); + } + + if ( snapY ) + { + out.y = RoundToUnits( in.y, scale ); + } + + return out; +} + + +//-------------------------------------------------------------------------------------------------------------- +float CNavMesh::SnapToGrid( float x, bool forceGrid ) const +{ + int scale = GetGridSize( forceGrid ); + if ( !scale ) + { + return x; + } + + x = RoundToUnits( x, scale ); + return x; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::GetEditVectors( Vector *pos, Vector *forward ) +{ + if ( !pos || !forward ) + { + return; + } + + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( !player ) + { + return; + } + + //DOTA places the edit cursor where the 2D cursor is located. +#ifdef DOTA_SERVER_DLL + CDOTAPlayer *pDOTAPlayer = ToDOTAPlayer( player ); + + if ( pDOTAPlayer && pDOTAPlayer->GetMoveType() != MOVETYPE_NOCLIP ) + { + Vector dir = pDOTAPlayer->GetCrosshairTracePos() - player->EyePosition(); + VectorNormalize( dir ); + + *forward = dir; + } + else + { + AngleVectors( player->EyeAngles() + player->GetPunchAngle(), forward ); + } +#else + Vector dir; + AngleVectors( player->EyeAngles() + player->GetPunchAngle(), forward ); +#endif + + *pos = player->EyePosition(); + +#ifdef SERVER_USES_VGUI +// GetNavUIEditVectors( pos, forward ); +#endif // SERVER_USES_VGUI +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Change the edit mode + */ +void CNavMesh::SetEditMode( EditModeType mode ) +{ + m_markedLadder = NULL; + m_markedArea = NULL; + m_markedCorner = NUM_CORNERS; + + m_editMode = mode; + + m_isContinuouslySelecting = false; + m_isContinuouslyDeselecting = false; + m_bIsDragDeselecting = false; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CNavMesh::FindNavAreaOrLadderAlongRay( const Vector &start, const Vector &end, CNavArea **bestArea, CNavLadder **bestLadder, CNavArea *ignore ) +{ + if ( !m_grid.Count() ) + return false; + + Ray_t ray; + ray.Init( start, end, vec3_origin, vec3_origin ); + + *bestArea = NULL; + *bestLadder = NULL; + + float bestDist = 1.0f; // 0..1 fraction + + for ( int i=0; iGetNormal(), right, up ); + right *= ladder->m_width * 0.5f; + left = -right; + + Vector c1 = ladder->m_top + right; + Vector c2 = ladder->m_top + left; + Vector c3 = ladder->m_bottom + right; + Vector c4 = ladder->m_bottom + left; + float dist = IntersectRayWithTriangle( ray, c1, c2, c4, false ); + if ( dist > 0 && dist < bestDist ) + { + *bestLadder = ladder; + bestDist = dist; + } + + dist = IntersectRayWithTriangle( ray, c1, c4, c3, false ); + if ( dist > 0 && dist < bestDist ) + { + *bestLadder = ladder; + bestDist = dist; + } + } + + Extent extent; + extent.lo = extent.hi = start; + extent.Encompass( end ); + + int loX = WorldToGridX( extent.lo.x ); + int loY = WorldToGridY( extent.lo.y ); + int hiX = WorldToGridX( extent.hi.x ); + int hiY = WorldToGridY( extent.hi.y ); + + for( int y = loY; y <= hiY; ++y ) + { + for( int x = loX; x <= hiX; ++x ) + { + NavAreaVector &areaGrid = m_grid[ x + y*m_gridSizeX ]; + + FOR_EACH_VEC( areaGrid, it ) + { + CNavArea *area = areaGrid[ it ]; + if ( area == ignore ) + continue; + + Vector nw = area->m_nwCorner; + Vector se = area->m_seCorner; + Vector ne, sw; + ne.x = se.x; + ne.y = nw.y; + ne.z = area->m_neZ; + sw.x = nw.x; + sw.y = se.y; + sw.z = area->m_swZ; + + float dist = IntersectRayWithTriangle( ray, nw, ne, se, false ); + if ( dist > 0 && dist < bestDist ) + { + *bestArea = area; + bestDist = dist; + } + + dist = IntersectRayWithTriangle( ray, se, sw, nw, false ); + if ( dist > 0 && dist < bestDist ) + { + *bestArea = area; + bestDist = dist; + } + } + } + } + + if ( *bestArea ) + { + *bestLadder = NULL; + } + + return bestDist < 1.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Convenience function to find the nav area a player is looking at, for editing commands + */ +bool CNavMesh::FindActiveNavArea( void ) +{ + VPROF( "CNavMesh::FindActiveNavArea" ); + + m_splitAlongX = false; + m_splitEdge = 0.0f; + m_selectedArea = NULL; + m_climbableSurface = false; + m_selectedLadder = NULL; + + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return false; + + Vector from, dir; + GetEditVectors( &from, &dir ); + + float maxRange = 2000.0f; // 500 + bool isClippingRayAtFeet = false; + if ( nav_create_area_at_feet.GetBool() ) + { + if ( dir.z < 0 ) + { + float eyeHeight = player->GetViewOffset().z; + if ( eyeHeight != 0.0f ) + { + float rayHeight = -dir.z * maxRange; + maxRange = maxRange * eyeHeight / rayHeight; + isClippingRayAtFeet = true; + } + } + } + + Vector to = from + maxRange * dir; + + trace_t result; + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + UTIL_TraceLine( from, to, (nav_solid_props.GetBool()) ? MASK_NPCSOLID : MASK_NPCSOLID_BRUSHONLY, &filter, &result ); + + if (result.fraction != 1.0f) + { + if ( !IsEditMode( CREATING_AREA ) ) + { + m_climbableSurface = physprops->GetSurfaceData( result.surface.surfaceProps )->game.climbable != 0; + if ( !m_climbableSurface ) + { + m_climbableSurface = (result.contents & CONTENTS_LADDER) != 0; + } + m_surfaceNormal = result.plane.normal; + + if ( m_climbableSurface ) + { + // check if we're on the same plane as the original point when we're building a ladder + if ( IsEditMode( CREATING_LADDER ) ) + { + if ( m_surfaceNormal != m_ladderNormal ) + { + m_climbableSurface = false; + } + } + + if ( m_surfaceNormal.z > 0.9f ) + { + m_climbableSurface = false; // don't try to build ladders on flat ground + } + } + } + + if ( ( m_climbableSurface && !IsEditMode( CREATING_LADDER ) ) || !IsEditMode( CREATING_AREA ) ) + { + float closestDistSqr = 200.0f * 200.0f; + + for ( int i=0; im_bottom; + Vector absMax = ladder->m_top; + + Vector left( 0, 0, 0), right(0, 0, 0), up( 0, 0, 0); + VectorVectors( ladder->GetNormal(), right, up ); + right *= ladder->m_width * 0.5f; + left = -right; + + absMin.x += MIN( left.x, right.x ); + absMin.y += MIN( left.y, right.y ); + + absMax.x += MAX( left.x, right.x ); + absMax.y += MAX( left.y, right.y ); + + Extent e; + e.lo = absMin + Vector( -5, -5, -5 ); + e.hi = absMax + Vector( 5, 5, 5 ); + + if ( e.Contains( m_editCursorPos ) ) + { + m_selectedLadder = ladder; + break; + } + + if ( !m_climbableSurface ) + continue; + + Vector p1 = (ladder->m_bottom + ladder->m_top)/2; + Vector p2 = m_editCursorPos; + float distSqr = p1.DistToSqr( p2 ); + + if ( distSqr < closestDistSqr ) + { + m_selectedLadder = ladder; + closestDistSqr = distSqr; + } + } + } + + m_editCursorPos = result.endpos; + + // find the area the player is pointing at + if ( !m_climbableSurface && !m_selectedLadder ) + { + // Try to clip our trace to nav areas + FindNavAreaOrLadderAlongRay( result.startpos, result.endpos + 100.0f * dir, &m_selectedArea, &m_selectedLadder ); // extend a few units into the ground + + // Failing that, get the closest area to the endpoint + if ( !m_selectedArea && !m_selectedLadder ) + { + m_selectedArea = TheNavMesh->GetNearestNavArea( result.endpos, false, 500.0f ); + } + } + + if ( m_selectedArea ) + { + float yaw = player->EyeAngles().y; + while( yaw > 360.0f ) + yaw -= 360.0f; + + while( yaw < 0.0f ) + yaw += 360.0f; + + if ((yaw < 45.0f || yaw > 315.0f) || (yaw > 135.0f && yaw < 225.0f)) + { + m_splitEdge = SnapToGrid( result.endpos.y, true ); + m_splitAlongX = true; + } + else + { + m_splitEdge = SnapToGrid( result.endpos.x, true ); + m_splitAlongX = false; + } + } + + if ( !m_climbableSurface && !IsEditMode( CREATING_LADDER ) ) + { + m_editCursorPos = SnapToGrid( m_editCursorPos ); + } + + return true; + } + else if ( isClippingRayAtFeet ) + { + m_editCursorPos = SnapToGrid( result.endpos ); + } + + if ( IsEditMode( CREATING_LADDER ) || IsEditMode( CREATING_AREA ) ) + return false; + + // We started solid. Look for areas in front of us. + FindNavAreaOrLadderAlongRay( from, to, &m_selectedArea, &m_selectedLadder ); + + return (m_selectedArea != NULL || m_selectedLadder != NULL || isClippingRayAtFeet); +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CNavMesh::FindLadderCorners( Vector *corner1, Vector *corner2, Vector *corner3 ) +{ + if ( !corner1 || !corner2 || !corner3 ) + return false; + + Vector ladderRight, ladderUp; + VectorVectors( m_ladderNormal, ladderRight, ladderUp ); + + Vector from, dir; + GetEditVectors( &from, &dir ); + + const float maxDist = 100000.0f; + + Ray_t ray; + ray.Init( from, from + dir * maxDist, vec3_origin, vec3_origin ); + + *corner1 = m_ladderAnchor + ladderUp * maxDist + ladderRight * maxDist; + *corner2 = m_ladderAnchor + ladderUp * maxDist - ladderRight * maxDist; + *corner3 = m_ladderAnchor - ladderUp * maxDist - ladderRight * maxDist; + float dist = IntersectRayWithTriangle( ray, *corner1, *corner2, *corner3, false ); + if ( dist < 0 ) + { + *corner2 = m_ladderAnchor - ladderUp * maxDist + ladderRight * maxDist; + dist = IntersectRayWithTriangle( ray, *corner1, *corner2, *corner3, false ); + } + + *corner3 = m_editCursorPos; + if ( dist > 0 && dist < maxDist ) + { + *corner3 = from + dir * dist * maxDist; + + float vertDistance = corner3->z - m_ladderAnchor.z; + float val = vertDistance / ladderUp.z; + + *corner1 = m_ladderAnchor + val * ladderUp; + *corner2 = *corner3 - val * ladderUp; + + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CheckForClimbableSurface( const Vector &start, const Vector &end ) +{ + trace_t result; + UTIL_TraceLine( start, end, MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + + bool climbableSurface = false; + if (result.fraction != 1.0f) + { + climbableSurface = physprops->GetSurfaceData( result.surface.surfaceProps )->game.climbable != 0; + if ( !climbableSurface ) + { + climbableSurface = (result.contents & CONTENTS_LADDER) != 0; + } + } + + return climbableSurface; +} + + +//-------------------------------------------------------------------------------------------------------------- +void StepAlongClimbableSurface( Vector &pos, const Vector &increment, const Vector &probe ) +{ + while ( CheckForClimbableSurface( pos + increment - probe, pos + increment + probe ) ) + { + pos += increment; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavBuildLadder( void ) +{ + if ( !IsEditMode( NORMAL ) || !m_climbableSurface ) + { + return; + } + + // We've got a ladder at m_editCursorPos, with a normal of m_surfaceNormal + Vector right, up; + VectorVectors( -m_surfaceNormal, right, up ); + + m_ladderNormal = m_surfaceNormal; + + Vector startPos = m_editCursorPos; + + Vector leftEdge = startPos; + Vector rightEdge = startPos; + + // trace to the sides to find the width + Vector probe = m_surfaceNormal * -HalfHumanWidth; + const float StepSize = 1.0f; + StepAlongClimbableSurface( leftEdge, right * -StepSize, probe ); + StepAlongClimbableSurface( rightEdge, right * StepSize, probe ); + + Vector topEdge = (leftEdge + rightEdge) * 0.5f; + Vector bottomEdge = topEdge; + StepAlongClimbableSurface( topEdge, up * StepSize, probe ); + StepAlongClimbableSurface( bottomEdge, up * -StepSize, probe ); + + Vector top = (leftEdge + rightEdge) * 0.5f; + top.z = topEdge.z; + + Vector bottom = top; + bottom.z = bottomEdge.z; + + CreateLadder( topEdge, bottomEdge, leftEdge.DistTo( rightEdge ), m_ladderNormal.AsVector2D(), 0.0f ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Flood fills all areas with current place + */ +class PlaceFloodFillFunctor +{ +public: + PlaceFloodFillFunctor( CNavArea *area ) + { + m_initialPlace = area->GetPlace(); + } + + bool operator() ( CNavArea *area ) + { + if (area->GetPlace() != m_initialPlace) + return false; + + area->SetPlace( TheNavMesh->GetNavPlace() ); + + return true; + } + +private: + unsigned int m_initialPlace; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called when edit mode has just been enabled + */ +void CNavMesh::OnEditModeStart( void ) +{ + ClearSelectedSet(); + m_isContinuouslySelecting = false; + m_isContinuouslyDeselecting = false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called when edit mode has just been disabled + */ +void CNavMesh::OnEditModeEnd( void ) +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +class DrawSelectedSet +{ +public: + DrawSelectedSet( const Vector &shift ) + { + m_count = 0; + m_shift = shift; + } + + bool operator() ( CNavArea *area ) + { + if (TheNavMesh->IsInSelectedSet( area )) + { + area->DrawSelectedSet( m_shift ); + ++m_count; + } + + return (m_count < nav_draw_limit.GetInt()); + } + + int m_count; + Vector m_shift; +}; + + +//-------------------------------------------------------------------------------------------------------- +class AddToDragSet +{ +public: + AddToDragSet( Extent &area, int zMin, int zMax, bool bDragDeselecting ) + { + m_nTolerance = 1; + m_dragArea = area; + m_zMin = zMin - m_nTolerance; + m_zMax = zMax + m_nTolerance; + m_bDragDeselecting = bDragDeselecting; + } + + bool operator() ( CNavArea *area ) + { + bool bShouldBeInSelectedSet = m_bDragDeselecting; + if ( ( TheNavMesh->IsInSelectedSet( area ) == bShouldBeInSelectedSet ) && area->IsOverlapping( m_dragArea ) && area->GetCenter().z >= m_zMin && area->GetCenter().z <= m_zMax ) + { + TheNavMesh->AddToDragSelectionSet( area ); + } + return true; + } + + Extent m_dragArea; + int m_zMin; + int m_zMax; + int m_nTolerance; + bool m_bDragDeselecting; +}; + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::UpdateDragSelectionSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + Extent dragArea; + int xmin = MIN( m_anchor.x, m_editCursorPos.x ); + int xmax = MAX( m_anchor.x, m_editCursorPos.x ); + int ymin = MIN( m_anchor.y, m_editCursorPos.y ); + int ymax = MAX( m_anchor.y, m_editCursorPos.y ); + + dragArea.lo = Vector( xmin, ymin, m_anchor.z ); + dragArea.hi = Vector( xmax, ymax, m_anchor.z ); + + ClearDragSelectionSet(); + + AddToDragSet add( dragArea, m_anchor.z - m_nDragSelectionVolumeZMin, m_anchor.z + m_nDragSelectionVolumeZMax, m_bIsDragDeselecting ); + ForAllAreas( add ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw navigation areas and edit them + * @todo Clean the whole edit system up - its structure is legacy from peculiarities in GoldSrc. + */ +ConVar nav_show_compass( "nav_show_compass", "0", FCVAR_CHEAT ); +void CNavMesh::DrawEditMode( void ) +{ + VPROF( "CNavMesh::DrawEditMode" ); + + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( IsGenerating() ) + return; + + // TODO: remove this when host_thread_mode 1 stops breaking NDEBUG_PERSIST_TILL_NEXT_SERVER overlays + static ConVarRef host_thread_mode( "host_thread_mode" ); + host_thread_mode.SetValue( 0 ); + + const float maxRange = 1000.0f; // 500 + +#if DEBUG_NAV_NODES + if ( nav_show_nodes.GetBool() ) + { + for ( CNavNode *node = CNavNode::GetFirst(); node != NULL; node = node->GetNext() ) + { + if ( m_editCursorPos.DistToSqr( *node->GetPosition() ) < 150*150 ) + { + node->Draw(); + } + } + } +#endif // DEBUG_NAV_NODES + + Vector from, dir; + GetEditVectors( &from, &dir ); + + Vector to = from + maxRange * dir; + + /* IN_PROGRESS: + if ( !IsEditMode( PLACE_PAINTING ) && nav_snap_to_grid.GetBool() ) + { + Vector center = SnapToGrid( m_editCursorPos ); + + const int GridCount = 3; + const int GridArraySize = GridCount * 2 + 1; + const int GridSize = GetGridSize(); + + // fill in an array of heights for the grid + Vector pos[GridArraySize][GridArraySize]; + int x, y; + for ( x=0; xDrawDragSelectionSet( dragSelectionColor ); + } + } + else if ( IsEditMode( CREATING_LADDER ) ) + { + Vector corner1, corner2, corner3; + if ( FindLadderCorners( &corner1, &corner2, &corner3 ) ) + { + NavEditColor color = NavCreationColor; + if ( !m_climbableSurface ) + { + color = NavInvalidCreationColor; + } + + NavDrawLine( m_ladderAnchor, corner1, color ); + NavDrawLine( corner1, corner3, color ); + NavDrawLine( corner3, corner2, color ); + NavDrawLine( corner2, m_ladderAnchor, color ); + } + } + + if ( m_selectedLadder ) + { + m_lastSelectedArea = NULL; + + // if ladder changed, print its ID + if (m_selectedLadder != m_lastSelectedLadder || nav_show_area_info.GetBool()) + { + m_lastSelectedLadder = m_selectedLadder; + + // print ladder info + char buffer[80]; + + CBaseEntity *ladderEntity = m_selectedLadder->GetLadderEntity(); + if ( ladderEntity ) + { + V_snprintf( buffer, sizeof( buffer ), "Ladder #%d (Team %s)\n", m_selectedLadder->GetID(), GetGlobalTeam( ladderEntity->GetTeamNumber() )->GetName() ); + } + else + { + V_snprintf( buffer, sizeof( buffer ), "Ladder #%d\n", m_selectedLadder->GetID() ); + } + NDebugOverlay::ScreenText( 0.5, 0.53, buffer, 255, 255, 0, 128, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + + // draw the ladder we are pointing at and all connected areas + m_selectedLadder->DrawLadder(); + m_selectedLadder->DrawConnectedAreas(); + } + + if ( m_markedLadder && !IsEditMode( PLACE_PAINTING ) ) + { + // draw the "marked" ladder + m_markedLadder->DrawLadder(); + } + + if ( m_markedArea && !IsEditMode( PLACE_PAINTING ) ) + { + // draw the "marked" area + m_markedArea->Draw(); + } + + // find the area the player is pointing at + if (m_selectedArea) + { + m_lastSelectedLadder = NULL; + + // if area changed, print its ID + if ( m_selectedArea != m_lastSelectedArea ) + { + m_showAreaInfoTimer.Start( nav_show_area_info.GetFloat() ); + m_lastSelectedArea = m_selectedArea; + } + + if (m_showAreaInfoTimer.HasStarted() && !m_showAreaInfoTimer.IsElapsed() ) + { + char buffer[80]; + char attrib[80]; + char locName[80]; + + if (m_selectedArea->GetPlace()) + { + const char *name = TheNavMesh->PlaceToName( m_selectedArea->GetPlace() ); + if (name) + V_strcpy_safe( locName, name ); + else + V_strcpy_safe( locName, "ERROR" ); + } + else + { + locName[0] = '\000'; + } + + if (IsEditMode( PLACE_PAINTING )) + { + attrib[0] = '\000'; + } + else + { + attrib[0] = 0; + int attributes = m_selectedArea->GetAttributes(); + if ( attributes & NAV_MESH_CROUCH ) Q_strncat( attrib, "CROUCH ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_JUMP ) Q_strncat( attrib, "JUMP ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_PRECISE ) Q_strncat( attrib, "PRECISE ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_NO_JUMP ) Q_strncat( attrib, "NO_JUMP ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_STOP ) Q_strncat( attrib, "STOP ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_RUN ) Q_strncat( attrib, "RUN ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_WALK ) Q_strncat( attrib, "WALK ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_AVOID ) Q_strncat( attrib, "AVOID ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_TRANSIENT ) Q_strncat( attrib, "TRANSIENT ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_DONT_HIDE ) Q_strncat( attrib, "DONT_HIDE ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_STAND ) Q_strncat( attrib, "STAND ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_NO_HOSTAGES )Q_strncat( attrib, "NO HOSTAGES ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_STAIRS ) Q_strncat( attrib, "STAIRS ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_OBSTACLE_TOP ) Q_strncat( attrib, "OBSTACLE ", sizeof( attrib ), -1 ); + if ( attributes & NAV_MESH_CLIFF ) Q_strncat( attrib, "CLIFF ", sizeof( attrib ), -1 ); +#ifdef TERROR + if ( attributes & TerrorNavArea::NAV_PLAYERCLIP ) Q_strncat( attrib, "PLAYERCLIP ", sizeof( attrib ), -1 ); + if ( attributes & TerrorNavArea::NAV_BREAKABLEWALL ) Q_strncat( attrib, "BREAKABLEWALL ", sizeof( attrib ), -1 ); + if ( m_selectedArea->IsBlocked( TEAM_SURVIVOR ) ) Q_strncat( attrib, "BLOCKED_SURVIVOR ", sizeof( attrib ), -1 ); + if ( m_selectedArea->IsBlocked( TEAM_ZOMBIE ) ) Q_strncat( attrib, "BLOCKED_ZOMBIE ", sizeof( attrib ), -1 ); +#else + if ( m_selectedArea->IsBlocked( TEAM_ANY ) ) Q_strncat( attrib, "BLOCKED ", sizeof( attrib ), -1 ); +#endif + if ( m_selectedArea->HasAvoidanceObstacle() ) Q_strncat( attrib, "OBSTRUCTED ", sizeof( attrib ), -1 ); + if ( m_selectedArea->IsDamaging() ) Q_strncat( attrib, "DAMAGING ", sizeof( attrib ), -1 ); + if ( m_selectedArea->IsUnderwater() ) Q_strncat( attrib, "UNDERWATER ", sizeof( attrib ), -1 ); + + int connected = 0; + connected += m_selectedArea->GetAdjacentCount( NORTH ); + connected += m_selectedArea->GetAdjacentCount( SOUTH ); + connected += m_selectedArea->GetAdjacentCount( EAST ); + connected += m_selectedArea->GetAdjacentCount( WEST ); + Q_strncat( attrib, UTIL_VarArgs( "%d Connections ", connected ), sizeof( attrib ), -1 ); + } + + Q_snprintf( buffer, sizeof( buffer ), "Area #%d %s %s\n", m_selectedArea->GetID(), locName, attrib ); + NDebugOverlay::ScreenText( 0.5, 0.53, buffer, 255, 255, 0, 128, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + + // do "place painting" + if ( m_isPlacePainting ) + { + if (m_selectedArea->GetPlace() != TheNavMesh->GetNavPlace()) + { + m_selectedArea->SetPlace( TheNavMesh->GetNavPlace() ); + player->EmitSound( "Bot.EditSwitchOn" ); + } + } + } + + + // do continuous selecting into selected set + if ( m_isContinuouslySelecting ) + { + AddToSelectedSet( m_selectedArea ); + } + else if ( m_isContinuouslyDeselecting ) + { + // do continuous de-selecting into selected set + RemoveFromSelectedSet( m_selectedArea ); + } + + + if (IsEditMode( PLACE_PAINTING )) + { + m_selectedArea->DrawConnectedAreas(); + } + else // normal editing mode + { + // draw split line + Extent extent; + m_selectedArea->GetExtent( &extent ); + + float yaw = player->EyeAngles().y; + while( yaw > 360.0f ) + yaw -= 360.0f; + + while( yaw < 0.0f ) + yaw += 360.0f; + + if (m_splitAlongX) + { + from.x = extent.lo.x; + from.y = m_splitEdge; + from.z = m_selectedArea->GetZ( from ); + + to.x = extent.hi.x; + to.y = m_splitEdge; + to.z = m_selectedArea->GetZ( to ); + } + else + { + from.x = m_splitEdge; + from.y = extent.lo.y; + from.z = m_selectedArea->GetZ( from ); + + to.x = m_splitEdge; + to.y = extent.hi.y; + to.z = m_selectedArea->GetZ( to ); + } + + NavDrawLine( from, to, NavSplitLineColor ); + + // draw the area we are pointing at and all connected areas + m_selectedArea->DrawConnectedAreas(); + } + } + + // render the selected set + if (!IsSelectedSetEmpty()) + { + Vector shift( 0, 0, 0 ); + + if (IsEditMode( SHIFTING_XY )) + { + shift = m_editCursorPos - m_anchor; + shift.z = 0.0f; + } + + DrawSelectedSet draw( shift ); + + // if the selected set is small, just blast it out + if (m_selectedSet.Count() < nav_draw_limit.GetInt()) + { + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + draw( area ); + } + } + else + { + // draw the part nearest the player + CNavArea *nearest = NULL; + float nearRange = 9999999999.9f; + + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + float range = (player->GetAbsOrigin() - area->GetCenter()).LengthSqr(); + if (range < nearRange) + { + nearRange = range; + nearest = area; + } + } + + SearchSurroundingAreas( nearest, nearest->GetCenter(), draw, -1, INCLUDE_INCOMING_CONNECTIONS | INCLUDE_BLOCKED_AREAS ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::SetMarkedLadder( CNavLadder *ladder ) +{ + m_markedLadder = ladder; + m_markedArea = NULL; + m_markedCorner = NUM_CORNERS; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::SetMarkedArea( CNavArea *area ) +{ + m_markedLadder = NULL; + m_markedArea = area; + m_markedCorner = NUM_CORNERS; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavDelete( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + if (IsSelectedSetEmpty()) + { + // the old way + CNavArea *markedArea = GetMarkedArea(); + CNavLadder *markedLadder = GetMarkedLadder(); + FindActiveNavArea(); + + if( markedArea ) + { + player->EmitSound( "EDIT_DELETE" ); + TheNavAreas.FindAndRemove( markedArea ); + TheNavMesh->OnEditDestroyNotify( markedArea ); + TheNavMesh->DestroyArea( markedArea ); + } + else if( markedLadder ) + { + player->EmitSound( "EDIT_DELETE" ); + m_ladders.FindAndRemove( markedLadder ); + OnEditDestroyNotify( markedLadder ); + delete markedLadder; + } + else if ( m_selectedArea ) + { + player->EmitSound( "EDIT_DELETE" ); + TheNavAreas.FindAndRemove( m_selectedArea ); + CNavArea *deadArea = m_selectedArea; + OnEditDestroyNotify( deadArea ); + TheNavMesh->DestroyArea( deadArea ); + } + else if ( m_selectedLadder ) + { + player->EmitSound( "EDIT_DELETE" ); + m_ladders.FindAndRemove( m_selectedLadder ); + CNavLadder *deadLadder = m_selectedLadder; + OnEditDestroyNotify( deadLadder ); + delete deadLadder; + } + } + else + { + // delete all areas in the selected set + player->EmitSound( "EDIT_DELETE" ); + + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + TheNavAreas.FindAndRemove( area ); + + OnEditDestroyNotify( area ); + + TheNavMesh->DestroyArea( area ); + } + + Msg( "Deleted %d areas\n", m_selectedSet.Count() ); + + ClearSelectedSet(); + } + + StripNavigationAreas(); + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +class SelectCollector +{ +public: + SelectCollector( void ) + { + m_count = 0; + } + + bool operator() ( CNavArea *area ) + { + // already selected areas terminate flood select + if (TheNavMesh->IsInSelectedSet( area )) + return false; + + TheNavMesh->AddToSelectedSet( area ); + ++m_count; + + return true; + } + + int m_count; +}; + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavDeleteMarked( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + CNavArea *markedArea = GetMarkedArea(); + if( markedArea ) + { + player->EmitSound( "EDIT_DELETE" ); + TheNavMesh->OnEditDestroyNotify( markedArea ); + TheNavAreas.FindAndRemove( markedArea ); + TheNavMesh->DestroyArea( markedArea ); + } + + CNavLadder *markedLadder = GetMarkedLadder(); + if( markedLadder ) + { + player->EmitSound( "EDIT_DELETE" ); + m_ladders.FindAndRemove( markedLadder ); + delete markedLadder; + } + + StripNavigationAreas(); + + ClearSelectedSet(); + + SetMarkedArea( NULL ); // unmark the mark area + SetMarkedLadder( NULL ); // unmark the mark ladder + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Select the current nav area and all recursively connected areas + */ +void CNavMesh::CommandNavFloodSelect( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + FindActiveNavArea(); + + CNavArea *start = m_selectedArea; + if ( !start ) + { + start = m_markedArea; + } + + if ( start ) + { + player->EmitSound( "EDIT_DELETE" ); + + int connections = INCLUDE_BLOCKED_AREAS | INCLUDE_INCOMING_CONNECTIONS; + if ( args.ArgC() == 2 && FStrEq( "out", args[1] ) ) + { + connections = INCLUDE_BLOCKED_AREAS; + } + if ( args.ArgC() == 2 && FStrEq( "in", args[1] ) ) + { + connections = INCLUDE_BLOCKED_AREAS | INCLUDE_INCOMING_CONNECTIONS | EXCLUDE_OUTGOING_CONNECTIONS; + } + + // collect all areas connected to this area + SelectCollector collector; + SearchSurroundingAreas( start, start->GetCenter(), collector, -1, connections ); + + Msg( "Selected %d areas.\n", collector.m_count ); + } + + SetMarkedArea( NULL ); // unmark the mark area +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Toggles all areas into/out of the selected set +*/ +void CNavMesh::CommandNavToggleSelectedSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + player->EmitSound( "EDIT_DELETE" ); + + NavAreaVector notInSelectedSet; + + // Build a list of all areas not in the selected set + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[it]; + if ( !IsInSelectedSet( area ) ) + { + notInSelectedSet.AddToTail( area ); + } + } + + // Clear out the selected set + ClearSelectedSet(); + + // Add areas back into the selected set + FOR_EACH_VEC( notInSelectedSet, nit ) + { + CNavArea *area = notInSelectedSet[nit]; + AddToSelectedSet( area ); + } + + Msg( "Selected %d areas.\n", notInSelectedSet.Count() ); + + SetMarkedArea( NULL ); // unmark the mark area +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Saves the current selected set for later retrieval. +*/ +void CNavMesh::CommandNavStoreSelectedSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + player->EmitSound( "EDIT_DELETE" ); + + m_storedSelectedSet.RemoveAll(); + FOR_EACH_VEC( m_selectedSet, it ) + { + m_storedSelectedSet.AddToTail( m_selectedSet[it]->GetID() ); + } +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Restores an older selected set. +*/ +void CNavMesh::CommandNavRecallSelectedSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + player->EmitSound( "EDIT_DELETE" ); + + ClearSelectedSet(); + + for ( int i=0; iEmitSound( "EDIT_MARK.Enable" ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Add area ID to selected set +*/ +void CNavMesh::CommandNavAddToSelectedSetByID( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) || args.ArgC() < 2 ) + return; + + int id = atoi( args[1] ); + CNavArea *area = GetNavAreaByID( id ); + if ( area ) + { + AddToSelectedSet( area ); + player->EmitSound( "EDIT_MARK.Enable" ); + Msg( "Added area %d. ( to go there: setpos %f %f %f )\n", id, area->GetCenter().x, area->GetCenter().y, area->GetCenter().z + 5 ); + } + else + { + Msg( "No area with id %d\n", id ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Remove current area from selected set + */ +void CNavMesh::CommandNavRemoveFromSelectedSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + RemoveFromSelectedSet( m_selectedArea ); + player->EmitSound( "EDIT_MARK.Disable" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Add/remove current area from selected set +*/ +void CNavMesh::CommandNavToggleInSelectedSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (IsInSelectedSet( m_selectedArea )) + { + RemoveFromSelectedSet( m_selectedArea ); + } + else + { + AddToSelectedSet( m_selectedArea ); + } + player->EmitSound( "EDIT_MARK.Disable" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Clear the selected set to empty + */ +void CNavMesh::CommandNavClearSelectedSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + ClearSelectedSet(); + player->EmitSound( "EDIT_MARK.Disable" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Start continuously selecting areas into the selected set + */ +void CNavMesh::CommandNavBeginSelecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + m_isContinuouslySelecting = true; + m_isContinuouslyDeselecting = false; + + player->EmitSound( "EDIT_BEGIN_AREA.Creating" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Stop continuously selecting areas into the selected set + */ +void CNavMesh::CommandNavEndSelecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + m_isContinuouslySelecting = false; + m_isContinuouslyDeselecting = false; + + player->EmitSound( "EDIT_END_AREA.Creating" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavBeginDragSelecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) && !IsEditMode( DRAG_SELECTING ) ) + return; + + FindActiveNavArea(); + + if ( IsEditMode( DRAG_SELECTING ) ) + { + ClearDragSelectionSet(); + SetEditMode( NORMAL ); + player->EmitSound( "EDIT_BEGIN_AREA.NotCreating" ); + } + else + { + player->EmitSound( "EDIT_BEGIN_AREA.NotCreating" ); + + SetEditMode( DRAG_SELECTING ); + + // m_anchor starting corner + m_anchor = m_editCursorPos; + m_nDragSelectionVolumeZMax = nav_drag_selection_volume_zmax_offset.GetInt(); + m_nDragSelectionVolumeZMin = nav_drag_selection_volume_zmin_offset.GetInt(); + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavEndDragSelecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( IsEditMode( DRAG_SELECTING ) ) + { + // Transfer drag selected areas to the selected set + FOR_EACH_VEC( m_dragSelectionSet, it ) + { + AddToSelectedSet( m_dragSelectionSet[it] ); + } + SetEditMode( NORMAL ); + } + else + { + player->EmitSound( "EDIT_END_AREA.NotCreating" ); + } + + ClearDragSelectionSet(); + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavBeginDragDeselecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) && !IsEditMode( DRAG_SELECTING ) ) + return; + + FindActiveNavArea(); + + if ( IsEditMode( DRAG_SELECTING ) ) + { + ClearDragSelectionSet(); + SetEditMode( NORMAL ); + player->EmitSound( "EDIT_BEGIN_AREA.NotCreating" ); + } + else + { + player->EmitSound( "EDIT_BEGIN_AREA.NotCreating" ); + + SetEditMode( DRAG_SELECTING ); + m_bIsDragDeselecting = true; + + // m_anchor starting corner + m_anchor = m_editCursorPos; + m_nDragSelectionVolumeZMax = nav_drag_selection_volume_zmax_offset.GetInt(); + m_nDragSelectionVolumeZMin = nav_drag_selection_volume_zmin_offset.GetInt(); + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavEndDragDeselecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( IsEditMode( DRAG_SELECTING ) ) + { + // Remove drag selected areas from the selected set + FOR_EACH_VEC( m_dragSelectionSet, it ) + { + RemoveFromSelectedSet( m_dragSelectionSet[it] ); + } + SetEditMode( NORMAL ); + } + else + { + player->EmitSound( "EDIT_END_AREA.NotCreating" ); + } + + ClearDragSelectionSet(); + m_bIsDragDeselecting = false; + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavRaiseDragVolumeMax( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + m_nDragSelectionVolumeZMax += 32; + nav_drag_selection_volume_zmax_offset.SetValue( m_nDragSelectionVolumeZMax ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavLowerDragVolumeMax( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + m_nDragSelectionVolumeZMax = MAX( 0, m_nDragSelectionVolumeZMax - 32 ); + nav_drag_selection_volume_zmax_offset.SetValue( m_nDragSelectionVolumeZMax ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavRaiseDragVolumeMin( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + m_nDragSelectionVolumeZMin = MAX( 0, m_nDragSelectionVolumeZMin - 32 ); + nav_drag_selection_volume_zmin_offset.SetValue( m_nDragSelectionVolumeZMin ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavLowerDragVolumeMin( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + m_nDragSelectionVolumeZMin += 32; + nav_drag_selection_volume_zmin_offset.SetValue( m_nDragSelectionVolumeZMin ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Start/stop continuously selecting areas into the selected set + */ +void CNavMesh::CommandNavToggleSelecting( bool playSound ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + m_isContinuouslySelecting = !m_isContinuouslySelecting; + m_isContinuouslyDeselecting = false; + + if ( playSound ) + { + player->EmitSound( "EDIT_END_AREA.Creating" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Start continuously de-selecting areas from the selected set + */ +void CNavMesh::CommandNavBeginDeselecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + m_isContinuouslyDeselecting = true; + m_isContinuouslySelecting = false; + + player->EmitSound( "EDIT_BEGIN_AREA.Creating" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Stop continuously de-selecting areas from the selected set + */ +void CNavMesh::CommandNavEndDeselecting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + m_isContinuouslyDeselecting = false; + m_isContinuouslySelecting = false; + + player->EmitSound( "EDIT_END_AREA.Creating" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Start/stop continuously de-selecting areas from the selected set + */ +void CNavMesh::CommandNavToggleDeselecting( bool playSound ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + m_isContinuouslyDeselecting = !m_isContinuouslyDeselecting; + m_isContinuouslySelecting = false; + + if ( playSound ) + { + player->EmitSound( "EDIT_END_AREA.Creating" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Selects all areas that intersect the half-space + * Arguments: "+z 100", or "-x 30", etc. + */ +void CNavMesh::CommandNavSelectHalfSpace( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + if ( args.ArgC() != 3 ) + { + Warning( "Error: <+X|-X|+Y|-Y|+Z|-Z> \n" ); + return; + } + + enum HalfSpaceType + { + PLUS_X, MINUS_X, + PLUS_Y, MINUS_Y, + PLUS_Z, MINUS_Z, + } + halfSpace = PLUS_X; + + if (FStrEq( "+x", args[1] )) + { + halfSpace = PLUS_X; + } + else if (FStrEq( "-x", args[1] )) + { + halfSpace = MINUS_X; + } + else if (FStrEq( "+y", args[1] )) + { + halfSpace = PLUS_Y; + } + else if (FStrEq( "-y", args[1] )) + { + halfSpace = MINUS_Y; + } + else if (FStrEq( "+z", args[1] )) + { + halfSpace = PLUS_Z; + } + else if (FStrEq( "-z", args[1] )) + { + halfSpace = MINUS_Z; + } + + float value = atof( args[2] ); + + Extent extent; + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[it]; + area->GetExtent( &extent ); + + switch( halfSpace ) + { + case PLUS_X: + if (extent.lo.x < value && extent.hi.x < value) + { + continue; + } + break; + + case PLUS_Y: + if (extent.lo.y < value && extent.hi.y < value) + { + continue; + } + break; + + case PLUS_Z: + if (extent.lo.z < value && extent.hi.z < value) + { + continue; + } + break; + + case MINUS_X: + if (extent.lo.x > value && extent.hi.x > value) + { + continue; + } + break; + + case MINUS_Y: + if (extent.lo.y > value && extent.hi.y > value) + { + continue; + } + break; + + case MINUS_Z: + if (extent.lo.z > value && extent.hi.z > value) + { + continue; + } + break; + } + + // toggle membership + if ( IsInSelectedSet( area ) ) + { + RemoveFromSelectedSet( area ); + } + else + { + AddToSelectedSet( area ); + } + } + + player->EmitSound( "EDIT_DELETE" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin shifting selected set in the XY plane + */ +void CNavMesh::CommandNavBeginShiftXY( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if (GetEditMode() == SHIFTING_XY) + { + SetEditMode( NORMAL ); + player->EmitSound( "EDIT_END_AREA.Creating" ); + return; + } + else + { + SetEditMode( SHIFTING_XY ); + player->EmitSound( "EDIT_BEGIN_AREA.Creating" ); + } + + // m_anchor starting corner + m_anchor = m_editCursorPos; +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Shift a set of areas, and all connected ladders + */ +class ShiftSet +{ +public: + ShiftSet( const Vector &shift ) + { + m_shift = shift; + } + + bool operator()( CNavArea *area ) + { + area->Shift( m_shift ); + + const NavLadderConnectVector *ladders = area->GetLadders( CNavLadder::LADDER_UP ); + int it; + for( it = 0; it < ladders->Count(); ++it ) + { + CNavLadder *ladder = (*ladders)[ it ].ladder; + if ( !m_ladders.HasElement( ladder ) ) + { + ladder->Shift( m_shift ); + m_ladders.AddToTail( ladder ); + } + } + + ladders = area->GetLadders( CNavLadder::LADDER_DOWN ); + for( it = 0; it < ladders->Count(); ++it ) + { + CNavLadder *ladder = (*ladders)[ it ].ladder; + if ( !m_ladders.HasElement( ladder ) ) + { + ladder->Shift( m_shift ); + m_ladders.AddToTail( ladder ); + } + } + + return true; + } + +private: + CUtlVector< CNavLadder * > m_ladders; + Vector m_shift; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * End shifting selected set in the XY plane + */ +void CNavMesh::CommandNavEndShiftXY( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + SetEditMode( NORMAL ); + + Vector shiftAmount = m_editCursorPos - m_anchor; + shiftAmount.z = 0.0f; + + ShiftSet shift( shiftAmount ); + + // update the position of all areas in the selected set + TheNavMesh->ForAllSelectedAreas( shift ); + + player->EmitSound( "EDIT_END_AREA.Creating" ); +} + + +//-------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_shift, "Shifts the selected areas by the specified amount", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + TheNavMesh->SetEditMode( CNavMesh::NORMAL ); + + Vector shiftAmount( vec3_origin ); + if ( args.ArgC() > 1 ) + { + shiftAmount.x = atoi( args[1] ); + + if ( args.ArgC() > 2 ) + { + shiftAmount.y = atoi( args[2] ); + + if ( args.ArgC() > 3 ) + { + shiftAmount.z = atoi( args[3] ); + } + } + } + + ShiftSet shift( shiftAmount ); + + // update the position of all areas in the selected set + TheNavMesh->ForAllSelectedAreas( shift ); + + player->EmitSound( "EDIT_END_AREA.Creating" ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CommandNavCenterInWorld( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + TheNavMesh->SetEditMode( CNavMesh::NORMAL ); + + // Build the nav mesh's extent + Extent navExtent; + bool first = true; + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[it]; + if ( first ) + { + area->GetExtent( &navExtent ); + first = false; + } + else + { + navExtent.Encompass( area->GetCorner( NORTH_WEST ) ); + navExtent.Encompass( area->GetCorner( NORTH_EAST ) ); + navExtent.Encompass( area->GetCorner( SOUTH_WEST ) ); + navExtent.Encompass( area->GetCorner( SOUTH_EAST ) ); + } + } + + // Get the world's extent + CWorld *world = dynamic_cast< CWorld * >( CBaseEntity::Instance( INDEXENT( 0 ) ) ); + if ( !world ) + return; + + Extent worldExtent; + world->GetWorldBounds( worldExtent.lo, worldExtent.hi ); + + // Compute the difference, and shift in XY + Vector navCenter = ( navExtent.lo + navExtent.hi ) * 0.5f; + Vector worldCenter = ( worldExtent.lo + worldExtent.hi ) * 0.5f; + Vector shift = worldCenter - navCenter; + shift.z = 0.0f; + + // update the position of all areas + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + area->Shift( shift ); + } + + player->EmitSound( "EDIT_END_AREA.Creating" ); + + Msg( "Shifting mesh by %f,%f\n", shift.x, shift.y ); +} +ConCommand nav_world_center( "nav_world_center", CommandNavCenterInWorld, "Centers the nav mesh in the world", FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add invalid areas to selected set + */ +void CNavMesh::CommandNavSelectInvalidAreas( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + ClearSelectedSet(); + + Extent areaExtent; + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area ) + { + area->GetExtent( &areaExtent ); + for ( float x = areaExtent.lo.x; x + GenerationStepSize <= areaExtent.hi.x; x += GenerationStepSize ) + { + for ( float y = areaExtent.lo.y; y + GenerationStepSize <= areaExtent.hi.y; y += GenerationStepSize ) + { + float nw = area->GetZ( x, y ); + float ne = area->GetZ( x + GenerationStepSize, y ); + float sw = area->GetZ( x, y + GenerationStepSize ); + float se = area->GetZ( x + GenerationStepSize, y + GenerationStepSize ); + + if ( !IsHeightDifferenceValid( nw, ne, sw, se ) || + !IsHeightDifferenceValid( ne, nw, sw, se ) || + !IsHeightDifferenceValid( sw, ne, nw, se ) || + !IsHeightDifferenceValid( se, ne, sw, nw ) ) + { + AddToSelectedSet( area ); + } + } + } + } + } + + Msg( "Selected %d areas.\n", m_selectedSet.Count() ); + + if ( m_selectedSet.Count() ) + { + player->EmitSound( "EDIT_MARK.Enable" ); + } + else + { + player->EmitSound( "EDIT_MARK.Disable" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add blocked areas to selected set + */ +void CNavMesh::CommandNavSelectBlockedAreas( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + ClearSelectedSet(); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area && area->IsBlocked( TEAM_ANY ) ) + { + AddToSelectedSet( area ); + } + } + + Msg( "Selected %d areas.\n", m_selectedSet.Count() ); + + if ( m_selectedSet.Count() ) + { + player->EmitSound( "EDIT_MARK.Enable" ); + } + else + { + player->EmitSound( "EDIT_MARK.Disable" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add obstructed areas to selected set + */ +void CNavMesh::CommandNavSelectObstructedAreas( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + ClearSelectedSet(); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area && area->HasAvoidanceObstacle() ) + { + AddToSelectedSet( area ); + } + } + + Msg( "Selected %d areas.\n", m_selectedSet.Count() ); + + if ( m_selectedSet.Count() ) + { + player->EmitSound( "EDIT_MARK.Enable" ); + } + else + { + player->EmitSound( "EDIT_MARK.Disable" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add damaging areas to selected set + */ +void CNavMesh::CommandNavSelectDamagingAreas( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + ClearSelectedSet(); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area && area->IsDamaging() ) + { + AddToSelectedSet( area ); + } + } + + Msg( "Selected %d areas.\n", m_selectedSet.Count() ); + + if ( m_selectedSet.Count() ) + { + player->EmitSound( "EDIT_MARK.Enable" ); + } + else + { + player->EmitSound( "EDIT_MARK.Disable" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Adds stairs areas to the selected set + */ +void CNavMesh::CommandNavSelectStairs( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + ClearSelectedSet(); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area && area->HasAttributes( NAV_MESH_STAIRS ) ) + { + AddToSelectedSet( area ); + } + } + + Msg( "Selected %d areas.\n", m_selectedSet.Count() ); + + if ( m_selectedSet.Count() ) + { + player->EmitSound( "EDIT_MARK.Enable" ); + } + else + { + player->EmitSound( "EDIT_MARK.Disable" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +// Adds areas not connected to mesh to the selected set +void CNavMesh::CommandNavSelectOrphans( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) && !IsEditMode( PLACE_PAINTING ) ) + return; + + FindActiveNavArea(); + + CNavArea *start = m_selectedArea; + if ( !start ) + { + start = m_markedArea; + } + + if ( start ) + { + player->EmitSound( "EDIT_DELETE" ); + + int connections = INCLUDE_BLOCKED_AREAS | INCLUDE_INCOMING_CONNECTIONS; + + // collect all areas connected to this area + SelectCollector collector; + SearchSurroundingAreas( start, start->GetCenter(), collector, -1, connections ); + + // toggle the selected set to reveal the orphans + CommandNavToggleSelectedSet(); + } + + SetMarkedArea( NULL ); // unmark the mark area + +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavSplit( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (m_selectedArea->SplitEdit( m_splitAlongX, m_splitEdge )) + player->EmitSound( "EDIT_SPLIT.MarkedArea" ); + else + player->EmitSound( "EDIT_SPLIT.NoMarkedArea" ); + } + + StripNavigationAreas(); + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +bool MakeSniperSpots( CNavArea *area ) +{ + if ( !area ) + return false; + + bool splitAlongX; + float splitEdge; + + const float minSplitSize = 2.0f; // ensure the first split is larger than this + + float sizeX = area->GetSizeX(); + float sizeY = area->GetSizeY(); + + if ( sizeX > GenerationStepSize && sizeX > sizeY ) + { + splitEdge = RoundToUnits( area->GetCorner( NORTH_WEST ).x, GenerationStepSize ); + if ( splitEdge < area->GetCorner( NORTH_WEST ).x + minSplitSize ) + splitEdge += GenerationStepSize; + splitAlongX = false; + } + else if ( sizeY > GenerationStepSize && sizeY > sizeX ) + { + splitEdge = RoundToUnits( area->GetCorner( NORTH_WEST ).y, GenerationStepSize ); + if ( splitEdge < area->GetCorner( NORTH_WEST ).y + minSplitSize ) + splitEdge += GenerationStepSize; + splitAlongX = true; + } + else + { + return false; + } + + CNavArea *first, *second; + if ( !area->SplitEdit( splitAlongX, splitEdge, &first, &second ) ) + { + return false; + } + + first->Disconnect( second ); + second->Disconnect( first ); + + MakeSniperSpots( first ); + MakeSniperSpots( second ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavMakeSniperSpots( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + // recursively split the area + if ( MakeSniperSpots( m_selectedArea ) ) + { + player->EmitSound( "EDIT_SPLIT.MarkedArea" ); + } + else + { + player->EmitSound( "EDIT_SPLIT.NoMarkedArea" ); + } + } + else + { + player->EmitSound( "EDIT_SPLIT.NoMarkedArea" ); + } + + StripNavigationAreas(); + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavMerge( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + CNavArea *other = m_markedArea; + if ( !m_markedArea && m_selectedSet.Count() == 1 ) + { + other = m_selectedSet[0]; + } + + if ( other && other != m_selectedArea ) + { + if ( m_selectedArea->MergeEdit( other ) ) + player->EmitSound( "EDIT_MERGE.Enable" ); + else + player->EmitSound( "EDIT_MERGE.Disable" ); + } + else + { + Msg( "To merge, mark an area, highlight a second area, then invoke the merge command" ); + player->EmitSound( "EDIT_MERGE.Disable" ); + } + } + + StripNavigationAreas(); + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection + ClearSelectedSet(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavMark( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + if (!IsSelectedSetEmpty()) + { + // add or remove areas from the selected set + if (IsInSelectedSet( m_selectedArea )) + { + // remove from set + player->EmitSound( "EDIT_MARK.Disable" ); + RemoveFromSelectedSet( m_selectedArea ); + } + else + { + // add to set + player->EmitSound( "EDIT_MARK.Enable" ); + AddToSelectedSet( m_selectedArea ); + } + return; + } + + FindActiveNavArea(); + + if ( m_markedArea || m_markedLadder ) + { + // Unmark area or ladder + player->EmitSound( "EDIT_MARK.Enable" ); + Msg("Area unmarked.\n"); + SetMarkedArea( NULL ); + } + else if ( args.ArgC() > 1 ) + { + if ( FStrEq( args[1], "ladder" ) ) + { + if ( args.ArgC() > 2 ) + { + const char *ladderIDNameToMark = args[2]; + if ( ladderIDNameToMark ) + { + unsigned int ladderIDToMark = atoi( ladderIDNameToMark ); + if ( ladderIDToMark != 0 ) + { + CNavLadder *ladder = TheNavMesh->GetLadderByID( ladderIDToMark ); + if ( ladder ) + { + player->EmitSound( "EDIT_MARK.Disable" ); + SetMarkedLadder( ladder ); + + int connected = 0; + connected += m_markedLadder->m_topForwardArea != NULL; + connected += m_markedLadder->m_topLeftArea != NULL; + connected += m_markedLadder->m_topRightArea != NULL; + connected += m_markedLadder->m_topBehindArea != NULL; + connected += m_markedLadder->m_bottomArea != NULL; + + Msg( "Marked Ladder is connected to %d Areas\n", connected ); + } + } + } + } + } + else + { + const char *areaIDNameToMark = args[1]; + if( areaIDNameToMark != NULL ) + { + unsigned int areaIDToMark = atoi(areaIDNameToMark); + if( areaIDToMark != 0 ) + { + CNavArea *areaToMark = NULL; + FOR_EACH_VEC( TheNavAreas, nit ) + { + if( TheNavAreas[nit]->GetID() == areaIDToMark ) + { + areaToMark = TheNavAreas[nit]; + break; + } + } + if( areaToMark ) + { + player->EmitSound( "EDIT_MARK.Disable" ); + SetMarkedArea( areaToMark ); + + int connected = 0; + connected += GetMarkedArea()->GetAdjacentCount( NORTH ); + connected += GetMarkedArea()->GetAdjacentCount( SOUTH ); + connected += GetMarkedArea()->GetAdjacentCount( EAST ); + connected += GetMarkedArea()->GetAdjacentCount( WEST ); + + Msg( "Marked Area is connected to %d other Areas\n", connected ); + } + } + } + } + } + else if ( m_selectedArea ) + { + // Mark an area + player->EmitSound( "EDIT_MARK.Disable" ); + SetMarkedArea( m_selectedArea ); + + int connected = 0; + connected += GetMarkedArea()->GetAdjacentCount( NORTH ); + connected += GetMarkedArea()->GetAdjacentCount( SOUTH ); + connected += GetMarkedArea()->GetAdjacentCount( EAST ); + connected += GetMarkedArea()->GetAdjacentCount( WEST ); + + Msg( "Marked Area is connected to %d other Areas\n", connected ); + } + else if ( m_selectedLadder ) + { + // Mark a ladder + player->EmitSound( "EDIT_MARK.Disable" ); + SetMarkedLadder( m_selectedLadder ); + + int connected = 0; + connected += m_markedLadder->m_topForwardArea != NULL; + connected += m_markedLadder->m_topLeftArea != NULL; + connected += m_markedLadder->m_topRightArea != NULL; + connected += m_markedLadder->m_topBehindArea != NULL; + connected += m_markedLadder->m_bottomArea != NULL; + + Msg( "Marked Ladder is connected to %d Areas\n", connected ); + } + + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavUnmark( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + player->EmitSound( "EDIT_MARK.Enable" ); + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavBeginArea( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !(IsEditMode( CREATING_AREA ) || IsEditMode( CREATING_LADDER ) || IsEditMode( NORMAL )) ) + { + player->EmitSound( "EDIT_END_AREA.NotCreating" ); + return; + } + + FindActiveNavArea(); + + if ( IsEditMode( CREATING_AREA ) ) + { + SetEditMode( NORMAL ); + player->EmitSound( "EDIT_BEGIN_AREA.Creating" ); + } + else if ( IsEditMode( CREATING_LADDER ) ) + { + SetEditMode( NORMAL ); + player->EmitSound( "EDIT_BEGIN_AREA.Creating" ); + } + else if ( m_climbableSurface ) + { + player->EmitSound( "EDIT_BEGIN_AREA.NotCreating" ); + + SetEditMode( CREATING_LADDER ); + + // m_ladderAnchor starting corner + m_ladderAnchor = m_editCursorPos; + m_ladderNormal = m_surfaceNormal; + } + else + { + player->EmitSound( "EDIT_BEGIN_AREA.NotCreating" ); + + SetEditMode( CREATING_AREA ); + + // m_anchor starting corner + m_anchor = m_editCursorPos; + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavEndArea( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !(IsEditMode( CREATING_AREA ) || IsEditMode( CREATING_LADDER ) || IsEditMode( NORMAL )) ) + { + player->EmitSound( "EDIT_END_AREA.NotCreating" ); + return; + } + + if ( IsEditMode( CREATING_AREA ) ) + { + SetEditMode( NORMAL ); + + // create the new nav area + Vector endPos = m_editCursorPos; + endPos.z = m_anchor.z; + + // We're a manually-created area, so let's look around to see what's nearby + CNavArea *nearby = GetMarkedArea(); + if ( !nearby ) + { + nearby = TheNavMesh->GetNearestNavArea( m_editCursorPos + Vector( 0, 0, HalfHumanHeight ), false, 10000.0f, true ); + } + if ( !nearby ) + { + nearby = TheNavMesh->GetNearestNavArea( endPos + Vector( 0, 0, HalfHumanHeight ), false, 10000.0f, true ); + } + if ( !nearby ) + { + nearby = TheNavMesh->GetNearestNavArea( m_editCursorPos ); + } + if ( !nearby ) + { + nearby = TheNavMesh->GetNearestNavArea( endPos ); + } + + CNavArea *newArea = CreateArea(); + if (newArea == NULL) + { + Warning( "NavEndArea: Out of memory\n" ); + player->EmitSound( "EDIT_END_AREA.NotCreating" ); + return; + } + + newArea->Build( m_anchor, endPos ); + + if ( nearby ) + { + newArea->InheritAttributes( nearby ); // inherit from the nearby area + } + + TheNavAreas.AddToTail( newArea ); + TheNavMesh->AddNavArea( newArea ); + player->EmitSound( "EDIT_END_AREA.Creating" ); + + if ( nav_create_place_on_ground.GetBool() ) + { + newArea->PlaceOnGround( NUM_CORNERS ); + } + + // if we have a marked area, inter-connect the two + if (GetMarkedArea()) + { + Extent extent; + GetMarkedArea()->GetExtent( &extent ); + + if (m_anchor.x > extent.hi.x && m_editCursorPos.x > extent.hi.x) + { + GetMarkedArea()->ConnectTo( newArea, EAST ); + newArea->ConnectTo( GetMarkedArea(), WEST ); + } + else if (m_anchor.x < extent.lo.x && m_editCursorPos.x < extent.lo.x) + { + GetMarkedArea()->ConnectTo( newArea, WEST ); + newArea->ConnectTo( GetMarkedArea(), EAST ); + } + else if (m_anchor.y > extent.hi.y && m_editCursorPos.y > extent.hi.y) + { + GetMarkedArea()->ConnectTo( newArea, SOUTH ); + newArea->ConnectTo( GetMarkedArea(), NORTH ); + } + else if (m_anchor.y < extent.lo.y && m_editCursorPos.y < extent.lo.y) + { + GetMarkedArea()->ConnectTo( newArea, NORTH ); + newArea->ConnectTo( GetMarkedArea(), SOUTH ); + } + + // propogate marked area to new area + SetMarkedArea( newArea ); + } + + TheNavMesh->OnEditCreateNotify( newArea ); + } + else if ( IsEditMode( CREATING_LADDER ) ) + { + SetEditMode( NORMAL ); + + player->EmitSound( "EDIT_END_AREA.Creating" ); + + Vector corner1, corner2, corner3; + if ( m_climbableSurface && FindLadderCorners( &corner1, &corner2, &corner3 ) ) + { + // m_ladderAnchor and corner2 are at the same Z, and corner1 & corner3 share Z. + Vector top = (m_ladderAnchor + corner2) * 0.5f; + Vector bottom = (corner1 + corner3) * 0.5f; + if ( top.z < bottom.z ) + { + Vector tmp = top; + top = bottom; + bottom = tmp; + } + + float width = m_ladderAnchor.DistTo( corner2 ); + Vector2D ladderDir = m_surfaceNormal.AsVector2D(); + + CreateLadder( top, bottom, width, ladderDir, HumanHeight ); + } + else + { + player->EmitSound( "EDIT_END_AREA.NotCreating" ); + } + } + else + { + player->EmitSound( "EDIT_END_AREA.NotCreating" ); + } + + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavConnect( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + Vector center; + float halfWidth; + if ( m_selectedSet.Count() > 1 ) + { + bool bValid = true; + for ( int i = 1; i < m_selectedSet.Count(); ++i ) + { + // Make sure all connections are valid + CNavArea *first = m_selectedSet[0]; + CNavArea *second = m_selectedSet[i]; + + NavDirType dir = second->ComputeLargestPortal( first, ¢er, &halfWidth ); + if (dir == NUM_DIRECTIONS) + { + player->EmitSound( "EDIT_CONNECT.AllDirections" ); + bValid = false; + break; + } + + dir = first->ComputeLargestPortal( second, ¢er, &halfWidth ); + if (dir == NUM_DIRECTIONS) + { + player->EmitSound( "EDIT_CONNECT.AllDirections" ); + bValid = false; + break; + } + } + + if ( bValid ) + { + for ( int i = 1; i < m_selectedSet.Count(); ++i ) + { + CNavArea *first = m_selectedSet[0]; + CNavArea *second = m_selectedSet[i]; + + NavDirType dir = second->ComputeLargestPortal( first, ¢er, &halfWidth ); + second->ConnectTo( first, dir ); + + dir = first->ComputeLargestPortal( second, ¢er, &halfWidth ); + first->ConnectTo( second, dir ); + player->EmitSound( "EDIT_CONNECT.Added" ); + } + } + } + else if ( m_selectedArea ) + { + if ( m_markedLadder ) + { + m_markedLadder->ConnectTo( m_selectedArea ); + player->EmitSound( "EDIT_CONNECT.Added" ); + } + else if ( m_markedArea ) + { + NavDirType dir = GetMarkedArea()->ComputeLargestPortal( m_selectedArea, ¢er, &halfWidth ); + if (dir == NUM_DIRECTIONS) + { + player->EmitSound( "EDIT_CONNECT.AllDirections" ); + } + else + { + m_markedArea->ConnectTo( m_selectedArea, dir ); + player->EmitSound( "EDIT_CONNECT.Added" ); + } + } + else + { + if ( m_selectedSet.Count() == 1 ) + { + CNavArea *area = m_selectedSet[0]; + NavDirType dir = area->ComputeLargestPortal( m_selectedArea, ¢er, &halfWidth ); + if (dir == NUM_DIRECTIONS) + { + player->EmitSound( "EDIT_CONNECT.AllDirections" ); + } + else + { + area->ConnectTo( m_selectedArea, dir ); + player->EmitSound( "EDIT_CONNECT.Added" ); + } + } + else + { + Msg( "To connect areas, mark an area, highlight a second area, then invoke the connect command. Make sure the cursor is directly north, south, east, or west of the marked area." ); + player->EmitSound( "EDIT_CONNECT.AllDirections" ); + } + } + } + else if ( m_selectedLadder ) + { + if ( m_markedArea ) + { + m_markedArea->ConnectTo( m_selectedLadder ); + player->EmitSound( "EDIT_CONNECT.Added" ); + } + else + { + Msg( "To connect areas, mark an area, highlight a second area, then invoke the connect command. Make sure the cursor is directly north, south, east, or west of the marked area." ); + player->EmitSound( "EDIT_CONNECT.AllDirections" ); + } + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection + ClearSelectedSet(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavDisconnect( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedSet.Count() > 1 ) + { + bool bValid = true; + for ( int i = 1; i < m_selectedSet.Count(); ++i ) + { + // 2 areas are selected, so connect them bi-directionally + CNavArea *first = m_selectedSet[0]; + CNavArea *second = m_selectedSet[i]; + if ( !first->IsConnected( second, NUM_DIRECTIONS ) && !second->IsConnected( first, NUM_DIRECTIONS ) ) + { + player->EmitSound( "EDIT_CONNECT.AllDirections" ); + bValid = false; + break; + } + } + + if ( bValid ) + { + for ( int i = 1; i < m_selectedSet.Count(); ++i ) + { + // 2 areas are selected, so connect them bi-directionally + CNavArea *first = m_selectedSet[0]; + CNavArea *second = m_selectedSet[i]; + first->Disconnect( second ); + second->Disconnect( first ); + } + player->EmitSound( "EDIT_DISCONNECT.MarkedArea" ); + } + } + else if ( m_selectedArea ) + { + if ( m_markedArea ) + { + m_markedArea->Disconnect( m_selectedArea ); + m_selectedArea->Disconnect( m_markedArea ); + player->EmitSound( "EDIT_DISCONNECT.MarkedArea" ); + } + else if ( m_selectedSet.Count() == 1 ) + { + m_selectedSet[0]->Disconnect( m_selectedArea ); + m_selectedArea->Disconnect( m_selectedSet[0] ); + player->EmitSound( "EDIT_DISCONNECT.MarkedArea" ); + } + else + { + if ( m_markedLadder ) + { + m_markedLadder->Disconnect( m_selectedArea ); + m_selectedArea->Disconnect( m_markedLadder ); + player->EmitSound( "EDIT_DISCONNECT.MarkedArea" ); + } + else + { + Msg( "To disconnect areas, mark an area, highlight a second area, then invoke the disconnect command. This will remove all connections between the two areas." ); + player->EmitSound( "EDIT_DISCONNECT.NoMarkedArea" ); + } + } + } + else if ( m_selectedLadder ) + { + if ( m_markedArea ) + { + m_markedArea->Disconnect( m_selectedLadder ); + m_selectedLadder->Disconnect( m_markedArea ); + player->EmitSound( "EDIT_DISCONNECT.MarkedArea" ); + } + if ( m_selectedSet.Count() == 1 ) + { + m_selectedSet[0]->Disconnect( m_selectedLadder ); + m_selectedLadder->Disconnect( m_selectedSet[0] ); + player->EmitSound( "EDIT_DISCONNECT.MarkedArea" ); + } + else + { + Msg( "To disconnect areas, mark an area, highlight a second area, then invoke the disconnect command. This will remove all connections between the two areas." ); + player->EmitSound( "EDIT_DISCONNECT.NoMarkedArea" ); + } + } + + ClearSelectedSet(); + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +// Disconnect all outgoing one-way connects from each area in the selected set +void CNavMesh::CommandNavDisconnectOutgoingOneWays( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( !player ) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + if ( m_selectedSet.Count() == 0 ) + { + FindActiveNavArea(); + + if ( !m_selectedArea ) + { + return; + } + + m_selectedSet.AddToTail( m_selectedArea ); + } + + for ( int i = 0; i < m_selectedSet.Count(); ++i ) + { + CNavArea *area = m_selectedSet[i]; + + CUtlVector< CNavArea * > adjVector; + area->CollectAdjacentAreas( &adjVector ); + + for( int j=0; jIsConnected( area, NUM_DIRECTIONS ) ) + { + // no connect back - this is a one-way connection + area->Disconnect( adj ); + } + } + } + player->EmitSound( "EDIT_DISCONNECT.MarkedArea" ); + + ClearSelectedSet(); + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavSplice( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (GetMarkedArea()) + { + if (m_selectedArea->SpliceEdit( GetMarkedArea() )) + player->EmitSound( "EDIT_SPLICE.MarkedArea" ); + else + player->EmitSound( "EDIT_SPLICE.NoMarkedArea" ); + } + else + { + Msg( "To splice, mark an area, highlight a second area, then invoke the splice command to create an area between them" ); + player->EmitSound( "EDIT_SPLICE.NoMarkedArea" ); + } + } + + SetMarkedArea( NULL ); // unmark the mark area + ClearSelectedSet(); + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Toggle an attribute on given area + */ +void CNavMesh::DoToggleAttribute( CNavArea *area, NavAttributeType attribute ) +{ + area->SetAttributes( area->GetAttributes() ^ attribute ); + + // keep a list of all "transient" nav areas + if ( attribute == NAV_MESH_TRANSIENT ) + { + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + m_transientAreas.AddToTail( area ); + } + else + { + m_transientAreas.FindAndRemove( area ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavToggleAttribute( NavAttributeType attribute ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + if ( IsSelectedSetEmpty() ) + { + // the old way + FindActiveNavArea(); + + if ( m_selectedArea ) + { + player->EmitSound( "EDIT.ToggleAttribute" ); + DoToggleAttribute( m_selectedArea, attribute ); + } + } + else + { + // toggle the attribute in all areas in the selected set + player->EmitSound( "EDIT.ToggleAttribute" ); + + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + DoToggleAttribute( area, attribute ); + } + + Msg( "Changed attribute in %d areas\n", m_selectedSet.Count() ); + + ClearSelectedSet(); + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavTogglePlaceMode( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( IsEditMode( PLACE_PAINTING ) ) + { + SetEditMode( NORMAL ); + } + else + { + SetEditMode( PLACE_PAINTING ); + } + + player->EmitSound( "EDIT_TOGGLE_PLACE_MODE" ); + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavPlaceFloodFill( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( PLACE_PAINTING ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + PlaceFloodFillFunctor pff( m_selectedArea ); + SearchSurroundingAreas( m_selectedArea, m_selectedArea->GetCenter(), pff ); + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavPlaceSet( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( PLACE_PAINTING ) ) + return; + + if ( !IsSelectedSetEmpty() ) + { + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + area->SetPlace( TheNavMesh->GetNavPlace() ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavPlacePick( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( PLACE_PAINTING ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + player->EmitSound( "EDIT_PLACE_PICK" ); + TheNavMesh->SetNavPlace( m_selectedArea->GetPlace() ); + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavTogglePlacePainting( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( PLACE_PAINTING ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (m_isPlacePainting) + { + m_isPlacePainting = false; + player->EmitSound( "Bot.EditSwitchOff" ); + } + else + { + m_isPlacePainting = true; + + player->EmitSound( "Bot.EditSwitchOn" ); + + // paint the initial area + m_selectedArea->SetPlace( TheNavMesh->GetNavPlace() ); + } + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavMarkUnnamed( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (GetMarkedArea()) + { + player->EmitSound( "EDIT_MARK_UNNAMED.Enable" ); + SetMarkedArea( NULL ); + } + else + { + SetMarkedArea( NULL ); + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area->GetPlace() == 0 ) + { + SetMarkedArea( area ); + break; + } + } + if ( !GetMarkedArea() ) + { + player->EmitSound( "EDIT_MARK_UNNAMED.NoMarkedArea" ); + } + else + { + player->EmitSound( "EDIT_MARK_UNNAMED.MarkedArea" ); + + int connected = 0; + connected += GetMarkedArea()->GetAdjacentCount( NORTH ); + connected += GetMarkedArea()->GetAdjacentCount( SOUTH ); + connected += GetMarkedArea()->GetAdjacentCount( EAST ); + connected += GetMarkedArea()->GetAdjacentCount( WEST ); + + int totalUnnamedAreas = 0; + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + if ( area->GetPlace() == 0 ) + { + ++totalUnnamedAreas; + } + } + + Msg( "Marked Area is connected to %d other Areas - there are %d total unnamed areas\n", connected, totalUnnamedAreas ); + } + } + } + + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavCornerSelect( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (GetMarkedArea()) + { + int corner = (m_markedCorner + 1) % (NUM_CORNERS + 1); + m_markedCorner = (NavCornerType)corner; + player->EmitSound( "EDIT_SELECT_CORNER.MarkedArea" ); + } + else + { + player->EmitSound( "EDIT_SELECT_CORNER.NoMarkedArea" ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavCornerRaise( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + int amount = 1; + if ( args.ArgC() > 1 ) + { + amount = atoi( args[1] ); + } + + if (IsSelectedSetEmpty()) + { + // the old way + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (GetMarkedArea()) + { + GetMarkedArea()->RaiseCorner( m_markedCorner, amount ); + player->EmitSound( "EDIT_MOVE_CORNER.MarkedArea" ); + } + else + { + player->EmitSound( "EDIT_MOVE_CORNER.NoMarkedArea" ); + } + } + } + else + { + // raise all areas in the selected set + player->EmitSound( "EDIT_MOVE_CORNER.MarkedArea" ); + + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + area->RaiseCorner( NUM_CORNERS, amount, false ); + } + + Msg( "Raised %d areas\n", m_selectedSet.Count() ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavCornerLower( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + int amount = -1; + if ( args.ArgC() > 1 ) + { + amount = -atoi( args[1] ); + } + + if (IsSelectedSetEmpty()) + { + // the old way + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if (GetMarkedArea()) + { + GetMarkedArea()->RaiseCorner( m_markedCorner, amount ); + player->EmitSound( "EDIT_MOVE_CORNER.MarkedArea" ); + } + else + { + player->EmitSound( "EDIT_MOVE_CORNER.NoMarkedArea" ); + } + } + } + else + { + // raise all areas in the selected set + player->EmitSound( "EDIT_MOVE_CORNER.MarkedArea" ); + + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + area->RaiseCorner( NUM_CORNERS, amount, false ); + } + + Msg( "Lowered %d areas\n", m_selectedSet.Count() ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavCornerPlaceOnGround( const CCommand &args ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + float inset = 0.0f; + if ( args.ArgC() == 2 ) + { + inset = atof(args[1]); + } + + if (IsSelectedSetEmpty()) + { + // the old way + FindActiveNavArea(); + + if ( m_selectedArea ) + { + if ( m_markedArea ) + { + m_markedArea->PlaceOnGround( m_markedCorner, inset ); + } + else + { + m_selectedArea->PlaceOnGround( NUM_CORNERS, inset ); + } + player->EmitSound( "EDIT_MOVE_CORNER.MarkedArea" ); + } + else + { + player->EmitSound( "EDIT_MOVE_CORNER.NoMarkedArea" ); + } + } + else + { + // snap all areas in the selected set to the ground + player->EmitSound( "EDIT_MOVE_CORNER.MarkedArea" ); + + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + area->PlaceOnGround( NUM_CORNERS, inset ); + } + + Msg( "Placed %d areas on the ground\n", m_selectedSet.Count() ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavWarpToMark( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + CNavArea *targetArea = GetMarkedArea(); + if ( !targetArea && !IsSelectedSetEmpty() ) + { + targetArea = m_selectedSet[0]; + } + + if ( targetArea ) + { + Vector origin = targetArea->GetCenter() + Vector( 0, 0, 0.75f * HumanHeight ); + QAngle angles = player->GetAbsAngles(); + + if ( ( player->IsDead() || player->IsObserver() ) && player->GetObserverMode() == OBS_MODE_ROAMING ) + { + UTIL_SetOrigin( player, origin ); + player->EmitSound( "EDIT_WARP_TO_MARK" ); + } + else + { + player->Teleport( &origin, &angles, &vec3_origin ); + player->EmitSound( "EDIT_WARP_TO_MARK" ); + } + } + else if ( GetMarkedLadder() ) + { + CNavLadder *ladder = GetMarkedLadder(); + + QAngle angles = player->GetAbsAngles(); + Vector origin = (ladder->m_top + ladder->m_bottom)/2; + origin.x += ladder->GetNormal().x * GenerationStepSize; + origin.y += ladder->GetNormal().y * GenerationStepSize; + + if ( ( player->IsDead() || player->IsObserver() ) && player->GetObserverMode() == OBS_MODE_ROAMING ) + { + UTIL_SetOrigin( player, origin ); + player->EmitSound( "EDIT_WARP_TO_MARK" ); + } + else + { + player->Teleport( &origin, &angles, &vec3_origin ); + player->EmitSound( "EDIT_WARP_TO_MARK" ); + } + } + else + { + player->EmitSound( "EDIT_WARP_TO_MARK" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavLadderFlip( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + if ( !IsEditMode( NORMAL ) ) + return; + + FindActiveNavArea(); + + if ( m_selectedLadder ) + { + CNavArea *area; + + // flip direction + player->EmitSound( "EDIT_MOVE_CORNER.MarkedArea" ); + m_selectedLadder->SetDir( OppositeDirection( m_selectedLadder->GetDir() ) ); + + // and reverse ladder's area pointers + area = m_selectedLadder->m_topBehindArea; + m_selectedLadder->m_topBehindArea = m_selectedLadder->m_topForwardArea; + m_selectedLadder->m_topForwardArea = area; + + area = m_selectedLadder->m_topRightArea; + m_selectedLadder->m_topRightArea = m_selectedLadder->m_topLeftArea; + m_selectedLadder->m_topLeftArea = area; + } + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +class RadiusSelect +{ + Vector m_origin; + float m_radiusSquared; + int m_selected; + +public: + RadiusSelect( const Vector &origin, float radius ) + { + m_origin = origin; + m_radiusSquared = radius * radius; + m_selected = 0; + } + + bool operator()( CNavArea *area ) + { + if ( TheNavMesh->IsInSelectedSet( area ) ) + return true; + + Vector close; + area->GetClosestPointOnArea( m_origin, &close ); + if ( close.DistToSqr( m_origin ) < m_radiusSquared ) + { + TheNavMesh->AddToSelectedSet( area ); + ++m_selected; + } + + return true; + } + + int GetNumSelected( void ) const + { + return m_selected; + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_radius, "Adds all areas in a radius to the selection set", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() || engine->IsDedicatedServer() ) + return; + + if ( args.ArgC() < 2 ) + { + Msg( "Needs a radius\n" ); + return; + } + + float radius = atof( args[ 1 ] ); + CBasePlayer *host = UTIL_GetListenServerHost(); + if ( !host ) + return; + + RadiusSelect select( host->GetAbsOrigin(), radius ); + TheNavMesh->ForAllAreas( select ); + + Msg( "%d areas added to selection\n", select.GetNumSelected() ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add area to the currently selected set + */ +void CNavMesh::AddToSelectedSet( CNavArea *area ) +{ + if ( !area ) + return; + + // make sure area is not already in list + if (m_selectedSet.Find( area ) != m_selectedSet.InvalidIndex()) + return; + + m_selectedSet.AddToTail( area ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Remove area from the currently selected set + */ +void CNavMesh::RemoveFromSelectedSet( CNavArea *area ) +{ + m_selectedSet.FindAndRemove( area ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add area to the drag selection set + */ +void CNavMesh::AddToDragSelectionSet( CNavArea *area ) +{ + if ( !area ) + return; + + // make sure area is not already in list + if (m_dragSelectionSet.Find( area ) != m_dragSelectionSet.InvalidIndex()) + return; + + m_dragSelectionSet.AddToTail( area ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Remove area from the drag selection set + */ +void CNavMesh::RemoveFromDragSelectionSet( CNavArea *area ) +{ + m_dragSelectionSet.FindAndRemove( area ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Clear the currently selected set to empty + */ +void CNavMesh::ClearDragSelectionSet( void ) +{ + m_dragSelectionSet.RemoveAll(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Clear the currently selected set to empty + */ +void CNavMesh::ClearSelectedSet( void ) +{ + m_selectedSet.RemoveAll(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the selected set is empty + */ +bool CNavMesh::IsSelectedSetEmpty( void ) const +{ + return (m_selectedSet.Count() == 0); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return size of the selected set + */ +int CNavMesh::GetSelecteSetSize( void ) const +{ + return m_selectedSet.Count(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the selected set + */ +const NavAreaVector &CNavMesh::GetSelectedSet( void ) const +{ + return m_selectedSet; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the given area is in the selected set + */ +bool CNavMesh::IsInSelectedSet( const CNavArea *area ) const +{ + FOR_EACH_VEC( m_selectedSet, it ) + { + const CNavArea *setArea = m_selectedSet[ it ]; + + if (setArea == area) + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when given area has just been added to the mesh in edit mode + */ +void CNavMesh::OnEditCreateNotify( CNavArea *newArea ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + TheNavAreas[ it ]->OnEditCreateNotify( newArea ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when given area has just been deleted from the mesh in edit mode + */ +void CNavMesh::OnEditDestroyNotify( CNavArea *deadArea ) +{ + // clean up any edit hooks + m_markedArea = NULL; + m_selectedArea = NULL; + m_lastSelectedArea = NULL; + m_selectedLadder = NULL; + m_lastSelectedLadder = NULL; + m_markedLadder = NULL; + + m_avoidanceObstacleAreas.FindAndRemove( deadArea ); + m_blockedAreas.FindAndRemove( deadArea ); + + FOR_EACH_VEC( TheNavAreas, it ) + { + TheNavAreas[ it ]->OnEditDestroyNotify( deadArea ); + } + + EditDestroyNotification notification( deadArea ); + ForEachActor( notification ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when given ladder has just been deleted from the mesh in edit mode + * @TODO: Implement me + */ +void CNavMesh::OnEditDestroyNotify( CNavLadder *deadLadder ) +{ +} + + + + diff --git a/sp/src/game/server/nav_entities.cpp b/sp/src/game/server/nav_entities.cpp new file mode 100644 index 00000000..83a7dd83 --- /dev/null +++ b/sp/src/game/server/nav_entities.cpp @@ -0,0 +1,709 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_entities.cpp +// AI Navigation entities +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#include "cbase.h" + +#include "nav_mesh.h" +#include "nav_node.h" +#include "nav_pathfind.h" +#include "nav_colors.h" +#include "fmtstr.h" +#include "props_shared.h" +#include "func_breakablesurf.h" + +#ifdef TERROR +#include "func_elevator.h" +#include "AmbientLight.h" +#endif + +#ifdef TF_DLL +#include "tf_player.h" +#include "bot/tf_bot.h" +#endif + +#include "Color.h" +#include "collisionutils.h" +#include "functorutils.h" +#include "team.h" +#include "nav_entities.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------- +BEGIN_DATADESC( CFuncNavCost ) + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_KEYFIELD( m_iszTags, FIELD_STRING, "tags" ), + DEFINE_KEYFIELD( m_team, FIELD_INTEGER, "team" ), + DEFINE_KEYFIELD( m_isDisabled, FIELD_BOOLEAN, "start_disabled" ), + + DEFINE_THINKFUNC( CostThink ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_nav_avoid, CFuncNavAvoid ); +LINK_ENTITY_TO_CLASS( func_nav_prefer, CFuncNavPrefer ); + +CUtlVector< CHandle< CFuncNavCost > > CFuncNavCost::gm_masterCostVector; +CountdownTimer CFuncNavCost::gm_dirtyTimer; + +#define UPDATE_DIRTY_TIME 0.2f + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavCost::Spawn( void ) +{ + BaseClass::Spawn(); + + gm_masterCostVector.AddToTail( this ); + gm_dirtyTimer.Start( UPDATE_DIRTY_TIME ); + + SetSolid( SOLID_BSP ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); + AddEffects( EF_NODRAW ); + SetCollisionGroup( COLLISION_GROUP_NONE ); + + VPhysicsInitShadow( false, false ); + + SetThink( &CFuncNavCost::CostThink ); + SetNextThink( gpGlobals->curtime + UPDATE_DIRTY_TIME ); + + m_tags.RemoveAll(); + + const char *tags = STRING( m_iszTags ); + + // chop space-delimited string into individual tokens + if ( tags ) + { + char *buffer = new char [ strlen( tags ) + 1 ]; + Q_strcpy( buffer, tags ); + + for( char *token = strtok( buffer, " " ); token; token = strtok( NULL, " " ) ) + { + m_tags.AddToTail( CFmtStr( "%s", token ) ); + } + + delete [] buffer; + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavCost::UpdateOnRemove( void ) +{ + gm_masterCostVector.FindAndFastRemove( this ); + BaseClass::UpdateOnRemove(); + + gm_dirtyTimer.Start( UPDATE_DIRTY_TIME ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavCost::InputEnable( inputdata_t &inputdata ) +{ + m_isDisabled = false; + gm_dirtyTimer.Start( UPDATE_DIRTY_TIME ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavCost::InputDisable( inputdata_t &inputdata ) +{ + m_isDisabled = true; + gm_dirtyTimer.Start( UPDATE_DIRTY_TIME ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavCost::CostThink( void ) +{ + SetNextThink( gpGlobals->curtime + UPDATE_DIRTY_TIME ); + + if ( gm_dirtyTimer.HasStarted() && gm_dirtyTimer.IsElapsed() ) + { + // one or more avoid entities have changed - update nav decoration + gm_dirtyTimer.Invalidate(); + + UpdateAllNavCostDecoration(); + } +} + + +//-------------------------------------------------------------------------------------------------------- +bool CFuncNavCost::HasTag( const char *groupname ) const +{ + for( int i=0; i 0 ) + { + if ( who->GetTeamNumber() != m_team ) + { + return false; + } + } + +#ifdef TF_DLL + // TODO: Make group comparison efficient and move to base combat character + CTFBot *bot = ToTFBot( who ); + if ( bot ) + { + if ( bot->HasTheFlag() ) + { + if ( HasTag( "bomb_carrier" ) ) + { + return true; + } + + // check custom bomb_carrier tags for this bot + for( int i=0; iHasTag( pszTag ) ) + { + return true; + } + } + } + + // the bomb carrier only pays attention to bomb_carrier costs + return false; + } + + if ( bot->HasMission( CTFBot::MISSION_DESTROY_SENTRIES ) ) + { + if ( HasTag( "mission_sentry_buster" ) ) + { + return true; + } + } + + if ( bot->HasMission( CTFBot::MISSION_SNIPER ) ) + { + if ( HasTag( "mission_sniper" ) ) + { + return true; + } + } + + if ( bot->HasMission( CTFBot::MISSION_SPY ) ) + { + if ( HasTag( "mission_spy" ) ) + { + return true; + } + } + + if ( bot->HasMission( CTFBot::MISSION_REPROGRAMMED ) ) + { + return false; + } + + if ( !bot->IsOnAnyMission() ) + { + if ( HasTag( "common" ) ) + { + return true; + } + } + + if ( HasTag( bot->GetPlayerClass()->GetName() ) ) + { + return true; + } + + // check custom tags for this bot + for( int i=0; iHasTag( m_tags[i] ) ) + { + return true; + } + } + + // this cost doesn't apply to me + return false; + } +#endif + + return false; +} + + +//-------------------------------------------------------------------------------------------------------- +// Reevaluate all func_nav_cost entities and update the nav decoration accordingly. +// This is required to handle overlapping func_nav_cost entities. +void CFuncNavCost::UpdateAllNavCostDecoration( void ) +{ + int i, j; + + // first, clear all avoid decoration from the mesh + for( i=0; iClearAllNavCostEntities(); + } + + // now, mark all areas with active cost entities overlapping them + for( i=0; iIsEnabled() ) + { + continue; + } + + Extent extent; + extent.Init( cost ); + + CUtlVector< CNavArea * > overlapVector; + TheNavMesh->CollectAreasOverlappingExtent( extent, &overlapVector ); + + Ray_t ray; + trace_t tr; + ICollideable *pCollide = cost->CollisionProp(); + + for( j=0; jGetCenter(), overlapVector[j]->GetCenter() ); + + enginetrace->ClipRayToCollideable( ray, MASK_ALL, pCollide, &tr ); + + if ( tr.startsolid ) + { + overlapVector[j]->AddFuncNavCostEntity( cost ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------- +// Return pathfind cost multiplier for the given actor +float CFuncNavAvoid::GetCostMultiplier( CBaseCombatCharacter *who ) const +{ + if ( IsApplicableTo( who ) ) + { + return 25.0f; + } + + return 1.0f; +} + + +//-------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------- +// Return pathfind cost multiplier for the given actor +float CFuncNavPrefer::GetCostMultiplier( CBaseCombatCharacter *who ) const +{ + if ( IsApplicableTo( who ) ) + { + return 0.04f; // 1/25th + } + + return 1.0f; +} + + + +//-------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------- +BEGIN_DATADESC( CFuncNavBlocker ) + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "BlockNav", InputBlockNav ), + DEFINE_INPUTFUNC( FIELD_VOID, "UnblockNav", InputUnblockNav ), + DEFINE_KEYFIELD( m_blockedTeamNumber, FIELD_INTEGER, "teamToBlock" ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( func_nav_blocker, CFuncNavBlocker ); + + +CUtlLinkedList CFuncNavBlocker::gm_NavBlockers; + +//----------------------------------------------------------------------------------------------------- +int CFuncNavBlocker::DrawDebugTextOverlays( void ) +{ + int offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + CFmtStr str; + + // FIRST_GAME_TEAM skips TEAM_SPECTATOR and TEAM_UNASSIGNED, so we can print + // useful team names in a non-game-specific fashion. + for ( int i=FIRST_GAME_TEAM; iGetName() ), 0 ); + } + else + { + EntityText( offset++, str.sprintf( "blocking team %d", i ), 0 ); + } + } + } + + NavAreaCollector collector( true ); + Extent extent; + extent.Init( this ); + TheNavMesh->ForAllAreasOverlappingExtent( collector, extent ); + + for ( int i=0; iGetExtent( &areaExtent ); + debugoverlay->AddBoxOverlay( vec3_origin, areaExtent.lo, areaExtent.hi, vec3_angle, 0, 255, 0, 10, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + + return offset; +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavBlocker::UpdateBlocked() +{ + NavAreaCollector collector( true ); + Extent extent; + extent.Init( this ); + TheNavMesh->ForAllAreasOverlappingExtent( collector, extent ); + + for ( int i=0; iUpdateBlocked( true ); + } + +} + + +//-------------------------------------------------------------------------------------------------------- +// Forces nav areas to unblock when the nav blocker is deleted (round restart) so flow can compute properly +void CFuncNavBlocker::UpdateOnRemove( void ) +{ + UnblockNav(); + + gm_NavBlockers.FindAndRemove( this ); + + BaseClass::UpdateOnRemove(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavBlocker::Spawn( void ) +{ + gm_NavBlockers.AddToTail( this ); + + if ( !m_blockedTeamNumber ) + m_blockedTeamNumber = TEAM_ANY; + + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); + AddEffects( EF_NODRAW ); + SetCollisionGroup( COLLISION_GROUP_NONE ); + SetSolid( SOLID_NONE ); + AddSolidFlags( FSOLID_NOT_SOLID ); + CollisionProp()->WorldSpaceAABB( &m_CachedMins, &m_CachedMaxs ); + + if ( m_bDisabled ) + { + UnblockNav(); + } + else + { + BlockNav(); + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavBlocker::InputBlockNav( inputdata_t &inputdata ) +{ + BlockNav(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavBlocker::InputUnblockNav( inputdata_t &inputdata ) +{ + UnblockNav(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavBlocker::BlockNav( void ) +{ + if ( m_blockedTeamNumber == TEAM_ANY ) + { + for ( int i=0; iForAllAreasOverlappingExtent( *this, extent ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavBlocker::UnblockNav( void ) +{ + if ( m_blockedTeamNumber == TEAM_ANY ) + { + for ( int i=0; iMarkAsBlocked( m_blockedTeamNumber, this ); + return true; +} + + +//-------------------------------------------------------------------------------------------------------- +bool CFuncNavBlocker::CalculateBlocked( bool *pResultByTeam, const Vector &vecMins, const Vector &vecMaxs ) +{ + int nTeamsBlocked = 0; + int i; + bool bBlocked = false; + for ( i=0; im_isBlockingNav[i] ) + { + if ( !pResultByTeam[i] ) + { + if ( bIsIntersecting || ( bIsIntersecting = IsBoxIntersectingBox( pBlocker->m_CachedMins, pBlocker->m_CachedMaxs, vecMins, vecMaxs ) ) != false ) + { + bBlocked = true; + pResultByTeam[i] = true; + nTeamsBlocked++; + } + else + { + continue; + } + } + } + } + + if ( nTeamsBlocked == MAX_NAV_TEAMS ) + { + break; + } + } + return bBlocked; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * An entity that can obstruct nav areas. This is meant for semi-transient areas that obstruct + * pathfinding but can be ignored for longer-term queries like computing L4D flow distances and + * escape routes. + */ +class CFuncNavObstruction : public CBaseEntity, public INavAvoidanceObstacle +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CFuncNavObstruction, CBaseEntity ); + +public: + void Spawn(); + virtual void UpdateOnRemove( void ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + virtual bool IsPotentiallyAbleToObstructNavAreas( void ) const { return true; } // could we at some future time obstruct nav? + virtual float GetNavObstructionHeight( void ) const { return JumpCrouchHeight; } // height at which to obstruct nav areas + virtual bool CanObstructNavAreas( void ) const { return !m_bDisabled; } // can we obstruct nav right this instant? + virtual CBaseEntity *GetObstructingEntity( void ) { return this; } + virtual void OnNavMeshLoaded( void ) + { + if ( !m_bDisabled ) + { + ObstructNavAreas(); + } + } + + int DrawDebugTextOverlays( void ); + + bool operator()( CNavArea *area ); // functor that obstructs areas in our extent + +private: + + void ObstructNavAreas( void ); + bool m_bDisabled; +}; + + + +//-------------------------------------------------------------------------------------------------------- +BEGIN_DATADESC( CFuncNavObstruction ) + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( func_nav_avoidance_obstacle, CFuncNavObstruction ); + + +//----------------------------------------------------------------------------------------------------- +int CFuncNavObstruction::DrawDebugTextOverlays( void ) +{ + int offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + if ( CanObstructNavAreas() ) + { + EntityText( offset++, "Obstructing nav", NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + EntityText( offset++, "Not obstructing nav", NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + + return offset; +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavObstruction::UpdateOnRemove( void ) +{ + TheNavMesh->UnregisterAvoidanceObstacle( this ); + + BaseClass::UpdateOnRemove(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavObstruction::Spawn( void ) +{ + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); + AddEffects( EF_NODRAW ); + SetCollisionGroup( COLLISION_GROUP_NONE ); + SetSolid( SOLID_NONE ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + if ( !m_bDisabled ) + { + ObstructNavAreas(); + TheNavMesh->RegisterAvoidanceObstacle( this ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavObstruction::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; + ObstructNavAreas(); + TheNavMesh->RegisterAvoidanceObstacle( this ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavObstruction::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; + TheNavMesh->UnregisterAvoidanceObstacle( this ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CFuncNavObstruction::ObstructNavAreas( void ) +{ + Extent extent; + extent.Init( this ); + TheNavMesh->ForAllAreasOverlappingExtent( *this, extent ); +} + + +//-------------------------------------------------------------------------------------------------------- +// functor that blocks areas in our extent +bool CFuncNavObstruction::operator()( CNavArea *area ) +{ + area->MarkObstacleToAvoid( GetNavObstructionHeight() ); + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- diff --git a/sp/src/game/server/nav_entities.h b/sp/src/game/server/nav_entities.h new file mode 100644 index 00000000..4d7576b2 --- /dev/null +++ b/sp/src/game/server/nav_entities.h @@ -0,0 +1,130 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_entities.h +// Navigation entities +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#ifndef NAV_ENTITIES_H +#define NAV_ENTITIES_H + +//----------------------------------------------------------------------------------------------------- +/** + * An entity that modifies pathfinding cost to all areas it overlaps, to allow map designers + * to tell bots to avoid/prefer certain regions. + */ +class CFuncNavCost : public CBaseEntity +{ +public: + DECLARE_DATADESC(); + DECLARE_CLASS( CFuncNavCost, CBaseEntity ); + + virtual void Spawn( void ); + virtual void UpdateOnRemove( void ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + bool IsEnabled( void ) const { return !m_isDisabled; } + + void CostThink( void ); + + bool IsApplicableTo( CBaseCombatCharacter *who ) const; // Return true if this cost applies to the given actor + + virtual float GetCostMultiplier( CBaseCombatCharacter *who ) const { return 1.0f; } + +protected: + int m_team; + bool m_isDisabled; + string_t m_iszTags; + + static CUtlVector< CHandle< CFuncNavCost > > gm_masterCostVector; + static CountdownTimer gm_dirtyTimer; + void UpdateAllNavCostDecoration( void ); + + CUtlVector< CFmtStr > m_tags; + bool HasTag( const char *groupname ) const; +}; + + +//----------------------------------------------------------------------------------------------------- +class CFuncNavAvoid : public CFuncNavCost +{ +public: + DECLARE_CLASS( CFuncNavAvoid, CFuncNavCost ); + + virtual float GetCostMultiplier( CBaseCombatCharacter *who ) const; // return pathfind cost multiplier for the given actor +}; + + +//----------------------------------------------------------------------------------------------------- +class CFuncNavPrefer : public CFuncNavCost +{ +public: + DECLARE_CLASS( CFuncNavPrefer, CFuncNavCost ); + + virtual float GetCostMultiplier( CBaseCombatCharacter *who ) const; // return pathfind cost multiplier for the given actor +}; + + +//----------------------------------------------------------------------------------------------------- +/** + * An entity that can block/unblock nav areas. This is meant for semi-transient areas that block + * pathfinding but can be ignored for longer-term queries like computing L4D flow distances and + * escape routes. + */ +class CFuncNavBlocker : public CBaseEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CFuncNavBlocker, CBaseEntity ); + +public: + void Spawn(); + virtual void UpdateOnRemove( void ); + + void InputBlockNav( inputdata_t &inputdata ); + void InputUnblockNav( inputdata_t &inputdata ); + + inline bool IsBlockingNav( int teamNumber ) const + { + if ( teamNumber == TEAM_ANY ) + { + bool isBlocked = false; + for ( int i=0; i gm_NavBlockers; + + void BlockNav( void ); + void UnblockNav( void ); + bool m_isBlockingNav[MAX_NAV_TEAMS]; + int m_blockedTeamNumber; + bool m_bDisabled; + Vector m_CachedMins, m_CachedMaxs; + +}; + +#endif // NAV_ENTITIES_H diff --git a/sp/src/game/server/nav_file.cpp b/sp/src/game/server/nav_file.cpp new file mode 100644 index 00000000..7de114ec --- /dev/null +++ b/sp/src/game/server/nav_file.cpp @@ -0,0 +1,1696 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_file.cpp +// Reading and writing nav files +// Author: Michael S. Booth (mike@turtlerockstudios.com), January-September 2003 + +#include "cbase.h" +#include "nav_mesh.h" +#include "gamerules.h" +#include "datacache/imdlcache.h" + +#ifdef TERROR +#include "func_elevator.h" +#endif + +#include "tier1/lzmaDecoder.h" + +#ifdef CSTRIKE_DLL +#include "cs_shareddefs.h" +#include "nav_pathfind.h" +#include "cs_nav_area.h" +#endif + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +/// The current version of the nav file format + +/// IMPORTANT: If this version changes, the swap function in makegamedata +/// must be updated to match. If not, this will break the Xbox 360. +// TODO: Was changed from 15, update when latest 360 code is integrated (MSB 5/5/09) +const int NavCurrentVersion = 16; + +//-------------------------------------------------------------------------------------------------------------- +// +// The 'place directory' is used to save and load places from +// nav files in a size-efficient manner that also allows for the +// order of the place ID's to change without invalidating the +// nav files. +// +// The place directory is stored in the nav file as a list of +// place name strings. Each nav area then contains an index +// into that directory, or zero if no place has been assigned to +// that area. +// +PlaceDirectory::PlaceDirectory( void ) +{ + Reset(); +} + +void PlaceDirectory::Reset( void ) +{ + m_directory.RemoveAll(); + m_hasUnnamedAreas = false; +} + +/// return true if this place is already in the directory +bool PlaceDirectory::IsKnown( Place place ) const +{ + return m_directory.HasElement( place ); +} + +/// return the directory index corresponding to this Place (0 = no entry) +PlaceDirectory::IndexType PlaceDirectory::GetIndex( Place place ) const +{ + if (place == UNDEFINED_PLACE) + return 0; + + int i = m_directory.Find( place ); + + if (i < 0) + { + AssertMsg( false, "PlaceDirectory::GetIndex failure" ); + return 0; + } + + return (IndexType)(i+1); +} + +/// add the place to the directory if not already known +void PlaceDirectory::AddPlace( Place place ) +{ + if (place == UNDEFINED_PLACE) + { + m_hasUnnamedAreas = true; + return; + } + + Assert( place < 1000 ); + + if (IsKnown( place )) + return; + + m_directory.AddToTail( place ); +} + +/// given an index, return the Place +Place PlaceDirectory::IndexToPlace( IndexType entry ) const +{ + if (entry == 0) + return UNDEFINED_PLACE; + + int i = entry-1; + + if (i >= m_directory.Count()) + { + AssertMsg( false, "PlaceDirectory::IndexToPlace: Invalid entry" ); + return UNDEFINED_PLACE; + } + + return m_directory[ i ]; +} + +/// store the directory +void PlaceDirectory::Save( CUtlBuffer &fileBuffer ) +{ + // store number of entries in directory + IndexType count = (IndexType)m_directory.Count(); + fileBuffer.PutUnsignedShort( count ); + + // store entries + for( int i=0; iPlaceToName( m_directory[i] ); + + // store string length followed by string itself + unsigned short len = (unsigned short)(strlen( placeName ) + 1); + fileBuffer.PutUnsignedShort( len ); + fileBuffer.Put( placeName, len ); + } + + fileBuffer.PutUnsignedChar( m_hasUnnamedAreas ); +} + +/// load the directory +void PlaceDirectory::Load( CUtlBuffer &fileBuffer, int version ) +{ + // read number of entries + IndexType count = fileBuffer.GetUnsignedShort(); + + m_directory.RemoveAll(); + + // read each entry + char placeName[256]; + unsigned short len; + for( int i=0; iNameToPlace( placeName ); + if (place == UNDEFINED_PLACE) + { + Warning( "Warning: NavMesh place %s is undefined?\n", placeName ); + } + AddPlace( place ); + } + + if ( version > 11 ) + { + m_hasUnnamedAreas = fileBuffer.GetUnsignedChar() != 0; + } +} + + + +PlaceDirectory placeDirectory; + +#if defined( _X360 ) + #define FORMAT_BSPFILE "maps\\%s.360.bsp" + #define FORMAT_NAVFILE "maps\\%s.360.nav" +#else + #define FORMAT_BSPFILE "maps\\%s.bsp" + #define FORMAT_NAVFILE "maps\\%s.nav" +#endif + +//-------------------------------------------------------------------------------------------------------------- +/** + * Replace extension with "bsp" + */ +char *GetBspFilename( const char *navFilename ) +{ + static char bspFilename[256]; + + Q_snprintf( bspFilename, sizeof( bspFilename ), FORMAT_BSPFILE, STRING( gpGlobals->mapname ) ); + + int len = strlen( bspFilename ); + if (len < 3) + return NULL; + + bspFilename[ len-3 ] = 'b'; + bspFilename[ len-2 ] = 's'; + bspFilename[ len-1 ] = 'p'; + + return bspFilename; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Save a navigation area to the opened binary stream + */ +void CNavArea::Save( CUtlBuffer &fileBuffer, unsigned int version ) const +{ + // save ID + fileBuffer.PutUnsignedInt( m_id ); + + // save attribute flags + fileBuffer.PutInt( m_attributeFlags ); + + // save extent of area + fileBuffer.Put( &m_nwCorner, 3*sizeof(float) ); + fileBuffer.Put( &m_seCorner, 3*sizeof(float) ); + + // save heights of implicit corners + fileBuffer.PutFloat( m_neZ ); + fileBuffer.PutFloat( m_swZ ); + + // save connections to adjacent areas + // in the enum order NORTH, EAST, SOUTH, WEST + for( int d=0; dm_id ); + } + } + + // + // Store hiding spots for this area + // + unsigned char count; + if (m_hidingSpots.Count() > 255) + { + count = 255; + Warning( "Warning: NavArea #%d: Truncated hiding spot list to 255\n", m_id ); + } + else + { + count = (unsigned char)m_hidingSpots.Count(); + } + fileBuffer.PutUnsignedChar( count ); + + // store HidingSpot objects + unsigned int saveCount = 0; + FOR_EACH_VEC( m_hidingSpots, hit ) + { + HidingSpot *spot = m_hidingSpots[ hit ]; + + spot->Save( fileBuffer, version ); + + // overflow check + if (++saveCount == count) + break; + } + + // + // Save encounter spots for this area + // + { + // save number of encounter paths for this area + unsigned int count = m_spotEncounters.Count(); + fileBuffer.PutUnsignedInt( count ); + + SpotEncounter *e; + FOR_EACH_VEC( m_spotEncounters, it ) + { + e = m_spotEncounters[ it ]; + + if (e->from.area) + fileBuffer.PutUnsignedInt( e->from.area->m_id ); + else + fileBuffer.PutUnsignedInt( 0 ); + + unsigned char dir = (unsigned char)e->fromDir; + fileBuffer.PutUnsignedChar( dir ); + + if (e->to.area) + fileBuffer.PutUnsignedInt( e->to.area->m_id ); + else + fileBuffer.PutUnsignedInt( 0 ); + + dir = (unsigned char)e->toDir; + fileBuffer.PutUnsignedChar( dir ); + + // write list of spots along this path + unsigned char spotCount; + if (e->spots.Count() > 255) + { + spotCount = 255; + Warning( "Warning: NavArea #%d: Truncated encounter spot list to 255\n", m_id ); + } + else + { + spotCount = (unsigned char)e->spots.Count(); + } + fileBuffer.PutUnsignedChar( spotCount ); + + saveCount = 0; + FOR_EACH_VEC( e->spots, sit ) + { + SpotOrder *order = &e->spots[ sit ]; + + // order->spot may be NULL if we've loaded a nav mesh that has been edited but not re-analyzed + unsigned int id = (order->spot) ? order->spot->GetID() : 0; + fileBuffer.PutUnsignedInt( id ); + + unsigned char t = (unsigned char)(255 * order->t); + fileBuffer.PutUnsignedChar( t ); + + // overflow check + if (++saveCount == spotCount) + break; + } + } + } + + // store place dictionary entry + PlaceDirectory::IndexType entry = placeDirectory.GetIndex( GetPlace() ); + fileBuffer.Put( &entry, sizeof(entry) ); + + // write out ladder info + int i; + for ( i=0; iGetID(); + fileBuffer.PutUnsignedInt( id ); + } + } + + // save earliest occupy times + for( i=0; iGetID() : 0; + + fileBuffer.PutUnsignedInt( id ); + fileBuffer.PutUnsignedChar( m_potentiallyVisibleAreas[ vit ].attributes ); + } + + // store area we inherit visibility from + unsigned int id = ( m_inheritVisibilityFrom.area ) ? m_inheritVisibilityFrom.area->GetID() : 0; + fileBuffer.PutUnsignedInt( id ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load a navigation area from the file + */ +NavErrorType CNavArea::Load( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ) +{ + // load ID + m_id = fileBuffer.GetUnsignedInt(); + + // update nextID to avoid collisions + if (m_id >= m_nextID) + m_nextID = m_id+1; + + // load attribute flags + if ( version <= 8 ) + { + m_attributeFlags = fileBuffer.GetUnsignedChar(); + } + else if ( version < 13 ) + { + m_attributeFlags = fileBuffer.GetUnsignedShort(); + } + else + { + m_attributeFlags = fileBuffer.GetInt(); + } + + // load extent of area + fileBuffer.Get( &m_nwCorner, 3*sizeof(float) ); + fileBuffer.Get( &m_seCorner, 3*sizeof(float) ); + + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + + DevWarning( "Degenerate Navigation Area #%d at setpos %g %g %g\n", + m_id, m_center.x, m_center.y, m_center.z ); + } + + // load heights of implicit corners + m_neZ = fileBuffer.GetFloat(); + m_swZ = fileBuffer.GetFloat(); + + CheckWaterLevel(); + + // load connections (IDs) to adjacent areas + // in the enum order NORTH, EAST, SOUTH, WEST + for( int d=0; dCreateHidingSpot(); + spot->SetPosition( pos ); + spot->SetFlags( HidingSpot::IN_COVER ); + m_hidingSpots.AddToTail( spot ); + } + } + else + { + // load HidingSpot objects for this area + for( int h=0; hCreateHidingSpot(); + + spot->Load( fileBuffer, version ); + + m_hidingSpots.AddToTail( spot ); + } + } + + if ( version < 15 ) + { + // + // Eat the approach areas + // + int nToEat = fileBuffer.GetUnsignedChar(); + + // load approach area info (IDs) + for( int a=0; afrom.id = fileBuffer.GetUnsignedInt(); + + unsigned char dir = fileBuffer.GetUnsignedChar(); + encounter->fromDir = static_cast( dir ); + + encounter->to.id = fileBuffer.GetUnsignedInt(); + + dir = fileBuffer.GetUnsignedChar(); + encounter->toDir = static_cast( dir ); + + // read list of spots along this path + unsigned char spotCount = fileBuffer.GetUnsignedChar(); + + SpotOrder order; + for( int s=0; sspots.AddToTail( order ); + } + + m_spotEncounters.AddToTail( encounter ); + } + + if (version < 5) + return NAV_OK; + + // + // Load Place data + // + PlaceDirectory::IndexType entry = fileBuffer.GetUnsignedShort(); + + // convert entry to actual Place + SetPlace( placeDirectory.IndexToPlace( entry ) ); + + if ( version < 7 ) + return NAV_OK; + + // load ladder data + for ( int dir=0; dirAllocLevelStaticData( nBytes ), visibleAreaCount ); +*/ + } + + for( unsigned int j=0; jGetLadders().Find( connect.ladder ) == TheNavMesh->GetLadders().InvalidIndex() ) + { + connect.ladder = TheNavMesh->GetLadderByID( id ); + } + + if (id && connect.ladder == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation ladder data. Cannot connect Navigation Areas.\n" ); + error = NAV_CORRUPT_DATA; + } + } + } + + // connect areas together + for( int d=0; did; + connect->area = TheNavMesh->GetNavAreaByID( id ); + if (id && connect->area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Cannot connect Navigation Areas.\n" ); + error = NAV_CORRUPT_DATA; + } + connect->length = ( connect->area->GetCenter() - GetCenter() ).Length(); + } + } + + // resolve spot encounter IDs + SpotEncounter *e; + FOR_EACH_VEC( m_spotEncounters, it ) + { + e = m_spotEncounters[ it ]; + + e->from.area = TheNavMesh->GetNavAreaByID( e->from.id ); + if (e->from.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing \"from\" Navigation Area for Encounter Spot.\n" ); + error = NAV_CORRUPT_DATA; + } + + e->to.area = TheNavMesh->GetNavAreaByID( e->to.id ); + if (e->to.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing \"to\" Navigation Area for Encounter Spot.\n" ); + error = NAV_CORRUPT_DATA; + } + + if (e->from.area && e->to.area) + { + // compute path + float halfWidth; + ComputePortal( e->to.area, e->toDir, &e->path.to, &halfWidth ); + ComputePortal( e->from.area, e->fromDir, &e->path.from, &halfWidth ); + + const float eyeHeight = HalfHumanHeight; + e->path.from.z = e->from.area->GetZ( e->path.from ) + eyeHeight; + e->path.to.z = e->to.area->GetZ( e->path.to ) + eyeHeight; + } + + // resolve HidingSpot IDs + FOR_EACH_VEC( e->spots, sit ) + { + SpotOrder *order = &e->spots[ sit ]; + + order->spot = GetHidingSpotByID( order->id ); + if (order->spot == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Hiding Spot\n" ); + error = NAV_CORRUPT_DATA; + } + } + } + + // convert visible ID's to pointers to actual areas + for ( int it=0; itGetNavAreaByID( info.id ); + if ( info.area == NULL ) + { + Warning( "Invalid area in visible set for area #%d\n", GetID() ); + } + } + + m_inheritVisibilityFrom.area = TheNavMesh->GetNavAreaByID( m_inheritVisibilityFrom.id ); + Assert( m_inheritVisibilityFrom.area != this ); + + // remove any invalid areas from the list + AreaBindInfo bad; + bad.area = NULL; + while( m_potentiallyVisibleAreas.FindAndRemove( bad ) ); + + // func avoid/prefer attributes are controlled by func_nav_cost entities + ClearAllNavCostEntities(); + + return error; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute travel distance along shortest path from startPos to goalPos. + * Return -1 if can't reach endPos from goalPos. + */ +template< typename CostFunctor > +float NavAreaTravelDistance( const Vector &startPos, const Vector &goalPos, CostFunctor &costFunc ) +{ + CNavArea *startArea = TheNavMesh->GetNearestNavArea( startPos ); + if (startArea == NULL) + { + return -1.0f; + } + + // compute path between areas using given cost heuristic + CNavArea *goalArea = NULL; + if (NavAreaBuildPath( startArea, NULL, &goalPos, costFunc, &goalArea ) == false) + { + return -1.0f; + } + + // compute distance along path + if (goalArea->GetParent() == NULL) + { + // both points are in the same area - return euclidean distance + return (goalPos - startPos).Length(); + } + else + { + CNavArea *area; + float distance; + + // goalPos is assumed to be inside goalArea (or very close to it) - skip to next area + area = goalArea->GetParent(); + distance = (goalPos - area->GetCenter()).Length(); + + for( ; area->GetParent(); area = area->GetParent() ) + { + distance += (area->GetCenter() - area->GetParent()->GetCenter()).Length(); + } + + // add in distance to startPos + distance += (startPos - area->GetCenter()).Length(); + + return distance; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine the earliest time this hiding spot can be reached by either team + */ +void CNavArea::ComputeEarliestOccupyTimes( void ) +{ +#ifdef CSTRIKE_DLL + /// @todo Derive cstrike-specific navigation classes + + for( int i=0; iGetAbsOrigin(), m_center, cost ); + if (travelDistance < 0.0f) + continue; + + float travelTime = travelDistance / playerSpeed; + if (travelTime < m_earliestOccupyTime[ team ]) + { + m_earliestOccupyTime[ team ] = travelTime; + } + } + + + // determine the shortest time it will take a CT to reach this area + team = TEAM_CT % MAX_NAV_TEAMS; + for( spot = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); + spot; + spot = gEntList.FindEntityByClassname( spot, "info_player_counterterrorist" ) ) + { + float travelDistance = NavAreaTravelDistance( spot->GetAbsOrigin(), m_center, cost ); + if (travelDistance < 0.0f) + continue; + + float travelTime = travelDistance / playerSpeed; + if (travelTime < m_earliestOccupyTime[ team ]) + { + m_earliestOccupyTime[ team ] = travelTime; + } + } + +#else + for( int i=0; iGetNavArea( tSpawn->GetAbsOrigin() ); + if (tArea == NULL) + continue; + + for( ctSpawn = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); + ctSpawn; + ctSpawn = gEntList.FindEntityByClassname( ctSpawn, "info_player_counterterrorist" ) ) + { + CNavArea *ctArea = TheNavMesh->GetNavArea( ctSpawn->GetAbsOrigin() ); + + if (ctArea == NULL) + continue; + + if (tArea == ctArea) + { + m_isBattlefront = true; + return; + } + + // build path between these two spawn points - assume if path fails, it at least got close + // (ie: imagine spawn points that you jump down from - can't path to) + CNavArea *goalArea = NULL; + NavAreaBuildPath( tArea, ctArea, NULL, cost, &goalArea ); + + if (goalArea == NULL) + continue; + + +/** + * @todo Need to enumerate ALL paths between all pairs of spawn points to find all battlefront areas + */ + + // find the area with the earliest overlapping occupy times + CNavArea *battlefront = NULL; + float earliestTime = 999999.9f; + + const float epsilon = 1.0f; + CNavArea *area; + for( area = goalArea; area; area = area->GetParent() ) + { + if (fabs(area->GetEarliestOccupyTime( TEAM_TERRORIST ) - area->GetEarliestOccupyTime( TEAM_CT )) < epsilon) + { + } + + } + } + } +#endif +#endif +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the filename for this map's "nav map" file + */ +const char *CNavMesh::GetFilename( void ) const +{ + // filename is local to game dir for Steam, so we need to prepend game dir for regular file save + char gamePath[256]; + engine->GetGameDir( gamePath, 256 ); + + // persistant return value + static char filename[256]; + Q_snprintf( filename, sizeof( filename ), "%s\\" FORMAT_NAVFILE, gamePath, STRING( gpGlobals->mapname ) ); + + return filename; +} + +//-------------------------------------------------------------------------------------------------------------- +/* +============ +COM_FixSlashes + +Changes all '/' characters into '\' characters, in place. +============ +*/ +inline void COM_FixSlashes( char *pname ) +{ +#ifdef _WIN32 + while ( *pname ) + { + if ( *pname == '/' ) + *pname = '\\'; + pname++; + } +#else + while ( *pname ) + { + if ( *pname == '\\' ) + *pname = '/'; + pname++; + } +#endif +} + +static void WarnIfMeshNeedsAnalysis( int version ) +{ + // Quick check to warn about needing to analyze: nav_strip, nav_delete, etc set + // every CNavArea's m_approachCount to 0, and delete their m_spotEncounterList. + // So, if no area has either, odds are good we need an analyze. + + if ( version >= 14 ) + { + if ( !TheNavMesh->IsAnalyzed() ) + { + Warning( "The nav mesh needs a full nav_analyze\n" ); + return; + } + } +#ifdef CSTRIKE_DLL + else + { + bool hasApproachAreas = false; + bool hasSpotEncounters = false; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CCSNavArea *area = dynamic_cast< CCSNavArea * >( TheNavAreas[ it ] ); + if ( area ) + { + if ( area->GetApproachInfoCount() ) + { + hasApproachAreas = true; + } + + if ( area->GetSpotEncounterCount() ) + { + hasSpotEncounters = true; + } + } + } + + if ( !hasApproachAreas || !hasSpotEncounters ) + { + Warning( "The nav mesh needs a full nav_analyze\n" ); + } + } +#endif +} + +/** + * Store Navigation Mesh to a file + */ +bool CNavMesh::Save( void ) const +{ + WarnIfMeshNeedsAnalysis( NavCurrentVersion ); + + const char *filename = GetFilename(); + if (filename == NULL) + return false; + + // + // Store the NAV file + // + COM_FixSlashes( const_cast(filename) ); + + // get size of source bsp file for later (before we open the nav file for writing, in + // case of failure) + char *bspFilename = GetBspFilename( filename ); + if (bspFilename == NULL) + { + return false; + } + + CUtlBuffer fileBuffer( 4096, 1024*1024 ); + + // store "magic number" to help identify this kind of file + unsigned int magic = NAV_MAGIC_NUMBER; + fileBuffer.PutUnsignedInt( magic ); + + // store version number of file + // 1 = hiding spots as plain vector array + // 2 = hiding spots as HidingSpot objects + // 3 = Encounter spots use HidingSpot ID's instead of storing vector again + // 4 = Includes size of source bsp file to verify nav data correlation + // ---- Beta Release at V4 ----- + // 5 = Added Place info + // ---- Conversion to Src ------ + // 6 = Added Ladder info + // 7 = Areas store ladder ID's so ladders can have one-way connections + // 8 = Added earliest occupy times (2 floats) to each area + // 9 = Promoted CNavArea's attribute flags to a short + // 10 - Added sub-version number to allow derived classes to have custom area data + // 11 - Added light intensity to each area + // 12 - Storing presence of unnamed areas in the PlaceDirectory + // 13 - Widened NavArea attribute bits from unsigned short to int + // 14 - Added a bool for if the nav needs analysis + // 15 - removed approach areas + // 16 - Added visibility data to the base mesh + fileBuffer.PutUnsignedInt( NavCurrentVersion ); + + // The sub-version number is maintained and owned by classes derived from CNavMesh and CNavArea + // and allows them to track their custom data just as we do at this top level + fileBuffer.PutUnsignedInt( GetSubVersionNumber() ); + + // store the size of source bsp file in the nav file + // so we can test if the bsp changed since the nav file was made + unsigned int bspSize = filesystem->Size( bspFilename ); + DevMsg( "Size of bsp file '%s' is %u bytes.\n", bspFilename, bspSize ); + + fileBuffer.PutUnsignedInt( bspSize ); + + // Store the analysis state + fileBuffer.PutUnsignedChar( m_isAnalyzed ); + + // + // Build a directory of the Places in this map + // + placeDirectory.Reset(); + + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + + Place place = area->GetPlace(); + placeDirectory.AddPlace( place ); + } + + placeDirectory.Save( fileBuffer ); + + SaveCustomDataPreArea( fileBuffer ); + + // + // Store navigation areas + // + { + // store number of areas + unsigned int count = TheNavAreas.Count(); + fileBuffer.PutUnsignedInt( count ); + + // store each area + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + area->Save( fileBuffer, NavCurrentVersion ); + } + } + + // + // Store ladders + // + { + // store number of ladders + unsigned int count = m_ladders.Count(); + fileBuffer.PutUnsignedInt( count ); + + // store each ladder + for ( int i=0; iSave( fileBuffer, NavCurrentVersion ); + } + } + + // + // Store derived class mesh info + // + SaveCustomData( fileBuffer ); + + if ( !filesystem->WriteFile( filename, "MOD", fileBuffer ) ) + { + Warning( "Unable to save %d bytes to %s\n", fileBuffer.Size(), filename ); + return false; + } + + unsigned int navSize = filesystem->Size( filename ); + DevMsg( "Size of nav file '%s' is %u bytes.\n", filename, navSize ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +static NavErrorType CheckNavFile( const char *bspFilename ) +{ + if ( !bspFilename ) + return NAV_CANT_ACCESS_FILE; + + char baseName[256]; + Q_StripExtension(bspFilename,baseName,sizeof(baseName)); + char bspPathname[256]; + Q_snprintf(bspPathname,sizeof(bspPathname), FORMAT_BSPFILE, baseName); + char filename[256]; + Q_snprintf(filename,sizeof(filename), FORMAT_NAVFILE, baseName); + + bool navIsInBsp = false; + FileHandle_t file = filesystem->Open( filename, "rb", "MOD" ); // this ignores .nav files embedded in the .bsp ... + if ( !file ) + { + navIsInBsp = true; + file = filesystem->Open( filename, "rb", "GAME" ); // ... and this looks for one if it's the only one around. + } + + if (!file) + { + return NAV_CANT_ACCESS_FILE; + } + + // check magic number + int result; + unsigned int magic; + result = filesystem->Read( &magic, sizeof(unsigned int), file ); + if (!result || magic != NAV_MAGIC_NUMBER) + { + filesystem->Close( file ); + return NAV_INVALID_FILE; + } + + // read file version number + unsigned int version; + result = filesystem->Read( &version, sizeof(unsigned int), file ); + if (!result || version > NavCurrentVersion || version < 4) + { + filesystem->Close( file ); + return NAV_BAD_FILE_VERSION; + } + + // get size of source bsp file and verify that the bsp hasn't changed + unsigned int saveBspSize; + filesystem->Read( &saveBspSize, sizeof(unsigned int), file ); + + // verify size + unsigned int bspSize = filesystem->Size( bspPathname ); + + if (bspSize != saveBspSize && !navIsInBsp) + { + return NAV_FILE_OUT_OF_DATE; + } + + return NAV_OK; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavCheckFileConsistency( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + FileFindHandle_t findHandle; + const char *bspFilename = filesystem->FindFirstEx( "maps/*.bsp", "MOD", &findHandle ); + while ( bspFilename ) + { + switch ( CheckNavFile( bspFilename ) ) + { + case NAV_CANT_ACCESS_FILE: + Warning( "Missing nav file for %s\n", bspFilename ); + break; + case NAV_INVALID_FILE: + Warning( "Invalid nav file for %s\n", bspFilename ); + break; + case NAV_BAD_FILE_VERSION: + Warning( "Old nav file for %s\n", bspFilename ); + break; + case NAV_FILE_OUT_OF_DATE: + Warning( "The nav file for %s is built from an old version of the map\n", bspFilename ); + break; + case NAV_OK: + Msg( "The nav file for %s is up-to-date\n", bspFilename ); + break; + } + + bspFilename = filesystem->FindNext( findHandle ); + } + filesystem->FindClose( findHandle ); +} +static ConCommand nav_check_file_consistency( "nav_check_file_consistency", CommandNavCheckFileConsistency, "Scans the maps directory and reports any missing/out-of-date navigation files.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reads the used place names from the nav file (can be used to selectively precache before the nav is loaded) + */ +const CUtlVector< Place > *CNavMesh::GetPlacesFromNavFile( bool *hasUnnamedPlaces ) +{ + placeDirectory.Reset(); + // nav filename is derived from map filename + char filename[256]; + Q_snprintf( filename, sizeof( filename ), FORMAT_NAVFILE, STRING( gpGlobals->mapname ) ); + + CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::READ_ONLY ); + if ( !filesystem->ReadFile( filename, "GAME", fileBuffer ) ) // this ignores .nav files embedded in the .bsp ... + { + if ( !filesystem->ReadFile( filename, "BSP", fileBuffer ) ) // ... and this looks for one if it's the only one around. + { + return NULL; + } + } + + if ( IsX360() ) + { + // 360 has compressed NAVs + CLZMA lzma; + if ( lzma.IsCompressed( (unsigned char *)fileBuffer.Base() ) ) + { + int originalSize = lzma.GetActualSize( (unsigned char *)fileBuffer.Base() ); + unsigned char *pOriginalData = new unsigned char[originalSize]; + lzma.Uncompress( (unsigned char *)fileBuffer.Base(), pOriginalData ); + fileBuffer.AssumeMemory( pOriginalData, originalSize, originalSize, CUtlBuffer::READ_ONLY ); + } + } + + // check magic number + unsigned int magic = fileBuffer.GetUnsignedInt(); + if ( !fileBuffer.IsValid() || magic != NAV_MAGIC_NUMBER ) + { + return NULL; // Corrupt nav file? + } + + // read file version number + unsigned int version = fileBuffer.GetUnsignedInt(); + if ( !fileBuffer.IsValid() || version > NavCurrentVersion ) + { + return NULL; // Unknown nav file version + } + + if ( version < 5 ) + { + return NULL; // Too old to have place names + } + + unsigned int subVersion = 0; + if ( version >= 10 ) + { + subVersion = fileBuffer.GetUnsignedInt(); + if ( !fileBuffer.IsValid() ) + { + return NULL; // No sub-version + } + } + + fileBuffer.GetUnsignedInt(); // skip BSP file size + if ( version >= 14 ) + { + fileBuffer.GetUnsignedChar(); // skip m_isAnalyzed + } + + placeDirectory.Load( fileBuffer, version ); + + LoadCustomDataPreArea( fileBuffer, subVersion ); + + if ( hasUnnamedPlaces ) + { + *hasUnnamedPlaces = placeDirectory.HasUnnamedPlaces(); + } + + return placeDirectory.GetPlaces(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load AI navigation data from a file + */ +NavErrorType CNavMesh::Load( void ) +{ + MDLCACHE_CRITICAL_SECTION(); + + // free previous navigation mesh data + Reset(); + placeDirectory.Reset(); + CNavVectorNoEditAllocator::Reset(); + + GameRules()->OnNavMeshLoad(); + + CNavArea::m_nextID = 1; + + // nav filename is derived from map filename + char filename[256]; + Q_snprintf( filename, sizeof( filename ), FORMAT_NAVFILE, STRING( gpGlobals->mapname ) ); + + bool navIsInBsp = false; + CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::READ_ONLY ); + if ( !filesystem->ReadFile( filename, "MOD", fileBuffer ) ) // this ignores .nav files embedded in the .bsp ... + { + navIsInBsp = true; + if ( !filesystem->ReadFile( filename, "BSP", fileBuffer ) ) // ... and this looks for one if it's the only one around. + { + return NAV_CANT_ACCESS_FILE; + } + } + + if ( IsX360() ) + { + // 360 has compressed NAVs + CLZMA lzma; + if ( lzma.IsCompressed( (unsigned char *)fileBuffer.Base() ) ) + { + int originalSize = lzma.GetActualSize( (unsigned char *)fileBuffer.Base() ); + unsigned char *pOriginalData = new unsigned char[originalSize]; + lzma.Uncompress( (unsigned char *)fileBuffer.Base(), pOriginalData ); + fileBuffer.AssumeMemory( pOriginalData, originalSize, originalSize, CUtlBuffer::READ_ONLY ); + } + } + + // check magic number + unsigned int magic = fileBuffer.GetUnsignedInt(); + if ( !fileBuffer.IsValid() || magic != NAV_MAGIC_NUMBER ) + { + Msg( "Invalid navigation file '%s'.\n", filename ); + return NAV_INVALID_FILE; + } + + // read file version number + unsigned int version = fileBuffer.GetUnsignedInt(); + if ( !fileBuffer.IsValid() || version > NavCurrentVersion ) + { + Msg( "Unknown navigation file version.\n" ); + return NAV_BAD_FILE_VERSION; + } + + unsigned int subVersion = 0; + if ( version >= 10 ) + { + subVersion = fileBuffer.GetUnsignedInt(); + if ( !fileBuffer.IsValid() ) + { + Msg( "Error reading sub-version number.\n" ); + return NAV_INVALID_FILE; + } + } + + if ( version >= 4 ) + { + // get size of source bsp file and verify that the bsp hasn't changed + unsigned int saveBspSize = fileBuffer.GetUnsignedInt(); + + // verify size + char *bspFilename = GetBspFilename( filename ); + if ( bspFilename == NULL ) + { + return NAV_INVALID_FILE; + } + + unsigned int bspSize = filesystem->Size( bspFilename ); + + if ( bspSize != saveBspSize && !navIsInBsp ) + { + if ( engine->IsDedicatedServer() ) + { + // Warning doesn't print to the dedicated server console, so we'll use Msg instead + DevMsg( "The Navigation Mesh was built using a different version of this map.\n" ); + } + else + { + DevWarning( "The Navigation Mesh was built using a different version of this map.\n" ); + } + m_isOutOfDate = true; + } + } + + if ( version >= 14 ) + { + m_isAnalyzed = fileBuffer.GetUnsignedChar() != 0; + } + else + { + m_isAnalyzed = false; + } + + // load Place directory + if ( version >= 5 ) + { + placeDirectory.Load( fileBuffer, version ); + } + + LoadCustomDataPreArea( fileBuffer, subVersion ); + + // get number of areas + unsigned int count = fileBuffer.GetUnsignedInt(); + unsigned int i; + + if ( count == 0 ) + { + return NAV_INVALID_FILE; + } + + Extent extent; + extent.lo.x = 9999999999.9f; + extent.lo.y = 9999999999.9f; + extent.hi.x = -9999999999.9f; + extent.hi.y = -9999999999.9f; + + // load the areas and compute total extent + TheNavMesh->PreLoadAreas( count ); + Extent areaExtent; + for( i=0; iCreateArea(); + area->Load( fileBuffer, version, subVersion ); + TheNavAreas.AddToTail( area ); + + area->GetExtent( &areaExtent ); + + if (areaExtent.lo.x < extent.lo.x) + extent.lo.x = areaExtent.lo.x; + if (areaExtent.lo.y < extent.lo.y) + extent.lo.y = areaExtent.lo.y; + if (areaExtent.hi.x > extent.hi.x) + extent.hi.x = areaExtent.hi.x; + if (areaExtent.hi.y > extent.hi.y) + extent.hi.y = areaExtent.hi.y; + } + + // add the areas to the grid + AllocateGrid( extent.lo.x, extent.hi.x, extent.lo.y, extent.hi.y ); + + FOR_EACH_VEC( TheNavAreas, it ) + { + AddNavArea( TheNavAreas[ it ] ); + } + + + // + // Set up all the ladders + // + if (version >= 6) + { + count = fileBuffer.GetUnsignedInt(); + m_ladders.EnsureCapacity( count ); + + // load the ladders + for( i=0; iLoad( fileBuffer, version ); + m_ladders.AddToTail( ladder ); + } + } + else + { + BuildLadders(); + } + + // mark stairways (TODO: this can be removed once all maps are re-saved with this attribute in them) + MarkStairAreas(); + + // + // Load derived class mesh info + // + LoadCustomData( fileBuffer, subVersion ); + + // + // Bind pointers, etc + // + NavErrorType loadResult = PostLoad( version ); + + WarnIfMeshNeedsAnalysis( version ); + + return loadResult; +} + + +struct OneWayLink_t +{ + CNavArea *destArea; + CNavArea *area; + int backD; + + static int Compare(const OneWayLink_t *lhs, const OneWayLink_t *rhs ) + { + int result = ( lhs->destArea - rhs->destArea ); + if ( result != 0 ) + { + return result; + } + return ( lhs->backD - rhs->backD ); + } +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked after all areas have been loaded - for pointer binding, etc + */ +NavErrorType CNavMesh::PostLoad( unsigned int version ) +{ + // allow areas to connect to each other, etc + FOR_EACH_VEC( TheNavAreas, pit ) + { + CNavArea *area = TheNavAreas[ pit ]; + area->PostLoad(); + } + + // allow hiding spots to compute information + FOR_EACH_VEC( TheHidingSpots, hit ) + { + HidingSpot *spot = TheHidingSpots[ hit ]; + spot->PostLoad(); + } + + if ( version < 8 ) + { + // Old nav meshes need to compute earliest occupy times + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + area->ComputeEarliestOccupyTimes(); + } + } + + ComputeBattlefrontAreas(); + + // + // Allow each nav area to know what other areas have one-way connections to it. Need to gather + // then sort due to allocation restrictions on the 360 + // + + + OneWayLink_t oneWayLink; + CUtlVectorFixedGrowable oneWayLinks; + + FOR_EACH_VEC( TheNavAreas, oit ) + { + oneWayLink.area = TheNavAreas[ oit ]; + + for( int d=0; dGetAdjacentAreas( (NavDirType)d ); + + FOR_EACH_VEC( (*connectList), it ) + { + NavConnect connect = (*connectList)[ it ]; + oneWayLink.destArea = connect.area; + + // if the area we connect to has no connection back to us, allow that area to remember us as an incoming connection + oneWayLink.backD = OppositeDirection( (NavDirType)d ); + const NavConnectVector *backConnectList = oneWayLink.destArea->GetAdjacentAreas( (NavDirType)oneWayLink.backD ); + bool isOneWay = true; + FOR_EACH_VEC( (*backConnectList), bit ) + { + NavConnect backConnect = (*backConnectList)[ bit ]; + if (backConnect.area->GetID() == oneWayLink.area->GetID()) + { + isOneWay = false; + break; + } + } + + if (isOneWay) + { + oneWayLinks.AddToTail( oneWayLink ); + } + } + } + } + + oneWayLinks.Sort( &OneWayLink_t::Compare ); + + for ( int i = 0; i < oneWayLinks.Count(); i++ ) + { + // add this one-way connection + oneWayLinks[i].destArea->AddIncomingConnection( oneWayLinks[i].area, (NavDirType)oneWayLinks[i].backD ); + } + + ValidateNavAreaConnections(); + + // TERROR: loading into a map directly creates entities before the mesh is loaded. Tell the preexisting + // entities now that the mesh is loaded so they can update areas. + for ( int i=0; iOnNavMeshLoaded(); + } + + // the Navigation Mesh has been successfully loaded + m_isLoaded = true; + + return NAV_OK; +} diff --git a/sp/src/game/server/nav_generate.cpp b/sp/src/game/server/nav_generate.cpp new file mode 100644 index 00000000..c9d2ea4b --- /dev/null +++ b/sp/src/game/server/nav_generate.cpp @@ -0,0 +1,4934 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_generate.cpp +// Auto-generate a Navigation Mesh by sampling the current map +// Author: Michael S. Booth (mike@turtlerockstudios.com), 2003 + +#include "cbase.h" +#include "util_shared.h" +#include "nav_mesh.h" +#include "nav_node.h" +#include "nav_pathfind.h" +#include "viewport_panel_names.h" +//#include "terror/TerrorShared.h" +#include "fmtstr.h" + +#ifdef TERROR +#include "func_simpleladder.h" +#endif + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + + +enum { MAX_BLOCKED_AREAS = 256 }; +static unsigned int blockedID[ MAX_BLOCKED_AREAS ]; +static int blockedIDCount = 0; +static float lastMsgTime = 0.0f; + +bool TraceAdjacentNode( int depth, const Vector& start, const Vector& end, trace_t *trace, float zLimit = DeathDrop ); +bool StayOnFloor( trace_t *trace, float zLimit = DeathDrop ); + +ConVar nav_slope_limit( "nav_slope_limit", "0.7", FCVAR_CHEAT, "The ground unit normal's Z component must be greater than this for nav areas to be generated." ); +ConVar nav_slope_tolerance( "nav_slope_tolerance", "0.1", FCVAR_CHEAT, "The ground unit normal's Z component must be this close to the nav area's Z component to be generated." ); +ConVar nav_displacement_test( "nav_displacement_test", "10000", FCVAR_CHEAT, "Checks for nodes embedded in displacements (useful for in-development maps)" ); +ConVar nav_generate_fencetops( "nav_generate_fencetops", "1", FCVAR_CHEAT, "Autogenerate nav areas on fence and obstacle tops" ); +ConVar nav_generate_fixup_jump_areas( "nav_generate_fixup_jump_areas", "1", FCVAR_CHEAT, "Convert obsolete jump areas into 2-way connections" ); +ConVar nav_generate_incremental_range( "nav_generate_incremental_range", "2000", FCVAR_CHEAT ); +ConVar nav_generate_incremental_tolerance( "nav_generate_incremental_tolerance", "0", FCVAR_CHEAT, "Z tolerance for adding new nav areas." ); +ConVar nav_area_max_size( "nav_area_max_size", "50", FCVAR_CHEAT, "Max area size created in nav generation" ); + +// Common bounding box for traces +Vector NavTraceMins( -0.45, -0.45, 0 ); +Vector NavTraceMaxs( 0.45, 0.45, HumanCrouchHeight ); +bool FindGroundForNode( Vector *pos, Vector *normal ); // find a ground Z for pos that is clear for NavTraceMins -> NavTraceMaxs + +const float MaxTraversableHeight = StepHeight; // max internal obstacle height that can occur between nav nodes and safely disregarded +const float MinObstacleAreaWidth = 10.0f; // min width of a nav area we will generate on top of an obstacle + +//-------------------------------------------------------------------------------------------------------------- +/** + * Shortest path cost, paying attention to "blocked" areas + */ +class ApproachAreaCost +{ +public: + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator ) + { + // check if this area is "blocked" + for( int i=0; iGetID() == blockedID[i]) + { + return -1.0f; + } + } + + if (fromArea == NULL) + { + // first area in path, no cost + return 0.0f; + } + else + { + // compute distance traveled along path so far + float dist; + + if (ladder) + { + dist = ladder->m_length; + } + else + { + dist = (area->GetCenter() - fromArea->GetCenter()).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + return cost; + } + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Start at given position and find first area in given direction + */ +inline CNavArea *findFirstAreaInDirection( const Vector *start, NavDirType dir, float range, float beneathLimit, CBaseEntity *traceIgnore = NULL, Vector *closePos = NULL ) +{ + CNavArea *area = NULL; + + Vector pos = *start; + + int end = (int)((range / GenerationStepSize) + 0.5f); + + for( int i=1; i<=end; i++ ) + { + AddDirectionVector( &pos, dir, GenerationStepSize ); + + // make sure we dont look thru the wall + trace_t result; + + UTIL_TraceHull( *start, pos, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), traceIgnore, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + break; + + area = TheNavMesh->GetNavArea( pos, beneathLimit ); + if (area) + { + if (closePos) + { + closePos->x = pos.x; + closePos->y = pos.y; + closePos->z = area->GetZ( pos.x, pos.y ); + } + + break; + } + } + + return area; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * For each ladder in the map, create a navigation representation of it. + */ +void CNavMesh::BuildLadders( void ) +{ + // remove any left-over ladders + DestroyLadders(); + +#ifdef TERROR + CFuncSimpleLadder *ladder = NULL; + while( (ladder = dynamic_cast< CFuncSimpleLadder * >(gEntList.FindEntityByClassname( ladder, "func_simpleladder" ))) != NULL ) + { + Vector mins, maxs; + ladder->CollisionProp()->WorldSpaceSurroundingBounds( &mins, &maxs ); + CreateLadder( mins, maxs, 0.0f ); + } +#endif +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Create a navigation representation of a ladder. + */ +void CNavMesh::CreateLadder( const Vector& absMin, const Vector& absMax, float maxHeightAboveTopArea ) +{ + CNavLadder *ladder = new CNavLadder; + + // compute top & bottom of ladder + ladder->m_top.x = (absMin.x + absMax.x) / 2.0f; + ladder->m_top.y = (absMin.y + absMax.y) / 2.0f; + ladder->m_top.z = absMax.z; + + ladder->m_bottom.x = ladder->m_top.x; + ladder->m_bottom.y = ladder->m_top.y; + ladder->m_bottom.z = absMin.z; + + // determine facing - assumes "normal" runged ladder + float xSize = absMax.x - absMin.x; + float ySize = absMax.y - absMin.y; + trace_t result; + if (xSize > ySize) + { + // ladder is facing north or south - determine which way + // "pull in" traceline from bottom and top in case ladder abuts floor and/or ceiling + Vector from = ladder->m_bottom + Vector( 0.0f, GenerationStepSize, GenerationStepSize/2 ); + Vector to = ladder->m_top + Vector( 0.0f, GenerationStepSize, -GenerationStepSize/2 ); + + UTIL_TraceLine( from, to, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction != 1.0f || result.startsolid) + ladder->SetDir( NORTH ); + else + ladder->SetDir( SOUTH ); + + ladder->m_width = xSize; + } + else + { + // ladder is facing east or west - determine which way + Vector from = ladder->m_bottom + Vector( GenerationStepSize, 0.0f, GenerationStepSize/2 ); + Vector to = ladder->m_top + Vector( GenerationStepSize, 0.0f, -GenerationStepSize/2 ); + + UTIL_TraceLine( from, to, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction != 1.0f || result.startsolid) + ladder->SetDir( WEST ); + else + ladder->SetDir( EAST ); + + ladder->m_width = ySize; + } + + // adjust top and bottom of ladder to make sure they are reachable + // (cs_office has a crate right in front of the base of a ladder) + Vector along = ladder->m_top - ladder->m_bottom; + float length = along.NormalizeInPlace(); + Vector on, out; + const float minLadderClearance = 32.0f; + + // adjust bottom to bypass blockages + const float inc = 10.0f; + float t; + for( t = 0.0f; t <= length; t += inc ) + { + on = ladder->m_bottom + t * along; + + out = on + ladder->GetNormal() * minLadderClearance; + + UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f && !result.startsolid) + { + // found viable ladder bottom + ladder->m_bottom = on; + break; + } + } + + // adjust top to bypass blockages + for( t = 0.0f; t <= length; t += inc ) + { + on = ladder->m_top - t * along; + + out = on + ladder->GetNormal() * minLadderClearance; + + UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f && !result.startsolid) + { + // found viable ladder top + ladder->m_top = on; + break; + } + } + + ladder->m_length = (ladder->m_top - ladder->m_bottom).Length(); + + ladder->SetDir( ladder->GetDir() ); // now that we've adjusted the top and bottom, re-check the normal + + ladder->m_bottomArea = NULL; + ladder->m_topForwardArea = NULL; + ladder->m_topLeftArea = NULL; + ladder->m_topRightArea = NULL; + ladder->m_topBehindArea = NULL; + ladder->ConnectGeneratedLadder( maxHeightAboveTopArea ); + + // add ladder to global list + m_ladders.AddToTail( ladder ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Create a navigation representation of a ladder. + */ +void CNavMesh::CreateLadder( const Vector &top, const Vector &bottom, float width, const Vector2D &ladderDir, float maxHeightAboveTopArea ) +{ + CNavLadder *ladder = new CNavLadder; + + ladder->m_top = top; + ladder->m_bottom = bottom; + ladder->m_width = width; + if ( fabs( ladderDir.x ) > fabs( ladderDir.y ) ) + { + if ( ladderDir.x > 0.0f ) + { + ladder->SetDir( EAST ); + } + else + { + ladder->SetDir( WEST ); + } + } + else + { + if ( ladderDir.y > 0.0f ) + { + ladder->SetDir( SOUTH ); + } + else + { + ladder->SetDir( NORTH ); + } + } + + // adjust top and bottom of ladder to make sure they are reachable + // (cs_office has a crate right in front of the base of a ladder) + Vector along = ladder->m_top - ladder->m_bottom; + float length = along.NormalizeInPlace(); + Vector on, out; + const float minLadderClearance = 32.0f; + + // adjust bottom to bypass blockages + const float inc = 10.0f; + float t; + trace_t result; + for( t = 0.0f; t <= length; t += inc ) + { + on = ladder->m_bottom + t * along; + + out = on + ladder->GetNormal() * minLadderClearance; + + UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f && !result.startsolid) + { + // found viable ladder bottom + ladder->m_bottom = on; + break; + } + } + + // adjust top to bypass blockages + for( t = 0.0f; t <= length; t += inc ) + { + on = ladder->m_top - t * along; + + out = on + ladder->GetNormal() * minLadderClearance; + + UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f && !result.startsolid) + { + // found viable ladder top + ladder->m_top = on; + break; + } + } + + ladder->m_length = (ladder->m_top - ladder->m_bottom).Length(); + + ladder->SetDir( ladder->GetDir() ); // now that we've adjusted the top and bottom, re-check the normal + + ladder->m_bottomArea = NULL; + ladder->m_topForwardArea = NULL; + ladder->m_topLeftArea = NULL; + ladder->m_topRightArea = NULL; + ladder->m_topBehindArea = NULL; + ladder->ConnectGeneratedLadder( maxHeightAboveTopArea ); + + // add ladder to global list + m_ladders.AddToTail( ladder ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavLadder::ConnectGeneratedLadder( float maxHeightAboveTopArea ) +{ + const float nearLadderRange = 75.0f; // 50 + + // + // Find naviagtion area at bottom of ladder + // + + // get approximate postion of player on ladder + Vector center = m_bottom + Vector( 0, 0, GenerationStepSize ); + AddDirectionVector( ¢er, m_dir, HalfHumanWidth ); + + m_bottomArea = TheNavMesh->GetNearestNavArea( center, true ); + if (!m_bottomArea) + { + DevMsg( "ERROR: Unconnected ladder bottom at ( %g, %g, %g )\n", m_bottom.x, m_bottom.y, m_bottom.z ); + } + else + { + // store reference to ladder in the area + m_bottomArea->AddLadderUp( this ); + } + + // + // Find adjacent navigation areas at the top of the ladder + // + + // get approximate postion of player on ladder + center = m_top + Vector( 0, 0, GenerationStepSize ); + AddDirectionVector( ¢er, m_dir, HalfHumanWidth ); + + float beneathLimit = MIN( 120.0f, m_top.z - m_bottom.z + HalfHumanWidth ); + + // find "ahead" area + m_topForwardArea = findFirstAreaInDirection( ¢er, OppositeDirection( m_dir ), nearLadderRange, beneathLimit, NULL ); + if (m_topForwardArea == m_bottomArea) + m_topForwardArea = NULL; + + // find "left" area + m_topLeftArea = findFirstAreaInDirection( ¢er, DirectionLeft( m_dir ), nearLadderRange, beneathLimit, NULL ); + if (m_topLeftArea == m_bottomArea) + m_topLeftArea = NULL; + + // find "right" area + m_topRightArea = findFirstAreaInDirection( ¢er, DirectionRight( m_dir ), nearLadderRange, beneathLimit, NULL ); + if (m_topRightArea == m_bottomArea) + m_topRightArea = NULL; + + // find "behind" area - must look farther, since ladder is against the wall away from this area + m_topBehindArea = findFirstAreaInDirection( ¢er, m_dir, 2.0f*nearLadderRange, beneathLimit, NULL ); + if (m_topBehindArea == m_bottomArea) + m_topBehindArea = NULL; + + // can't include behind area, since it is not used when going up a ladder + if (!m_topForwardArea && !m_topLeftArea && !m_topRightArea) + DevMsg( "ERROR: Unconnected ladder top at ( %g, %g, %g )\n", m_top.x, m_top.y, m_top.z ); + + // store reference to ladder in the area(s) + if (m_topForwardArea) + m_topForwardArea->AddLadderDown( this ); + + if (m_topLeftArea) + m_topLeftArea->AddLadderDown( this ); + + if (m_topRightArea) + m_topRightArea->AddLadderDown( this ); + + if (m_topBehindArea) + { + m_topBehindArea->AddLadderDown( this ); + Disconnect( m_topBehindArea ); + } + + // adjust top of ladder to highest connected area + float topZ = m_bottom.z + 5.0f; + bool topAdjusted = false; + CNavArea *topAreaList[4]; + topAreaList[0] = m_topForwardArea; + topAreaList[1] = m_topLeftArea; + topAreaList[2] = m_topRightArea; + topAreaList[3] = m_topBehindArea; + + for( int a=0; a<4; ++a ) + { + CNavArea *topArea = topAreaList[a]; + if (topArea == NULL) + continue; + + Vector close; + topArea->GetClosestPointOnArea( m_top, &close ); + if (topZ < close.z) + { + topZ = close.z; + topAdjusted = true; + } + } + + if (topAdjusted) + { + if ( maxHeightAboveTopArea > 0.0f ) + { + m_top.z = MIN( topZ + maxHeightAboveTopArea, m_top.z ); + } + else + { + m_top.z = topZ; // not manually specifying a top, so snap exactly + } + } + + // + // Determine whether this ladder is "dangling" or not + // "Dangling" ladders are too high to go up + // + if (m_bottomArea) + { + Vector bottomSpot; + m_bottomArea->GetClosestPointOnArea( m_bottom, &bottomSpot ); + if (m_bottom.z - bottomSpot.z > HumanHeight) + { + m_bottomArea->Disconnect( this ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +class JumpConnector +{ +public: + bool operator()( CNavArea *jumpArea ) + { + if ( !(jumpArea->GetAttributes() & NAV_MESH_JUMP) ) + { + return true; + } + + for ( int i=0; iGetIncomingConnections( incomingDir ); + const NavConnectVector *from = jumpArea->GetAdjacentAreas( incomingDir ); + const NavConnectVector *dest = jumpArea->GetAdjacentAreas( outgoingDir ); + + TryToConnect( jumpArea, incoming, dest, outgoingDir ); + TryToConnect( jumpArea, from, dest, outgoingDir ); + } + + return true; + } + +private: + struct Connection + { + CNavArea *source; + CNavArea *dest; + NavDirType direction; + }; + + void TryToConnect( CNavArea *jumpArea, const NavConnectVector *source, const NavConnectVector *dest, NavDirType outgoingDir ) + { + FOR_EACH_VEC( (*source), sourceIt ) + { + CNavArea *sourceArea = const_cast< CNavArea * >( (*source)[ sourceIt ].area ); + if ( !sourceArea->IsConnected( jumpArea, outgoingDir ) ) + { + continue; + } + + if ( sourceArea->HasAttributes( NAV_MESH_JUMP ) ) + { + NavDirType incomingDir = OppositeDirection( outgoingDir ); + const NavConnectVector *in1 = sourceArea->GetIncomingConnections( incomingDir ); + const NavConnectVector *in2 = sourceArea->GetAdjacentAreas( incomingDir ); + + TryToConnect( jumpArea, in1, dest, outgoingDir ); + TryToConnect( jumpArea, in2, dest, outgoingDir ); + + continue; + } + + TryToConnect( jumpArea, sourceArea, dest, outgoingDir ); + } + } + + void TryToConnect( CNavArea *jumpArea, CNavArea *sourceArea, const NavConnectVector *dest, NavDirType outgoingDir ) + { + FOR_EACH_VEC( (*dest), destIt ) + { + CNavArea *destArea = const_cast< CNavArea * >( (*dest)[ destIt ].area ); + if ( destArea->HasAttributes( NAV_MESH_JUMP ) ) + { + // Don't connect areas across 2 jump areas. This means we'll have some missing links due to sampling errors. + // This is preferable to generating incorrect links across multiple jump areas, which is far more common. + continue; + } + + Vector center; + float halfWidth; + sourceArea->ComputePortal( destArea, outgoingDir, ¢er, &halfWidth ); + + // Don't create corner-to-corner connections + if ( halfWidth <= 0.0f ) + { + continue; + } + + Vector dir( vec3_origin ); + AddDirectionVector( &dir, outgoingDir, 5.0f ); + + if ( halfWidth > 0.0f ) + { + Vector sourcePos, destPos; + sourceArea->GetClosestPointOnArea( center, &sourcePos ); + destArea->GetClosestPointOnArea( center, &destPos ); + + // No jumping up from stairs. + if ( sourceArea->HasAttributes( NAV_MESH_STAIRS ) && sourcePos.z + StepHeight < destPos.z ) + { + continue; + } + + if ( (sourcePos-destPos).AsVector2D().IsLengthLessThan( GenerationStepSize * 3 ) ) + { + sourceArea->ConnectTo( destArea, outgoingDir ); +// DevMsg( "Connected %d->%d via %d (len %f)\n", +// sourceArea->GetID(), destArea->GetID(), jumpArea->GetID(), sourcePos.DistTo( destPos ) ); + } + } + } + } +}; + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::MarkPlayerClipAreas( void ) +{ +#ifdef TERROR + FOR_EACH_VEC( TheNavAreas, it ) + { + TerrorNavArea *area = static_cast< TerrorNavArea * >(TheNavAreas[it]); + + // Trace upward a bit from our center point just colliding wtih PLAYERCLIP to see if we're in one, if we are, mark us as accordingly. + trace_t trace; + Vector start = area->GetCenter() + Vector(0.0f, 0.0f, 16.0f ); + Vector end = area->GetCenter() + Vector(0.0f, 0.0f, 32.0f ); + UTIL_TraceHull( start, end, Vector(0,0,0), Vector(0,0,0), CONTENTS_PLAYERCLIP, NULL, &trace); + + if ( trace.fraction < 1.0 ) + { + area->SetAttributes( area->GetAttributes() | TerrorNavArea::NAV_PLAYERCLIP ); + } + } +#endif +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Mark all areas that require a jump to get through them. + * This currently relies on jump areas having extreme slope. + */ +void CNavMesh::MarkJumpAreas( void ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + if ( !area->HasNodes() ) + continue; + + Vector normal, otherNormal; + area->ComputeNormal( &normal ); + area->ComputeNormal( &otherNormal, true ); + + float lowestNormalZ = MIN( normal.z, otherNormal.z ); + if (lowestNormalZ < nav_slope_limit.GetFloat()) + { + // The area is a jump area, and we don't merge jump areas together + area->SetAttributes( area->GetAttributes() | NAV_MESH_JUMP | NAV_MESH_NO_MERGE ); + } + else if ( lowestNormalZ < nav_slope_limit.GetFloat() + nav_slope_tolerance.GetFloat() ) + { + Vector testPos = area->GetCenter(); + testPos.z += HalfHumanHeight; + Vector groundNormal; + float dummy; + if ( GetSimpleGroundHeight( testPos, &dummy, &groundNormal ) ) + { + // If the ground normal is divergent from the area's normal, mark it as a jump area - it's not + // really representative of the ground. + float deltaNormalZ = fabs( groundNormal.z - lowestNormalZ ); + if ( deltaNormalZ > nav_slope_tolerance.GetFloat() ) + { + // The area is a jump area, and we don't merge jump areas together + area->SetAttributes( area->GetAttributes() | NAV_MESH_JUMP | NAV_MESH_NO_MERGE ); + } + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Remove all areas marked as jump areas and connect the areas connecting to them +* +*/ +void CNavMesh::StichAndRemoveJumpAreas( void ) +{ + // Now, go through and remove jump areas, connecting areas to make up for it + JumpConnector connector; + ForAllAreas( connector ); + RemoveJumpAreas(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Adjusts obstacle start and end distances such that obstacle width (end-start) is not less than MinObstacleAreaWidth, +* and end distance is not greater than maxAllowedDist +*/ +void AdjustObstacleDistances( float *pObstacleStartDist, float *pObstacleEndDist, float maxAllowedDist ) +{ + float obstacleWidth = *pObstacleEndDist - *pObstacleStartDist; + // is the obstacle width too narrow? + if ( obstacleWidth < MinObstacleAreaWidth ) + { + float halfDelta = ( MinObstacleAreaWidth - obstacleWidth ) /2; + // move start so it's half of min width from center, but no less than zero + *pObstacleStartDist = MAX( *pObstacleStartDist - halfDelta, 0 ); + // move end so it's min width from start + *pObstacleEndDist = *pObstacleStartDist + MinObstacleAreaWidth; + + // if this pushes the end past max allowed distance, pull start and end back so that end is within allowed distance + if ( *pObstacleEndDist > maxAllowedDist ) + { + float delta = *pObstacleEndDist - maxAllowedDist; + *pObstacleStartDist -= delta; + *pObstacleEndDist -= delta; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Makes sure tall, slim obstacles like fencetops, railings and narrow walls have nav areas placed on top of them +* to allow climbing & traversal +*/ +void CNavMesh::HandleObstacleTopAreas( void ) +{ + if ( !nav_generate_fencetops.GetBool() ) + return; + + // For any 1x1 area that is internally blocked by an obstacle, raise it on top of the obstacle and size to fit. + RaiseAreasWithInternalObstacles(); + + // Create new areas as required + CreateObstacleTopAreas(); + + // It's possible for obstacle top areas to wind up overlapping one another, fix any such cases + RemoveOverlappingObstacleTopAreas(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* For any nav area that has internal obstacles between its corners of greater than traversable height, +* raise that nav area to sit at the top of the obstacle, and shrink it to fit the obstacle. Such nav +* areas are already restricted to be 1x1 so this will only be performed on areas that are already small. +*/ +void CNavMesh::RaiseAreasWithInternalObstacles() +{ + // obstacle areas next to stairs are bad - delete them + CUtlVector< CNavArea * > areasToDelete; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + // any nav area with internal obstacles will be 1x1 (width and height = GenerationStepSize), so + // only need to consider areas of that size + if ( ( area->GetSizeX() != GenerationStepSize ) || (area->GetSizeY() != GenerationStepSize ) ) + continue; + + float obstacleZ[2] = { -FLT_MAX, -FLT_MAX }; + float obstacleZMax = -FLT_MAX; + NavDirType obstacleDir = NORTH; + float obstacleStartDist = GenerationStepSize; + float obstacleEndDist = 0; + + bool isStairNeighbor = false; + + // Look at all 4 directions and determine if there are obstacles in that direction. Find the direction with the highest obstacle, if any. + for ( int i = 0; i < NUM_DIRECTIONS; i++ ) + { + NavDirType dir = (NavDirType) i; + + // For this direction, look at the left and right edges of the nav area relative to this direction and determined if they are both blocked + // by obstacles. We only consider this area obstructed if both edges are blocked (e.g. fence runs all the way through it). + + NavCornerType corner[2]; + int iEdgesBlocked = 0; + corner[0] = (NavCornerType) ( ( i + 3 ) % NUM_CORNERS ); // lower left-hand corner relative to current direction + corner[1] = (NavCornerType) ( ( i + 2 ) % NUM_CORNERS ); // lower right-hand corner relative to current direction + float obstacleZThisDir[2] = { -FLT_MAX, -FLT_MAX }; // absolute Z pos of obstacle for left and right edge in this direction + float obstacleStartDistThisDir = GenerationStepSize; // closest obstacle start distance in this direction + float obstacleEndDistThisDir = 0; // farthest obstacle end distance in this direction + + // consider left and right edges of nav area relative to current direction + for ( int iEdge = 0; iEdge < 2; iEdge++ ) + { + NavCornerType cornerType = corner[iEdge]; + CNavNode *nodeFrom = area->m_node[cornerType]; + if ( nodeFrom ) + { + // is there an obstacle going from corner to corner along this edge? + float obstacleHeight = nodeFrom->m_obstacleHeight[dir]; + if ( obstacleHeight > MaxTraversableHeight ) + { + // yes, this edge is blocked + iEdgesBlocked++; + // keep track of obstacle height and start and end distance for this edge + float obstacleZ = nodeFrom->GetPosition()->z + obstacleHeight; + if ( obstacleZ > obstacleZThisDir[iEdge] ) + { + obstacleZThisDir[iEdge] = obstacleZ; + } + obstacleStartDistThisDir = MIN( nodeFrom->m_obstacleStartDist[dir], obstacleStartDistThisDir ); + obstacleEndDistThisDir = MAX( nodeFrom->m_obstacleEndDist[dir], obstacleEndDistThisDir ); + } + } + } + + int BlockedEdgeCutoff = 2; + const NavConnectVector *connections = area->GetAdjacentAreas( dir ); + if ( connections ) + { + for ( int conIndex=0; conIndexCount(); ++conIndex ) + { + const CNavArea *connectedArea = connections->Element( conIndex ).area; + if ( connectedArea && connectedArea->HasAttributes( NAV_MESH_STAIRS ) ) + { + isStairNeighbor = true; + BlockedEdgeCutoff = 1; // one blocked edge is already too much when we're next to a stair + break; + } + } + } + + // are both edged blocked in this direction, and is the obstacle height in this direction the tallest we've seen? + if ( (iEdgesBlocked >= BlockedEdgeCutoff ) && ( MAX( obstacleZThisDir[0], obstacleZThisDir[1] ) ) > obstacleZMax ) + { + // this is the tallest obstacle we've encountered so far, remember its details + obstacleZ[0] = obstacleZThisDir[0]; + obstacleZ[1] = obstacleZThisDir[1]; + obstacleZMax = MAX( obstacleZ[0], obstacleZ[1] ); + obstacleDir = dir; + obstacleStartDist = obstacleStartDistThisDir; + obstacleEndDist = obstacleStartDistThisDir; + } + } + + if ( isStairNeighbor && obstacleZMax > -FLT_MAX ) + { + areasToDelete.AddToTail( area ); + continue; + } + + // if we found an obstacle, raise this nav areas and size it to fit + if ( obstacleZMax > -FLT_MAX ) + { + // enforce minimum obstacle width so we don't shrink to become a teensy nav area + AdjustObstacleDistances( &obstacleStartDist, &obstacleEndDist, GenerationStepSize ); + Assert( obstacleEndDist - obstacleStartDist >= MinObstacleAreaWidth ); + + // get current corner coords + Vector corner[4]; + for ( int i = NORTH_WEST; i < NUM_CORNERS; i++ ) + { + corner[i] = area->GetCorner( (NavCornerType) i ); + } + + // adjust our size to fit the obstacle + switch ( obstacleDir ) + { + case NORTH: + corner[NORTH_WEST].y = corner[SOUTH_WEST].y - obstacleEndDist; + corner[NORTH_EAST].y = corner[SOUTH_EAST].y - obstacleEndDist; + corner[SOUTH_WEST].y -= obstacleStartDist; + corner[SOUTH_EAST].y -= obstacleStartDist; + break; + case SOUTH: + corner[SOUTH_WEST].y = corner[NORTH_WEST].y + obstacleEndDist; + corner[SOUTH_EAST].y = corner[NORTH_EAST].y + obstacleEndDist; + corner[NORTH_WEST].y += obstacleStartDist; + corner[NORTH_EAST].y += obstacleStartDist; + ::V_swap( obstacleZ[0], obstacleZ[1] ); // swap left and right Z heights for obstacle so we can run common code below + break; + case EAST: + corner[NORTH_EAST].x = corner[NORTH_WEST].x + obstacleEndDist; + corner[SOUTH_EAST].x = corner[SOUTH_WEST].x + obstacleEndDist; + corner[NORTH_WEST].x += obstacleStartDist; + corner[SOUTH_WEST].x += obstacleStartDist; + case WEST: + corner[NORTH_WEST].x = corner[NORTH_EAST].x - obstacleEndDist; + corner[SOUTH_WEST].x = corner[SOUTH_EAST].x - obstacleEndDist; + corner[NORTH_EAST].x -= obstacleStartDist; + corner[SOUTH_EAST].x -= obstacleStartDist; + ::V_swap( obstacleZ[0], obstacleZ[1] ); // swap left and right Z heights for obstacle so we can run common code below + break; + } + + // adjust Z positions to be z pos of obstacle top + corner[NORTH_WEST].z = obstacleZ[0]; + corner[NORTH_EAST].z = obstacleZ[1]; + corner[SOUTH_EAST].z = obstacleZ[1]; + corner[SOUTH_WEST].z = obstacleZ[0]; + + // move the area + RemoveNavArea( area ); + area->Build( corner[NORTH_WEST], corner[NORTH_EAST], corner[SOUTH_EAST], corner[SOUTH_WEST] ); + Assert( !area->IsDegenerate() ); + AddNavArea( area ); + + // remove side-to-side connections if there are any so AI does try to do things like run along fencetops + area->RemoveOrthogonalConnections( obstacleDir ); + area->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE | NAV_MESH_OBSTACLE_TOP ); + area->SetAttributes( area->GetAttributes() & ( ~NAV_MESH_JUMP ) ); + // clear out the nodes associated with this area's corners -- corners don't match the node positions any more + for ( int i = 0; i < NUM_CORNERS; i++ ) + { + area->m_node[i] = NULL; + } + } + } + + for ( int i=0; iGetAttributes() & ( NAV_MESH_JUMP | NAV_MESH_OBSTACLE_TOP ) ) + return; + + // Look in all directions + for ( int i = NORTH; i < NUM_DIRECTIONS; i++ ) + { + NavDirType dir = (NavDirType) i; + + // Look at all adjacent areas in this direction + int iConnections = area->GetAdjacentCount( dir ); + for ( int j = 0; j < iConnections; j++ ) + { + CNavArea *areaOther = area->GetAdjacentArea( dir, j ); + // if this is a jump node (which will ultimately get removed) or is an obstacle top, ignore it + if ( areaOther->GetAttributes() & ( NAV_MESH_JUMP | NAV_MESH_OBSTACLE_TOP ) ) + continue; + + // create an obstacle top if there is a one-node separation between the areas and there is an intra-node obstacle within that separation + if ( !CreateObstacleTopAreaIfNecessary( area, areaOther, dir, false ) ) + { + // if not, create an obstacle top if there is a two-node separation between the areas and the intermediate node is significantly + // higher than the two areas, which means there's some geometry there that causes the middle node to be higher + CreateObstacleTopAreaIfNecessary( area, areaOther, dir, true ); + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Creates a new nav area if an obstacle exists between the two nav areas. If bMultiNode is false, this checks +* if there's a one-node separation between the areas, and if so if there is an obstacle detected between the nodes. +* If bMultiNode is true, checks if there is a two-node separation between the areas, and if so if the middle node is +* higher than the two areas, suggesting an obstacle in the middle. +*/ +bool CNavMesh::CreateObstacleTopAreaIfNecessary( CNavArea *area, CNavArea *areaOther, NavDirType dir, bool bMultiNode ) +{ + float obstacleHeightMin = FLT_MAX; + float obstacleHeightMax = 0; + float obstacleHeightStart = 0; + float obstacleHeightEnd = 0; + float obstacleDistMin = GenerationStepSize; + float obstacleDistMax = 0; + + Vector center; + float halfPortalWidth; + + area->ComputePortal( areaOther, dir, ¢er, &halfPortalWidth ); + + if ( halfPortalWidth > 0 ) + { + // get the corners to left and right of direction toward other area + NavCornerType cornerStart = (NavCornerType) dir; + NavCornerType cornerEnd = (NavCornerType) ( ( dir + 1 ) % NUM_CORNERS ); + CNavNode *node = area->m_node[cornerStart]; + CNavNode *nodeEnd = area->m_node[cornerEnd]; + NavDirType dirEdge = (NavDirType) ( ( dir + 1 ) % NUM_DIRECTIONS ); + obstacleHeightMin = FLT_MAX; + float zStart = 0, zEnd = 0; + // along the edge of this area that faces the other area, look at every node that's in the portal between the two + while ( node ) + { + Vector vecToPortalCenter = *node->GetPosition() - center; + vecToPortalCenter.z = 0; + if ( vecToPortalCenter.IsLengthLessThan( halfPortalWidth + 1.0f ) ) + { + // this node is in the portal + + float obstacleHeight = 0; + float obstacleDistStartCur = node->m_obstacleStartDist[dir]; + float obstacleDistEndCur = node->m_obstacleEndDist[dir]; + + if ( !bMultiNode ) + { + // use the inter-node obstacle height from this node toward the next area + obstacleHeight = node->m_obstacleHeight[dir]; + } + else + { + if ( !areaOther->Contains( *node->GetPosition() ) ) + { + // step one node toward the other area + CNavNode *nodeTowardOtherArea = node->GetConnectedNode( dir ); + if ( nodeTowardOtherArea ) + { + // see if that step took us upward a significant amount + float deltaZ = nodeTowardOtherArea->GetPosition()->z - node->GetPosition()->z; + if ( deltaZ > MaxTraversableHeight ) + { + // see if we've arrived in the other area + bool bInOtherArea = false; + if ( areaOther->Contains( *nodeTowardOtherArea->GetPosition() ) ) + { + float z = areaOther->GetZ( nodeTowardOtherArea->GetPosition()->x, nodeTowardOtherArea->GetPosition()->y ); + float deltaZ = fabs( nodeTowardOtherArea->GetPosition()->z - z ); + if ( deltaZ < 2.0f ) + { + bInOtherArea = true; + } + } + + // if we have not arrived in the other area yet, take one more step in the same direction + if ( !bInOtherArea ) + { + CNavNode *nodeTowardOtherArea2 = nodeTowardOtherArea->GetConnectedNode( dir ); + if ( nodeTowardOtherArea2 && areaOther->Contains( *nodeTowardOtherArea2->GetPosition() ) ) + { + float areaDeltaZ = node->GetPosition()->z - nodeTowardOtherArea2->GetPosition()->z; + if ( fabs( areaDeltaZ ) <= MaxTraversableHeight ) + { + // if we arrived in the other area, the obstacle height to get here was the peak deltaZ of the node above to get here + obstacleHeight = deltaZ; + // make a nav area MinObstacleAreaWidth wide centered on the peak node, which is GenerationStepSize away from where we started + obstacleDistStartCur = GenerationStepSize - (MinObstacleAreaWidth / 2); + obstacleDistEndCur = GenerationStepSize + (MinObstacleAreaWidth / 2); + } + } + } + } + } + } + } + + obstacleHeightMin = MIN( obstacleHeight, obstacleHeightMin ); + obstacleHeightMax = MAX( obstacleHeight, obstacleHeightMax ); + obstacleDistMin = MIN( obstacleDistStartCur, obstacleDistMin ); + obstacleDistMax = MAX( obstacleDistEndCur, obstacleDistMax ); + + if ( obstacleHeightStart == 0 ) + { + // keep track of the obstacle height and node z pos at the start of the edge + obstacleHeightStart = obstacleHeight; + zStart = node->GetPosition()->z; + } + // keep track of the obstacle height and node z pos at the end of the edge + obstacleHeightEnd = obstacleHeight; + zEnd = node->GetPosition()->z; + + } + if ( node == nodeEnd ) + break; + + node = node->GetConnectedNode( dirEdge ); + } + + + + + // if we had some obstacle height from EVERY node along the portal, then getting from this area to the other requires scaling an obstacle, + // need to generate a nav area on top of it + if ( ( obstacleHeightMax > MaxTraversableHeight ) && ( obstacleHeightMin > MaxTraversableHeight ) ) + { + // If the maximum obstacle height was greater than both the height at start and end of the edge, then the obstacle is highest somewhere + // in the middle. Use that as the height of both ends. + if ( ( obstacleHeightMax > obstacleHeightStart ) && ( obstacleHeightMax > obstacleHeightEnd ) ) + { + obstacleHeightStart = obstacleHeightMax; + obstacleHeightEnd = obstacleHeightMax; + } + + // for south and west, swap "start" and "end" values of edges so we can use common code below + if ( dir == SOUTH || dir == WEST ) + { + ::V_swap( obstacleHeightStart, obstacleHeightEnd ); + ::V_swap( zStart, zEnd ); + } + + // Enforce min area width for new area + AdjustObstacleDistances( &obstacleDistMin, &obstacleDistMax, bMultiNode ? GenerationStepSize * 2 : GenerationStepSize ); + Assert( obstacleDistMin < obstacleDistMax ); + Assert( obstacleDistMax - obstacleDistMin >= MinObstacleAreaWidth ); + float newAreaWidth = obstacleDistMax - obstacleDistMin; + Assert( newAreaWidth > 0 ); + + // Calculate new area coordinates + AddDirectionVector( ¢er, dir, obstacleDistMin + (newAreaWidth/2) ); + + Vector cornerNW, cornerNE, cornerSE, cornerSW; + switch ( dir ) + { + case NORTH: + case SOUTH: + cornerNW.Init( center.x - halfPortalWidth, center.y - (newAreaWidth/2), zStart + obstacleHeightStart ); + cornerNE.Init( center.x + halfPortalWidth, center.y - (newAreaWidth/2), zEnd + obstacleHeightEnd ); + cornerSE.Init( center.x + halfPortalWidth, center.y + (newAreaWidth/2), zEnd + obstacleHeightEnd ); + cornerSW.Init( center.x - halfPortalWidth, center.y + (newAreaWidth/2), zStart + obstacleHeightStart ); + break; + case EAST: + case WEST: + cornerNW.Init( center.x - (newAreaWidth/2), center.y - halfPortalWidth, zStart + obstacleHeightStart ); + cornerNE.Init( center.x + (newAreaWidth/2), center.y - halfPortalWidth, zEnd + obstacleHeightEnd ); + cornerSE.Init( center.x + (newAreaWidth/2), center.y + halfPortalWidth, zEnd + obstacleHeightEnd ); + cornerSW.Init( center.x - (newAreaWidth/2), center.y + halfPortalWidth, zStart + obstacleHeightStart ); + break; + } + + CNavArea *areaNew = CreateArea(); + areaNew->Build( cornerNW, cornerNE, cornerSE, cornerSW ); + + // add it to the nav area list + TheNavAreas.AddToTail( areaNew ); + AddNavArea( areaNew ); + + Assert( !areaNew->IsDegenerate() ); + + Msg( "Created new fencetop area %d(%x) between %d(%x) and %d(%x)\n", areaNew->GetID(), areaNew->GetDebugID(), area->GetID(), area->GetDebugID(), areaOther->GetID(), areaOther->GetDebugID() ); + + areaNew->SetAttributes( area->GetAttributes() ); + areaNew->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE | NAV_MESH_OBSTACLE_TOP ); + + area->Disconnect( areaOther ); + area->ConnectTo( areaNew, dir ); + + areaNew->ConnectTo( area, OppositeDirection( dir ) ); + areaNew->ConnectTo( areaOther, dir ); + if ( areaOther->IsConnected( area, OppositeDirection( dir ) ) ) + { + areaOther->Disconnect( area ); + areaOther->ConnectTo( areaNew, OppositeDirection( dir ) ); + } +// AddToSelectedSet( areaNew ); + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Remove any obstacle top areas which overlap. +*/ +void CNavMesh::RemoveOverlappingObstacleTopAreas() +{ + // What we really want is the union of all obstacle top areas that get generated. That would be hard to compute exactly, + // so instead we'll just remove any that overlap. The obstacle top areas don't have to be exact, we just need enough of + // them so there is generally a path to get over any obstacle. + + // make a list of just the obstacle top areas to reduce the N of the N squared operation we're about to do + CUtlVector vecObstacleTopAreas; + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + if ( area->GetAttributes() & NAV_MESH_OBSTACLE_TOP ) + { + vecObstacleTopAreas.AddToTail( area ); + } + } + + // look at every pair of obstacle top areas + CUtlVector vecAreasToRemove; + FOR_EACH_VEC( vecObstacleTopAreas, it ) + { + CNavArea *area = vecObstacleTopAreas[it]; + + Vector normal, otherNormal; + area->ComputeNormal( &normal ); + area->ComputeNormal( &otherNormal, true ); + + // Remove any obstacle areas that are steep enough to be jump areas + float lowestNormalZ = MIN( normal.z, otherNormal.z ); + if ( lowestNormalZ < nav_slope_limit.GetFloat() ) + { + vecAreasToRemove.AddToTail( area ); + } + + for ( int it2 = it+1; it2 < vecObstacleTopAreas.Count(); it2++ ) + { + CNavArea *areaOther = vecObstacleTopAreas[it2]; + if ( area->IsOverlapping( areaOther ) ) + { + if ( area->Contains( areaOther ) ) + { + // if one entirely contains the other, mark the other for removal + vecAreasToRemove.AddToTail( areaOther ); + } + else if ( areaOther->Contains( area ) ) + { + // if one entirely contains the other, mark the other for removal + vecAreasToRemove.AddToTail( area ); + } + else + { + // if they overlap without one being a superset of the other, just remove the smaller area + CNavArea *areaToRemove = ( area->GetSizeX() * area->GetSizeY() > areaOther->GetSizeX() * areaOther->GetSizeY() ? areaOther : area ); + vecAreasToRemove.AddToTail( areaToRemove ); + } + } + } + } + + // now go delete all the areas we want to remove + while ( vecAreasToRemove.Count() > 0 ) + { + CNavArea *areaToDelete = vecAreasToRemove[0]; + RemoveFromSelectedSet( areaToDelete ); + TheNavMesh->OnEditDestroyNotify( areaToDelete ); + TheNavAreas.FindAndRemove( areaToDelete ); + TheNavMesh->DestroyArea( areaToDelete ); + + // remove duplicates so we don't double-delete + while ( vecAreasToRemove.FindAndRemove( areaToDelete ) ); + } + +} + +static void CommandNavCheckStairs( void ) +{ + TheNavMesh->MarkStairAreas(); +} +static ConCommand nav_check_stairs( "nav_check_stairs", CommandNavCheckStairs, "Update the nav mesh STAIRS attribute" ); + +//-------------------------------------------------------------------------------------------------------------- +/** + * Mark all areas that are on stairs. + */ +void CNavMesh::MarkStairAreas( void ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + area->TestStairs(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +enum StairTestType +{ + STAIRS_NO, + STAIRS_YES, + STAIRS_MAYBE, +}; + + +//-------------------------------------------------------------------------------------------------------- +// Test if a line across a nav area could be part of a stairway +StairTestType IsStairs( const Vector &start, const Vector &end, StairTestType ret ) +{ + if ( ret == STAIRS_NO ) + return ret; + + const float inc = 5.0f; + + // the minimum height change each step to be a step and not a slope + const float minStepZ = inc * tan( acos( nav_slope_limit.GetFloat() ) ); + const float MinStairNormal = 0.97f; // we don't care about ramps, just actual flat steps + + float t; + Vector pos, normal; + float height, priorHeight; + + // walk the line, checking for step height discontinuities + float length = start.AsVector2D().DistTo( end.AsVector2D() ); + + trace_t trace; + CTraceFilterNoNPCsOrPlayer filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT ); + Vector hullMins( -inc/2, -inc/2, 0 ); + Vector hullMaxs( inc/2, inc/2, 0 ); + hullMaxs.z = 1; // don't care about vertical clearance + + if ( fabs( start.x - end.x ) > fabs( start.y - end.y ) ) + { + hullMins.x = -8; + hullMaxs.x = 8; + } + else + { + hullMins.y = -8; + hullMaxs.y = 8; + } + + Vector traceOffset( 0, 0, VEC_DUCK_HULL_MAX.z ); + + // total height change must exceed a single step to be stairs + if ( abs( start.z - end.z ) > StepHeight ) + { + // initialize the height delta + UTIL_TraceHull( start + traceOffset, start - traceOffset, hullMins, hullMaxs, MASK_NPCSOLID, &filter, &trace ); + if ( trace.startsolid || trace.IsDispSurface() ) + { + return STAIRS_NO; + } + priorHeight = trace.endpos.z; + + // Save a copy for debug overlays + Vector prevGround = start; + prevGround.z = priorHeight; + + float traceIncrement = inc / length; + for( t = 0.0f; t <= 1.0f; t += traceIncrement ) + { + pos = start + t * ( end - start ); + + UTIL_TraceHull( pos + traceOffset, pos - traceOffset, hullMins, hullMaxs, MASK_NPCSOLID, &filter, &trace ); + if ( trace.startsolid || trace.IsDispSurface() ) + { + return STAIRS_NO; + } + height = trace.endpos.z; + normal = trace.plane.normal; + + // Save a copy for debug overlays + Vector ground( pos ); + ground.z = height; + //NDebugOverlay::Cross3D( ground, 3, 0, 0, 255, true, 100.0f ); + //NDebugOverlay::Box( ground, hullMins, hullMaxs, 0, 0, 255, 0.0f, 100.0f ); + + if ( t == 0.0f && fabs( height - start.z ) > StepHeight ) + { + // Discontinuity at start + return STAIRS_NO; + } + + if ( t == 1.0f && fabs( height - end.z ) > StepHeight ) + { + // Discontinuity at end + return STAIRS_NO; + } + + if ( normal.z < MinStairNormal ) + { + // too steep here + return STAIRS_NO; + } + + + float deltaZ = abs( height - priorHeight ); + + if ( deltaZ >= minStepZ && deltaZ <= StepHeight ) + { + // found a step + ret = STAIRS_YES; + } + else if ( deltaZ > StepHeight ) + { + // too steep here + //NDebugOverlay::Cross3D( ground, 5, 255, 0, 0, true, 10.0f ); + //NDebugOverlay::Cross3D( prevGround, 5, 0, 255, 0, true, 10.0f ); + return STAIRS_NO; + } + + // Save a copy for debug overlays + prevGround = pos; + prevGround.z = height; + + priorHeight = height; + } + } + + return ret; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Test an area for being on stairs + * NOTE: This assumes a globally constant "step height", + * and walkable surface normal, which really should be locomotor-specific. + */ +bool CNavArea::TestStairs( void ) +{ + // clear STAIRS attribute + SetAttributes( GetAttributes() & ~NAV_MESH_STAIRS ); + + if ( GetSizeX() <= GenerationStepSize && GetSizeY() <= GenerationStepSize ) + { + // Don't bother with stairs on small areas + return false; + } + + const float MatchingNormalDot = 0.95f; + Vector firstNormal, secondNormal; + ComputeNormal( &firstNormal ); + ComputeNormal( &secondNormal, true ); + if ( firstNormal.Dot( secondNormal ) < MatchingNormalDot ) + { + // area corners aren't coplanar - no stairs + return false; + } + + // test center and edges north-to-south, and east-to-west + StairTestType ret = STAIRS_MAYBE; + Vector from, to; + + const float inset = 5.0f; // inset to keep the tests completely inside the nav area + + from = GetCorner( NORTH_WEST ) + Vector( inset, inset, 0 ); + to = GetCorner( NORTH_EAST ) + Vector( -inset, inset, 0 ); + ret = IsStairs( from, to, ret ); + + from = GetCorner( SOUTH_WEST ) + Vector( inset, -inset, 0 ); + to = GetCorner( SOUTH_EAST ) + Vector( -inset, -inset, 0 ); + ret = IsStairs( from, to, ret ); + + from = GetCorner( NORTH_WEST ) + Vector( inset, inset, 0 ); + to = GetCorner( SOUTH_WEST ) + Vector( inset, -inset, 0 ); + ret = IsStairs( from, to, ret ); + + from = GetCorner( NORTH_EAST ) + Vector( -inset, inset, 0 ); + to = GetCorner( SOUTH_EAST ) + Vector( -inset, -inset, 0 ); + ret = IsStairs( from, to, ret ); + + from = ( GetCorner( NORTH_WEST ) + GetCorner( NORTH_EAST ) ) / 2.0f + Vector( 0, inset, 0 ); + to = ( GetCorner( SOUTH_WEST ) + GetCorner( SOUTH_EAST ) ) / 2.0f + Vector( 0, -inset, 0 ); + ret = IsStairs( from, to, ret ); + + from = ( GetCorner( NORTH_EAST ) + GetCorner( SOUTH_EAST ) ) / 2.0f + Vector( -inset, 0, 0 ); + to = ( GetCorner( NORTH_WEST ) + GetCorner( SOUTH_WEST ) ) / 2.0f + Vector( inset, 0, 0 ); + ret = IsStairs( from, to, ret ); + + if ( ret == STAIRS_YES ) + { + SetAttributes( NAV_MESH_STAIRS ); + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_test_stairs, "Test the selected set for being on stairs", FCVAR_CHEAT ) +{ + int count = 0; + + const NavAreaVector &selectedSet = TheNavMesh->GetSelectedSet(); + for ( int i=0; iTestStairs() ) + { + ++count; + } + } + + Msg( "Marked %d areas as stairs\n", count ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Jump areas aren't used by the NextBot. Delete them, connecting adjacent areas. + */ +void CNavMesh::RemoveJumpAreas( void ) +{ + if ( !nav_generate_fixup_jump_areas.GetBool() ) + { + return; + } + + CUtlVector< CNavArea * > unusedAreas; + + int i; + for ( i=0; iGetAttributes() & NAV_MESH_JUMP) ) + { + continue; + } + + unusedAreas.AddToTail( testArea ); + } + + for ( i=0; iOnEditDestroyNotify( areaToDelete ); + TheNavAreas.FindAndRemove( areaToDelete ); + TheNavMesh->DestroyArea( areaToDelete ); + } + + StripNavigationAreas(); + + SetMarkedArea( NULL ); // unmark the mark area + m_markedCorner = NUM_CORNERS; // clear the corner selection +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavRemoveJumpAreas( void ) +{ + JumpConnector connector; + ForAllAreas( connector ); + + int before = TheNavAreas.Count(); + RemoveJumpAreas(); + int after = TheNavAreas.Count(); + + Msg( "Removed %d jump areas\n", before - after ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Recursively chop area in half along X until child areas are roughly square + */ +static void splitX( CNavArea *area ) +{ + if (area->IsRoughlySquare()) + return; + + float split = area->GetSizeX(); + split /= 2.0f; + split += area->GetCorner( NORTH_WEST ).x; + + split = TheNavMesh->SnapToGrid( split ); + + const float epsilon = 0.1f; + if (fabs(split - area->GetCorner( NORTH_WEST ).x) < epsilon || + fabs(split - area->GetCorner( SOUTH_EAST ).x) < epsilon) + { + // too small to subdivide + return; + } + + CNavArea *alpha, *beta; + if (area->SplitEdit( false, split, &alpha, &beta )) + { + // split each new area until square + splitX( alpha ); + splitX( beta ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Recursively chop area in half along Y until child areas are roughly square + */ +static void splitY( CNavArea *area ) +{ + if (area->IsRoughlySquare()) + return; + + float split = area->GetSizeY(); + split /= 2.0f; + split += area->GetCorner( NORTH_WEST ).y; + + split = TheNavMesh->SnapToGrid( split ); + + const float epsilon = 0.1f; + if (fabs(split - area->GetCorner( NORTH_WEST ).y) < epsilon || + fabs(split - area->GetCorner( SOUTH_EAST ).y) < epsilon) + { + // too small to subdivide + return; + } + + CNavArea *alpha, *beta; + if (area->SplitEdit( true, split, &alpha, &beta )) + { + // split each new area until square + splitY( alpha ); + splitY( beta ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Split any long, thin, areas into roughly square chunks. + */ +void CNavMesh::SquareUpAreas( void ) +{ + int it = 0; + + while( it < TheNavAreas.Count() ) + { + CNavArea *area = TheNavAreas[ it ]; + + // move the iterator in case the current area is split and deleted + ++it; + + if (area->HasNodes() && !area->IsRoughlySquare()) + { + // chop this area into square pieces + if (area->GetSizeX() > area->GetSizeY()) + splitX( area ); + else + splitY( area ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +static bool testStitchConnection( CNavArea *source, CNavArea *target, const Vector &sourcePos, const Vector &targetPos ) +{ + trace_t result; + Vector from( sourcePos ); + Vector pos( targetPos ); + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + Vector to, toNormal; + bool success = false; + if ( TraceAdjacentNode( 0, from, pos, &result ) ) + { + to = result.endpos; + toNormal = result.plane.normal; + success = true; + } + else + { + // test going up ClimbUpHeight + bool success = false; + for ( float height = StepHeight; height <= ClimbUpHeight; height += 1.0f ) + { + trace_t tr; + Vector start( from ); + Vector end( pos ); + start.z += height; + end.z += height; + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, &tr ); + if ( !tr.startsolid && tr.fraction == 1.0f ) + { + if ( !StayOnFloor( &tr ) ) + { + break; + } + + to = tr.endpos; + toNormal = tr.plane.normal; + + start = end = from; + end.z += height; + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, &tr ); + if ( tr.fraction < 1.0f ) + { + break; + } + + success = true; + break; + } + } + } + + return success; +} + + + + +//-------------------------------------------------------------------------------------------------------- +class IncrementallyGeneratedAreas +{ +public: + bool operator()( CNavArea *area ) + { + return area->HasNodes(); + } +}; + + +//-------------------------------------------------------------------------------------------------------- +/** + * Incremental generation fixup for where edges lap up against the existing nav mesh: + * we have nodes, but the surrounding areas don't. So, we trace outward, to see if we + * can walk/fall to an adjacent area. This handles dropping down into existing areas etc. + * TODO: test pre-existing areas for drop-downs into the newly-generated areas. + */ +void CNavMesh::StitchGeneratedAreas( void ) +{ + if ( m_generationMode == GENERATE_INCREMENTAL ) + { + IncrementallyGeneratedAreas incrementalAreas; + StitchMesh( incrementalAreas ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +class AreaSet +{ +public: + AreaSet( CUtlVector< CNavArea * > *areas ) + { + m_areas = areas; + } + + bool operator()( CNavArea *area ) + { + return ( m_areas->HasElement( area ) ); + } + +private: + CUtlVector< CNavArea * > *m_areas; +}; + + +//-------------------------------------------------------------------------------------------------------- +/** + * Stitches an arbitrary set of areas (newly-merged, for example) into the existing mesh + */ +void CNavMesh::StitchAreaSet( CUtlVector< CNavArea * > *areas ) +{ + AreaSet areaSet( areas ); + StitchMesh( areaSet ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine if we can "jump down" from given point + */ +inline bool testJumpDown( const Vector *fromPos, const Vector *toPos ) +{ + float dz = fromPos->z - toPos->z; + + // drop can't be too far, or too short (or nonexistant) + if (dz <= JumpCrouchHeight || dz >= DeathDrop) + return false; + + // + // Check LOS out and down + // + // +-----+ + // | | + // F | + // | + // T + // + + Vector from, to; + float up; + trace_t result; + + // Try to go up and out, up to ClimbUpHeight, to get over obstacles + for ( up=1.0f; up<=ClimbUpHeight; up += 1.0f ) + { + from = *fromPos; + to.Init( fromPos->x, fromPos->y, fromPos->z + up ); + + UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction <= 0.0f || result.startsolid) + continue; + + from.Init( fromPos->x, fromPos->y, result.endpos.z - 0.5f ); + to.Init( toPos->x, toPos->y, from.z ); + + UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction != 1.0f || result.startsolid) + continue; + + // Success! + break; + } + + if ( up > ClimbUpHeight ) + return false; + + // We've made it up and out, so see if we can drop down + from = to; + to.z = toPos->z + 2.0f; + UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction <= 0.0f || result.startsolid) + return false; + + // Allow a little fudge so we can drop down onto stairs + if ( result.endpos.z > to.z + StepHeight ) + return false; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +inline CNavArea *findJumpDownArea( const Vector *fromPos, NavDirType dir ) +{ + Vector start( fromPos->x, fromPos->y, fromPos->z + HalfHumanHeight ); + AddDirectionVector( &start, dir, GenerationStepSize/2.0f ); + + Vector toPos; + CNavArea *downArea = findFirstAreaInDirection( &start, dir, 4.0f * GenerationStepSize, DeathDrop, NULL, &toPos ); + + if (downArea && testJumpDown( fromPos, &toPos )) + return downArea; + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +template < typename Functor > +void CNavMesh::StitchAreaIntoMesh( CNavArea *area, NavDirType dir, Functor &func ) +{ + Vector corner1, corner2; + switch ( dir ) + { + case NORTH: + corner1 = area->GetCorner( NORTH_WEST ); + corner2 = area->GetCorner( NORTH_EAST ); + break; + case SOUTH: + corner1 = area->GetCorner( SOUTH_WEST ); + corner2 = area->GetCorner( SOUTH_EAST ); + break; + case EAST: + corner1 = area->GetCorner( NORTH_EAST ); + corner2 = area->GetCorner( SOUTH_EAST ); + break; + case WEST: + corner1 = area->GetCorner( NORTH_WEST ); + corner2 = area->GetCorner( SOUTH_WEST ); + break; + } + + Vector edgeDir = corner2 - corner1; + edgeDir.z = 0.0f; + + float edgeLength = edgeDir.NormalizeInPlace(); + + for ( float n=0; nGetNavArea( targetPos ); + if ( targetArea && !func( targetArea ) ) + { + targetPos.z = targetArea->GetZ( targetPos.x, targetPos.y ) + HalfHumanHeight; + + // outgoing connection + if ( testStitchConnection( area, targetArea, sourcePos, targetPos ) ) + { + area->ConnectTo( targetArea, dir ); + } + + // incoming connection + if ( testStitchConnection( targetArea, area, targetPos, sourcePos ) ) + { + targetArea->ConnectTo( area, OppositeDirection( dir ) ); + } + } + else + { + sourcePos.z -= HalfHumanHeight; + sourcePos.z += 1; + CNavArea *downArea = findJumpDownArea( &sourcePos, dir ); + if ( downArea && downArea != area && !func( downArea ) ) + { + area->ConnectTo( downArea, dir ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Checks to see if there is a cliff - a drop of at least CliffHeight - in specified direction. +*/ +inline bool CheckCliff( const Vector *fromPos, NavDirType dir, bool bExhaustive = true ) +{ + // cliffs are half-baked, not used by any existing AI, and create poorly behaved nav areas (ie: long, thin, strips) (MSB 8/7/09) + return false; + + + Vector toPos( fromPos->x, fromPos->y, fromPos->z ); + AddDirectionVector( &toPos, dir, GenerationStepSize ); + + trace_t trace; + // trace a step in specified direction and see where we'd find up + if ( TraceAdjacentNode( 0, *fromPos, toPos, &trace, DeathDrop * 10 ) && !trace.allsolid && !trace.startsolid ) + { + float deltaZ = fromPos->z - trace.endpos.z; + // would we fall off a cliff? + if ( deltaZ > CliffHeight ) + return true; + + // if not, special case for south and east. South and east edges are not considered part of a nav area, so + // we look ahead two steps for south and east. This ensures that the n-1th row and column of nav nodes + // on the south and east sides of a nav area reflect any cliffs on the nth row and column. + + // if we're looking to south or east, and the first node we found was approximately flat, and this is the top-level + // call, recurse one level to check one more step in this direction + if ( ( dir == SOUTH || dir == EAST ) && ( fabs( deltaZ ) < StepHeight ) && bExhaustive ) + { + return CheckCliff( &trace.endpos, dir, false ); + } + } + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Define connections between adjacent generated areas + */ +void CNavMesh::ConnectGeneratedAreas( void ) +{ + Msg( "Connecting navigation areas...\n" ); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + // scan along edge nodes, stepping one node over into the next area + // for now, only use bi-directional connections + + // north edge + CNavNode *node; + for( node = area->m_node[ NORTH_WEST ]; node != area->m_node[ NORTH_EAST ]; node = node->GetConnectedNode( EAST ) ) + { + CNavNode *adj = node->GetConnectedNode( NORTH ); + + if (adj && adj->GetArea() && adj->GetConnectedNode( SOUTH ) == node ) + { + area->ConnectTo( adj->GetArea(), NORTH ); + } + else + { + CNavArea *downArea = findJumpDownArea( node->GetPosition(), NORTH ); + if (downArea && downArea != area) + area->ConnectTo( downArea, NORTH ); + } + } + + // west edge + for( node = area->m_node[ NORTH_WEST ]; node != area->m_node[ SOUTH_WEST ]; node = node->GetConnectedNode( SOUTH ) ) + { + CNavNode *adj = node->GetConnectedNode( WEST ); + + if (adj && adj->GetArea() && adj->GetConnectedNode( EAST ) == node ) + { + area->ConnectTo( adj->GetArea(), WEST ); + } + else + { + CNavArea *downArea = findJumpDownArea( node->GetPosition(), WEST ); + if (downArea && downArea != area) + area->ConnectTo( downArea, WEST ); + } + } + + // south edge - this edge's nodes are actually part of adjacent areas + // move one node north, and scan west to east + /// @todo This allows one-node-wide areas - do we want this? + node = area->m_node[ SOUTH_WEST ]; + if ( node ) // pre-existing areas in incremental generates won't have nodes + { + node = node->GetConnectedNode( NORTH ); + } + if (node) + { + CNavNode *end = area->m_node[ SOUTH_EAST ]->GetConnectedNode( NORTH ); + /// @todo Figure out why cs_backalley gets a NULL node in here... + for( ; node && node != end; node = node->GetConnectedNode( EAST ) ) + { + CNavNode *adj = node->GetConnectedNode( SOUTH ); + + if (adj && adj->GetArea() && adj->GetConnectedNode( NORTH ) == node ) + { + area->ConnectTo( adj->GetArea(), SOUTH ); + } + else + { + CNavArea *downArea = findJumpDownArea( node->GetPosition(), SOUTH ); + if (downArea && downArea != area) + area->ConnectTo( downArea, SOUTH ); + } + } + } + + // south edge part 2 - scan the actual south edge. If the node is not part of an adjacent area, then it + // really belongs to us. This will happen if our area runs right up against a ledge. + for( node = area->m_node[ SOUTH_WEST ]; node != area->m_node[ SOUTH_EAST ]; node = node->GetConnectedNode( EAST ) ) + { + if ( node->GetArea() ) + continue; // some other area owns this node, pay no attention to it + + CNavNode *adj = node->GetConnectedNode( SOUTH ); + + if ( node->IsBlockedInAnyDirection() || (adj && adj->IsBlockedInAnyDirection()) ) + continue; // The space around this node is blocked, so don't connect across it + + // Don't directly connect to adj's area, since it's already 1 cell removed from our area. + // There was no area in between, presumably for good reason. Only look for jump down links. + if ( !adj || !adj->GetArea() ) + { + CNavArea *downArea = findJumpDownArea( node->GetPosition(), SOUTH ); + if (downArea && downArea != area) + area->ConnectTo( downArea, SOUTH ); + } + } + + // east edge - this edge's nodes are actually part of adjacent areas + node = area->m_node[ NORTH_EAST ]; + if ( node ) // pre-existing areas in incremental generates won't have nodes + { + node = node->GetConnectedNode( WEST ); + } + if (node) + { + CNavNode *end = area->m_node[ SOUTH_EAST ]->GetConnectedNode( WEST ); + for( ; node && node != end; node = node->GetConnectedNode( SOUTH ) ) + { + CNavNode *adj = node->GetConnectedNode( EAST ); + + if (adj && adj->GetArea() && adj->GetConnectedNode( WEST ) == node ) + { + area->ConnectTo( adj->GetArea(), EAST ); + } + else + { + CNavArea *downArea = findJumpDownArea( node->GetPosition(), EAST ); + if (downArea && downArea != area) + area->ConnectTo( downArea, EAST ); + } + } + } + + // east edge part 2 - scan the actual east edge. If the node is not part of an adjacent area, then it + // really belongs to us. This will happen if our area runs right up against a ledge. + for( node = area->m_node[ NORTH_EAST ]; node != area->m_node[ SOUTH_EAST ]; node = node->GetConnectedNode( SOUTH ) ) + { + if ( node->GetArea() ) + continue; // some other area owns this node, pay no attention to it + + CNavNode *adj = node->GetConnectedNode( EAST ); + + if ( node->IsBlockedInAnyDirection() || (adj && adj->IsBlockedInAnyDirection()) ) + continue; // The space around this node is blocked, so don't connect across it + + // Don't directly connect to adj's area, since it's already 1 cell removed from our area. + // There was no area in between, presumably for good reason. Only look for jump down links. + if ( !adj || !adj->GetArea() ) + { + CNavArea *downArea = findJumpDownArea( node->GetPosition(), EAST ); + if (downArea && downArea != area) + area->ConnectTo( downArea, EAST ); + } + } + } + + StitchGeneratedAreas(); +} + +//-------------------------------------------------------------------------------------------------------------- +bool CNavArea::IsAbleToMergeWith( CNavArea *other ) const +{ + if ( !HasNodes() || ( GetAttributes() & NAV_MESH_NO_MERGE ) ) + return false; + + if ( !other->HasNodes() || ( other->GetAttributes() & NAV_MESH_NO_MERGE ) ) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Merge areas together to make larger ones (must remain rectangular - convex). + * Areas can only be merged if their attributes match. + */ +void CNavMesh::MergeGeneratedAreas( void ) +{ + Msg( "Merging navigation areas...\n" ); + + bool merged; + + do + { + merged = false; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + if ( !area->HasNodes() || ( area->GetAttributes() & NAV_MESH_NO_MERGE ) ) + continue; + + // north edge + FOR_EACH_VEC( area->m_connect[ NORTH ], nit ) + { + CNavArea *adjArea = area->m_connect[ NORTH ][ nit ].area; + if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes + continue; + + if ( area->GetSizeY() + adjArea->GetSizeY() > GenerationStepSize * nav_area_max_size.GetInt() ) + continue; + + if (area->m_node[ NORTH_WEST ] == adjArea->m_node[ SOUTH_WEST ] && + area->m_node[ NORTH_EAST ] == adjArea->m_node[ SOUTH_EAST ] && + area->GetAttributes() == adjArea->GetAttributes() && + area->IsCoplanar( adjArea )) + { + // merge vertical + area->m_node[ NORTH_WEST ] = adjArea->m_node[ NORTH_WEST ]; + area->m_node[ NORTH_EAST ] = adjArea->m_node[ NORTH_EAST ]; + + merged = true; + //CONSOLE_ECHO( " Merged (north) areas #%d and #%d\n", area->m_id, adjArea->m_id ); + + area->FinishMerge( adjArea ); + + // restart scan - iterator is invalidated + break; + } + } + + if (merged) + break; + + // south edge + FOR_EACH_VEC( area->m_connect[ SOUTH ], sit ) + { + CNavArea *adjArea = area->m_connect[ SOUTH ][ sit ].area; + if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes + continue; + + if ( area->GetSizeY() + adjArea->GetSizeY() > GenerationStepSize * nav_area_max_size.GetInt() ) + continue; + + if (adjArea->m_node[ NORTH_WEST ] == area->m_node[ SOUTH_WEST ] && + adjArea->m_node[ NORTH_EAST ] == area->m_node[ SOUTH_EAST ] && + area->GetAttributes() == adjArea->GetAttributes() && + area->IsCoplanar( adjArea )) + { + // merge vertical + area->m_node[ SOUTH_WEST ] = adjArea->m_node[ SOUTH_WEST ]; + area->m_node[ SOUTH_EAST ] = adjArea->m_node[ SOUTH_EAST ]; + + merged = true; + //CONSOLE_ECHO( " Merged (south) areas #%d and #%d\n", area->m_id, adjArea->m_id ); + + area->FinishMerge( adjArea ); + + // restart scan - iterator is invalidated + break; + } + + } + + if (merged) + break; + + + // west edge + FOR_EACH_VEC( area->m_connect[ WEST ], wit ) + { + CNavArea *adjArea = area->m_connect[ WEST ][ wit ].area; + if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes + continue; + + if ( area->GetSizeX() + adjArea->GetSizeX() > GenerationStepSize * nav_area_max_size.GetInt() ) + continue; + + if (area->m_node[ NORTH_WEST ] == adjArea->m_node[ NORTH_EAST ] && + area->m_node[ SOUTH_WEST ] == adjArea->m_node[ SOUTH_EAST ] && + area->GetAttributes() == adjArea->GetAttributes() && + area->IsCoplanar( adjArea )) + { + // merge horizontal + area->m_node[ NORTH_WEST ] = adjArea->m_node[ NORTH_WEST ]; + area->m_node[ SOUTH_WEST ] = adjArea->m_node[ SOUTH_WEST ]; + + merged = true; + //CONSOLE_ECHO( " Merged (west) areas #%d and #%d\n", area->m_id, adjArea->m_id ); + + area->FinishMerge( adjArea ); + + // restart scan - iterator is invalidated + break; + } + + } + + if (merged) + break; + + // east edge + FOR_EACH_VEC( area->m_connect[ EAST ], eit ) + { + CNavArea *adjArea = area->m_connect[ EAST ][ eit ].area; + if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes + continue; + + if ( area->GetSizeX() + adjArea->GetSizeX() > GenerationStepSize * nav_area_max_size.GetInt() ) + continue; + + if (adjArea->m_node[ NORTH_WEST ] == area->m_node[ NORTH_EAST ] && + adjArea->m_node[ SOUTH_WEST ] == area->m_node[ SOUTH_EAST ] && + area->GetAttributes() == adjArea->GetAttributes() && + area->IsCoplanar( adjArea )) + { + // merge horizontal + area->m_node[ NORTH_EAST ] = adjArea->m_node[ NORTH_EAST ]; + area->m_node[ SOUTH_EAST ] = adjArea->m_node[ SOUTH_EAST ]; + + merged = true; + //CONSOLE_ECHO( " Merged (east) areas #%d and #%d\n", area->m_id, adjArea->m_id ); + + area->FinishMerge( adjArea ); + + // restart scan - iterator is invalidated + break; + } + } + + if (merged) + break; + } + } + while( merged ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Given arbitrary corners of a compass grid-aligned rectangle, classify them by compass direction. +* Input: vec[4]: arbitrary corners +* Output: vecNW, vecNE, vecSE, vecSW: filled in with which corner is in which compass direction +*/ +void ClassifyCorners( Vector vec[4], Vector &vecNW, Vector &vecNE, Vector &vecSE, Vector &vecSW ) +{ + vecNW = vecNE = vecSE = vecSW = vec[0]; + + for ( int i = 0; i < 4; i++ ) + { + if ( ( vec[i].x <= vecNW.x ) && ( vec[i].y <= vecNW.y ) ) + { + vecNW = vec[i]; + } + if ( ( vec[i].x >= vecNE.x ) && ( vec[i].y <= vecNE.y ) ) + { + vecNE = vec[i]; + } + if ( ( vec[i].x >= vecSE.x ) && ( vec[i].y >= vecSE.y ) ) + { + vecSE = vec[i]; + } + if ( ( vec[i].x <= vecSW.x ) && ( vec[i].y >= vecSW.y ) ) + { + vecSW = vec[i]; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Perform miscellaneous fixups to generated mesh +*/ +void CNavMesh::FixUpGeneratedAreas( void ) +{ + FixCornerOnCornerAreas(); + FixConnections(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::FixConnections( void ) +{ + // Test the steep sides of stairs for any outgoing links that cross nodes that were partially obstructed. + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + if ( !area->HasAttributes( NAV_MESH_STAIRS ) ) + continue; + + if ( !area->HasNodes() ) + continue; + + for ( int dir=0; dirGetCorner( cornerType[0] ).z - area->GetCorner( cornerType[1] ).z ); + if ( cornerDeltaZ < StepHeight ) + continue; + + const NavConnectVector *connectedAreas = area->GetAdjacentAreas( (NavDirType)dir ); + CUtlVector< CNavArea * > areasToDisconnect; + for ( int i=0; iCount(); ++i ) + { + CNavArea *adjArea = connectedAreas->Element(i).area; + if ( !adjArea->HasNodes() ) + continue; + + Vector pos, adjPos; + float width; + area->ComputePortal( adjArea, (NavDirType)dir, &pos, &width ); + adjArea->GetClosestPointOnArea( pos, &adjPos ); + + CNavNode *node = area->FindClosestNode( pos, (NavDirType)dir ); + CNavNode *adjNode = adjArea->FindClosestNode( adjPos, OppositeDirection( (NavDirType)dir ) ); + pos = *node->GetPosition(); + adjPos = *adjNode->GetPosition(); + + if ( !node || !adjNode ) + continue; + + NavCornerType adjCornerType[2]; + GetCornerTypesInDirection( OppositeDirection((NavDirType)dir), &adjCornerType[0], &adjCornerType[1] ); + + // From the stair's perspective, we can't go up more than step height to reach the adjacent area. + // Also, if the adjacent area has to jump up higher than StepHeight above the stair area to reach the stairs, + // there's an obstruction close to the adjacent area that could prevent walking from the stairs down. + if ( node->GetGroundHeightAboveNode( cornerType[0] ) > StepHeight ) + { + areasToDisconnect.AddToTail( adjArea ); + } + else if ( node->GetGroundHeightAboveNode( cornerType[1] ) > StepHeight ) + { + areasToDisconnect.AddToTail( adjArea ); + } + else if ( adjPos.z + adjNode->GetGroundHeightAboveNode( adjCornerType[0] ) > pos.z + StepHeight ) + { + areasToDisconnect.AddToTail( adjArea ); + } + else if ( adjPos.z + adjNode->GetGroundHeightAboveNode( adjCornerType[1] ) > pos.z + StepHeight ) + { + areasToDisconnect.AddToTail( adjArea ); + } + } + + for ( int i=0; iDisconnect( areasToDisconnect[i] ); + } + } + } + + // Test to prevent A->C if A->B->C. This can happen in doorways and dropdowns from rooftops. + // @TODO: find the root cause of A->C links. + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + CUtlVector< CNavArea * > areasToDisconnect; + for ( int dir=0; dirGetAdjacentAreas( (NavDirType)dir ); + for ( int i=0; iCount(); ++i ) + { + CNavArea *adjArea = connectedAreas->Element(i).area; + const NavConnectVector *adjConnectedAreas = adjArea->GetAdjacentAreas( (NavDirType)dir ); + for ( int j=0; jCount(); ++j ) + { + CNavArea *farArea = adjConnectedAreas->Element(j).area; + + if ( area->IsConnected( farArea, (NavDirType)dir ) ) + { + areasToDisconnect.AddToTail( farArea ); + } + } + } + } + + for ( int i=0; iDisconnect( areasToDisconnect[i] ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Fix any spots where we there are nav nodes touching only corner-on-corner but we intend bots to be able to traverse +*/ +void CNavMesh::FixCornerOnCornerAreas( void ) +{ + const float MaxDrop = StepHeight; // don't make corner on corner areas that are too steep + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + // determine if we have any corners where the only nav area we touch is diagonally corner-to-corner. + // if there are, generate additional small (0.5 x 0.5 grid size) nav areas in the corners between + // them if map geometry allows and make connections in cardinal compass directions to create a path + // between the two areas. + +// +// XXXXXXXXX XXXXXXXXX +// X X X X +// X other X ****X other X +// X X *newX X +// XXXXXXXXXXXXXXXXX => XXXXXXXXXXXXXXXXX +// X X X Xnew* +// X area X X area X**** +// X X X X +// XXXXXXXXX XXXXXXXXX +// + + // check each corner + for ( int iCorner = NORTH_WEST; iCorner < NUM_CORNERS; iCorner++ ) + { + // get cardinal direction to right and left of this corner + NavDirType dirToRight = (NavDirType) iCorner; + NavDirType dirToLeft = (NavDirType) ( ( iCorner+3 ) % NUM_DIRECTIONS ); + + // if we have any connections on cardinal compass directions on edge on either side of corner we're OK, skip this nav area + if ( area->GetAdjacentCount( dirToLeft ) > 0 || area->GetAdjacentCount( dirToRight ) > 0 || + area->GetIncomingConnections( dirToLeft )->Count() > 0 || area->GetIncomingConnections( dirToRight )->Count() > 0 ) + continue; + + Vector cornerPos = area->GetCorner( (NavCornerType) iCorner ); + NavDirType dirToRightTwice = DirectionRight( dirToRight ); + NavDirType dirToLeftTwice = DirectionLeft( dirToLeft ); + NavDirType dirsAlongOtherEdge[2] = { dirToLeft, dirToRight }; + NavDirType dirsAlongOurEdge[2] = { dirToLeftTwice, dirToRightTwice }; + + // consider 2 potential new nav areas, to left and right of the corner we're considering + for ( int iDir = 0; iDir < ARRAYSIZE( dirsAlongOtherEdge ); iDir++ ) + { + NavDirType dirAlongOtherEdge = dirsAlongOtherEdge[iDir]; + NavDirType dirAlongOurEdge = dirsAlongOurEdge[iDir]; + + // look at the point 0.5 grid units along edge of other nav area + Vector vecDeltaOtherEdge; + DirectionToVector2D( dirAlongOtherEdge, (Vector2D *) &vecDeltaOtherEdge ); + vecDeltaOtherEdge.z = 0; + vecDeltaOtherEdge *= GenerationStepSize * 0.5; + Vector vecOtherEdgePos = cornerPos + vecDeltaOtherEdge; + + // see if there is a nav area at that location + CNavArea *areaOther = GetNavArea( vecOtherEdgePos ); + Assert( areaOther != area ); + if ( !areaOther ) + continue; // no other area in that location, we're not touching on corner + + // see if we can move from our corner in that direction + trace_t result; + if ( !TraceAdjacentNode( 0, cornerPos, vecOtherEdgePos, &result, MaxDrop ) ) + continue; // something is blocking movement, don't create additional nodes to aid movement + + // get the corner of the other nav area that might touch our corner + int iCornerOther = ( ( iCorner + 2 ) % NUM_CORNERS ); + Vector cornerPosOther = areaOther->GetCorner( (NavCornerType) iCornerOther ); + + if ( cornerPos != cornerPosOther ) + continue; // that nav area does not touch us on corner + + // we are touching corner-to-corner with the other nav area and don't have connections in cardinal directions around + // the corner that touches, this is a candidate to generate new small helper nav areas. + + // calculate the corners of the 0.5 x 0.5 nav area we would consider building between us and the other nav area whose corner we touch + Vector vecDeltaOurEdge; + DirectionToVector2D( dirAlongOurEdge, (Vector2D *) &vecDeltaOurEdge ); + vecDeltaOurEdge.z = 0; + vecDeltaOurEdge *= GenerationStepSize * 0.5; + Vector vecOurEdgePos = cornerPos + vecDeltaOurEdge; + Vector vecCorner[4]; + vecCorner[0] = cornerPos + vecDeltaOtherEdge + vecDeltaOurEdge; // far corner of new nav area + vecCorner[1] = cornerPos + vecDeltaOtherEdge; // intersection of far edge of new nav area with other nav area we touch + vecCorner[2] = cornerPos; // common corner of this nav area, nav area we touch, and new nav area + vecCorner[3] = cornerPos + vecDeltaOurEdge; // intersection of far edge of new nav area with this nav area + + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + if ( !TraceAdjacentNode( 0, vecCorner[1], vecCorner[0], &result, MaxDrop ) || // can we move from edge of other area to far corner of new node + !TraceAdjacentNode( 0, vecCorner[3], vecCorner[0], &result, MaxDrop ) ) // can we move from edge of this area to far corner of new node + continue; // new node would not fit + + // as sanity check, make sure there's not already a nav area there, shouldn't be + CNavArea *areaTest = GetNavArea( vecCorner[0] ); + Assert ( !areaTest ); + if ( areaTest ) + continue; + + vecCorner[0] = result.endpos; + + // create a new nav area + CNavArea *areaNew = CreateArea(); + + // arrange the corners of the new nav area by compass direction + Vector vecNW, vecNE, vecSE, vecSW; + ClassifyCorners( vecCorner, vecNW, vecNE, vecSE, vecSW ); + areaNew->Build( vecNW, vecNE, vecSE, vecSW ); + + // add it to the nav area list + TheNavAreas.AddToTail( areaNew ); + AddNavArea( areaNew ); + + areaNew->SetAttributes( area->GetAttributes() ); + + // reciprocally connect between this area and new area + area->ConnectTo( areaNew, dirAlongOtherEdge ); + areaNew->ConnectTo( area, OppositeDirection( dirAlongOtherEdge ) ); + + // reciprocally connect between other area and new area + areaOther->ConnectTo( areaNew, dirAlongOurEdge ); + areaNew->ConnectTo( areaOther, OppositeDirection( dirAlongOurEdge ) ); + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Fix any areas where one nav area overhangs another and the two nav areas are connected. Subdivide the lower +* nav area such that the upper nav area doesn't overhang any area it's connected to. +*/ +void CNavMesh::SplitAreasUnderOverhangs( void ) +{ + // restart the whole process whenever this gets set to true + bool bRestartProcessing = false; + + do + { + bRestartProcessing = false; + + // iterate all nav areas + for ( int it = 0; it < TheNavAreas.Count() && !bRestartProcessing; it++ ) + { + CNavArea *area = TheNavAreas[ it ]; + Extent areaExtent; + area->GetExtent( &areaExtent ); + + // iterate all directions + for ( int dir = NORTH; dir < NUM_DIRECTIONS && !bRestartProcessing; dir++ ) + { + // iterate all connections in that direction + const NavConnectVector *pConnections = area->GetAdjacentAreas( (NavDirType) dir ); + for ( int iConnection = 0; iConnection < pConnections->Count() && !bRestartProcessing; iConnection++ ) + { + CNavArea *otherArea = (*pConnections)[iConnection].area; + Extent otherAreaExtent; + otherArea->GetExtent( &otherAreaExtent ); + + // see if the area we are connected to overlaps our X/Y extents + if ( area->IsOverlapping( otherArea ) ) + { + // if the upper area isn't at least crouch height above the lower area, this is some weird minor + // overlap, disregard it + const float flMinSeparation = HumanCrouchHeight; + if ( !( areaExtent.lo.z > otherAreaExtent.hi.z + flMinSeparation ) && + !( otherAreaExtent.lo.z > areaExtent.hi.z + flMinSeparation ) ) + continue; + + // figure out which area is above and which is below + CNavArea *areaBelow = area, *areaAbove = otherArea; + NavDirType dirFromAboveToBelow = OppositeDirection( (NavDirType) dir ); + if ( otherAreaExtent.lo.z < areaExtent.lo.z ) + { + areaBelow = otherArea; + areaAbove = area; + dirFromAboveToBelow = OppositeDirection( dirFromAboveToBelow ); + } + NavDirType dirFromBelowToAbove = OppositeDirection( dirFromAboveToBelow ); + + // Msg( "area %d overhangs area %d and is connected\n", areaAbove->GetID(), areaBelow->GetID() ); + + Extent extentBelow, extentAbove; + areaBelow->GetExtent( &extentBelow ); + areaAbove->GetExtent( &extentAbove ); + + float splitCoord; // absolute world coordinate along which we will split lower nav area (X or Y, depending on axis we split on) + float splitLen; // length of the segment of lower nav area that is in shadow of the upper nav area + float splitEdgeSize; // current length of the edge of nav area that is getting split + bool bSplitAlongX = false; + + // determine along what edge we are splitting and make some key measurements + if ( ( dirFromAboveToBelow == EAST ) || ( dirFromAboveToBelow == WEST ) ) + { + splitEdgeSize = extentBelow.hi.x - extentBelow.lo.x; + if ( extentAbove.hi.x < extentBelow.hi.x ) + { + splitCoord = extentAbove.hi.x; + splitLen = splitCoord - extentBelow.lo.x; + } + else + { + splitCoord = extentAbove.lo.x; + splitLen = extentBelow.hi.x - splitCoord; + } + } + else + { + splitEdgeSize = extentBelow.hi.y - extentBelow.lo.y; + bSplitAlongX = true; + if ( extentAbove.hi.y < extentBelow.hi.y ) + { + splitCoord = extentAbove.hi.y; + splitLen = splitCoord - extentBelow.lo.y; + } + else + { + splitCoord = extentAbove.lo.y; + splitLen = extentBelow.hi.y - splitCoord; + } + } + Assert( splitLen >= 0 ); + Assert( splitEdgeSize > 0 ); + + // if we split the lower nav area right where it's in shadow of the upper nav area, will it create a really tiny strip? + if ( splitLen < GenerationStepSize ) + { + // if the "in shadow" part of the lower nav area is really small or the lower nav area is really small to begin with, + // don't split it, we're better off as is + if ( ( splitLen < GenerationStepSize*0.3 ) || ( splitEdgeSize <= GenerationStepSize * 2 ) ) + continue; + + // Move our split point so we don't create a really tiny strip on the lower nav area. Move the split point away from + // the upper nav area so the "in shadow" area expands to be GenerationStepSize. The checks above ensure we have room to do this. + float splitDelta = GenerationStepSize - splitLen; + splitCoord += splitDelta * ( ( ( dirFromAboveToBelow == NORTH ) || ( dirFromAboveToBelow == WEST ) ) ? -1 : 1 ); + } + + // remove any connections between the two areas (so they don't get inherited by the new areas when we split the lower area), + // but remember what the connections were. + bool bConnectionFromBelow = false, bConnectionFromAbove = false; + if ( areaBelow->IsConnected( areaAbove, dirFromBelowToAbove ) ) + { + bConnectionFromBelow = true; + areaBelow->Disconnect( areaAbove ); + } + if ( areaAbove->IsConnected( areaBelow, dirFromAboveToBelow ) ) + { + bConnectionFromAbove = true; + areaAbove->Disconnect( areaBelow ); + } + + CNavArea *pNewAlpha = NULL,*pNewBeta = NULL; +// int idBelow = areaBelow->GetID(); +// AddToSelectedSet( areaBelow ); + // split the lower nav area + if ( areaBelow->SplitEdit( bSplitAlongX, splitCoord, &pNewAlpha, &pNewBeta ) ) + { +// Msg( "Split area %d into %d and %d\n", idBelow, pNewAlpha->GetID(), pNewBeta->GetID() ); + + // determine which of the two new lower areas is the one *not* in shadow of the upper nav area. This is the one we want to + // reconnect to + CNavArea *pNewNonoverlappedArea = ( ( dirFromAboveToBelow == NORTH ) || ( dirFromAboveToBelow == WEST ) ) ? pNewAlpha : pNewBeta; + + // restore the previous connections from the upper nav area to the new lower nav area that is not in shadow of the upper + if ( bConnectionFromAbove ) + { + areaAbove->ConnectTo( pNewNonoverlappedArea, dirFromAboveToBelow ); + } + if ( bConnectionFromBelow ) + { + areaBelow->ConnectTo( pNewNonoverlappedArea, OppositeDirection( dirFromAboveToBelow ) ); + } + + // Now we need to just start the whole process over. We've just perturbed the list we're iterating on (removed a nav area, added two + // new ones, when we did the split), and it's possible we may have to subdivide a lower nav area twice if the upper nav area + // overhangs a corner of the lower area. We just start all over again each time we do a split until no more overhangs occur. + bRestartProcessing = true; + } + else + { +// Msg( "Failed to split area %d\n", idBelow ); + } + } + } + } + } + } + while ( bRestartProcessing ); +} + + +//-------------------------------------------------------------------------------------------------------------- +bool TestForValidCrouchArea( CNavNode *node ) +{ + // must make sure we don't have a bogus crouch area. check up to JumpCrouchHeight above + // the node for a HumanCrouchHeight space. + + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_EVERYTHING ); + trace_t tr; + Vector start( *node->GetPosition() ); + Vector end( *node->GetPosition() ); + end.z += JumpCrouchHeight; + + Vector mins( 0, 0, 0 ); + Vector maxs( GenerationStepSize, GenerationStepSize, HumanCrouchHeight ); + + UTIL_TraceHull( + start, + end, + mins, + maxs, + TheNavMesh->GetGenerationTraceMask(), + &filter, + &tr ); + + return ( !tr.allsolid ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Make sure that if other* are similar, test is also close. Used in TestForValidJumpArea. + */ +bool IsHeightDifferenceValid( float test, float other1, float other2, float other3 ) +{ + // Make sure the other nodes are level. + const float CloseDelta = StepHeight / 2; + if ( fabs( other1 - other2 ) > CloseDelta ) + return true; + + if ( fabs( other1 - other3 ) > CloseDelta ) + return true; + + if ( fabs( other2 - other3 ) > CloseDelta ) + return true; + + // Now make sure the test node is near the others. If it is more than StepHeight away, + // it'll form a distorted jump area. + const float MaxDelta = StepHeight; + if ( fabs( test - other1 ) > MaxDelta ) + return false; + + if ( fabs( test - other2 ) > MaxDelta ) + return false; + + if ( fabs( test - other3 ) > MaxDelta ) + return false; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check that a 1x1 area with 'node' at the northwest corner has a valid shape - if 3 corners + * are flat, and the 4th is significantly higher or lower, it would form a jump area that bots + * can't navigate over well. + */ +bool TestForValidJumpArea( CNavNode *node ) +{ + return true; + + CNavNode *east = node->GetConnectedNode( EAST ); + CNavNode *south = node->GetConnectedNode( SOUTH ); + if ( !east || !south ) + return false; + + CNavNode *southEast = east->GetConnectedNode( SOUTH ); + if ( !southEast ) + return false; + + if ( !IsHeightDifferenceValid( + node->GetPosition()->z, + south->GetPosition()->z, + southEast->GetPosition()->z, + east->GetPosition()->z ) ) + return false; + + if ( !IsHeightDifferenceValid( + south->GetPosition()->z, + node->GetPosition()->z, + southEast->GetPosition()->z, + east->GetPosition()->z ) ) + return false; + + if ( !IsHeightDifferenceValid( + southEast->GetPosition()->z, + south->GetPosition()->z, + node->GetPosition()->z, + east->GetPosition()->z ) ) + return false; + + if ( !IsHeightDifferenceValid( + east->GetPosition()->z, + south->GetPosition()->z, + southEast->GetPosition()->z, + node->GetPosition()->z ) ) + return false; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +class TestOverlapping +{ + Vector m_nw; + Vector m_ne; + Vector m_sw; + Vector m_se; +public: + TestOverlapping( const Vector &nw, const Vector &ne, const Vector &sw, const Vector &se ) : + m_nw( nw ), m_ne( ne ), m_sw( sw ), m_se( se ) + { + } + + // This approximates CNavArea::GetZ, so we can pretend our four corners delineate a nav area + float GetZ( const Vector &pos ) const + { + float dx = m_se.x - m_nw.x; + float dy = m_se.y - m_nw.y; + + // guard against division by zero due to degenerate areas + if (dx == 0.0f || dy == 0.0f) + return m_ne.z; + + float u = (pos.x - m_nw.x) / dx; + float v = (pos.y - m_nw.y) / dy; + + // clamp Z values to (x,y) volume + if (u < 0.0f) + u = 0.0f; + else if (u > 1.0f) + u = 1.0f; + + if (v < 0.0f) + v = 0.0f; + else if (v > 1.0f) + v = 1.0f; + + float northZ = m_nw.z + u * (m_ne.z - m_nw.z); + float southZ = m_sw.z + u * (m_se.z - m_sw.z); + + return northZ + v * (southZ - northZ); + } + + bool OverlapsExistingArea( void ) + { + CNavArea *overlappingArea = NULL; + CNavLadder *overlappingLadder = NULL; + + Vector nw = m_nw; + Vector se = m_se; + Vector start = nw; + start.x += GenerationStepSize/2; + start.y += GenerationStepSize/2; + + while ( start.x < se.x ) + { + start.y = nw.y + GenerationStepSize/2; + while ( start.y < se.y ) + { + start.z = GetZ( start ); + Vector end = start; + start.z -= StepHeight; + end.z += HalfHumanHeight; + + if ( TheNavMesh->FindNavAreaOrLadderAlongRay( start, end, &overlappingArea, &overlappingLadder, NULL ) ) + { + if ( overlappingArea ) + { + return true; + } + } + + start.y += GenerationStepSize; + } + start.x += GenerationStepSize; + } + return false; + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check if an rectangular area of the given size can be + * made starting from the given node as the NW corner. + * Only consider fully connected nodes for this check. + * All of the nodes within the test area must have the same attributes. + * All of the nodes must be approximately co-planar w.r.t the NW node's normal, with the + * exception of 1x1 areas which can be any angle. + */ +bool CNavMesh::TestArea( CNavNode *node, int width, int height ) +{ + Vector normal = *node->GetNormal(); + float d = -DotProduct( normal, *node->GetPosition() ); + + bool nodeCrouch = node->m_crouch[ SOUTH_EAST ]; + + // The area's interior will be the south-east side of this north-west node. + // If that interior space is blocked, there's no space to build an area. + if ( node->m_isBlocked[ SOUTH_EAST ] ) + { + return false; + } + + int nodeAttributes = node->GetAttributes() & ~NAV_MESH_CROUCH; + + const float offPlaneTolerance = 5.0f; + + CNavNode *vertNode, *horizNode; + + vertNode = node; + int x,y; + for( y=0; ym_crouch[ SOUTH_EAST ]; + if ( horizNode->m_isBlocked[ SOUTH_EAST ] ) + { + return false; + } + } + else if ( northEdge && eastEdge ) + { + // interior space of the area extends one more cell to the east past the easternmost nodes. + // This means we need to check to the southeast as well as the southwest. + horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ SOUTH_WEST ]; + if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ SOUTH_WEST ] ) + { + return false; + } + } + else if ( southEdge && westEdge ) + { + // The interior space of the area extends one more cell to the south past the southernmost nodes. + // This means we need to check to the southeast as well as the southwest. + horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ NORTH_EAST ]; + if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ NORTH_EAST ] ) + { + return false; + } + } + else if ( southEdge && eastEdge ) + { + // This node is completely in the interior of the area, so we need to check in all directions. + horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0; + if ( horizNode->IsBlockedInAnyDirection() ) + { + return false; + } + } + // check sides next + else if ( northEdge ) + { + horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ SOUTH_WEST ]; + if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ SOUTH_WEST ] ) + { + return false; + } + } + else if ( southEdge ) + { + // This node is completely in the interior of the area, so we need to check in all directions. + horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0; + if ( horizNode->IsBlockedInAnyDirection() ) + { + return false; + } + } + else if ( eastEdge ) + { + // This node is completely in the interior of the area, so we need to check in all directions. + horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0; + if ( horizNode->IsBlockedInAnyDirection() ) + { + return false; + } + } + else if ( westEdge ) + { + horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ NORTH_EAST ]; + if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ NORTH_EAST ] ) + { + return false; + } + } + // finally, we have a center node + else + { + // This node is completely in the interior of the area, so we need to check in all directions. + horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0; + if ( horizNode->IsBlockedInAnyDirection() ) + { + return false; + } + } + + // all nodes must be crouch/non-crouch + if ( nodeCrouch != horizNodeCrouch ) + return false; + + // all nodes must have the same non-crouch attributes + int horizNodeAttributes = horizNode->GetAttributes() & ~NAV_MESH_CROUCH; + if (horizNodeAttributes != nodeAttributes) + return false; + + if (horizNode->IsCovered()) + return false; + + if (!horizNode->IsClosedCell()) + return false; + + if ( !CheckObstacles( horizNode, width, height, x, y ) ) + return false; + + horizNode = horizNode->GetConnectedNode( EAST ); + if (horizNode == NULL) + return false; + + // nodes must lie on/near the plane + if (width > 1 || height > 1) + { + float dist = (float)fabs( DotProduct( *horizNode->GetPosition(), normal ) + d ); + if (dist > offPlaneTolerance) + return false; + } + } + + // Check the final (x=width) node, the above only checks thru x=width-1 + if ( !CheckObstacles( horizNode, width, height, x, y ) ) + return false; + + vertNode = vertNode->GetConnectedNode( SOUTH ); + if (vertNode == NULL) + return false; + + // nodes must lie on/near the plane + if (width > 1 || height > 1) + { + float dist = (float)fabs( DotProduct( *vertNode->GetPosition(), normal ) + d ); + if (dist > offPlaneTolerance) + return false; + } + } + + // check planarity of southern edge + if (width > 1 || height > 1) + { + horizNode = vertNode; + + for( x=0; xGetConnectedNode( EAST ); + if (horizNode == NULL) + return false; + + // nodes must lie on/near the plane + float dist = (float)fabs( DotProduct( *horizNode->GetPosition(), normal ) + d ); + if (dist > offPlaneTolerance) + return false; + } + + // Check the final (x=width) node, the above only checks thru x=width-1 + if ( !CheckObstacles( horizNode, width, height, x, y ) ) + return false; + } + + vertNode = node; + for( y=0; yGetConnectedNode( EAST ); + } + + vertNode = vertNode->GetConnectedNode( SOUTH ); + } + + if ( m_generationMode == GENERATE_INCREMENTAL ) + { + // Incremental generation needs to check that it's not overlapping existing areas... + const Vector *nw = node->GetPosition(); + + vertNode = node; + for( int y=0; yGetConnectedNode( SOUTH ); + } + const Vector *sw = vertNode->GetPosition(); + + horizNode = node; + for( int x=0; xGetConnectedNode( EAST ); + } + const Vector *ne = horizNode->GetPosition(); + + vertNode = horizNode; + for( int y=0; yGetConnectedNode( SOUTH ); + } + const Vector *se = vertNode->GetPosition(); + + TestOverlapping test( *nw, *ne, *sw, *se ); + if ( test.OverlapsExistingArea() ) + return false; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Checks if a node has an untraversable obstacle in any direction to a neighbor. +* width and height are size of nav area this node would be a part of, x and y are node's position +* within that grid +*/ +bool CNavMesh::CheckObstacles( CNavNode *node, int width, int height, int x, int y ) +{ + // any area bigger than 1x1 can't have obstacles in any connection between nodes + if ( width > 1 || height > 1 ) + { + if ( ( x > 0 ) && ( node->m_obstacleHeight[WEST] > MaxTraversableHeight ) ) + return false; + + if ( ( y > 0 ) && ( node->m_obstacleHeight[NORTH] > MaxTraversableHeight ) ) + return false; + + if ( ( x < width-1 ) && ( node->m_obstacleHeight[EAST] > MaxTraversableHeight ) ) + return false; + + if ( ( y < height-1 ) && ( node->m_obstacleHeight[SOUTH] > MaxTraversableHeight ) ) + return false; + } + + // 1x1 area can have obstacles, that area will get fixed up later + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Create a nav area, and mark all nodes it overlaps as "covered" + * NOTE: Nodes on the east and south edges are not included. + * Returns number of nodes covered by this area, or -1 for error; + */ +int CNavMesh::BuildArea( CNavNode *node, int width, int height ) +{ + CNavNode *nwNode = node; + CNavNode *neNode = NULL; + CNavNode *swNode = NULL; + CNavNode *seNode = NULL; + + CNavNode *vertNode = node; + CNavNode *horizNode; + + int coveredNodes = 0; + + for( int y=0; yCover(); + ++coveredNodes; + + horizNode = horizNode->GetConnectedNode( EAST ); + } + + if (y == 0) + neNode = horizNode; + + vertNode = vertNode->GetConnectedNode( SOUTH ); + } + + swNode = vertNode; + + horizNode = vertNode; + for( int x=0; xGetConnectedNode( EAST ); + } + seNode = horizNode; + + if (!nwNode || !neNode || !seNode || !swNode) + { + Error( "BuildArea - NULL node.\n" ); + return -1; + } + + CNavArea *area = CreateArea(); + if (area == NULL) + { + Error( "BuildArea: Out of memory.\n" ); + return -1; + } + + area->Build( nwNode, neNode, seNode, swNode ); + + TheNavAreas.AddToTail( area ); + // since all internal nodes have the same attributes, set this area's attributes + + area->SetAttributes( node->GetAttributes() ); + + // If any of the corners have an obstacle in the direction of another corner, then there's an internal obstruction of this nav node. + // Mark it as not mergable so it doesn't become a part of anything else and we will fix it up later. + if ( nwNode->m_obstacleHeight[SOUTH] > MaxTraversableHeight || nwNode->m_obstacleHeight[EAST] > MaxTraversableHeight || + neNode->m_obstacleHeight[WEST] > MaxTraversableHeight || neNode->m_obstacleHeight[SOUTH] > MaxTraversableHeight || + seNode->m_obstacleHeight[NORTH] > MaxTraversableHeight || seNode->m_obstacleHeight[WEST] > MaxTraversableHeight || + swNode->m_obstacleHeight[EAST] > MaxTraversableHeight || swNode->m_obstacleHeight[NORTH] > MaxTraversableHeight ) + { + Assert( width == 1 ); // We should only ever try to build a 1x1 area out of any two nodes that have an obstruction between them + Assert( height == 1 ); + + area->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE ); + } + + // Check that the node was crouch in the right direction + bool nodeCrouch = node->m_crouch[ SOUTH_EAST ]; + if ( (area->GetAttributes() & NAV_MESH_CROUCH) && !nodeCrouch ) + { + area->SetAttributes( area->GetAttributes() & ~NAV_MESH_CROUCH ); + } + + return coveredNodes; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * This function uses the CNavNodes that have been sampled from the map to + * generate CNavAreas - rectangular areas of "walkable" space. These areas + * are connected to each other, proving information on know how to move from + * area to area. + * + * This is a "greedy" algorithm that attempts to cover the walkable area + * with the fewest, largest, rectangles. + */ +void CNavMesh::CreateNavAreasFromNodes( void ) +{ + // haven't yet seen a map use larger than 30... + int tryWidth = nav_area_max_size.GetInt(); + int tryHeight = tryWidth; + int uncoveredNodes = CNavNode::GetListLength(); + + while( uncoveredNodes > 0 ) + { + for( CNavNode *node = CNavNode::GetFirst(); node; node = node->GetNext() ) + { + if (node->IsCovered()) + continue; + + if (TestArea( node, tryWidth, tryHeight )) + { + int covered = BuildArea( node, tryWidth, tryHeight ); + if (covered < 0) + { + Error( "Generate: Error - Data corrupt.\n" ); + return; + } + + uncoveredNodes -= covered; + } + } + + if (tryWidth >= tryHeight) + --tryWidth; + else + --tryHeight; + + if (tryWidth <= 0 || tryHeight <= 0) + break; + } + + if ( !TheNavAreas.Count() ) + { + // If we somehow have no areas, don't try to create an impossibly-large grid + AllocateGrid( 0, 0, 0, 0 ); + return; + } + + Extent extent; + extent.lo.x = 9999999999.9f; + extent.lo.y = 9999999999.9f; + extent.hi.x = -9999999999.9f; + extent.hi.y = -9999999999.9f; + + // compute total extent + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + Extent areaExtent; + area->GetExtent( &areaExtent ); + + if (areaExtent.lo.x < extent.lo.x) + extent.lo.x = areaExtent.lo.x; + if (areaExtent.lo.y < extent.lo.y) + extent.lo.y = areaExtent.lo.y; + if (areaExtent.hi.x > extent.hi.x) + extent.hi.x = areaExtent.hi.x; + if (areaExtent.hi.y > extent.hi.y) + extent.hi.y = areaExtent.hi.y; + } + + // add the areas to the grid + AllocateGrid( extent.lo.x, extent.hi.x, extent.lo.y, extent.hi.y ); + + FOR_EACH_VEC( TheNavAreas, git ) + { + AddNavArea( TheNavAreas[ git ] ); + } + + + ConnectGeneratedAreas(); + MarkPlayerClipAreas(); + MarkJumpAreas(); // mark jump areas before we merge generated areas, so we don't merge jump and non-jump areas + MergeGeneratedAreas(); + SplitAreasUnderOverhangs(); + SquareUpAreas(); + MarkStairAreas(); + StichAndRemoveJumpAreas(); + HandleObstacleTopAreas(); + FixUpGeneratedAreas(); + + /// @TODO: incremental generation doesn't create ladders yet + if ( m_generationMode != GENERATE_INCREMENTAL ) + { + for ( int i=0; iConnectGeneratedLadder( 0.0f ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +// adds walkable positions for any/all positions a mod specifies +void CNavMesh::AddWalkableSeeds( void ) +{ + CBaseEntity *spawn = gEntList.FindEntityByClassname( NULL, GetPlayerSpawnName() ); + + if (spawn ) + { + // snap it to the sampling grid + Vector pos = spawn->GetAbsOrigin(); + pos.x = TheNavMesh->SnapToGrid( pos.x ); + pos.y = TheNavMesh->SnapToGrid( pos.y ); + + Vector normal; + if ( FindGroundForNode( &pos, &normal ) ) + { + AddWalkableSeed( pos, normal ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Initiate the generation process + */ +void CNavMesh::BeginGeneration( bool incremental ) +{ + IGameEvent *event = gameeventmanager->CreateEvent( "nav_generate" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + +#ifdef TERROR + engine->ServerCommand( "director_stop\nnb_delete_all\n" ); + if ( !incremental && !engine->IsDedicatedServer() ) + { + CBasePlayer *host = UTIL_GetListenServerHost(); + if ( host ) + { + host->ChangeTeam( TEAM_SPECTATOR ); + } + } +#else + engine->ServerCommand( "bot_kick\n" ); +#endif + + // Right now, incrementally-generated areas won't connect to existing areas automatically. + // Since this means hand-editing will be necessary, don't do a full analyze. + if ( incremental ) + { + nav_quicksave.SetValue( 1 ); + } + + m_generationState = SAMPLE_WALKABLE_SPACE; + m_sampleTick = 0; + m_generationMode = (incremental) ? GENERATE_INCREMENTAL : GENERATE_FULL; + lastMsgTime = 0.0f; + + // clear any previous mesh + DestroyNavigationMesh( incremental ); + + SetNavPlace( UNDEFINED_PLACE ); + + // build internal representations of ladders, which are used to find new walkable areas + if ( !incremental ) ///< @incremental update doesn't build ladders to avoid overlapping existing ones + { + BuildLadders(); + } + + // start sampling from a spawn point + if ( !incremental ) + { + AddWalkableSeeds(); + } + + // the system will see this NULL and select the next walkable seed + m_currentNode = NULL; + + // if there are no seed points, we can't generate + if (m_walkableSeeds.Count() == 0) + { + m_generationMode = GENERATE_NONE; + Msg( "No valid walkable seed positions. Cannot generate Navigation Mesh.\n" ); + return; + } + + // initialize seed list index + m_seedIdx = 0; + + Msg( "Generating Navigation Mesh...\n" ); + m_generationStartTime = Plat_FloatTime(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Re-analyze an existing Mesh. Determine Hiding Spots, Encounter Spots, etc. + */ +void CNavMesh::BeginAnalysis( bool quitWhenFinished ) +{ +#ifdef TERROR + if ( !engine->IsDedicatedServer() ) + { + CBasePlayer *host = UTIL_GetListenServerHost(); + if ( host ) + { + host->ChangeTeam( TEAM_SPECTATOR ); + engine->ServerCommand( "director_no_death_check 1\ndirector_stop\nnb_delete_all\n" ); + + ConVarRef mat_fullbright( "mat_fullbright" ); + ConVarRef mat_hdr_level( "mat_hdr_level" ); + + if( mat_fullbright.GetBool() ) + { + Warning( "Setting mat_fullbright 0\n" ); + mat_fullbright.SetValue( 0 ); + } + + if ( mat_hdr_level.GetInt() < 2 ) + { + Warning( "Enabling HDR and reloading materials\n" ); + mat_hdr_level.SetValue( 2 ); + engine->ClientCommand( host->edict(), "mat_reloadallmaterials\n" ); + } + + // Running a threaded server breaks our lighting calculations + ConVarRef host_thread_mode( "host_thread_mode" ); + m_hostThreadModeRestoreValue = host_thread_mode.GetInt(); + host_thread_mode.SetValue( 0 ); + ConVarRef mat_queue_mode( "mat_queue_mode" ); + mat_queue_mode.SetValue( 0 ); + } + } +#endif + + // Remove and re-add elements in TheNavAreas, to ensure indices are useful for progress feedback + NavAreaVector tmpSet; + { + FOR_EACH_VEC( TheNavAreas, it ) + { + tmpSet.AddToTail( TheNavAreas[it] ); + } + } + TheNavAreas.RemoveAll(); + { + FOR_EACH_VEC( tmpSet, it ) + { + TheNavAreas.AddToTail( tmpSet[it] ); + } + } + + DestroyHidingSpots(); + m_generationState = FIND_HIDING_SPOTS; + m_generationIndex = 0; + m_generationMode = GENERATE_ANALYSIS_ONLY; + m_bQuitWhenFinished = quitWhenFinished; + lastMsgTime = 0.0f; + m_generationStartTime = Plat_FloatTime(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void ShowViewPortPanelToAll( const char * name, bool bShow, KeyValues *data ) +{ + CRecipientFilter filter; + filter.AddAllPlayers(); + filter.MakeReliable(); + + int count = 0; + KeyValues *subkey = NULL; + + if ( data ) + { + subkey = data->GetFirstSubKey(); + while ( subkey ) + { + count++; subkey = subkey->GetNextKey(); + } + + subkey = data->GetFirstSubKey(); // reset + } + + UserMessageBegin( filter, "VGUIMenu" ); + WRITE_STRING( name ); // menu name + WRITE_BYTE( bShow?1:0 ); + WRITE_BYTE( count ); + + // write additional data (be careful not more than 192 bytes!) + while ( subkey ) + { + WRITE_STRING( subkey->GetName() ); + WRITE_STRING( subkey->GetString() ); + subkey = subkey->GetNextKey(); + } + MessageEnd(); +} + + +//-------------------------------------------------------------------------------------------------------------- +static void AnalysisProgress( const char *msg, int ticks, int current, bool showPercent = true ) +{ + const float MsgInterval = 10.0f; + float now = Plat_FloatTime(); + if ( now > lastMsgTime + MsgInterval ) + { + if ( showPercent && ticks ) + { + Msg( "%s %.0f%%\n", msg, current*100.0f/ticks ); + } + else + { + Msg( "%s\n", msg ); + } + + lastMsgTime = now; + } + + KeyValues *data = new KeyValues("data"); + data->SetString( "msg", msg ); + data->SetInt( "total", ticks ); + data->SetInt( "current", current ); + + ShowViewPortPanelToAll( PANEL_NAV_PROGRESS, true, data ); + + data->deleteThis(); +} + + +//-------------------------------------------------------------------------------------------------------------- +static void HideAnalysisProgress( void ) +{ + KeyValues *data = new KeyValues("data"); + ShowViewPortPanelToAll( PANEL_NAV_PROGRESS, false, data ); + data->deleteThis(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Process the auto-generation for 'maxTime' seconds. return false if generation is complete. + */ +bool CNavMesh::UpdateGeneration( float maxTime ) +{ + double startTime = Plat_FloatTime(); + static unsigned int s_movedPlayerToArea = 0; // Last area we moved a player to for lighting calcs + static CountdownTimer s_playerSettleTimer; // Settle time after moving the player for lighting calcs + static CUtlVector s_unlitAreas; + static CUtlVector s_unlitSeedAreas; + + static ConVarRef host_thread_mode( "host_thread_mode" ); + + switch( m_generationState ) + { + //--------------------------------------------------------------------------- + case SAMPLE_WALKABLE_SPACE: + { + AnalysisProgress( "Sampling walkable space...", 100, m_sampleTick / 10, false ); + m_sampleTick = ( m_sampleTick + 1 ) % 1000; + + while ( SampleStep() ) + { + if ( Plat_FloatTime() - startTime > maxTime ) + { + return true; + } + } + + // sampling is complete, now build nav areas + m_generationState = CREATE_AREAS_FROM_SAMPLES; + + return true; + } + + //--------------------------------------------------------------------------- + case CREATE_AREAS_FROM_SAMPLES: + { + Msg( "Creating navigation areas from sampled data...\n" ); + + // Select all pre-existing areas + if ( m_generationMode == GENERATE_INCREMENTAL ) + { + ClearSelectedSet(); + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[nit]; + AddToSelectedSet( area ); + } + } + + // Create new areas + CreateNavAreasFromNodes(); + + // And toggle the selection, so we end up with the new areas + if ( m_generationMode == GENERATE_INCREMENTAL ) + { + CommandNavToggleSelectedSet(); + } + + DestroyHidingSpots(); + + // Remove and re-add elements in TheNavAreas, to ensure indices are useful for progress feedback + NavAreaVector tmpSet; + { + FOR_EACH_VEC( TheNavAreas, it ) + { + tmpSet.AddToTail( TheNavAreas[it] ); + } + } + TheNavAreas.RemoveAll(); + { + FOR_EACH_VEC( tmpSet, it ) + { + TheNavAreas.AddToTail( tmpSet[it] ); + } + } + + m_generationState = FIND_HIDING_SPOTS; + m_generationIndex = 0; + return true; + } + + //--------------------------------------------------------------------------- + case FIND_HIDING_SPOTS: + { + while( m_generationIndex < TheNavAreas.Count() ) + { + CNavArea *area = TheNavAreas[ m_generationIndex ]; + ++m_generationIndex; + + area->ComputeHidingSpots(); + + // don't go over our time allotment + if( Plat_FloatTime() - startTime > maxTime ) + { + AnalysisProgress( "Finding hiding spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() ); + return true; + } + } + + Msg( "Finding hiding spots...DONE\n" ); + + m_generationState = FIND_ENCOUNTER_SPOTS; + m_generationIndex = 0; + return true; + } + + //--------------------------------------------------------------------------- + case FIND_ENCOUNTER_SPOTS: + { + while( m_generationIndex < TheNavAreas.Count() ) + { + CNavArea *area = TheNavAreas[ m_generationIndex ]; + ++m_generationIndex; + + area->ComputeSpotEncounters(); + + // don't go over our time allotment + if( Plat_FloatTime() - startTime > maxTime ) + { + AnalysisProgress( "Finding encounter spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() ); + return true; + } + } + + Msg( "Finding encounter spots...DONE\n" ); + + m_generationState = FIND_SNIPER_SPOTS; + m_generationIndex = 0; + return true; + } + + //--------------------------------------------------------------------------- + case FIND_SNIPER_SPOTS: + { + while( m_generationIndex < TheNavAreas.Count() ) + { + CNavArea *area = TheNavAreas[ m_generationIndex ]; + ++m_generationIndex; + + area->ComputeSniperSpots(); + + // don't go over our time allotment + if( Plat_FloatTime() - startTime > maxTime ) + { + AnalysisProgress( "Finding sniper spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() ); + return true; + } + } + + Msg( "Finding sniper spots...DONE\n" ); + + m_generationState = COMPUTE_MESH_VISIBILITY; + m_generationIndex = 0; + BeginVisibilityComputations(); + Msg( "Computing mesh visibility...\n" ); + + return true; + } + + //--------------------------------------------------------------------------- + case COMPUTE_MESH_VISIBILITY: + { + while( m_generationIndex < TheNavAreas.Count() ) + { + CNavArea *area = TheNavAreas[ m_generationIndex ]; + ++m_generationIndex; + + area->ComputeVisibilityToMesh(); + + // don't go over our time allotment + if ( Plat_FloatTime() - startTime > maxTime ) + { + AnalysisProgress( "Computing mesh visibility...", 100, 100 * m_generationIndex / TheNavAreas.Count() ); + return true; + } + } + + Msg( "Optimizing mesh visibility...\n" ); + + EndVisibilityComputations(); + + Msg( "Computing mesh visibility...DONE\n" ); + + m_generationState = FIND_EARLIEST_OCCUPY_TIMES; + m_generationIndex = 0; + return true; + } + + //--------------------------------------------------------------------------- + case FIND_EARLIEST_OCCUPY_TIMES: + { + while( m_generationIndex < TheNavAreas.Count() ) + { + CNavArea *area = TheNavAreas[ m_generationIndex ]; + ++m_generationIndex; + + area->ComputeEarliestOccupyTimes(); + + // don't go over our time allotment + if( Plat_FloatTime() - startTime > maxTime ) + { + AnalysisProgress( "Finding earliest occupy times...", 100, 100 * m_generationIndex / TheNavAreas.Count() ); + return true; + } + } + + Msg( "Finding earliest occupy times...DONE\n" ); + +#ifdef NAV_ANALYZE_LIGHT_INTENSITY + bool shouldSkipLightComputation = ( m_generationMode == GENERATE_INCREMENTAL || engine->IsDedicatedServer() ); +#else + bool shouldSkipLightComputation = true; +#endif + + if ( shouldSkipLightComputation ) + { + m_generationState = CUSTOM; // no light intensity calcs for incremental generation or dedicated servers + } + else + { + m_generationState = FIND_LIGHT_INTENSITY; + s_playerSettleTimer.Invalidate(); + CNavArea::MakeNewMarker(); + s_unlitAreas.RemoveAll(); + FOR_EACH_VEC( TheNavAreas, nit ) + { + s_unlitAreas.AddToTail( TheNavAreas[nit] ); + s_unlitSeedAreas.AddToTail( TheNavAreas[nit] ); + } + } + + m_generationIndex = 0; + return true; + } + + //--------------------------------------------------------------------------- + case FIND_LIGHT_INTENSITY: + { + host_thread_mode.SetValue( 0 ); // need non-threaded server for light calcs + + CBasePlayer *host = UTIL_GetListenServerHost(); + + if ( !s_unlitAreas.Count() || !host ) + { + Msg( "Finding light intensity...DONE\n" ); + + m_generationState = CUSTOM; + m_generationIndex = 0; + return true; + } + + if ( !s_playerSettleTimer.IsElapsed() ) + return true; // wait for eyePos to settle + + // Now try to compute lighting for remaining areas + int sit = 0; + while( sit < s_unlitAreas.Count() ) + { + CNavArea *area = s_unlitAreas[sit]; + if ( area->ComputeLighting() ) + { + s_unlitSeedAreas.FindAndRemove( area ); + s_unlitAreas.Remove( sit ); + + continue; + } + else + { + ++sit; + } + } + + if ( s_unlitAreas.Count() ) + { + if ( s_unlitSeedAreas.Count() ) + { + CNavArea *moveArea = s_unlitSeedAreas[0]; + s_unlitSeedAreas.FastRemove( 0 ); + + //Msg( "Moving to new area %d to compute lighting for %d/%d areas\n", moveArea->GetID(), s_unlitAreas.Count(), TheNavAreas.Count() ); + + Vector eyePos = moveArea->GetCenter(); + float height; + if ( GetGroundHeight( eyePos, &height ) ) + { + eyePos.z = height + HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings + } + else + { + eyePos.z += HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings + } + host->SetAbsOrigin( eyePos ); + AnalysisProgress( "Finding light intensity...", 100, 100 * (TheNavAreas.Count() - s_unlitAreas.Count()) / TheNavAreas.Count() ); + s_movedPlayerToArea = moveArea->GetID(); + s_playerSettleTimer.Start( 0.1f ); + return true; + } + else + { + Msg( "Finding light intensity...DONE (%d unlit areas)\n", s_unlitAreas.Count() ); + if ( s_unlitAreas.Count() ) + { + Warning( "To see unlit areas:\n" ); + for ( int sit=0; sitGetID() ); + } + } + + m_generationState = CUSTOM; + m_generationIndex = 0; + } + } + + Msg( "Finding light intensity...DONE\n" ); + + m_generationState = CUSTOM; + m_generationIndex = 0; + return true; + } + + //--------------------------------------------------------------------------- + case CUSTOM: + { + if ( m_generationIndex == 0 ) + { + BeginCustomAnalysis( m_generationMode == GENERATE_INCREMENTAL ); + Msg( "Start custom...\n "); + } + while( m_generationIndex < TheNavAreas.Count() ) + { + CNavArea *area = TheNavAreas[ m_generationIndex ]; + ++m_generationIndex; + + area->CustomAnalysis( m_generationMode == GENERATE_INCREMENTAL ); + + // don't go over our time allotment + if( Plat_FloatTime() - startTime > maxTime ) + { + AnalysisProgress( "Custom game-specific analysis...", 100, 100 * m_generationIndex / TheNavAreas.Count() ); + return true; + } + } + + Msg( "Post custom...\n "); + PostCustomAnalysis(); + + EndCustomAnalysis(); + Msg( "Custom game-specific analysis...DONE\n" ); + + m_generationState = SAVE_NAV_MESH; + m_generationIndex = 0; + ConVarRef mat_queue_mode( "mat_queue_mode" ); + mat_queue_mode.SetValue( -1 ); + host_thread_mode.SetValue( m_hostThreadModeRestoreValue ); // restore this + return true; + } + + //--------------------------------------------------------------------------- + case SAVE_NAV_MESH: + { + if ( m_generationMode == GENERATE_ANALYSIS_ONLY || m_generationMode == GENERATE_FULL ) + { + m_isAnalyzed = true; + } + + // generation complete! + float generationTime = Plat_FloatTime() - m_generationStartTime; + Msg( "Generation complete! %0.1f seconds elapsed.\n", generationTime ); + bool restart = m_generationMode != GENERATE_INCREMENTAL; + m_generationMode = GENERATE_NONE; + m_isLoaded = true; + ClearWalkableSeeds(); + + HideAnalysisProgress(); + + // save the mesh + if (Save()) + { + Msg( "Navigation map '%s' saved.\n", GetFilename() ); + } + else + { + const char *filename = GetFilename(); + Msg( "ERROR: Cannot save navigation map '%s'.\n", (filename) ? filename : "(null)" ); + } + + if ( m_bQuitWhenFinished ) + { + engine->ServerCommand( "quit\n" ); + } + else if ( restart ) + { + engine->ChangeLevel( STRING( gpGlobals->mapname ), NULL ); + } + else + { + FOR_EACH_VEC( TheNavAreas, it ) + { + TheNavAreas[ it ]->ResetNodes(); + } + +#if !(DEBUG_NAV_NODES) + // destroy navigation nodes created during map generation + CNavNode *node, *next; + for( node = CNavNode::m_list; node; node = next ) + { + next = node->m_next; + delete node; + } + CNavNode::m_list = NULL; + CNavNode::m_listLength = 0; + CNavNode::m_nextID = 1; +#endif // !(DEBUG_NAV_NODES) + } + + return false; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Define the name of player spawn entities + */ +void CNavMesh::SetPlayerSpawnName( const char *name ) +{ + if (m_spawnName) + { + delete [] m_spawnName; + } + + m_spawnName = new char [ strlen(name) + 1 ]; + strcpy( m_spawnName, name ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return name of player spawn entity + */ +const char *CNavMesh::GetPlayerSpawnName( void ) const +{ + if (m_spawnName) + return m_spawnName; + + // default value + return "info_player_start"; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add a nav node and connect it. + * Node Z positions are ground level. + */ +CNavNode *CNavMesh::AddNode( const Vector &destPos, const Vector &normal, NavDirType dir, CNavNode *source, bool isOnDisplacement, + float obstacleHeight, float obstacleStartDist, float obstacleEndDist ) +{ + // check if a node exists at this location + CNavNode *node = CNavNode::GetNode( destPos ); + + // if no node exists, create one + bool useNew = false; + if (node == NULL) + { + node = new CNavNode( destPos, normal, source, isOnDisplacement ); + OnNodeAdded( node ); + useNew = true; + } + + // connect source node to new node + source->ConnectTo( node, dir, obstacleHeight, obstacleStartDist, obstacleEndDist ); + + // optimization: if deltaZ changes very little, assume connection is commutative + const float zTolerance = 50.0f; + float deltaZ = source->GetPosition()->z - destPos.z; + if (fabs( deltaZ ) < zTolerance) + { + if ( obstacleHeight > 0 ) + { + obstacleHeight = MAX( obstacleHeight + deltaZ, 0 ); + Assert( obstacleHeight > 0 ); + } + node->ConnectTo( source, OppositeDirection( dir ), obstacleHeight, GenerationStepSize - obstacleEndDist, GenerationStepSize - obstacleStartDist ); + node->MarkAsVisited( OppositeDirection( dir ) ); + } + + if (useNew) + { + // new node becomes current node + m_currentNode = node; + } + + node->CheckCrouch(); + + // determine if there's a cliff nearby and set an attribute on this node + for ( int i = 0; i < NUM_DIRECTIONS; i++ ) + { + NavDirType dir = (NavDirType) i; + if ( CheckCliff( node->GetPosition(), dir ) ) + { + node->SetAttributes( node->GetAttributes() | NAV_MESH_CLIFF ); + break; + } + } + + return node; +} + +//-------------------------------------------------------------------------------------------------------------- +inline CNavNode *LadderEndSearch( const Vector *pos, NavDirType mountDir ) +{ + Vector center = *pos; + AddDirectionVector( ¢er, mountDir, HalfHumanWidth ); + + // + // Test the ladder dismount point first, then each cardinal direction one and two steps away + // + for( int d=(-1); d<2*NUM_DIRECTIONS; ++d ) + { + Vector tryPos = center; + + if (d >= NUM_DIRECTIONS) + AddDirectionVector( &tryPos, (NavDirType)(d - NUM_DIRECTIONS), 2.0f*GenerationStepSize ); + else if (d >= 0) + AddDirectionVector( &tryPos, (NavDirType)d, GenerationStepSize ); + + // step up a rung, to ensure adjacent floors are below us + tryPos.z += GenerationStepSize; + + tryPos.x = TheNavMesh->SnapToGrid( tryPos.x ); + tryPos.y = TheNavMesh->SnapToGrid( tryPos.y ); + + // adjust height to account for sloping areas + Vector tryNormal; + if (TheNavMesh->GetGroundHeight( tryPos, &tryPos.z, &tryNormal ) == false) + continue; + + // make sure this point is not on the other side of a wall + const float fudge = 4.0f; + trace_t result; + UTIL_TraceHull( center + Vector( 0, 0, fudge ), tryPos + Vector( 0, 0, fudge ), NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction != 1.0f || result.startsolid) + continue; + + // if no node exists here, create one and continue the search + if (CNavNode::GetNode( tryPos ) == NULL) + { + return new CNavNode( tryPos, tryNormal, NULL, false ); + } + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CNavMesh::FindGroundForNode( Vector *pos, Vector *normal ) +{ + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_EVERYTHING ); + trace_t tr; + Vector start( pos->x, pos->y, pos->z + VEC_DUCK_HULL_MAX.z - 0.1f ); + Vector end( *pos ); + end.z -= DeathDrop; + + UTIL_TraceHull( + start, + end, + NavTraceMins, + NavTraceMaxs, + GetGenerationTraceMask(), + &filter, + &tr ); + + *pos = tr.endpos; + *normal = tr.plane.normal; + + return ( !tr.allsolid ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void DrawTrace( const trace_t *trace ) +{ + /* + if ( trace->fraction > 0.0f && !trace->startsolid ) + { + NDebugOverlay::SweptBox( trace->startpos, trace->endpos, NavTraceMins, NavTraceMaxs, vec3_angle, 0, 255, 0, 45, 100 ); + } + else + { + NDebugOverlay::SweptBox( trace->startpos, trace->endpos, NavTraceMins, NavTraceMaxs, vec3_angle, 255, 0, 0, 45, 100 ); + } + */ +} + + +//-------------------------------------------------------------------------------------------------------------- +bool StayOnFloor( trace_t *trace, float zLimit /* = DeathDrop */ ) +{ + Vector start( trace->endpos ); + Vector end( start ); + end.z -= zLimit; + + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace ); + DrawTrace( trace ); + + if ( trace->startsolid || trace->fraction >= 1.0f ) + { + return false; + } + + if ( trace->plane.normal.z < nav_slope_limit.GetFloat() ) + { + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool TraceAdjacentNode( int depth, const Vector& start, const Vector& end, trace_t *trace, float zLimit /* = DeathDrop */ ) +{ + const float MinDistance = 1.0f; // if we can't move at least this far, don't bother stepping up. + + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace ); + DrawTrace( trace ); + + // If we started in the ground for some reason, bail + if ( trace->startsolid ) + return false; + + // If we made it, so try to find the floor + if ( end.x == trace->endpos.x && end.y == trace->endpos.y ) + { + return StayOnFloor( trace, zLimit ); + } + + // If we didn't make enough progress, bail + if ( depth && start.AsVector2D().DistToSqr( trace->endpos.AsVector2D() ) < MinDistance * MinDistance ) + { + return false; + } + + // We made it more than MinDistance. If the slope is too steep, we can't go on. + if ( !StayOnFloor( trace, zLimit ) ) + { + return false; + } + + // Try to go up as if we stepped up, forward, and down. + Vector testStart( trace->endpos ); + Vector testEnd( testStart ); + testEnd.z += StepHeight; + UTIL_TraceHull( testStart, testEnd, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace ); + DrawTrace( trace ); + + Vector forwardTestStart = trace->endpos; + Vector forwardTestEnd = end; + forwardTestEnd.z = forwardTestStart.z; + return TraceAdjacentNode( depth+1, forwardTestStart, forwardTestEnd, trace ); +} + + +//-------------------------------------------------------------------------------------------------------- +static bool IsNodeOverlapped( const Vector& pos, const Vector& offset ) +{ + bool overlap = TheNavMesh->GetNavArea( pos + offset, HumanHeight ) != NULL; + if ( !overlap ) + { + Vector mins( -0.5f, -0.5f, -0.5f ); + Vector maxs( 0.5f, 0.5f, 0.5f ); + + Vector start = pos; + start.z += HalfHumanHeight; + Vector end = start; + end.x += offset.x * GenerationStepSize; + end.y += offset.y * GenerationStepSize; + trace_t trace; + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + UTIL_TraceHull( start, end, mins, maxs, TheNavMesh->GetGenerationTraceMask(), &filter, &trace ); + if ( trace.startsolid || trace.allsolid ) + { + return true; + } + + if ( trace.fraction < 0.1f ) + { + return true; + } + + start = trace.endpos; + end.z -= HalfHumanHeight * 2; + UTIL_TraceHull( start, end, mins, maxs, TheNavMesh->GetGenerationTraceMask(), &filter, &trace ); + if ( trace.startsolid || trace.allsolid ) + { + return true; + } + + if ( trace.fraction == 1.0f ) + { + return true; + } + + if ( trace.plane.normal.z < 0.7f ) + { + return true; + } + } + return overlap; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Search the world and build a map of possible movements. + * The algorithm begins at the bot's current location, and does a recursive search + * outwards, tracking all valid steps and generating a directed graph of CNavNodes. + * + * Sample the map one "step" in a cardinal direction to learn the map. + * + * Returns true if sampling needs to continue, or false if done. + */ +bool CNavMesh::SampleStep( void ) +{ + // take a step + while( true ) + { + if (m_currentNode == NULL) + { + // sampling is complete from current seed, try next one + m_currentNode = GetNextWalkableSeedNode(); + + if (m_currentNode == NULL) + { + if ( m_generationMode == GENERATE_INCREMENTAL || m_generationMode == GENERATE_SIMPLIFY ) + { + return false; + } + + // search is exhausted - continue search from ends of ladders + for ( int i=0; im_bottom, ladder->GetDir() )) != 0) + break; + + // check ladder top + if ((m_currentNode = LadderEndSearch( &ladder->m_top, ladder->GetDir() )) != 0) + break; + } + + if (m_currentNode == NULL) + { + // all seeds exhausted, sampling complete + return false; + } + } + } + + // + // Take a step from this node + // + for( int dir = NORTH; dir < NUM_DIRECTIONS; dir++ ) + { + if (!m_currentNode->HasVisited( (NavDirType)dir )) + { + // have not searched in this direction yet + + // start at current node position + Vector pos = *m_currentNode->GetPosition(); + + // snap to grid + int cx = SnapToGrid( pos.x ); + int cy = SnapToGrid( pos.y ); + + // attempt to move to adjacent node + switch( dir ) + { + case NORTH: cy -= GenerationStepSize; break; + case SOUTH: cy += GenerationStepSize; break; + case EAST: cx += GenerationStepSize; break; + case WEST: cx -= GenerationStepSize; break; + } + + pos.x = cx; + pos.y = cy; + + m_generationDir = (NavDirType)dir; + + // mark direction as visited + m_currentNode->MarkAsVisited( m_generationDir ); + + // sanity check to not generate across the world for incremental generation + const float incrementalRange = nav_generate_incremental_range.GetFloat(); + if ( m_generationMode == GENERATE_INCREMENTAL && incrementalRange > 0 ) + { + bool inRange = false; + for ( int i=0; iGetPosition() ); + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + Vector to, toNormal; + float obstacleHeight = 0, obstacleStartDist = 0, obstacleEndDist = GenerationStepSize; + if ( TraceAdjacentNode( 0, from, pos, &result ) ) + { + to = result.endpos; + toNormal = result.plane.normal; + } + else + { + // test going up ClimbUpHeight + bool success = false; + for ( float height = StepHeight; height <= ClimbUpHeight; height += 1.0f ) + { + trace_t tr; + Vector start( from ); + Vector end( pos ); + start.z += height; + end.z += height; + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr ); + if ( !tr.startsolid && tr.fraction == 1.0f ) + { + if ( !StayOnFloor( &tr ) ) + { + break; + } + + to = tr.endpos; + toNormal = tr.plane.normal; + + start = end = from; + end.z += height; + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr ); + if ( tr.fraction < 1.0f ) + { + break; + } + + // keep track of far up we had to go to find a path to the next node + obstacleHeight = height; + success = true; + break; + } + else + { + // Could not trace from node to node at this height, something is in the way. + // Trace in the other direction to see if we hit something + Vector vecToObstacleStart = tr.endpos - start; + Assert( vecToObstacleStart.LengthSqr() <= Square( GenerationStepSize ) ); + if ( vecToObstacleStart.LengthSqr() <= Square( GenerationStepSize ) ) + { + UTIL_TraceHull( end, start, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr ); + if ( !tr.startsolid && tr.fraction < 1.0 ) + { + // We hit something going the other direction. There is some obstacle between the two nodes. + Vector vecToObstacleEnd = tr.endpos - start; + Assert( vecToObstacleEnd.LengthSqr() <= Square( GenerationStepSize ) ); + if ( vecToObstacleEnd.LengthSqr() <= Square( GenerationStepSize ) ) + { + // Remember the distances to start and end of the obstacle (with respect to the "from" node). + // Keep track of the last distances to obstacle as we keep increasing the height we do a trace for. + // If we do eventually clear the obstacle, these values will be the start and end distance to the + // very tip of the obstacle. + obstacleStartDist = vecToObstacleStart.Length(); + obstacleEndDist = vecToObstacleEnd.Length(); + if ( obstacleEndDist == 0 ) + { + obstacleEndDist = GenerationStepSize; + } + } + } + } + } + } + + if ( !success ) + { + return true; + } + } + + // Don't generate nodes if we spill off the end of the world onto skybox + if ( result.surface.flags & ( SURF_SKY|SURF_SKY2D ) ) + { + return true; + } + + // If we're incrementally generating, don't overlap existing nav areas. + Vector testPos( to ); + bool overlapSE = IsNodeOverlapped( testPos, Vector( 1, 1, HalfHumanHeight ) ); + bool overlapSW = IsNodeOverlapped( testPos, Vector( -1, 1, HalfHumanHeight ) ); + bool overlapNE = IsNodeOverlapped( testPos, Vector( 1, -1, HalfHumanHeight ) ); + bool overlapNW = IsNodeOverlapped( testPos, Vector( -1, -1, HalfHumanHeight ) ); + if ( overlapSE && overlapSW && overlapNE && overlapNW && m_generationMode != GENERATE_SIMPLIFY ) + { + return true; + } + + int nTolerance = nav_generate_incremental_tolerance.GetInt(); + if ( nTolerance > 0 && m_generationMode == GENERATE_INCREMENTAL ) + { + bool bValid = false; + int zPos = to.z; + for ( int i=0; i= zMin && zPos <= zMax ) + { + bValid = true; + break; + } + } + + if ( !bValid ) + return true; + } + + + bool isOnDisplacement = result.IsDispSurface(); + + if ( nav_displacement_test.GetInt() > 0 ) + { + // Test for nodes under displacement surfaces. + // This happens during development, and is a pain because the space underneath a displacement + // is not 'solid'. + Vector start = to + Vector( 0, 0, 0 ); + Vector end = start + Vector( 0, 0, nav_displacement_test.GetInt() ); + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &result ); + + if ( result.fraction > 0 ) + { + end = start; + start = result.endpos; + UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &result ); + if ( result.fraction < 1 ) + { + // if we made it down to within StepHeight, maybe we're on a static prop + if ( result.endpos.z > to.z + StepHeight ) + { + return true; + } + } + } + } + + float deltaZ = to.z - m_currentNode->GetPosition()->z; + // If there's an obstacle in the way and it's traversable, or the obstacle is not higher than the destination node itself minus a small epsilon + // (meaning the obstacle was just the height change to get to the destination node, no extra obstacle between the two), clear obstacle height + // and distances + if ( ( obstacleHeight < MaxTraversableHeight ) || ( deltaZ > ( obstacleHeight - 2.0f ) ) ) + { + obstacleHeight = 0; + obstacleStartDist = 0; + obstacleEndDist = GenerationStepSize; + } + + // we can move here + // create a new navigation node, and update current node pointer + AddNode( to, toNormal, m_generationDir, m_currentNode, isOnDisplacement, obstacleHeight, obstacleStartDist, obstacleEndDist ); + + return true; + } + } + + // all directions have been searched from this node - pop back to its parent and continue + m_currentNode = m_currentNode->GetParent(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add given walkable position to list of seed positions for map sampling + */ +void CNavMesh::AddWalkableSeed( const Vector &pos, const Vector &normal ) +{ + WalkableSeedSpot seed; + + seed.pos.x = RoundToUnits( pos.x, GenerationStepSize ); + seed.pos.y = RoundToUnits( pos.y, GenerationStepSize ); + seed.pos.z = pos.z; + seed.normal = normal; + + m_walkableSeeds.AddToTail( seed ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the next walkable seed as a node + */ +CNavNode *CNavMesh::GetNextWalkableSeedNode( void ) +{ + if ( m_seedIdx >= m_walkableSeeds.Count() ) + return NULL; + + WalkableSeedSpot spot = m_walkableSeeds[ m_seedIdx ]; + ++m_seedIdx; + + // check if a node exists at this location + CNavNode *node = CNavNode::GetNode( spot.pos ); + if ( node ) + return NULL; + + return new CNavNode( spot.pos, spot.normal, NULL, false ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check LOS, ignoring any entities that we can walk through + */ +bool IsWalkableTraceLineClear( const Vector &from, const Vector &to, unsigned int flags ) +{ + trace_t result; + CBaseEntity *ignore = NULL; + Vector useFrom = from; + + CTraceFilterWalkableEntities traceFilter( NULL, COLLISION_GROUP_NONE, flags ); + + result.fraction = 0.0f; + + const int maxTries = 50; + for( int t=0; tGetSizeX() / 2.0f; + + if (split < GenerationStepSize) + { + if (canDivideY) + { + SubdivideY( area, false, canDivideY, depth ); + } + return; + } + + split += area->GetCorner( NORTH_WEST ).x; + + split = TheNavMesh->SnapToGrid( split ); + + CNavArea *alpha, *beta; + if (area->SplitEdit( false, split, &alpha, &beta )) + { + SubdivideY( alpha, canDivideX, canDivideY, depth ); + SubdivideY( beta, canDivideX, canDivideY, depth ); + } + } + + + void SubdivideY( CNavArea *area, bool canDivideX, bool canDivideY, int depth ) + { + if (!canDivideY) + return; + + float split = area->GetSizeY() / 2.0f; + + if (split < GenerationStepSize) + { + if (canDivideX) + { + SubdivideX( area, canDivideX, false, depth-1 ); + } + return; + } + + split += area->GetCorner( NORTH_WEST ).y; + + split = TheNavMesh->SnapToGrid( split ); + + CNavArea *alpha, *beta; + if (area->SplitEdit( true, split, &alpha, &beta )) + { + SubdivideX( alpha, canDivideX, canDivideY, depth-1 ); + SubdivideX( beta, canDivideX, canDivideY, depth-1 ); + } + } + + int m_depth; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Subdivide each nav area in X and Y to create 4 new areas + */ +void CNavMesh::CommandNavSubdivide( const CCommand &args ) +{ + int depth = 1; + + if (args.ArgC() == 2) + { + depth = atoi( args[1] ); + } + + Subdivider chop( depth ); + TheNavMesh->ForAllSelectedAreas( chop ); +} + +CON_COMMAND_F( nav_subdivide, "Subdivides all selected areas.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSubdivide( args ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Debugging code to verify that all nav area connections are internally consistent +*/ +void CNavMesh::ValidateNavAreaConnections( void ) +{ + // iterate all nav areas + NavConnect connect; + + for ( int it = 0; it < TheNavAreas.Count(); it++ ) + { + CNavArea *area = TheNavAreas[ it ]; + + for ( NavDirType dir = NORTH; dir < NUM_DIRECTIONS; dir = (NavDirType) ( ( (int) dir ) +1 ) ) + { + const NavConnectVector *pOutgoing = area->GetAdjacentAreas( dir ); + const NavConnectVector *pIncoming = area->GetIncomingConnections( dir ); + + for ( int iConnect = 0; iConnect < pOutgoing->Count(); iConnect++ ) + { + // make sure no area is on both the connection and incoming list + CNavArea *areaOther = (*pOutgoing)[iConnect].area; + connect.area = areaOther; + if ( pIncoming->Find( connect ) != pIncoming->InvalidIndex() ) + { + Msg( "Area %d has area %d on both 2-way and incoming list, should only be on one\n", area->GetID(), areaOther->GetID() ); + Assert( false ); + } + + // make sure there are no duplicate connections on the list + for ( int iConnectCheck = iConnect+1; iConnectCheck < pOutgoing->Count(); iConnectCheck++ ) + { + CNavArea *areaCheck = (*pOutgoing)[iConnectCheck].area; + if ( areaOther == areaCheck ) + { + Msg( "Area %d has multiple outgoing connections to area %d in direction %d\n", area->GetID(), areaOther->GetID(), dir ); + Assert( false ); + } + } + + const NavConnectVector *pOutgoingOther = areaOther->GetAdjacentAreas( OppositeDirection( dir ) ); + const NavConnectVector *pIncomingOther = areaOther->GetIncomingConnections( OppositeDirection( dir ) ); + + // if we have a one-way outgoing connection, make sure we are on the other area's incoming list + connect.area = area; + bool bIsTwoWay = pOutgoingOther->Find( connect ) != pOutgoingOther->InvalidIndex(); + if ( !bIsTwoWay ) + { + connect.area = area; + bool bOnOthersIncomingList = pIncomingOther->Find( connect ) != pIncomingOther->InvalidIndex(); + if ( !bOnOthersIncomingList ) + { + Msg( "Area %d has one-way connect to area %d but does not appear on the latter's incoming list\n", area->GetID(), areaOther->GetID() ); + } + } + } + + for ( int iConnect = 0; iConnect < pIncoming->Count(); iConnect++ ) + { + CNavArea *areaOther = (*pIncoming)[iConnect].area; + + // make sure there are not duplicate areas on the incoming list + for ( int iConnectCheck = iConnect+1; iConnectCheck < pIncoming->Count(); iConnectCheck++ ) + { + CNavArea *areaCheck = (*pIncoming)[iConnectCheck].area; + if ( areaOther == areaCheck ) + { + Msg( "Area %d has multiple incoming connections to area %d in direction %d\n", area->GetID(), areaOther->GetID(), dir ); + Assert( false ); + } + } + + const NavConnectVector *pOutgoingOther = areaOther->GetAdjacentAreas( OppositeDirection( dir ) ); + connect.area = area; + bool bOnOthersOutgoingList = pOutgoingOther->Find( connect ) != pOutgoingOther->InvalidIndex(); + if ( !bOnOthersOutgoingList ) + { + Msg( "Area %d has incoming connection from area %d but does not appear on latter's outgoing connection list\n", area->GetID(), areaOther->GetID() ); + Assert( false ); + } + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** +* Temp way to mark cliff areas after generation without regen'ing. Any area that is adjacent to a cliff +* gets marked as a cliff. This will leave some big areas marked as cliff just because one edge is adjacent to +* a cliff so it's not great. The code that does this at generation time is better because it ensures that +* areas next to cliffs don't get merged with no-cliff areas. +*/ +void CNavMesh::PostProcessCliffAreas() +{ + for ( int it = 0; it < TheNavAreas.Count(); it++ ) + { + CNavArea *area = TheNavAreas[ it ]; + if ( area->GetAttributes() & NAV_MESH_CLIFF ) + continue; + + for ( int i = 0; i < NUM_DIRECTIONS; i++ ) + { + bool bHasCliff = false; + NavDirType dir = (NavDirType) i; + NavCornerType corner[2]; + + // look at either corner along this edge + corner[0] = (NavCornerType) i; + corner[1] = (NavCornerType) ( ( i+ 1 ) % NUM_CORNERS ); + + for ( int j = 0; j < 2; j++ ) + { + Vector cornerPos = area->GetCorner( corner[j] ); + if ( CheckCliff( &cornerPos, dir ) ) + { + bHasCliff = true; + break; + } + } + + if ( bHasCliff ) + { + area->SetAttributes( area->GetAttributes() | NAV_MESH_CLIFF ); + break; + } + } + } +} + +CON_COMMAND_F( nav_gen_cliffs_approx, "Mark cliff areas, post-processing approximation", FCVAR_CHEAT ) +{ + TheNavMesh->PostProcessCliffAreas(); +} diff --git a/sp/src/game/server/nav_ladder.cpp b/sp/src/game/server/nav_ladder.cpp new file mode 100644 index 00000000..231974a8 --- /dev/null +++ b/sp/src/game/server/nav_ladder.cpp @@ -0,0 +1,654 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +// AI Navigation areas +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#include "cbase.h" + +#include "nav_mesh.h" +#include "nav_node.h" +#include "nav_pathfind.h" +#include "nav_colors.h" +#ifdef TERROR +#include "TerrorShared.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar nav_area_bgcolor; + +unsigned int CNavLadder::m_nextID = 1; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Shift the nav area + */ +void CNavLadder::Shift( const Vector &shift ) +{ + m_top += shift; + m_bottom += shift; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavLadder::CompressIDs( void ) +{ + m_nextID = 1; + + if ( TheNavMesh ) + { + for ( int i=0; iGetLadders().Count(); ++i ) + { + CNavLadder *ladder = TheNavMesh->GetLadders()[i]; + ladder->m_id = m_nextID++; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +CNavArea ** CNavLadder::GetConnection( LadderConnectionType dir ) +{ + switch ( dir ) + { + case LADDER_TOP_FORWARD: + return &m_topForwardArea; + case LADDER_TOP_LEFT: + return &m_topLeftArea; + case LADDER_TOP_RIGHT: + return &m_topRightArea; + case LADDER_TOP_BEHIND: + return &m_topBehindArea; + case LADDER_BOTTOM: + return &m_bottomArea; + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavLadder::OnSplit( CNavArea *original, CNavArea *alpha, CNavArea *beta ) +{ + for ( int con=0; conGetDistanceSquaredToPoint( m_top ); + float betaDistance = beta->GetDistanceSquaredToPoint( m_top ); + + if ( alphaDistance < betaDistance ) + { + *areaConnection = alpha; + } + else + { + *areaConnection = beta; + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Connect this ladder to given area + */ +void CNavLadder::ConnectTo( CNavArea *area ) +{ + float center = (m_top.z + m_bottom.z) * 0.5f; + + if (area->GetCenter().z > center) + { + // connect to top + NavDirType dir; + + Vector dirVector = area->GetCenter() - m_top; + if ( fabs( dirVector.x ) > fabs( dirVector.y ) ) + { + if ( dirVector.x > 0.0f ) // east + { + dir = EAST; + } + else // west + { + dir = WEST; + } + } + else + { + if ( dirVector.y > 0.0f ) // south + { + dir = SOUTH; + } + else // north + { + dir = NORTH; + } + } + + if ( m_dir == dir ) + { + m_topBehindArea = area; + } + else if ( OppositeDirection( m_dir ) == dir ) + { + m_topForwardArea = area; + } + else if ( DirectionLeft( m_dir ) == dir ) + { + m_topLeftArea = area; + } + else + { + m_topRightArea = area; + } + } + else + { + // connect to bottom + m_bottomArea = area; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Destructor + */ +CNavLadder::~CNavLadder() +{ + // tell the other areas we are going away + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + area->OnDestroyNotify( this ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * invoked when given area is going away + */ +void CNavLadder::OnDestroyNotify( CNavArea *dead ) +{ + Disconnect( dead ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Disconnect this ladder from given area + */ +void CNavLadder::Disconnect( CNavArea *area ) +{ + if ( m_topForwardArea == area ) + { + m_topForwardArea = NULL; + } + else if ( m_topLeftArea == area ) + { + m_topLeftArea = NULL; + } + else if ( m_topRightArea == area ) + { + m_topRightArea = NULL; + } + else if ( m_topBehindArea == area ) + { + m_topBehindArea = NULL; + } + else if ( m_bottomArea == area ) + { + m_bottomArea = NULL; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * returns true if given area is connected in given direction + */ +bool CNavLadder::IsConnected( const CNavArea *area, LadderDirectionType dir ) const +{ + if ( dir == LADDER_DOWN ) + { + return area == m_bottomArea; + } + else if ( dir == LADDER_UP ) + { + return ( area == m_topForwardArea || + area == m_topLeftArea || + area == m_topRightArea || + area == m_topBehindArea ); + } + else + { + return ( area == m_bottomArea || + area == m_topForwardArea || + area == m_topLeftArea || + area == m_topRightArea || + area == m_topBehindArea ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavLadder::SetDir( NavDirType dir ) +{ + m_dir = dir; + + m_normal.Init(); + AddDirectionVector( &m_normal, m_dir, 1.0f ); // worst-case, we have the NavDirType as a normal + + Vector from = (m_top + m_bottom) * 0.5f + m_normal * 5.0f; + Vector to = from - m_normal * 32.0f; + + trace_t result; +#ifdef TERROR + // TERROR: use the MASK_ZOMBIESOLID_BRUSHONLY contents, since that's what zombies use + UTIL_TraceLine( from, to, MASK_ZOMBIESOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); +#else + UTIL_TraceLine( from, to, MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); +#endif + + if (result.fraction != 1.0f) + { + bool climbableSurface = physprops->GetSurfaceData( result.surface.surfaceProps )->game.climbable != 0; + if ( !climbableSurface ) + { + climbableSurface = (result.contents & CONTENTS_LADDER) != 0; + } + if ( climbableSurface ) + { + m_normal = result.plane.normal; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavLadder::DrawLadder( void ) const +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if (player == NULL) + return; + + Vector dir; + const Vector &eye = player->EyePosition(); + AngleVectors( player->EyeAngles() + player->GetPunchAngle(), &dir ); + + float dx = eye.x - m_bottom.x; + float dy = eye.y - m_bottom.y; + + Vector2D eyeDir( dx, dy ); + eyeDir.NormalizeInPlace(); + bool isSelected = ( this == TheNavMesh->GetSelectedLadder() ); + bool isMarked = ( this == TheNavMesh->GetMarkedLadder() ); + bool isFront = DotProduct2D( eyeDir, GetNormal().AsVector2D() ) > 0; + + if ( TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING ) ) + { + isSelected = isMarked = false; + isFront = true; + } + + // Highlight ladder entity ------------------------------------------------ + CBaseEntity *ladderEntity = m_ladderEntity.Get(); + if ( ladderEntity ) + { + ladderEntity->DrawAbsBoxOverlay(); + } + + // Draw 'ladder' lines ---------------------------------------------------- + NavEditColor ladderColor = NavNormalColor; + if ( isFront ) + { + if ( isMarked ) + { + ladderColor = NavMarkedColor; + } + else if ( isSelected ) + { + ladderColor = NavSelectedColor; + } + else + { + ladderColor = NavSamePlaceColor; + } + } + else if ( isMarked ) + { + ladderColor = NavMarkedColor; + } + else if ( isSelected ) + { + ladderColor = NavSelectedColor; + } + + Vector right(0, 0, 0), up( 0, 0, 0 ); + VectorVectors( GetNormal(), right, up ); + if ( up.z <= 0.0f ) + { + AssertMsg( false, "A nav ladder has an invalid normal" ); + up.Init( 0, 0, 1 ); + } + + right *= m_width * 0.5f; + + Vector bottomLeft = m_bottom - right; + Vector bottomRight = m_bottom + right; + Vector topLeft = m_top - right; + Vector topRight = m_top + right; + + int bgcolor[4]; + if ( 4 == sscanf( nav_area_bgcolor.GetString(), "%d %d %d %d", &(bgcolor[0]), &(bgcolor[1]), &(bgcolor[2]), &(bgcolor[3]) ) ) + { + for ( int i=0; i<4; ++i ) + bgcolor[i] = clamp( bgcolor[i], 0, 255 ); + + if ( bgcolor[3] > 0 ) + { + Vector offset( 0, 0, 0 ); + AddDirectionVector( &offset, OppositeDirection( m_dir ), 1 ); + NDebugOverlay::Triangle( topLeft+offset, topRight+offset, bottomRight+offset, bgcolor[0], bgcolor[1], bgcolor[2], bgcolor[3], true, 0.15f ); + NDebugOverlay::Triangle( bottomRight+offset, bottomLeft+offset, topLeft+offset, bgcolor[0], bgcolor[1], bgcolor[2], bgcolor[3], true, 0.15f ); + } + } + + NavDrawLine( topLeft, bottomLeft, ladderColor ); + NavDrawLine( topRight, bottomRight, ladderColor ); + + while ( bottomRight.z < topRight.z ) + { + NavDrawLine( bottomRight, bottomLeft, ladderColor ); + bottomRight += up * (GenerationStepSize/2); + bottomLeft += up * (GenerationStepSize/2); + } + + // Draw connector lines --------------------------------------------------- + if ( !TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING ) ) + { + Vector bottom = m_bottom; + Vector top = m_top; + + NavDrawLine( top, bottom, NavConnectedTwoWaysColor ); + + if (m_bottomArea) + { + float offset = GenerationStepSize; + const Vector& areaBottom = m_bottomArea->GetCenter(); + + // don't draw the bottom connection too high if the ladder is very short + if ( top.z - bottom.z < GenerationStepSize * 1.5f ) + offset = 0.0f; + + // don't draw the bottom connection too high if the ladder is high above the area + if ( bottom.z - areaBottom.z > GenerationStepSize * 1.5f ) + offset = 0.0f; + + NavDrawLine( bottom + Vector( 0, 0, offset ), areaBottom, ((m_bottomArea->IsConnected( this, LADDER_UP ))?NavConnectedTwoWaysColor:NavConnectedOneWayColor) ); + } + + if (m_topForwardArea) + NavDrawLine( top, m_topForwardArea->GetCenter(), ((m_topForwardArea->IsConnected( this, LADDER_DOWN ))?NavConnectedTwoWaysColor:NavConnectedOneWayColor) ); + + if (m_topLeftArea) + NavDrawLine( top, m_topLeftArea->GetCenter(), ((m_topLeftArea->IsConnected( this, LADDER_DOWN ))?NavConnectedTwoWaysColor:NavConnectedOneWayColor) ); + + if (m_topRightArea) + NavDrawLine( top, m_topRightArea->GetCenter(), ((m_topRightArea->IsConnected( this, LADDER_DOWN ))?NavConnectedTwoWaysColor:NavConnectedOneWayColor) ); + + if (m_topBehindArea) + NavDrawLine( top, m_topBehindArea->GetCenter(), ((m_topBehindArea->IsConnected( this, LADDER_DOWN ))?NavConnectedTwoWaysColor:NavConnectedOneWayColor) ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavLadder::DrawConnectedAreas( void ) +{ + CUtlVector< CNavArea * > areas; + if ( m_topForwardArea ) + areas.AddToTail( m_topForwardArea ); + if ( m_topLeftArea ) + areas.AddToTail( m_topLeftArea ); + if ( m_topRightArea ) + areas.AddToTail( m_topRightArea ); + if ( m_topBehindArea ) + areas.AddToTail( m_topBehindArea ); + if ( m_bottomArea ) + areas.AddToTail( m_bottomArea ); + + for ( int i=0; iDraw(); + + if ( !TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING ) ) + { + adj->DrawHidingSpots(); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * invoked when a game round restarts + */ +void CNavLadder::OnRoundRestart( void ) +{ + FindLadderEntity(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavLadder::FindLadderEntity( void ) +{ + m_ladderEntity = gEntList.FindEntityByClassnameNearest( "func_simpleladder", (m_top + m_bottom) * 0.5f, HalfHumanWidth ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Save a navigation ladder to the opened binary stream + */ +void CNavLadder::Save( CUtlBuffer &fileBuffer, unsigned int version ) const +{ + // save ID + fileBuffer.PutUnsignedInt( m_id ); + + // save extent of ladder + fileBuffer.PutFloat( m_width ); + + // save top endpoint of ladder + fileBuffer.PutFloat( m_top.x ); + fileBuffer.PutFloat( m_top.y ); + fileBuffer.PutFloat( m_top.z ); + + // save bottom endpoint of ladder + fileBuffer.PutFloat( m_bottom.x ); + fileBuffer.PutFloat( m_bottom.y ); + fileBuffer.PutFloat( m_bottom.z ); + + // save ladder length + fileBuffer.PutFloat( m_length ); + + // save direction + fileBuffer.PutUnsignedInt( m_dir ); + + // save IDs of connecting areas + unsigned int id; + id = ( m_topForwardArea ) ? m_topForwardArea->GetID() : 0; + fileBuffer.PutUnsignedInt( id ); + + id = ( m_topLeftArea ) ? m_topLeftArea->GetID() : 0; + fileBuffer.PutUnsignedInt( id ); + + id = ( m_topRightArea ) ? m_topRightArea->GetID() : 0; + fileBuffer.PutUnsignedInt( id ); + + id = ( m_topBehindArea ) ? m_topBehindArea->GetID() : 0; + fileBuffer.PutUnsignedInt( id ); + + id = ( m_bottomArea ) ? m_bottomArea->GetID() : 0; + fileBuffer.PutUnsignedInt( id ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load a navigation ladder from the opened binary stream + */ +void CNavLadder::Load( CUtlBuffer &fileBuffer, unsigned int version ) +{ + // load ID + m_id = fileBuffer.GetUnsignedInt(); + + // update nextID to avoid collisions + if (m_id >= m_nextID) + m_nextID = m_id+1; + + // load extent of ladder + m_width = fileBuffer.GetFloat(); + + // load top endpoint of ladder + m_top.x = fileBuffer.GetFloat(); + m_top.y = fileBuffer.GetFloat(); + m_top.z = fileBuffer.GetFloat(); + + // load bottom endpoint of ladder + m_bottom.x = fileBuffer.GetFloat(); + m_bottom.y = fileBuffer.GetFloat(); + m_bottom.z = fileBuffer.GetFloat(); + + // load ladder length + m_length = fileBuffer.GetFloat(); + + // load direction + m_dir = (NavDirType)fileBuffer.GetUnsignedInt(); + SetDir( m_dir ); // regenerate the surface normal + + // load dangling status + if ( version == 6 ) + { + bool m_isDangling; + fileBuffer.Get( &m_isDangling, sizeof(m_isDangling) ); + } + + // load IDs of connecting areas + unsigned int id; + id = fileBuffer.GetUnsignedInt(); + m_topForwardArea = TheNavMesh->GetNavAreaByID( id ); + + id = fileBuffer.GetUnsignedInt(); + m_topLeftArea = TheNavMesh->GetNavAreaByID( id ); + + id = fileBuffer.GetUnsignedInt(); + m_topRightArea = TheNavMesh->GetNavAreaByID( id ); + + id = fileBuffer.GetUnsignedInt(); + m_topBehindArea = TheNavMesh->GetNavAreaByID( id ); + + id = fileBuffer.GetUnsignedInt(); + m_bottomArea = TheNavMesh->GetNavAreaByID( id ); + if ( !m_bottomArea ) + { + DevMsg( "ERROR: Unconnected ladder #%d bottom at ( %g, %g, %g )\n", m_id, m_bottom.x, m_bottom.y, m_bottom.z ); + DevWarning( "nav_unmark; nav_mark ladder %d; nav_warp_to_mark\n", m_id ); + } + else if (!m_topForwardArea && !m_topLeftArea && !m_topRightArea) // can't include behind area, since it is not used when going up a ladder + { + DevMsg( "ERROR: Unconnected ladder #%d top at ( %g, %g, %g )\n", m_id, m_top.x, m_top.y, m_top.z ); + DevWarning( "nav_unmark; nav_mark ladder %d; nav_warp_to_mark\n", m_id ); + } + + FindLadderEntity(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor returns true if ladder is free, or false if someone is on the ladder + */ +class IsLadderFreeFunctor +{ +public: + IsLadderFreeFunctor( const CNavLadder *ladder, const CBasePlayer *ignore ) + { + m_ladder = ladder; + m_ignore = ignore; + } + + bool operator() ( CBasePlayer *player ) + { + if (player == m_ignore) + return true; + + if (!player->IsOnLadder()) + return true; + + // player is on a ladder - is it this one? + const Vector &feet = player->GetAbsOrigin(); + + if (feet.z > m_ladder->m_top.z + HalfHumanHeight) + return true; + + if (feet.z + HumanHeight < m_ladder->m_bottom.z - HalfHumanHeight) + return true; + + Vector2D away( m_ladder->m_bottom.x - feet.x, m_ladder->m_bottom.y - feet.y ); + const float onLadderRange = 50.0f; + return away.IsLengthGreaterThan( onLadderRange ); + } + + const CNavLadder *m_ladder; + const CBasePlayer *m_ignore; +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if someone is on this ladder + */ +bool CNavLadder::IsInUse( const CBasePlayer *ignore ) const +{ + IsLadderFreeFunctor isLadderFree( this, ignore ); + return !ForEachPlayer( isLadderFree ); +} + +//-------------------------------------------------------------------------------------------------------------- +Vector CNavLadder::GetPosAtHeight( float height ) const +{ + if ( height < m_bottom.z ) + { + return m_bottom; + } + + if ( height > m_top.z ) + { + return m_top; + } + + if ( m_top.z == m_bottom.z ) + { + return m_top; + } + + float percent = ( height - m_bottom.z ) / ( m_top.z - m_bottom.z ); + + return m_top * percent + m_bottom * ( 1.0f - percent ); +} + +//-------------------------------------------------------------------------------------------------------------- diff --git a/sp/src/game/server/nav_ladder.h b/sp/src/game/server/nav_ladder.h new file mode 100644 index 00000000..f5a02b0f --- /dev/null +++ b/sp/src/game/server/nav_ladder.h @@ -0,0 +1,154 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +// Navigation ladders +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#ifndef _NAV_LADDER_H_ +#define _NAV_LADDER_H_ + +#include "nav.h" + +class CNavArea; + +//-------------------------------------------------------------------------------------------------------------- +/** + * The NavLadder represents ladders in the Navigation Mesh, and their connections to adjacent NavAreas + * @todo Deal with ladders that allow jumping off to areas in the middle + */ +class CNavLadder +{ +public: + CNavLadder( void ) + { + m_topForwardArea = NULL; + m_topRightArea = NULL; + m_topLeftArea = NULL; + m_topBehindArea = NULL; + m_bottomArea = NULL; + + // set an ID for interactive editing - loads will overwrite this + m_id = m_nextID++; + } + + ~CNavLadder(); + + void OnRoundRestart( void ); ///< invoked when a game round restarts + + void Save( CUtlBuffer &fileBuffer, unsigned int version ) const; + void Load( CUtlBuffer &fileBuffer, unsigned int version ); + + unsigned int GetID( void ) const { return m_id; } ///< return this ladder's unique ID + static void CompressIDs( void ); /// NavLadderVector; + + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavLadder::IsUsableByTeam( int teamNumber ) const +{ + if ( m_ladderEntity.Get() == NULL ) + return true; + + int ladderTeamNumber = m_ladderEntity->GetTeamNumber(); + return ( teamNumber == ladderTeamNumber || ladderTeamNumber == TEAM_UNASSIGNED ); +} + + +//-------------------------------------------------------------------------------------------------------------- +inline CBaseEntity *CNavLadder::GetLadderEntity( void ) const +{ + return m_ladderEntity.Get(); +} + + +//-------------------------------------------------------------------------------------------------------------- +inline NavDirType CNavLadder::GetDir( void ) const +{ + return m_dir; +} + + +//-------------------------------------------------------------------------------------------------------------- +inline const Vector &CNavLadder::GetNormal( void ) const +{ + return m_normal; +} + + +#endif // _NAV_LADDER_H_ diff --git a/sp/src/game/server/nav_merge.cpp b/sp/src/game/server/nav_merge.cpp new file mode 100644 index 00000000..d62764ab --- /dev/null +++ b/sp/src/game/server/nav_merge.cpp @@ -0,0 +1,350 @@ +// nav_merge.cpp +// Save/merge for partial nav meshes +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "cbase.h" +#include "fmtstr.h" +#include "tier0/vprof.h" +#include "utldict.h" + +#include "nav_mesh.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------- +void CNavArea::SaveToSelectedSet( KeyValues *areaKey ) const +{ + const char *placeName = TheNavMesh->PlaceToName( GetPlace() ); + areaKey->SetString( "Place", (placeName)?placeName:"" ); + + areaKey->SetInt( "Attributes", GetAttributes() ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavArea::RestoreFromSelectedSet( KeyValues *areaKey ) +{ + SetPlace( TheNavMesh->NameToPlace( areaKey->GetString( "Place" ) ) ); + + SetAttributes( areaKey->GetInt( "Attributes" ) ); +} + + +//-------------------------------------------------------------------------------------------------------- +class BuildSelectedSet +{ +public: + BuildSelectedSet( KeyValues *kv ) + { + m_kv = kv; + m_areaCount = 0; + } + + bool operator() ( CNavArea *area ) + { + CFmtStrN<32> name( "%d", area->GetID() ); + KeyValues *areaKey = m_kv->FindKey( name.Access(), true ); + if ( areaKey ) + { + ++m_areaCount; + + WriteCorner( area, areaKey, NORTH_WEST, "NorthWest" ); + WriteCorner( area, areaKey, NORTH_EAST, "NorthEast" ); + WriteCorner( area, areaKey, SOUTH_WEST, "SouthWest" ); + WriteCorner( area, areaKey, SOUTH_EAST, "SouthEast" ); + + WriteConnections( area, areaKey, NORTH, "North" ); + WriteConnections( area, areaKey, SOUTH, "South" ); + WriteConnections( area, areaKey, EAST, "East" ); + WriteConnections( area, areaKey, WEST, "West" ); + + area->SaveToSelectedSet( areaKey ); + } + return true; + } + + int Count( void ) const + { + return m_areaCount; + } + +private: + void WriteCorner( CNavArea *area, KeyValues *areaKey, NavCornerType corner, const char *cornerName ) + { + KeyValues *cornerKey = areaKey->FindKey( cornerName, true ); + if ( cornerKey ) + { + Vector pos = area->GetCorner( corner ); + cornerKey->SetFloat( "x", pos.x ); + cornerKey->SetFloat( "y", pos.y ); + cornerKey->SetFloat( "z", pos.z ); + } + } + + void WriteConnections( CNavArea *area, KeyValues *areaKey, NavDirType dir, const char *dirName ) + { + KeyValues *dirKey = areaKey->FindKey( dirName, true ); + if ( dirKey ) + { + for ( int i=0; iGetAdjacentCount( dir ); ++i ) + { + CNavArea *other = area->GetAdjacentArea( dir, i ); + if ( other && TheNavMesh->IsInSelectedSet( other ) ) + { + CFmtStrN<32> name( "%d", i ); + dirKey->SetInt( name.Access(), other->GetID() ); + } + } + } + } + + int m_areaCount; + KeyValues *m_kv; +}; + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavSaveSelected( const CCommand &args ) +{ + KeyValues *data = new KeyValues( "Selected Nav Areas" ); + data->SetInt( "version", 1 ); + + BuildSelectedSet setBuilder( data ); + TheNavMesh->ForAllSelectedAreas( setBuilder ); + + if ( !setBuilder.Count() ) + { + Msg( "Not saving empty selected set to disk.\n" ); + data->deleteThis(); + return; + } + + char fname[32]; + char path[MAX_PATH]; + if ( args.ArgC() == 2 ) + { + V_FileBase( args[0], fname, sizeof( fname ) ); + } + else + { + V_strncpy( fname, STRING( gpGlobals->mapname ), sizeof( fname ) ); + } + + int i; + for ( i=0; i<1000; ++i ) + { + V_snprintf( path, sizeof( path ), "maps/%s_selected_%4.4d.txt", fname, i ); + if ( !filesystem->FileExists( path ) ) + { + break; + } + } + + if ( i == 1000 ) + { + Msg( "Unable to find a filename to save the selected set to disk.\n" ); + data->deleteThis(); + return; + } + + if ( !data->SaveToFile( filesystem, path ) ) + { + Msg( "Unable to save the selected set to disk.\n" ); + } + + Msg( "Selected set saved to %s. Use 'nav_merge_mesh %s_selected_%4.4d' to merge it into another mesh.\n", path, fname, i ); + data->deleteThis(); +} + + +//-------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_save_selected, "Writes the selected set to disk for merging into another mesh via nav_merge_mesh.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSaveSelected( args ); +} + + +//-------------------------------------------------------------------------------------------------------- +Vector ReadCorner( KeyValues *areaKey, const char *cornerName ) +{ + Vector pos( vec3_origin ); + KeyValues *cornerKey = areaKey->FindKey( cornerName, false ); + if ( cornerKey ) + { + pos.x = cornerKey->GetFloat( "x" ); + pos.y = cornerKey->GetFloat( "y" ); + pos.z = cornerKey->GetFloat( "z" ); + } + + return pos; +} + + +//-------------------------------------------------------------------------------------------------------- +void ReconnectMergedArea( CUtlDict< CNavArea *, int > &newAreas, KeyValues *areaKey, NavDirType dir, const char *dirName ) +{ + int index = newAreas.Find( areaKey->GetName() ); + if ( index == newAreas.InvalidIndex() ) + { + Assert( false ); + return; + } + + CNavArea *area = newAreas[index]; + + KeyValues *dirKey = areaKey->FindKey( dirName, true ); + if ( dirKey ) + { + KeyValues *connection = dirKey->GetFirstValue(); + while ( connection ) + { + const char *otherID = connection->GetString(); + int otherIndex = newAreas.Find( otherID ); + Assert( otherIndex != newAreas.InvalidIndex() ); + if ( otherIndex != newAreas.InvalidIndex() ) + { + CNavArea *other = newAreas[otherIndex]; + + area->ConnectTo( other, dir ); // only a 1-way connection. the other area will connect back to us. + } + + connection = connection->GetNextValue(); + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavMergeMesh( const CCommand &args ) +{ + if ( args.ArgC() != 2 ) + { + Msg( "Usage: nav_merge_mesh filename\n" ); + return; + } + + char fname[64]; + char path[MAX_PATH]; + V_FileBase( args[1], fname, sizeof( fname ) ); + V_snprintf( path, sizeof( path ), "maps/%s.txt", fname ); + + KeyValues *data = new KeyValues( "Nav Selected Set" ); + if ( !data->LoadFromFile( filesystem, path ) ) + { + Msg( "Unable to load %s.\n", path ); + } + else + { + // Loaded the data - plug it into the existing mesh! + + // First add the areas, and put them in the correct places. We can save off the new area ID + // at the same time. + CUtlDict< CNavArea *, int > newAreas; + CUtlVector< CNavArea * > areaVector; + KeyValues *areaKey = data->GetFirstSubKey(); + while ( areaKey ) + { + Vector northWest = ReadCorner( areaKey, "NorthWest" ); + Vector northEast = ReadCorner( areaKey, "NorthEast" ); + Vector southWest = ReadCorner( areaKey, "SouthWest" ); + Vector southEast = ReadCorner( areaKey, "SouthEast" ); + + CNavArea *newArea = TheNavMesh->CreateArea(); + if (newArea == NULL) + { + Warning( "nav_merge_mesh: Out of memory\n" ); + return; + } + + newArea->Build( northWest, northEast, southEast, southWest ); + TheNavAreas.AddToTail( newArea ); + TheNavMesh->AddNavArea( newArea ); + areaVector.AddToTail( newArea ); + + // save the new ID for connections + int index = newAreas.Find( areaKey->GetName() ); + Assert( index == newAreas.InvalidIndex() ); + if ( index == newAreas.InvalidIndex() ) + { + newAreas.Insert( areaKey->GetName(), newArea ); + } + + // Restore additional data + newArea->RestoreFromSelectedSet( areaKey ); + + areaKey = areaKey->GetNextKey(); + } + + // Go back and reconnect the new areas to each other + areaKey = data->GetFirstSubKey(); + while ( areaKey ) + { + ReconnectMergedArea( newAreas, areaKey, NORTH, "North" ); + ReconnectMergedArea( newAreas, areaKey, SOUTH, "South" ); + ReconnectMergedArea( newAreas, areaKey, EAST, "East" ); + ReconnectMergedArea( newAreas, areaKey, WEST, "West" ); + + areaKey = areaKey->GetNextKey(); + } + + // Connect selected areas with pre-existing areas + StitchAreaSet( &areaVector ); + } + + data->deleteThis(); +} + + +//-------------------------------------------------------------------------------------------------------- +int NavMeshMergeAutocomplete( char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] ) +{ + char *commandName = "nav_merge_mesh"; + int numMatches = 0; + partial += Q_strlen( commandName ) + 1; + int partialLength = Q_strlen( partial ); + + FileFindHandle_t findHandle; + char txtFilenameNoExtension[ MAX_PATH ]; + const char *txtFilename = filesystem->FindFirstEx( "maps/*_selected_*.txt", "MOD", &findHandle ); + while ( txtFilename ) + { + Q_FileBase( txtFilename, txtFilenameNoExtension, sizeof( txtFilenameNoExtension ) ); + if ( !Q_strnicmp( txtFilenameNoExtension, partial, partialLength ) && V_stristr( txtFilenameNoExtension, "_selected_" ) ) + { + // Add the place name to the autocomplete array + Q_snprintf( commands[ numMatches++ ], COMMAND_COMPLETION_ITEM_LENGTH, "%s %s", commandName, txtFilenameNoExtension ); + + // Make sure we don't try to return too many place names + if ( numMatches == COMMAND_COMPLETION_MAXITEMS ) + return numMatches; + } + + txtFilename = filesystem->FindNext( findHandle ); + } + filesystem->FindClose( findHandle ); + + return numMatches; +} + + +//-------------------------------------------------------------------------------------------------------- +CON_COMMAND_F_COMPLETION( nav_merge_mesh, "Merges a saved selected set into the current mesh.", FCVAR_GAMEDLL | FCVAR_CHEAT, NavMeshMergeAutocomplete ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavMergeMesh( args ); +} + + +//-------------------------------------------------------------------------------------------------------- + + + + +//-------------------------------------------------------------------------------------------------------- diff --git a/sp/src/game/server/nav_mesh.cpp b/sp/src/game/server/nav_mesh.cpp new file mode 100644 index 00000000..8a15b39e --- /dev/null +++ b/sp/src/game/server/nav_mesh.cpp @@ -0,0 +1,3300 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// NavMesh.cpp +// Implementation of Navigation Mesh interface +// Author: Michael S. Booth, 2003-2004 + +#include "cbase.h" +#include "filesystem.h" +#include "nav_mesh.h" +#include "nav_node.h" +#include "fmtstr.h" +#include "utlbuffer.h" +#include "tier0/vprof.h" +#ifdef TERROR +#include "func_simpleladder.h" +#endif +#include "functorutils.h" + +#ifdef NEXT_BOT +#include "NextBot/NavMeshEntities/func_nav_prerequisite.h" +#endif + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + + +#define DrawLine( from, to, duration, red, green, blue ) NDebugOverlay::Line( from, to, red, green, blue, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ) + + +/** + * The singleton for accessing the navigation mesh + */ +CNavMesh *TheNavMesh = NULL; + +ConVar nav_edit( "nav_edit", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Set to one to interactively edit the Navigation Mesh. Set to zero to leave edit mode." ); +ConVar nav_quicksave( "nav_quicksave", "1", FCVAR_GAMEDLL | FCVAR_CHEAT, "Set to one to skip the time consuming phases of the analysis. Useful for data collection and testing." ); // TERROR: defaulting to 1, since we don't need the other data +ConVar nav_show_approach_points( "nav_show_approach_points", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show Approach Points in the Navigation Mesh." ); +ConVar nav_show_danger( "nav_show_danger", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show current 'danger' levels." ); +ConVar nav_show_player_counts( "nav_show_player_counts", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show current player counts in each area." ); +ConVar nav_show_func_nav_avoid( "nav_show_func_nav_avoid", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show areas of designer-placed bot avoidance due to func_nav_avoid entities" ); +ConVar nav_show_func_nav_prefer( "nav_show_func_nav_prefer", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show areas of designer-placed bot preference due to func_nav_prefer entities" ); +ConVar nav_show_func_nav_prerequisite( "nav_show_func_nav_prerequisite", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show areas of designer-placed bot preference due to func_nav_prerequisite entities" ); +ConVar nav_max_vis_delta_list_length( "nav_max_vis_delta_list_length", "64", FCVAR_CHEAT ); + +extern ConVar nav_show_potentially_visible; + +int g_DebugPathfindCounter = 0; + + +bool FindGroundForNode( Vector *pos, Vector *normal ); + + +//-------------------------------------------------------------------------------------------------------------- +CNavMesh::CNavMesh( void ) +{ + m_spawnName = NULL; + m_gridCellSize = 300.0f; + m_editMode = NORMAL; + m_bQuitWhenFinished = false; + m_hostThreadModeRestoreValue = 0; + m_placeCount = 0; + m_placeName = NULL; + + LoadPlaceDatabase(); + + ListenForGameEvent( "round_start" ); +// ListenForGameEvent( "round_start_pre_entity" ); + ListenForGameEvent( "break_prop" ); + ListenForGameEvent( "break_breakable" ); + ListenForGameEvent( "teamplay_round_start" ); + + Reset(); +} + +//-------------------------------------------------------------------------------------------------------------- +CNavMesh::~CNavMesh() +{ + if (m_spawnName) + delete [] m_spawnName; + + // !!!!bug!!! why does this crash in linux on server exit + for( unsigned int i=0; iResetNodes(); + } + } + + // destroy all hiding spots + DestroyHidingSpots(); + + // destroy navigation nodes created during map generation + CNavNode::CleanupGeneration(); + + if ( !incremental ) + { + // destroy the grid + m_grid.RemoveAll(); + m_gridSizeX = 0; + m_gridSizeY = 0; + } + + // clear the hash table + for( int i=0; iGetLastKnownArea() ) + { + CNavArea *eyepointArea = player->GetLastKnownArea(); + if ( eyepointArea ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[it]; + + if ( eyepointArea->IsCompletelyVisible( area ) ) + { + area->DrawFilled( 100, 100, 200, 255 ); + } + else if ( eyepointArea->IsPotentiallyVisible( area ) && nav_show_potentially_visible.GetInt() == 1 ) + { + area->DrawFilled( 100, 200, 100, 255 ); + } + } + } + } + } + + // draw any walkable seeds that have been marked + for ( int it=0; it < m_walkableSeeds.Count(); ++it ) + { + WalkableSeedSpot spot = m_walkableSeeds[ it ]; + + const float height = 50.0f; + const float width = 25.0f; + DrawLine( spot.pos, spot.pos + height * spot.normal, 3, 255, 0, 255 ); + DrawLine( spot.pos + Vector( width, 0, 0 ), spot.pos + height * spot.normal, 3, 255, 0, 255 ); + DrawLine( spot.pos + Vector( -width, 0, 0 ), spot.pos + height * spot.normal, 3, 255, 0, 255 ); + DrawLine( spot.pos + Vector( 0, width, 0 ), spot.pos + height * spot.normal, 3, 255, 0, 255 ); + DrawLine( spot.pos + Vector( 0, -width, 0 ), spot.pos + height * spot.normal, 3, 255, 0, 255 ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check all nav areas inside the breakable's extent to see if players would now fall through + */ +class CheckAreasOverlappingBreakable +{ +public: + CheckAreasOverlappingBreakable( CBaseEntity *breakable ) + { + m_breakable = breakable; + ICollideable *collideable = breakable->GetCollideable(); + collideable->WorldSpaceSurroundingBounds( &m_breakableExtent.lo, &m_breakableExtent.hi ); + + const float expand = 10.0f; + m_breakableExtent.lo += Vector( -expand, -expand, -expand ); + m_breakableExtent.hi += Vector( expand, expand, expand ); + } + + bool operator() ( CNavArea *area ) + { + if ( area->IsOverlapping( m_breakableExtent ) ) + { + // area overlaps the breakable + area->CheckFloor( m_breakable ); + } + + return true; + } + +private: + Extent m_breakableExtent; + CBaseEntity *m_breakable; +}; + + +//-------------------------------------------------------------------------------------------------------------- +class NavRoundRestart +{ +public: + bool operator()( CNavArea *area ) + { + area->OnRoundRestart(); + return true; + } + + bool operator()( CNavLadder *ladder ) + { + ladder->OnRoundRestart(); + return true; + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when the round restarts + */ +void CNavMesh::FireGameEvent( IGameEvent *gameEvent ) +{ + VPROF_BUDGET( "CNavMesh::FireGameEvent", VPROF_BUDGETGROUP_NPCS ); + + if ( FStrEq( gameEvent->GetName(), "break_prop" ) || FStrEq( gameEvent->GetName(), "break_breakable" ) ) + { + CheckAreasOverlappingBreakable collector( UTIL_EntityByIndex( gameEvent->GetInt( "entindex" ) ) ); + ForAllAreas( collector ); + } + + if ( FStrEq( gameEvent->GetName(), "round_start" ) || FStrEq( gameEvent->GetName(), "teamplay_round_start" ) ) + { + OnRoundRestart(); + + NavRoundRestart restart; + ForAllAreas( restart ); + ForAllLadders( restart ); + } + else if ( FStrEq( gameEvent->GetName(), "round_start_pre_entity" ) ) + { + OnRoundRestartPreEntity(); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + area->OnRoundRestartPreEntity(); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Allocate the grid and define its extents + */ +void CNavMesh::AllocateGrid( float minX, float maxX, float minY, float maxY ) +{ + m_grid.RemoveAll(); + + m_minX = minX; + m_minY = minY; + + m_gridSizeX = (int)((maxX - minX) / m_gridCellSize) + 1; + m_gridSizeY = (int)((maxY - minY) / m_gridCellSize) + 1; + + m_grid.SetCount( m_gridSizeX * m_gridSizeY ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Add an area to the mesh + */ +void CNavMesh::AddNavArea( CNavArea *area ) +{ + if ( !m_grid.Count() ) + { + // If we somehow have no grid (manually creating a nav area without loading or generating a mesh), don't crash + AllocateGrid( 0, 0, 0, 0 ); + } + + // add to grid + int loX = WorldToGridX( area->GetCorner( NORTH_WEST ).x ); + int loY = WorldToGridY( area->GetCorner( NORTH_WEST ).y ); + int hiX = WorldToGridX( area->GetCorner( SOUTH_EAST ).x ); + int hiY = WorldToGridY( area->GetCorner( SOUTH_EAST ).y ); + + for( int y = loY; y <= hiY; ++y ) + { + for( int x = loX; x <= hiX; ++x ) + { + m_grid[ x + y*m_gridSizeX ].AddToTail( const_cast( area ) ); + } + } + + // add to hash table + int key = ComputeHashKey( area->GetID() ); + + if (m_hashTable[key]) + { + // add to head of list in this slot + area->m_prevHash = NULL; + area->m_nextHash = m_hashTable[key]; + m_hashTable[key]->m_prevHash = area; + m_hashTable[key] = area; + } + else + { + // first entry in this slot + m_hashTable[key] = area; + area->m_nextHash = NULL; + area->m_prevHash = NULL; + } + + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + m_transientAreas.AddToTail( area ); + } + + ++m_areaCount; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Remove an area from the mesh + */ +void CNavMesh::RemoveNavArea( CNavArea *area ) +{ + // add to grid + int loX = WorldToGridX( area->GetCorner( NORTH_WEST ).x ); + int loY = WorldToGridY( area->GetCorner( NORTH_WEST ).y ); + int hiX = WorldToGridX( area->GetCorner( SOUTH_EAST ).x ); + int hiY = WorldToGridY( area->GetCorner( SOUTH_EAST ).y ); + + for( int y = loY; y <= hiY; ++y ) + { + for( int x = loX; x <= hiX; ++x ) + { + m_grid[ x + y*m_gridSizeX ].FindAndRemove( area ); + } + } + + // remove from hash table + int key = ComputeHashKey( area->GetID() ); + + if (area->m_prevHash) + { + area->m_prevHash->m_nextHash = area->m_nextHash; + } + else + { + // area was at start of list + m_hashTable[key] = area->m_nextHash; + + if (m_hashTable[key]) + { + m_hashTable[key]->m_prevHash = NULL; + } + } + + if (area->m_nextHash) + { + area->m_nextHash->m_prevHash = area->m_prevHash; + } + + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + BuildTransientAreaList(); + } + + m_avoidanceObstacleAreas.FindAndRemove( area ); + m_blockedAreas.FindAndRemove( area ); + + --m_areaCount; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when server loads a new map + */ +void CNavMesh::OnServerActivate( void ) +{ + FOR_EACH_VEC( TheNavAreas, pit ) + { + CNavArea *area = TheNavAreas[ pit ]; + area->OnServerActivate(); + } +} + +#ifdef NEXT_BOT + +//-------------------------------------------------------------------------------------------------------------- +class CRegisterPrerequisite +{ +public: + CRegisterPrerequisite( CFuncNavPrerequisite *prereq ) + { + m_prereq = prereq; + } + + bool operator() ( CNavArea *area ) + { + area->AddPrerequisite( m_prereq ); + return true; + } + + CFuncNavPrerequisite *m_prereq; +}; + +#endif + +//-------------------------------------------------------------------------------------------------------------- +/** +* Test all areas for blocked status +*/ +void CNavMesh::TestAllAreasForBlockedStatus( void ) +{ + FOR_EACH_VEC( TheNavAreas, pit ) + { + CNavArea *area = TheNavAreas[ pit ]; + area->UpdateBlocked( true ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when a game round restarts + */ +void CNavMesh::OnRoundRestart( void ) +{ + m_updateBlockedAreasTimer.Start( 1.0f ); + +#ifdef NEXT_BOT + FOR_EACH_VEC( TheNavAreas, pit ) + { + CNavArea *area = TheNavAreas[ pit ]; + area->RemoveAllPrerequisites(); + } + + // attach prerequisites + for ( int i=0; i( IFuncNavPrerequisiteAutoList::AutoList()[i] ); + + Extent prereqExtent; + prereqExtent.Init( prereq ); + + CRegisterPrerequisite apply( prereq ); + + ForAllAreasOverlappingExtent( apply, prereqExtent ); + } +#endif +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when a game round restarts, but before entities are deleted and recreated + */ +void CNavMesh::OnRoundRestartPreEntity( void ) +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::BuildTransientAreaList( void ) +{ + m_transientAreas.RemoveAll(); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + m_transientAreas.AddToTail( area ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavMesh::GridToWorld( int gridX, int gridY, Vector *pos ) const +{ + gridX = clamp( gridX, 0, m_gridSizeX-1 ); + gridY = clamp( gridY, 0, m_gridSizeY-1 ); + + pos->x = m_minX + gridX * m_gridCellSize; + pos->y = m_minY + gridY * m_gridCellSize; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given a position, return the nav area that IsOverlapping and is *immediately* beneath it + */ +CNavArea *CNavMesh::GetNavArea( const Vector &pos, float beneathLimit ) const +{ + VPROF_BUDGET( "CNavMesh::GetNavArea", "NextBot" ); + + if ( !m_grid.Count() ) + return NULL; + + // get list in cell that contains position + int x = WorldToGridX( pos.x ); + int y = WorldToGridY( pos.y ); + NavAreaVector *areaVector = &m_grid[ x + y*m_gridSizeX ]; + + // search cell list to find correct area + CNavArea *use = NULL; + float useZ = -99999999.9f; + Vector testPos = pos + Vector( 0, 0, 5 ); + + FOR_EACH_VEC( (*areaVector), it ) + { + CNavArea *area = (*areaVector)[ it ]; + + // check if position is within 2D boundaries of this area + if (area->IsOverlapping( testPos )) + { + // project position onto area to get Z + float z = area->GetZ( testPos ); + + // if area is above us, skip it + if (z > testPos.z) + continue; + + // if area is too far below us, skip it + if (z < pos.z - beneathLimit) + continue; + + // if area is higher than the one we have, use this instead + if (z > useZ) + { + use = area; + useZ = z; + } + } + } + + return use; +} + + +//---------------------------------------------------------------------------- +// Given a position, return the nav area that IsOverlapping and is *immediately* beneath it +//---------------------------------------------------------------------------- +CNavArea *CNavMesh::GetNavArea( CBaseEntity *pEntity, int nFlags, float flBeneathLimit ) const +{ + VPROF( "CNavMesh::GetNavArea [ent]" ); + + if ( !m_grid.Count() ) + return NULL; + + Vector testPos = pEntity->GetAbsOrigin(); + + float flStepHeight = 1e-3; + CBaseCombatCharacter *pBCC = pEntity->MyCombatCharacterPointer(); + if ( pBCC ) + { + // Check if we're still in the last area + CNavArea *pLastNavArea = pBCC->GetLastKnownArea(); + if ( pLastNavArea && pLastNavArea->IsOverlapping( testPos ) ) + { + float flZ = pLastNavArea->GetZ( testPos ); + if ( ( flZ <= testPos.z + StepHeight ) && ( flZ >= testPos.z - StepHeight ) ) + return pLastNavArea; + } + flStepHeight = StepHeight; + } + + // get list in cell that contains position + int x = WorldToGridX( testPos.x ); + int y = WorldToGridY( testPos.y ); + NavAreaVector *areaVector = &m_grid[ x + y*m_gridSizeX ]; + + // search cell list to find correct area + CNavArea *use = NULL; + float useZ = -99999999.9f; + + bool bSkipBlockedAreas = ( ( nFlags & GETNAVAREA_ALLOW_BLOCKED_AREAS ) == 0 ); + FOR_EACH_VEC( (*areaVector), it ) + { + CNavArea *pArea = (*areaVector)[ it ]; + + // check if position is within 2D boundaries of this area + if ( !pArea->IsOverlapping( testPos ) ) + continue; + + // don't consider blocked areas + if ( bSkipBlockedAreas && pArea->IsBlocked( pEntity->GetTeamNumber() ) ) + continue; + + // project position onto area to get Z + float z = pArea->GetZ( testPos ); + + // if area is above us, skip it + if ( z > testPos.z + flStepHeight ) + continue; + + // if area is too far below us, skip it + if ( z < testPos.z - flBeneathLimit ) + continue; + + // if area is lower than the one we have, skip it + if ( z <= useZ ) + continue; + + use = pArea; + useZ = z; + } + + // Check LOS if necessary + if ( use && ( nFlags && GETNAVAREA_CHECK_LOS ) && ( useZ < testPos.z - flStepHeight ) ) + { + // trace directly down to see if it's below us and unobstructed + trace_t result; + UTIL_TraceLine( testPos, Vector( testPos.x, testPos.y, useZ ), MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + if ( ( result.fraction != 1.0f ) && ( fabs( result.endpos.z - useZ ) > flStepHeight ) ) + return NULL; + } + return use; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given a position in the world, return the nav area that is closest + * and at the same height, or beneath it. + * Used to find initial area if we start off of the mesh. + * @todo Make sure area is not on the other side of the wall from goal. + */ +CNavArea *CNavMesh::GetNearestNavArea( const Vector &pos, bool anyZ, float maxDist, bool checkLOS, bool checkGround, int team ) const +{ + VPROF_BUDGET( "CNavMesh::GetNearestNavArea", "NextBot" ); + + if ( !m_grid.Count() ) + return NULL; + + CNavArea *close = NULL; + float closeDistSq = maxDist * maxDist; + + // quick check + if ( !checkLOS && !checkGround ) + { + close = GetNavArea( pos ); + if ( close ) + { + return close; + } + } + + // ensure source position is well behaved + Vector source; + source.x = pos.x; + source.y = pos.y; + if ( GetGroundHeight( pos, &source.z ) == false ) + { + if ( !checkGround ) + { + source.z = pos.z; + } + else + { + return NULL; + } + } + + source.z += HalfHumanHeight; + + // find closest nav area + + // use a unique marker for this method, so it can be used within a SearchSurroundingArea() call + static unsigned int searchMarker = RandomInt(0, 1024*1024 ); + + ++searchMarker; + + if ( searchMarker == 0 ) + { + ++searchMarker; + } + + + // get list in cell that contains position + int originX = WorldToGridX( pos.x ); + int originY = WorldToGridY( pos.y ); + + int shiftLimit = ceil(maxDist / m_gridCellSize); + + // + // Search in increasing rings out from origin, starting with cell + // that contains the given position. + // Once we find a close area, we must check one more step out in + // case our position is just against the edge of the cell boundary + // and an area in an adjacent cell is actually closer. + // + for( int shift=0; shift <= shiftLimit; ++shift ) + { + for( int x = originX - shift; x <= originX + shift; ++x ) + { + if ( x < 0 || x >= m_gridSizeX ) + continue; + + for( int y = originY - shift; y <= originY + shift; ++y ) + { + if ( y < 0 || y >= m_gridSizeY ) + continue; + + // only check these areas if we're on the outer edge of our spiral + if ( x > originX - shift && + x < originX + shift && + y > originY - shift && + y < originY + shift ) + continue; + + NavAreaVector *areaVector = &m_grid[ x + y*m_gridSizeX ]; + + // find closest area in this cell + FOR_EACH_VEC( (*areaVector), it ) + { + CNavArea *area = (*areaVector)[ it ]; + + // skip if we've already visited this area + if ( area->m_nearNavSearchMarker == searchMarker ) + continue; + + // don't consider blocked areas + if ( area->IsBlocked( team ) ) + continue; + + // mark as visited + area->m_nearNavSearchMarker = searchMarker; + + Vector areaPos; + area->GetClosestPointOnArea( source, &areaPos ); + + // TERROR: Using the original pos for distance calculations. Since it's a pure 3D distance, + // with no Z restrictions or LOS checks, this should work for passing in bot foot positions. + // This needs to be ported back to CS:S. + float distSq = ( areaPos - pos ).LengthSqr(); + + // keep the closest area + if ( distSq >= closeDistSq ) + continue; + + // check LOS to area + // REMOVED: If we do this for !anyZ, it's likely we wont have LOS and will enumerate every area in the mesh + // It is still good to do this in some isolated cases, however + if ( checkLOS ) + { + trace_t result; + + // make sure 'pos' is not embedded in the world + Vector safePos; + + UTIL_TraceLine( pos, pos + Vector( 0, 0, StepHeight ), MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + if ( result.startsolid ) + { + // it was embedded - move it out + safePos = result.endpos + Vector( 0, 0, 1.0f ); + } + else + { + safePos = pos; + } + + // Don't bother tracing from the nav area up to safePos.z if it's within StepHeight of the area, since areas can be embedded in the ground a bit + float heightDelta = fabs(areaPos.z - safePos.z); + if ( heightDelta > StepHeight ) + { + // trace to the height of the original point + UTIL_TraceLine( areaPos + Vector( 0, 0, StepHeight ), Vector( areaPos.x, areaPos.y, safePos.z ), MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + + if ( result.fraction != 1.0f ) + { + continue; + } + } + + // trace to the original point's height above the area + UTIL_TraceLine( safePos, Vector( areaPos.x, areaPos.y, safePos.z + StepHeight ), MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + + if ( result.fraction != 1.0f ) + { + continue; + } + } + + closeDistSq = distSq; + close = area; + + // look one more step outwards + shiftLimit = shift+1; + } + } + } + } + + return close; +} + + +//---------------------------------------------------------------------------- +// Given a position in the world, return the nav area that is closest +// and at the same height, or beneath it. +// Used to find initial area if we start off of the mesh. +// @todo Make sure area is not on the other side of the wall from goal. +//---------------------------------------------------------------------------- +CNavArea *CNavMesh::GetNearestNavArea( CBaseEntity *pEntity, int nFlags, float maxDist ) const +{ + VPROF( "CNavMesh::GetNearestNavArea [ent]" ); + + if ( !m_grid.Count() ) + return NULL; + + // quick check + CNavArea *pClose = GetNavArea( pEntity, nFlags ); + if ( pClose ) + return pClose; + + bool bCheckLOS = ( nFlags & GETNAVAREA_CHECK_LOS ) != 0; + bool bCheckGround = ( nFlags & GETNAVAREA_CHECK_GROUND ) != 0; + return GetNearestNavArea( pEntity->GetAbsOrigin(), false, maxDist, bCheckLOS, bCheckGround, pEntity->GetTeamNumber() ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given an ID, return the associated area + */ +CNavArea *CNavMesh::GetNavAreaByID( unsigned int id ) const +{ + if (id == 0) + return NULL; + + int key = ComputeHashKey( id ); + + for( CNavArea *area = m_hashTable[key]; area; area = area->m_nextHash ) + { + if (area->GetID() == id) + { + return area; + } + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given an ID, return the associated ladder + */ +CNavLadder *CNavMesh::GetLadderByID( unsigned int id ) const +{ + if (id == 0) + return NULL; + + for ( int i=0; iGetID() == id ) + { + return ladder; + } + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return radio chatter place for given coordinate + */ +unsigned int CNavMesh::GetPlace( const Vector &pos ) const +{ + CNavArea *area = GetNearestNavArea( pos, true ); + + if (area) + { + return area->GetPlace(); + } + + return UNDEFINED_PLACE; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load the place names from a file + */ +void CNavMesh::LoadPlaceDatabase( void ) +{ + m_placeCount = 0; + +#ifdef TERROR + // TODO: LoadPlaceDatabase happens during the constructor, so we can't override it! + // Population.txt holds all the info we need for place names in Left4Dead, so let's not + // make Phil edit yet another text file. + KeyValues *populationData = new KeyValues( "population" ); + if ( populationData->LoadFromFile( filesystem, "scripts/population.txt" ) ) + { + CUtlVector< char * > placeNames; + + for ( KeyValues *key = populationData->GetFirstTrueSubKey(); key != NULL; key = key->GetNextTrueSubKey() ) + { + if ( FStrEq( key->GetName(), "default" ) ) // default population is the undefined place + continue; + + placeNames.AddToTail( CloneString( key->GetName() ) ); + } + + m_placeCount = placeNames.Count(); + + // allocate place name array + m_placeName = new char * [ m_placeCount ]; + for ( unsigned int i=0; ideleteThis(); + return; + } + + populationData->deleteThis(); +#endif + + CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); + filesystem->ReadFile("NavPlace.db", "GAME", buf); + + if (!buf.Size()) + return; + + const int maxNameLength = 128; + char buffer[ maxNameLength ]; + + CUtlVector placeNames; + + // count the number of places + while( true ) + { + buf.GetLine( buffer, maxNameLength ); + + if ( !buf.IsValid() ) + break; + + int len = V_strlen( buffer ); + if ( len >= 2 ) + { + if ( buffer[len-1] == '\n' || buffer[len-1] == '\r' ) + buffer[len-1] = 0; + + if ( buffer[len-2] == '\r' ) + buffer[len-2] = 0; + + char *pName = new char[ len + 1 ]; + V_strncpy( pName, buffer, len+1 ); + placeNames.AddToTail( pName ); + } + } + + // allocate place name array + m_placeCount = placeNames.Count(); + m_placeName = new char * [ m_placeCount ]; + + for ( unsigned int i=0; i < m_placeCount; i++ ) + { + m_placeName[i] = placeNames[i]; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given a place, return its name. + * Reserve zero as invalid. + */ +const char *CNavMesh::PlaceToName( Place place ) const +{ + if (place >= 1 && place <= m_placeCount) + return m_placeName[ (int)place - 1 ]; + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given a place name, return a place ID or zero if no place is defined + * Reserve zero as invalid. + */ +Place CNavMesh::NameToPlace( const char *name ) const +{ + for( unsigned int i=0; i placeNames; + for ( i=0; i %-26s", placeNames[i] ); + else + Msg( "%-30s", placeNames[i] ); + + if ((i+1) % 3 == 0) + Msg( "\n" ); + } + + Msg( "\n" ); +} + +class CTraceFilterGroundEntities : public CTraceFilterWalkableEntities +{ + typedef CTraceFilterWalkableEntities BaseClass; + +public: + CTraceFilterGroundEntities( const IHandleEntity *passentity, int collisionGroup, unsigned int flags ) + : BaseClass( passentity, collisionGroup, flags ) + { + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity ); + if ( FClassnameIs( pEntity, "prop_door" ) || + FClassnameIs( pEntity, "prop_door_rotating" ) || + FClassnameIs( pEntity, "func_breakable" ) ) + { + return false; + } + + return BaseClass::ShouldHitEntity( pServerEntity, contentsMask ); + } +}; + +bool CNavMesh::GetGroundHeight( const Vector &pos, float *height, Vector *normal ) const +{ + VPROF( "CNavMesh::GetGroundHeight" ); + + const float flMaxOffset = 100.0f; + + CTraceFilterGroundEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + + trace_t result; + Vector to( pos.x, pos.y, pos.z - 10000.0f ); + Vector from( pos.x, pos.y, pos.z + HalfHumanHeight + 1e-3 ); + while( to.z - pos.z < flMaxOffset ) + { + UTIL_TraceLine( from, to, MASK_NPCSOLID_BRUSHONLY, &filter, &result ); + if ( !result.startsolid && (( result.fraction == 1.0f ) || ( ( from.z - result.endpos.z ) >= HalfHumanHeight ) ) ) + { + *height = result.endpos.z; + if ( normal ) + { + *normal = !result.plane.normal.IsZero() ? result.plane.normal : Vector( 0, 0, 1 ); + } + return true; + } + to.z = ( result.startsolid ) ? from.z : result.endpos.z; + from.z = to.z + HalfHumanHeight + 1e-3; + } + + *height = 0.0f; + if ( normal ) + { + normal->Init( 0.0f, 0.0f, 1.0f ); + } + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the "simple" ground height below this point in "height". + * This function is much faster, but less tolerant. Make sure the give position is "well behaved". + * Return false if position is invalid (outside of map, in a solid area, etc). + */ +bool CNavMesh::GetSimpleGroundHeight( const Vector &pos, float *height, Vector *normal ) const +{ + Vector to; + to.x = pos.x; + to.y = pos.y; + to.z = pos.z - 9999.9f; + + trace_t result; + + UTIL_TraceLine( pos, to, MASK_NPCSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + + if (result.startsolid) + return false; + + *height = result.endpos.z; + + if (normal) + *normal = result.plane.normal; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Show danger levels for debugging + */ +void CNavMesh::DrawDanger( void ) const +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + Vector center = area->GetCenter(); + Vector top; + center.z = area->GetZ( center ); + + float danger = area->GetDanger( 0 ); + if (danger > 0.1f) + { + top.x = center.x; + top.y = center.y; + top.z = center.z + 10.0f * danger; + DrawLine( center, top, 3, 255, 0, 0 ); + } + + danger = area->GetDanger( 1 ); + if (danger > 0.1f) + { + top.x = center.x; + top.y = center.y; + top.z = center.z + 10.0f * danger; + DrawLine( center, top, 3, 0, 0, 255 ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Show current player counts for debugging. + * NOTE: Assumes two teams. + */ +void CNavMesh::DrawPlayerCounts( void ) const +{ + CFmtStr msg; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if (area->GetPlayerCount() > 0) + { + NDebugOverlay::Text( area->GetCenter(), msg.sprintf( "%d (%d/%d)", area->GetPlayerCount(), area->GetPlayerCount(1), area->GetPlayerCount(2) ), false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw bot avoidance areas from func_nav_avoid entities + */ +void CNavMesh::DrawFuncNavAvoid( void ) const +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area->HasFuncNavAvoid() ) + { + area->DrawFilled( 255, 0, 0, 255 ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw bot preference areas from func_nav_prefer entities + */ +void CNavMesh::DrawFuncNavPrefer( void ) const +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area->HasFuncNavPrefer() ) + { + area->DrawFilled( 0, 255, 0, 255 ); + } + } +} + + +#ifdef NEXT_BOT +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw bot preference areas from func_nav_prerequisite entities + */ +void CNavMesh::DrawFuncNavPrerequisite( void ) const +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area->HasPrerequisite() ) + { + area->DrawFilled( 0, 0, 255, 255 ); + } + } +} +#endif + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Increase the danger of nav areas containing and near the given position + */ +void CNavMesh::IncreaseDangerNearby( int teamID, float amount, CNavArea *startArea, const Vector &pos, float maxRadius, float dangerLimit ) +{ + if (startArea == NULL) + return; + + CNavArea::MakeNewMarker(); + CNavArea::ClearSearchLists(); + + startArea->AddToOpenList(); + startArea->SetTotalCost( 0.0f ); + startArea->Mark(); + + float finalDanger = amount; + + if ( dangerLimit > 0.0f && startArea->GetDanger( teamID ) + finalDanger > dangerLimit ) + { + // clamp danger to given limit + finalDanger = dangerLimit - startArea->GetDanger( teamID ); + } + + startArea->IncreaseDanger( teamID, finalDanger ); + + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CNavArea *area = CNavArea::PopOpenList(); + + // explore adjacent areas + for( int dir=0; dirGetAdjacentCount( (NavDirType)dir ); + for( int i=0; iGetAdjacentArea( (NavDirType)dir, i ); + + if (!adjArea->IsMarked()) + { + // compute distance from danger source + float cost = (adjArea->GetCenter() - pos).Length(); + if (cost <= maxRadius) + { + adjArea->AddToOpenList(); + adjArea->SetTotalCost( cost ); + adjArea->Mark(); + + finalDanger = amount * cost/maxRadius; + + if ( dangerLimit > 0.0f && adjArea->GetDanger( teamID ) + finalDanger > dangerLimit ) + { + // clamp danger to given limit + finalDanger = dangerLimit - adjArea->GetDanger( teamID ); + } + + adjArea->IncreaseDanger( teamID, finalDanger ); + } + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavRemoveJumpAreas( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavRemoveJumpAreas(); +} +static ConCommand nav_remove_jump_areas( "nav_remove_jump_areas", CommandNavRemoveJumpAreas, "Removes legacy jump areas, replacing them with connections.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavDelete( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() || !nav_edit.GetBool() ) + return; + + TheNavMesh->CommandNavDelete(); +} +static ConCommand nav_delete( "nav_delete", CommandNavDelete, "Deletes the currently highlighted Area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavDeleteMarked( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() || !nav_edit.GetBool() ) + return; + + TheNavMesh->CommandNavDeleteMarked(); +} +static ConCommand nav_delete_marked( "nav_delete_marked", CommandNavDeleteMarked, "Deletes the currently marked Area (if any).", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_flood_select, "Selects the current Area and all Areas connected to it, recursively. To clear a selection, use this command again.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavFloodSelect( args ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavToggleSelectedSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleSelectedSet(); +} +static ConCommand nav_toggle_selected_set( "nav_toggle_selected_set", CommandNavToggleSelectedSet, "Toggles all areas into/out of the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavStoreSelectedSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavStoreSelectedSet(); +} +static ConCommand nav_store_selected_set( "nav_store_selected_set", CommandNavStoreSelectedSet, "Stores the current selected set for later retrieval.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavRecallSelectedSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavRecallSelectedSet(); +} +static ConCommand nav_recall_selected_set( "nav_recall_selected_set", CommandNavRecallSelectedSet, "Re-selects the stored selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavAddToSelectedSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavAddToSelectedSet(); +} +static ConCommand nav_add_to_selected_set( "nav_add_to_selected_set", CommandNavAddToSelectedSet, "Add current area to the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_add_to_selected_set_by_id, "Add specified area id to the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavAddToSelectedSetByID( args ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavRemoveFromSelectedSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavRemoveFromSelectedSet(); +} +static ConCommand nav_remove_from_selected_set( "nav_remove_from_selected_set", CommandNavRemoveFromSelectedSet, "Remove current area from the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavToggleInSelectedSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleInSelectedSet(); +} +static ConCommand nav_toggle_in_selected_set( "nav_toggle_in_selected_set", CommandNavToggleInSelectedSet, "Remove current area from the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavClearSelectedSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavClearSelectedSet(); +} +static ConCommand nav_clear_selected_set( "nav_clear_selected_set", CommandNavClearSelectedSet, "Clear the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//---------------------------------------------------------------------------------- +CON_COMMAND_F( nav_dump_selected_set_positions, "Write the (x,y,z) coordinates of the centers of all selected nav areas to a file.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + const NavAreaVector &selectedSet = TheNavMesh->GetSelectedSet(); + + CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::TEXT_BUFFER ); + + for( int i=0; iGetCenter(); + + fileBuffer.Printf( "%f %f %f\n", center.x, center.y, center.z ); + } + + // filename is local to game dir for Steam, so we need to prepend game dir for regular file save + char gamePath[256]; + engine->GetGameDir( gamePath, 256 ); + + char filename[256]; + Q_snprintf( filename, sizeof( filename ), "%s\\maps\\%s_xyz.txt", gamePath, STRING( gpGlobals->mapname ) ); + + if ( !filesystem->WriteFile( filename, "MOD", fileBuffer ) ) + { + Warning( "Unable to save %d bytes to %s\n", fileBuffer.Size(), filename ); + } + else + { + DevMsg( "Write %d nav area center positions to '%s'.\n", selectedSet.Count(), filename ); + } +}; + + +//---------------------------------------------------------------------------------- +CON_COMMAND_F( nav_show_dumped_positions, "Show the (x,y,z) coordinate positions of the given dump file.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::TEXT_BUFFER ); + + // filename is local to game dir for Steam, so we need to prepend game dir for regular file save + char gamePath[256]; + engine->GetGameDir( gamePath, 256 ); + + char filename[256]; + Q_snprintf( filename, sizeof( filename ), "%s\\maps\\%s_xyz.txt", gamePath, STRING( gpGlobals->mapname ) ); + + if ( !filesystem->ReadFile( filename, "MOD", fileBuffer ) ) + { + Warning( "Unable to read %s\n", filename ); + } + else + { + while( true ) + { + Vector center; + if ( fileBuffer.Scanf( "%f %f %f", ¢er.x, ¢er.y, ¢er.z ) <= 0 ) + { + break; + } + + NDebugOverlay::Cross3D( center, 5.0f, 255, 255, 0, true, 99999.9f ); + } + } +}; + + +//---------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_larger_than, "Select nav areas where both dimensions are larger than the given size.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( args.ArgC() > 1 ) + { + float minSize = atof( args[1] ); + + int selectedCount = 0; + + for( int i=0; iGetSizeX() > minSize && area->GetSizeY() > minSize ) + { + TheNavMesh->AddToSelectedSet( area ); + ++selectedCount; + } + } + + DevMsg( "Selected %d areas with dimensions larger than %3.2f units.\n", selectedCount, minSize ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavBeginSelecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavBeginSelecting(); +} +static ConCommand nav_begin_selecting( "nav_begin_selecting", CommandNavBeginSelecting, "Start continuously adding to the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavEndSelecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavEndSelecting(); +} +static ConCommand nav_end_selecting( "nav_end_selecting", CommandNavEndSelecting, "Stop continuously adding to the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavBeginDragSelecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavBeginDragSelecting(); +} +static ConCommand nav_begin_drag_selecting( "nav_begin_drag_selecting", CommandNavBeginDragSelecting, "Start dragging a selection area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavEndDragSelecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavEndDragSelecting(); +} +static ConCommand nav_end_drag_selecting( "nav_end_drag_selecting", CommandNavEndDragSelecting, "Stop dragging a selection area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavBeginDragDeselecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavBeginDragDeselecting(); +} +static ConCommand nav_begin_drag_deselecting( "nav_begin_drag_deselecting", CommandNavBeginDragDeselecting, "Start dragging a selection area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavEndDragDeselecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavEndDragDeselecting(); +} +static ConCommand nav_end_drag_deselecting( "nav_end_drag_deselecting", CommandNavEndDragDeselecting, "Stop dragging a selection area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavRaiseDragVolumeMax( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavRaiseDragVolumeMax(); +} +static ConCommand nav_raise_drag_volume_max( "nav_raise_drag_volume_max", CommandNavRaiseDragVolumeMax, "Raise the top of the drag select volume.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavLowerDragVolumeMax( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavLowerDragVolumeMax(); +} +static ConCommand nav_lower_drag_volume_max( "nav_lower_drag_volume_max", CommandNavLowerDragVolumeMax, "Lower the top of the drag select volume.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavRaiseDragVolumeMin( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavRaiseDragVolumeMin(); +} +static ConCommand nav_raise_drag_volume_min( "nav_raise_drag_volume_min", CommandNavRaiseDragVolumeMin, "Raise the bottom of the drag select volume.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavLowerDragVolumeMin( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavLowerDragVolumeMin(); +} +static ConCommand nav_lower_drag_volume_min( "nav_lower_drag_volume_min", CommandNavLowerDragVolumeMin, "Lower the bottom of the drag select volume.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavToggleSelecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleSelecting(); +} +static ConCommand nav_toggle_selecting( "nav_toggle_selecting", CommandNavToggleSelecting, "Start or stop continuously adding to the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavBeginDeselecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavBeginDeselecting(); +} +static ConCommand nav_begin_deselecting( "nav_begin_deselecting", CommandNavBeginDeselecting, "Start continuously removing from the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavEndDeselecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavEndDeselecting(); +} +static ConCommand nav_end_deselecting( "nav_end_deselecting", CommandNavEndDeselecting, "Stop continuously removing from the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavToggleDeselecting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleDeselecting(); +} +static ConCommand nav_toggle_deselecting( "nav_toggle_deselecting", CommandNavToggleDeselecting, "Start or stop continuously removing from the selected set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_half_space, "Selects any areas that intersect the given half-space.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSelectHalfSpace( args ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavBeginShiftXY( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavBeginShiftXY(); +} +static ConCommand nav_begin_shift_xy( "nav_begin_shift_xy", CommandNavBeginShiftXY, "Begin shifting the Selected Set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavEndShiftXY( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavEndShiftXY(); +} +static ConCommand nav_end_shift_xy( "nav_end_shift_xy", CommandNavEndShiftXY, "Finish shifting the Selected Set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavSelectInvalidAreas( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSelectInvalidAreas(); +} +static ConCommand nav_select_invalid_areas( "nav_select_invalid_areas", CommandNavSelectInvalidAreas, "Adds all invalid areas to the Selected Set.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_blocked_areas, "Adds all blocked areas to the selected set", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSelectBlockedAreas(); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_obstructed_areas, "Adds all obstructed areas to the selected set", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSelectObstructedAreas(); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_damaging_areas, "Adds all damaging areas to the selected set", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSelectDamagingAreas(); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_stairs, "Adds all stairway areas to the selected set", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSelectStairs(); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_select_orphans, "Adds all orphan areas to the selected set (highlight a valid area first).", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSelectOrphans(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavSplit( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSplit(); +} +static ConCommand nav_split( "nav_split", CommandNavSplit, "To split an Area into two, align the split line using your cursor and invoke the split command.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavMakeSniperSpots( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavMakeSniperSpots(); +} +static ConCommand nav_make_sniper_spots( "nav_make_sniper_spots", CommandNavMakeSniperSpots, "Chops the marked area into disconnected sub-areas suitable for sniper spots.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavMerge( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavMerge(); +} +static ConCommand nav_merge( "nav_merge", CommandNavMerge, "To merge two Areas into one, mark the first Area, highlight the second by pointing your cursor at it, and invoke the merge command.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavMark( const CCommand &args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavMark( args ); +} +static ConCommand nav_mark( "nav_mark", CommandNavMark, "Marks the Area or Ladder under the cursor for manipulation by subsequent editing commands.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavUnmark( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavUnmark(); +} +static ConCommand nav_unmark( "nav_unmark", CommandNavUnmark, "Clears the marked Area or Ladder.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavBeginArea( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavBeginArea(); +} +static ConCommand nav_begin_area( "nav_begin_area", CommandNavBeginArea, "Defines a corner of a new Area or Ladder. To complete the Area or Ladder, drag the opposite corner to the desired location and issue a 'nav_end_area' command.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavEndArea( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavEndArea(); +} +static ConCommand nav_end_area( "nav_end_area", CommandNavEndArea, "Defines the second corner of a new Area or Ladder and creates it.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavConnect( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavConnect(); +} +static ConCommand nav_connect( "nav_connect", CommandNavConnect, "To connect two Areas, mark the first Area, highlight the second Area, then invoke the connect command. Note that this creates a ONE-WAY connection from the first to the second Area. To make a two-way connection, also connect the second area to the first.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavDisconnect( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavDisconnect(); +} +static ConCommand nav_disconnect( "nav_disconnect", CommandNavDisconnect, "To disconnect two Areas, mark an Area, highlight a second Area, then invoke the disconnect command. This will remove all connections between the two Areas.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavDisconnectOutgoingOneWays( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavDisconnectOutgoingOneWays(); +} +static ConCommand nav_disconnect_outgoing_oneways( "nav_disconnect_outgoing_oneways", CommandNavDisconnectOutgoingOneWays, "For each area in the selected set, disconnect all outgoing one-way connections.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavSplice( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavSplice(); +} +static ConCommand nav_splice( "nav_splice", CommandNavSplice, "To splice, mark an area, highlight a second area, then invoke the splice command to create a new, connected area between them.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavCrouch( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_CROUCH ); +} +static ConCommand nav_crouch( "nav_crouch", CommandNavCrouch, "Toggles the 'must crouch in this area' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavPrecise( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_PRECISE ); +} +static ConCommand nav_precise( "nav_precise", CommandNavPrecise, "Toggles the 'dont avoid obstacles' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavJump( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_JUMP ); +} +static ConCommand nav_jump( "nav_jump", CommandNavJump, "Toggles the 'traverse this area by jumping' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavNoJump( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_NO_JUMP ); +} +static ConCommand nav_no_jump( "nav_no_jump", CommandNavNoJump, "Toggles the 'dont jump in this area' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavStop( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_STOP ); +} +static ConCommand nav_stop( "nav_stop", CommandNavStop, "Toggles the 'must stop when entering this area' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavWalk( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_WALK ); +} +static ConCommand nav_walk( "nav_walk", CommandNavWalk, "Toggles the 'traverse this area by walking' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavRun( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_RUN ); +} +static ConCommand nav_run( "nav_run", CommandNavRun, "Toggles the 'traverse this area by running' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavAvoid( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_AVOID ); +} +static ConCommand nav_avoid( "nav_avoid", CommandNavAvoid, "Toggles the 'avoid this area when possible' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavTransient( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_TRANSIENT ); +} +static ConCommand nav_transient( "nav_transient", CommandNavTransient, "Toggles the 'area is transient and may become blocked' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavDontHide( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_DONT_HIDE ); +} +static ConCommand nav_dont_hide( "nav_dont_hide", CommandNavDontHide, "Toggles the 'area is not suitable for hiding spots' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavStand( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_STAND ); +} +static ConCommand nav_stand( "nav_stand", CommandNavStand, "Toggles the 'stand while hiding' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavNoHostages( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavToggleAttribute( NAV_MESH_NO_HOSTAGES ); +} +static ConCommand nav_no_hostages( "nav_no_hostages", CommandNavNoHostages, "Toggles the 'hostages cannot use this area' flag used by the AI system.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavStrip( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->StripNavigationAreas(); +} +static ConCommand nav_strip( "nav_strip", CommandNavStrip, "Strips all Hiding Spots, Approach Points, and Encounter Spots from the current Area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavSave( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if (TheNavMesh->Save()) + { + Msg( "Navigation map '%s' saved.\n", TheNavMesh->GetFilename() ); + } + else + { + const char *filename = TheNavMesh->GetFilename(); + Msg( "ERROR: Cannot save navigation map '%s'.\n", (filename) ? filename : "(null)" ); + } +} +static ConCommand nav_save( "nav_save", CommandNavSave, "Saves the current Navigation Mesh to disk.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavLoad( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if (TheNavMesh->Load() != NAV_OK) + { + Msg( "ERROR: Navigation Mesh load failed.\n" ); + } +} +static ConCommand nav_load( "nav_load", CommandNavLoad, "Loads the Navigation Mesh for the current map.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +static int PlaceNameAutocompleteCallback( char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] ) +{ + return TheNavMesh->PlaceNameAutocomplete( partial, commands ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavUsePlace( const CCommand &args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if (args.ArgC() == 1) + { + // no arguments = list all available places + TheNavMesh->PrintAllPlaces(); + } + else + { + // single argument = set current place + Place place = TheNavMesh->PartialNameToPlace( args[ 1 ] ); + + if (place == UNDEFINED_PLACE) + { + Msg( "Ambiguous\n" ); + } + else + { + Msg( "Current place set to '%s'\n", TheNavMesh->PlaceToName( place ) ); + TheNavMesh->SetNavPlace( place ); + } + } +} +static ConCommand nav_use_place( "nav_use_place", CommandNavUsePlace, "If used without arguments, all available Places will be listed. If a Place argument is given, the current Place is set.", FCVAR_GAMEDLL | FCVAR_CHEAT, PlaceNameAutocompleteCallback ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavPlaceReplace( const CCommand &args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if (args.ArgC() != 3) + { + // no arguments + Msg( "Usage: nav_place_replace \n" ); + } + else + { + // two arguments - replace the first place with the second + Place oldPlace = TheNavMesh->PartialNameToPlace( args[ 1 ] ); + Place newPlace = TheNavMesh->PartialNameToPlace( args[ 2 ] ); + + if ( oldPlace == UNDEFINED_PLACE || newPlace == UNDEFINED_PLACE ) + { + Msg( "Ambiguous\n" ); + } + else + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[it]; + if ( area->GetPlace() == oldPlace ) + { + area->SetPlace( newPlace ); + } + } + } + } +} +static ConCommand nav_place_replace( "nav_place_replace", CommandNavPlaceReplace, "Replaces all instances of the first place with the second place.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavPlaceList( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CUtlVector< Place > placeDirectory; + + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + + Place place = area->GetPlace(); + + if ( place ) + { + if ( !placeDirectory.HasElement( place ) ) + { + placeDirectory.AddToTail( place ); + } + } + } + + Msg( "Map uses %d place names:\n", placeDirectory.Count() ); + for ( int i=0; iPlaceToName( placeDirectory[i] ) ); + } +} +static ConCommand nav_place_list( "nav_place_list", CommandNavPlaceList, "Lists all place names used in the map.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavTogglePlaceMode( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavTogglePlaceMode(); +} +static ConCommand nav_toggle_place_mode( "nav_toggle_place_mode", CommandNavTogglePlaceMode, "Toggle the editor into and out of Place mode. Place mode allows labelling of Area with Place names.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavSetPlaceMode( const CCommand &args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + bool on = true; + if ( args.ArgC() == 2 ) + { + on = (atoi( args[ 1 ] ) != 0); + } + + if ( on != TheNavMesh->IsEditMode( CNavMesh::PLACE_PAINTING ) ) + { + TheNavMesh->CommandNavTogglePlaceMode(); + } +} +static ConCommand nav_set_place_mode( "nav_set_place_mode", CommandNavSetPlaceMode, "Sets the editor into or out of Place mode. Place mode allows labelling of Area with Place names.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavPlaceFloodFill( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavPlaceFloodFill(); +} +static ConCommand nav_place_floodfill( "nav_place_floodfill", CommandNavPlaceFloodFill, "Sets the Place of the Area under the cursor to the curent Place, and 'flood-fills' the Place to all adjacent Areas. Flood-filling stops when it hits an Area with the same Place, or a different Place than that of the initial Area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavPlaceSet( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavPlaceSet(); +} +static ConCommand nav_place_set( "nav_place_set", CommandNavPlaceSet, "Sets the Place of all selected areas to the current Place.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavPlacePick( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavPlacePick(); +} +static ConCommand nav_place_pick( "nav_place_pick", CommandNavPlacePick, "Sets the current Place to the Place of the Area under the cursor.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavTogglePlacePainting( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavTogglePlacePainting(); +} +static ConCommand nav_toggle_place_painting( "nav_toggle_place_painting", CommandNavTogglePlacePainting, "Toggles Place Painting mode. When Place Painting, pointing at an Area will 'paint' it with the current Place.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavMarkUnnamed( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavMarkUnnamed(); +} +static ConCommand nav_mark_unnamed( "nav_mark_unnamed", CommandNavMarkUnnamed, "Mark an Area with no Place name. Useful for finding stray areas missed when Place Painting.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavCornerSelect( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavCornerSelect(); +} +static ConCommand nav_corner_select( "nav_corner_select", CommandNavCornerSelect, "Select a corner of the currently marked Area. Use multiple times to access all four corners.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_corner_raise, "Raise the selected corner of the currently marked Area.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavCornerRaise( args ); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_corner_lower, "Lower the selected corner of the currently marked Area.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavCornerLower( args ); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_corner_place_on_ground, "Places the selected corner of the currently marked Area on the ground.", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavCornerPlaceOnGround( args ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavWarpToMark( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavWarpToMark(); +} +static ConCommand nav_warp_to_mark( "nav_warp_to_mark", CommandNavWarpToMark, "Warps the player to the marked area.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavLadderFlip( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavLadderFlip(); +} +static ConCommand nav_ladder_flip( "nav_ladder_flip", CommandNavLadderFlip, "Flips the selected ladder's direction.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavGenerate( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->BeginGeneration(); +} +static ConCommand nav_generate( "nav_generate", CommandNavGenerate, "Generate a Navigation Mesh for the current map and save it to disk.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavGenerateIncremental( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->BeginGeneration( INCREMENTAL_GENERATION ); +} +static ConCommand nav_generate_incremental( "nav_generate_incremental", CommandNavGenerateIncremental, "Generate a Navigation Mesh for the current map and save it to disk.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavAnalyze( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( nav_edit.GetBool() ) + { + TheNavMesh->BeginAnalysis(); + } +} +static ConCommand nav_analyze( "nav_analyze", CommandNavAnalyze, "Re-analyze the current Navigation Mesh and save it to disk.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavAnalyzeScripted( const CCommand &args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *pszCmd = NULL; + int count = args.ArgC(); + if ( count > 0 ) + { + pszCmd = args[1]; + } + + bool bForceAnalyze = pszCmd && !Q_stricmp( pszCmd, "force" ); + + if ( TheNavMesh->IsAnalyzed() && !bForceAnalyze ) + { + engine->ServerCommand( "quit\n" ); + return; + } + + if ( nav_edit.GetBool() ) + { + TheNavMesh->BeginAnalysis( true ); + } +} +static ConCommand nav_analyze_scripted( "nav_analyze_scripted", CommandNavAnalyzeScripted, "commandline hook to run a nav_analyze and then quit.", FCVAR_GAMEDLL | FCVAR_CHEAT | FCVAR_HIDDEN ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavMarkWalkable( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavMarkWalkable(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::CommandNavMarkWalkable( void ) +{ + Vector pos; + + if (nav_edit.GetBool()) + { + // we are in edit mode, use the edit cursor's location + pos = GetEditCursorPosition(); + } + else + { + // we are not in edit mode, use the position of the local player + CBasePlayer *player = UTIL_GetListenServerHost(); + + if (player == NULL) + { + Msg( "ERROR: No local player!\n" ); + return; + } + + pos = player->GetAbsOrigin(); + } + + // snap position to the sampling grid + pos.x = SnapToGrid( pos.x, true ); + pos.y = SnapToGrid( pos.y, true ); + + Vector normal; + if ( !FindGroundForNode( &pos, &normal ) ) + { + Msg( "ERROR: Invalid ground position.\n" ); + return; + } + + AddWalkableSeed( pos, normal ); + + Msg( "Walkable position marked.\n" ); +} +static ConCommand nav_mark_walkable( "nav_mark_walkable", CommandNavMarkWalkable, "Mark the current location as a walkable position. These positions are used as seed locations when sampling the map to generate a Navigation Mesh.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavClearWalkableMarks( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->ClearWalkableSeeds(); +} +static ConCommand nav_clear_walkable_marks( "nav_clear_walkable_marks", CommandNavClearWalkableMarks, "Erase any previously placed walkable positions.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavCompressID( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CNavArea::CompressIDs(); + CNavLadder::CompressIDs(); +} +static ConCommand nav_compress_id( "nav_compress_id", CommandNavCompressID, "Re-orders area and ladder ID's so they are continuous.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +#ifdef TERROR +void CommandNavShowLadderBounds( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CFuncSimpleLadder *ladder = NULL; + while( (ladder = dynamic_cast< CFuncSimpleLadder * >(gEntList.FindEntityByClassname( ladder, "func_simpleladder" ))) != NULL ) + { + Vector mins, maxs; + ladder->CollisionProp()->WorldSpaceSurroundingBounds( &mins, &maxs ); + ladder->m_debugOverlays |= OVERLAY_TEXT_BIT | OVERLAY_ABSBOX_BIT; + NDebugOverlay::Box( vec3_origin, mins, maxs, 0, 255, 0, 0, 600 ); + } +} +static ConCommand nav_show_ladder_bounds( "nav_show_ladder_bounds", CommandNavShowLadderBounds, "Draws the bounding boxes of all func_ladders in the map.", FCVAR_GAMEDLL | FCVAR_CHEAT ); +#endif + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavBuildLadder( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavBuildLadder(); +} +static ConCommand nav_build_ladder( "nav_build_ladder", CommandNavBuildLadder, "Attempts to build a nav ladder on the climbable surface under the cursor.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------- +void NavEditClearAllAttributes( void ) +{ + NavAttributeClearer clear( (NavAttributeType)0xFFFF ); + TheNavMesh->ForAllSelectedAreas( clear ); + TheNavMesh->ClearSelectedSet(); +} +static ConCommand ClearAllNavAttributes( "wipe_nav_attributes", NavEditClearAllAttributes, "Clear all nav attributes of selected area.", FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------- +bool NavAttributeToggler::operator() ( CNavArea *area ) +{ + // only toggle if dealing with a single selected area + if ( TheNavMesh->IsSelectedSetEmpty() && (area->GetAttributes() & m_attribute) != 0 ) + { + area->SetAttributes( area->GetAttributes() & (~m_attribute) ); + } + else + { + area->SetAttributes( area->GetAttributes() | m_attribute ); + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------- +NavAttributeLookup TheNavAttributeTable[] = +{ + { "CROUCH", NAV_MESH_CROUCH }, + { "JUMP", NAV_MESH_JUMP }, + { "PRECISE", NAV_MESH_PRECISE }, + { "NO_JUMP", NAV_MESH_NO_JUMP }, + { "STOP", NAV_MESH_STOP }, + { "RUN", NAV_MESH_RUN }, + { "WALK", NAV_MESH_WALK }, + { "AVOID", NAV_MESH_AVOID }, + { "TRANSIENT", NAV_MESH_TRANSIENT }, + { "DONT_HIDE", NAV_MESH_DONT_HIDE }, + { "STAND", NAV_MESH_STAND }, + { "NO_HOSTAGES", NAV_MESH_NO_HOSTAGES }, + { "STAIRS", NAV_MESH_STAIRS }, + { "NO_MERGE", NAV_MESH_NO_MERGE }, + { "OBSTACLE_TOP", NAV_MESH_OBSTACLE_TOP }, + { "CLIFF", NAV_MESH_CLIFF }, +#ifdef TERROR + { "PLAYERCLIP", (NavAttributeType)CNavArea::NAV_PLAYERCLIP }, + { "BREAKABLEWALL", (NavAttributeType)CNavArea::NAV_BREAKABLEWALL }, +#endif + { NULL, NAV_MESH_INVALID } +}; + + +/** + * Can be used with any command that takes an attribute as its 2nd argument + */ +static int NavAttributeAutocomplete( const char *input, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] ) +{ + if ( Q_strlen( input ) >= COMMAND_COMPLETION_ITEM_LENGTH ) + { + return 0; + } + + char command[ COMMAND_COMPLETION_ITEM_LENGTH+1 ]; + Q_strncpy( command, input, sizeof( command ) ); + + // skip to start of argument + char *partialArg = Q_strrchr( command, ' ' ); + if ( partialArg == NULL ) + { + return 0; + } + + // chop command from partial argument + *partialArg = '\000'; + ++partialArg; + + int partialArgLength = Q_strlen( partialArg ); + + int count = 0; + for( unsigned int i=0; TheNavAttributeTable[i].name && count < COMMAND_COMPLETION_MAXITEMS; ++i ) + { + if ( !Q_strnicmp( TheNavAttributeTable[i].name, partialArg, partialArgLength ) ) + { + // Add to the autocomplete array + Q_snprintf( commands[ count++ ], COMMAND_COMPLETION_ITEM_LENGTH, "%s %s", command, TheNavAttributeTable[i].name ); + } + } + + return count; +} + + +//-------------------------------------------------------------------------------------------------------- +NavAttributeType NameToNavAttribute( const char *name ) +{ + for( unsigned int i=0; TheNavAttributeTable[i].name; ++i ) + { + if ( !Q_stricmp( TheNavAttributeTable[i].name, name ) ) + { + return TheNavAttributeTable[i].attribute; + } + } + + return (NavAttributeType)0; +} + + +//-------------------------------------------------------------------------------------------------------- +void NavEditClearAttribute( const CCommand &args ) +{ + if ( args.ArgC() != 2 ) + { + Msg( "Usage: %s \n", args[0] ); + return; + } + + NavAttributeType attribute = NameToNavAttribute( args[1] ); + + if ( attribute != 0 ) + { + NavAttributeClearer clear( attribute ); + TheNavMesh->ForAllSelectedAreas( clear ); + TheNavMesh->ClearSelectedSet(); + return; + } + + Msg( "Unknown attribute '%s'", args[1] ); +} +static ConCommand NavClearAttribute( "nav_clear_attribute", NavEditClearAttribute, "Remove given nav attribute from all areas in the selected set.", FCVAR_CHEAT, NavAttributeAutocomplete ); + + +//-------------------------------------------------------------------------------------------------------- +void NavEditMarkAttribute( const CCommand &args ) +{ + if ( args.ArgC() != 2 ) + { + Msg( "Usage: %s \n", args[0] ); + return; + } + + NavAttributeType attribute = NameToNavAttribute( args[1] ); + + if ( attribute != 0 ) + { + NavAttributeSetter setter( attribute ); + TheNavMesh->ForAllSelectedAreas( setter ); + TheNavMesh->ClearSelectedSet(); + return; + } + + Msg( "Unknown attribute '%s'", args[1] ); +} +static ConCommand NavMarkAttribute( "nav_mark_attribute", NavEditMarkAttribute, "Set nav attribute for all areas in the selected set.", FCVAR_CHEAT, NavAttributeAutocomplete ); + + +/* IN PROGRESS: +//-------------------------------------------------------------------------------------------------------------- +void CommandNavPickArea( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavPickArea(); +} +static ConCommand nav_pick_area( "nav_pick_area", CommandNavPickArea, "Marks an area (and corner) based on the surface under the cursor.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavResizeHorizontal( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavResizeHorizontal(); +} +static ConCommand nav_resize_horizontal( "nav_resize_horizontal", CommandNavResizeHorizontal, "TODO", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavResizeVertical( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavResizeVertical(); +} +static ConCommand nav_resize_vertical( "nav_resize_vertical", CommandNavResizeVertical, "TODO", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavResizeEnd( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + TheNavMesh->CommandNavResizeEnd(); +} +static ConCommand nav_resize_end( "nav_resize_end", CommandNavResizeEnd, "TODO", FCVAR_GAMEDLL | FCVAR_CHEAT ); +*/ + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Destroy ladder representations + */ +void CNavMesh::DestroyLadders( void ) +{ + for ( int i=0; iStrip(); + } + + m_isAnalyzed = false; +} + +//-------------------------------------------------------------------------------------------------------------- + +HidingSpotVector TheHidingSpots; +unsigned int HidingSpot::m_nextID = 1; +unsigned int HidingSpot::m_masterMarker = 0; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Hiding Spot factory + */ +HidingSpot *CNavMesh::CreateHidingSpot( void ) const +{ + return new HidingSpot; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavMesh::DestroyHidingSpots( void ) +{ + // remove all hiding spot references from the nav areas + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + area->m_hidingSpots.RemoveAll(); + } + + HidingSpot::m_nextID = 0; + + // free all the HidingSpots + FOR_EACH_VEC( TheHidingSpots, hit ) + { + delete TheHidingSpots[ hit ]; + } + + TheHidingSpots.RemoveAll(); +} + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- +/** + * Construct a Hiding Spot. Assign a unique ID which may be overwritten if loaded. + */ +HidingSpot::HidingSpot( void ) +{ + m_pos = Vector( 0, 0, 0 ); + m_id = m_nextID++; + m_flags = 0; + m_area = NULL; + + TheHidingSpots.AddToTail( this ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void HidingSpot::Save( CUtlBuffer &fileBuffer, unsigned int version ) const +{ + fileBuffer.PutUnsignedInt( m_id ); + fileBuffer.PutFloat( m_pos.x ); + fileBuffer.PutFloat( m_pos.y ); + fileBuffer.PutFloat( m_pos.z ); + fileBuffer.PutUnsignedChar( m_flags ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void HidingSpot::Load( CUtlBuffer &fileBuffer, unsigned int version ) +{ + m_id = fileBuffer.GetUnsignedInt(); + m_pos.x = fileBuffer.GetFloat(); + m_pos.y = fileBuffer.GetFloat(); + m_pos.z = fileBuffer.GetFloat(); + m_flags = fileBuffer.GetUnsignedChar(); + + // update next ID to avoid ID collisions by later spots + if (m_id >= m_nextID) + m_nextID = m_id+1; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Hiding Spot post-load processing + */ +NavErrorType HidingSpot::PostLoad( void ) +{ + // set our area + m_area = TheNavMesh->GetNavArea( m_pos + Vector( 0, 0, HalfHumanHeight ) ); + + if ( !m_area ) + { + DevWarning( "A Hiding Spot is off of the Nav Mesh at setpos %.0f %.0f %.0f\n", m_pos.x, m_pos.y, m_pos.z ); + } + + return NAV_OK; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Given a HidingSpot ID, return the associated HidingSpot + */ +HidingSpot *GetHidingSpotByID( unsigned int id ) +{ + FOR_EACH_VEC( TheHidingSpots, it ) + { + HidingSpot *spot = TheHidingSpots[ it ]; + + if (spot->GetID() == id) + return spot; + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------- +// invoked when the area becomes blocked +void CNavMesh::OnAreaBlocked( CNavArea *area ) +{ + if ( !m_blockedAreas.HasElement( area ) ) + { + m_blockedAreas.AddToTail( area ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +// invoked when the area becomes un-blocked +void CNavMesh::OnAreaUnblocked( CNavArea *area ) +{ + m_blockedAreas.FindAndRemove( area ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::UpdateBlockedAreas( void ) +{ + VPROF( "CNavMesh::UpdateBlockedAreas" ); + for ( int i=0; iUpdateBlocked(); + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::RegisterAvoidanceObstacle( INavAvoidanceObstacle *obstruction ) +{ + m_avoidanceObstacles.FindAndFastRemove( obstruction ); + m_avoidanceObstacles.AddToTail( obstruction ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::UnregisterAvoidanceObstacle( INavAvoidanceObstacle *obstruction ) +{ + m_avoidanceObstacles.FindAndFastRemove( obstruction ); +} + + +//-------------------------------------------------------------------------------------------------------- +// invoked when the area becomes blocked +void CNavMesh::OnAvoidanceObstacleEnteredArea( CNavArea *area ) +{ + if ( !m_avoidanceObstacleAreas.HasElement( area ) ) + { + m_avoidanceObstacleAreas.AddToTail( area ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +// invoked when the area becomes un-blocked +void CNavMesh::OnAvoidanceObstacleLeftArea( CNavArea *area ) +{ + m_avoidanceObstacleAreas.FindAndRemove( area ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::UpdateAvoidanceObstacleAreas( void ) +{ + VPROF( "CNavMesh::UpdateAvoidanceObstacleAreas" ); + for ( int i=0; iUpdateAvoidanceObstacles(); + } +} + + + +extern CUtlHash< NavVisPair_t, CVisPairHashFuncs, CVisPairHashFuncs > *g_pNavVisPairHash; + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::BeginVisibilityComputations( void ) +{ + if ( !g_pNavVisPairHash ) + { + g_pNavVisPairHash = new CUtlHash< NavVisPair_t, CVisPairHashFuncs, CVisPairHashFuncs >( 16*1024 ); + } + else + { + g_pNavVisPairHash->RemoveAll(); + } + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + area->ResetPotentiallyVisibleAreas(); + } +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Invoked when custom analysis step is complete + */ +void CNavMesh::EndVisibilityComputations( void ) +{ + g_pNavVisPairHash->RemoveAll(); + + int avgVisLength = 0; + int maxVisLength = 0; + int minVisLength = 999999999; + + // Optimize visibility storage of nav mesh by doing a kind of run-length encoding. + // Pick an "anchor" area and compare adjacent areas visibility lists to it. If the delta is + // small, point back to the anchor and just store the delta. + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = (CNavArea *)TheNavAreas[ it ]; + + int visLength = area->m_potentiallyVisibleAreas.Count(); + avgVisLength += visLength; + if ( visLength < minVisLength ) + { + minVisLength = visLength; + } + if ( visLength > maxVisLength ) + { + maxVisLength = visLength; + } + + if ( area->m_isInheritedFrom ) + { + // another area is inheriting from our vis data, we can't inherit + continue; + } + + // find adjacent area with the smallest change from our visibility list + CNavArea::CAreaBindInfoArray bestDelta; + CNavArea *anchor = NULL; + + for( int dir = NORTH; dir < NUM_DIRECTIONS; ++dir ) + { + int count = area->GetAdjacentCount( (NavDirType)dir ); + for( int i=0; iGetAdjacentArea( (NavDirType)dir, i ); + + // do not inherit from an area that is inheriting - use its ultimate source + if ( adjArea->m_inheritVisibilityFrom.area != NULL ) + { + adjArea = adjArea->m_inheritVisibilityFrom.area; + if ( adjArea == area ) + continue; // don't try to inherit visibility from ourselves + } + + const CNavArea::CAreaBindInfoArray &delta = area->ComputeVisibilityDelta( adjArea ); + + // keep the smallest delta + if ( !anchor || ( anchor && delta.Count() < bestDelta.Count() ) ) + { + bestDelta = delta; + anchor = adjArea; + Assert( anchor != area ); + } + } + } + + // if best delta is small enough, inherit our data from this anchor + if ( anchor && bestDelta.Count() <= nav_max_vis_delta_list_length.GetInt() && anchor != area ) + { + // inherit from anchor area's visibility list + area->m_inheritVisibilityFrom.area = anchor; + area->m_potentiallyVisibleAreas = bestDelta; + + // mark inherited-from area so it doesn't later try to inherit + anchor->m_isInheritedFrom = true; + } + else + { + // retain full list of visible areas + area->m_inheritVisibilityFrom.area = NULL; + } + } + + if ( TheNavAreas.Count() ) + { + avgVisLength /= TheNavAreas.Count(); + } + + Msg( "NavMesh Visibility List Lengths: min = %d, avg = %d, max = %d\n", minVisLength, avgVisLength, maxVisLength ); +} diff --git a/sp/src/game/server/nav_mesh.h b/sp/src/game/server/nav_mesh.h new file mode 100644 index 00000000..24faed41 --- /dev/null +++ b/sp/src/game/server/nav_mesh.h @@ -0,0 +1,1346 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_mesh.h +// The Navigation Mesh interface +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +// +// Author: Michael S. Booth (mike@turtlerockstudios.com), 2003 +// +// NOTE: The Navigation code uses Doxygen-style comments. If you run Doxygen over this code, it will +// auto-generate documentation. Visit www.doxygen.org to download the system for free. +// + +#ifndef _NAV_MESH_H_ +#define _NAV_MESH_H_ + +#include "utlbuffer.h" +#include "filesystem.h" +#include "GameEventListener.h" + +#include "nav.h" +#include "nav_area.h" +#include "nav_colors.h" + + +class CNavArea; +class CBaseEntity; +class CBreakable; + +extern ConVar nav_edit; +extern ConVar nav_quicksave; +extern ConVar nav_show_approach_points; +extern ConVar nav_show_danger; + +//-------------------------------------------------------------------------------------------------------- +class NavAreaCollector +{ + bool m_checkForDuplicates; +public: + NavAreaCollector( bool checkForDuplicates = false ) + { + m_checkForDuplicates = checkForDuplicates; + } + + bool operator() ( CNavArea *area ) + { + if ( m_checkForDuplicates && m_area.HasElement( area ) ) + return true; + + m_area.AddToTail( area ); + return true; + } + CUtlVector< CNavArea * > m_area; +}; + + +//-------------------------------------------------------------------------------------------------------- +class EditDestroyNotification +{ + CNavArea *m_deadArea; + +public: + EditDestroyNotification( CNavArea *deadArea ) + { + m_deadArea = deadArea; + } + + bool operator()( CBaseCombatCharacter *actor ) + { + actor->OnNavAreaRemoved( m_deadArea ); + return true; + } +}; + + +//-------------------------------------------------------------------------------------------------------- +class NavAttributeClearer +{ +public: + NavAttributeClearer( NavAttributeType attribute ) + { + m_attribute = attribute; + } + + bool operator() ( CNavArea *area ) + { + area->SetAttributes( area->GetAttributes() & (~m_attribute) ); + + return true; + } + + NavAttributeType m_attribute; +}; + + +//-------------------------------------------------------------------------------------------------------- +class NavAttributeSetter +{ +public: + NavAttributeSetter( NavAttributeType attribute ) + { + m_attribute = attribute; + } + + bool operator() ( CNavArea *area ) + { + area->SetAttributes( area->GetAttributes() | m_attribute ); + + return true; + } + + NavAttributeType m_attribute; +}; + + +//-------------------------------------------------------------------------------------------------------- +class NavAttributeToggler +{ +public: + NavAttributeToggler( NavAttributeType attribute ) + { + m_attribute = attribute; + } + + bool operator() ( CNavArea *area ); + + NavAttributeType m_attribute; +}; + + +//-------------------------------------------------------------------------------------------------------- +struct NavAttributeLookup +{ + const char *name; + NavAttributeType attribute; +}; + +extern NavAttributeLookup TheNavAttributeTable[]; + +//-------------------------------------------------------------------------------------------------------- +class SelectOverlappingAreas +{ +public: + bool operator()( CNavArea *area ); +}; + +//-------------------------------------------------------------------------------------------------------- +abstract_class INavAvoidanceObstacle +{ +public: + virtual bool IsPotentiallyAbleToObstructNavAreas( void ) const = 0; // could we at some future time obstruct nav? + virtual float GetNavObstructionHeight( void ) const = 0; // height at which to obstruct nav areas + virtual bool CanObstructNavAreas( void ) const = 0; // can we obstruct nav right this instant? + virtual CBaseEntity *GetObstructingEntity( void ) = 0; + virtual void OnNavMeshLoaded( void ) = 0; +}; + +//-------------------------------------------------------------------------------------------------------- +enum GetNavAreaFlags_t +{ + GETNAVAREA_CHECK_LOS = 0x1, + GETNAVAREA_ALLOW_BLOCKED_AREAS = 0x2, + GETNAVAREA_CHECK_GROUND = 0x4, +}; + + +//-------------------------------------------------------------------------------------------------------- +// for nav mesh visibilty computation +struct NavVisPair_t +{ + void SetPair( CNavArea *pArea1, CNavArea *pArea2 ) + { + int iArea1 = (int)( pArea1 > pArea2 ); + int iArea2 = ( iArea1 + 1 ) % 2; + pAreas[iArea1] = pArea1; + pAreas[iArea2] = pArea2; + } + + CNavArea *pAreas[2]; +}; + + +// for nav mesh visibilty computation +class CVisPairHashFuncs +{ +public: + CVisPairHashFuncs( int ) {} + + bool operator()( const NavVisPair_t &lhs, const NavVisPair_t &rhs ) const + { + return ( lhs.pAreas[0] == rhs.pAreas[0] && lhs.pAreas[1] == rhs.pAreas[1] ); + } + + unsigned int operator()( const NavVisPair_t &item ) const + { + COMPILE_TIME_ASSERT( sizeof(CNavArea *) == 4 ); + int key[2] = { (int)item.pAreas[0] + item.pAreas[1]->GetID(), (int)item.pAreas[1] + item.pAreas[0]->GetID() }; + return Hash8( key ); + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +// +// The 'place directory' is used to save and load places from +// nav files in a size-efficient manner that also allows for the +// order of the place ID's to change without invalidating the +// nav files. +// +// The place directory is stored in the nav file as a list of +// place name strings. Each nav area then contains an index +// into that directory, or zero if no place has been assigned to +// that area. +// +class PlaceDirectory +{ +public: + typedef unsigned short IndexType; // Loaded/Saved as UnsignedShort. Change this and you'll have to version. + + PlaceDirectory( void ); + void Reset( void ); + bool IsKnown( Place place ) const; /// return true if this place is already in the directory + IndexType GetIndex( Place place ) const; /// return the directory index corresponding to this Place (0 = no entry) + void AddPlace( Place place ); /// add the place to the directory if not already known + Place IndexToPlace( IndexType entry ) const; /// given an index, return the Place + void Save( CUtlBuffer &fileBuffer ); /// store the directory + void Load( CUtlBuffer &fileBuffer, int version ); /// load the directory + const CUtlVector< Place > *GetPlaces( void ) const + { + return &m_directory; + } + + bool HasUnnamedPlaces( void ) const + { + return m_hasUnnamedAreas; + } + + +private: + CUtlVector< Place > m_directory; + bool m_hasUnnamedAreas; +}; + +extern PlaceDirectory placeDirectory; + + + +//-------------------------------------------------------------------------------------------------------- +/** + * The CNavMesh is the global interface to the Navigation Mesh. + * @todo Make this an abstract base class interface, and derive mod-specific implementations. + */ +class CNavMesh : public CGameEventListener +{ +public: + CNavMesh( void ); + virtual ~CNavMesh(); + + virtual void PreLoadAreas( int nAreas ) {} + virtual CNavArea *CreateArea( void ) const; // CNavArea factory + virtual void DestroyArea( CNavArea * ) const; + virtual HidingSpot *CreateHidingSpot( void ) const; // Hiding Spot factory + + virtual void Reset( void ); // destroy Navigation Mesh data and revert to initial state + virtual void Update( void ); // invoked on each game frame + + virtual void FireGameEvent( IGameEvent *event ); // incoming event processing + + virtual NavErrorType Load( void ); // load navigation data from a file + virtual NavErrorType PostLoad( unsigned int version ); // (EXTEND) invoked after all areas have been loaded - for pointer binding, etc + bool IsLoaded( void ) const { return m_isLoaded; } // return true if a Navigation Mesh has been loaded + bool IsAnalyzed( void ) const { return m_isAnalyzed; } // return true if a Navigation Mesh has been analyzed + + /** + * Return true if nav mesh can be trusted for all climbing/jumping decisions because game environment is fairly simple. + * Authoritative meshes mean path followers can skip CPU intensive realtime scanning of unpredictable geometry. + */ + virtual bool IsAuthoritative( void ) const { return false; } + + const CUtlVector< Place > *GetPlacesFromNavFile( bool *hasUnnamedPlaces ); // Reads the used place names from the nav file (can be used to selectively precache before the nav is loaded) + + virtual bool Save( void ) const; // store Navigation Mesh to a file + bool IsOutOfDate( void ) const { return m_isOutOfDate; } // return true if the Navigation Mesh is older than the current map version + + virtual unsigned int GetSubVersionNumber( void ) const; // returns sub-version number of data format used by derived classes + virtual void SaveCustomData( CUtlBuffer &fileBuffer ) const { } // store custom mesh data for derived classes + virtual void LoadCustomData( CUtlBuffer &fileBuffer, unsigned int subVersion ) { } // load custom mesh data for derived classes + virtual void SaveCustomDataPreArea( CUtlBuffer &fileBuffer ) const { } // store custom mesh data for derived classes that needs to be loaded before areas are read in + virtual void LoadCustomDataPreArea( CUtlBuffer &fileBuffer, unsigned int subVersion ) { } // load custom mesh data for derived classes that needs to be loaded before areas are read in + + // events + virtual void OnServerActivate( void ); // (EXTEND) invoked when server loads a new map + virtual void OnRoundRestart( void ); // invoked when a game round restarts + virtual void OnRoundRestartPreEntity( void ); // invoked when a game round restarts, but before entities are deleted and recreated + virtual void OnBreakableCreated( CBaseEntity *breakable ) { } // invoked when a breakable is created + virtual void OnBreakableBroken( CBaseEntity *broken ) { } // invoked when a breakable is broken + virtual void OnAreaBlocked( CNavArea *area ); // invoked when the area becomes blocked + virtual void OnAreaUnblocked( CNavArea *area ); // invoked when the area becomes un-blocked + virtual void OnAvoidanceObstacleEnteredArea( CNavArea *area ); // invoked when the area becomes obstructed + virtual void OnAvoidanceObstacleLeftArea( CNavArea *area ); // invoked when the area becomes un-obstructed + + virtual void OnEditCreateNotify( CNavArea *newArea ); // invoked when given area has just been added to the mesh in edit mode + virtual void OnEditDestroyNotify( CNavArea *deadArea ); // invoked when given area has just been deleted from the mesh in edit mode + virtual void OnEditDestroyNotify( CNavLadder *deadLadder ); // invoked when given ladder has just been deleted from the mesh in edit mode + virtual void OnNodeAdded( CNavNode *node ) {}; + + // Obstructions + void RegisterAvoidanceObstacle( INavAvoidanceObstacle *obstruction ); + void UnregisterAvoidanceObstacle( INavAvoidanceObstacle *obstruction ); + const CUtlVector< INavAvoidanceObstacle * > &GetObstructions( void ) const { return m_avoidanceObstacles; } + + unsigned int GetNavAreaCount( void ) const { return m_areaCount; } // return total number of nav areas + + // See GetNavAreaFlags_t for flags + CNavArea *GetNavArea( const Vector &pos, float beneathLimt = 120.0f ) const; // given a position, return the nav area that IsOverlapping and is *immediately* beneath it + CNavArea *GetNavArea( CBaseEntity *pEntity, int nGetNavAreaFlags, float flBeneathLimit = 120.0f ) const; + CNavArea *GetNavAreaByID( unsigned int id ) const; + CNavArea *GetNearestNavArea( const Vector &pos, bool anyZ = false, float maxDist = 10000.0f, bool checkLOS = false, bool checkGround = true, int team = TEAM_ANY ) const; + CNavArea *GetNearestNavArea( CBaseEntity *pEntity, int nGetNavAreaFlags = GETNAVAREA_CHECK_GROUND, float maxDist = 10000.0f ) const; + + Place GetPlace( const Vector &pos ) const; // return Place at given coordinate + const char *PlaceToName( Place place ) const; // given a place, return its name + Place NameToPlace( const char *name ) const; // given a place name, return a place ID or zero if no place is defined + Place PartialNameToPlace( const char *name ) const; // given the first part of a place name, return a place ID or zero if no place is defined, or the partial match is ambiguous + void PrintAllPlaces( void ) const; // output a list of names to the console + int PlaceNameAutocomplete( char const *partial, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] ); // Given a partial place name, fill in possible place names for ConCommand autocomplete + + bool GetGroundHeight( const Vector &pos, float *height, Vector *normal = NULL ) const; // get the Z coordinate of the topmost ground level below the given point + bool GetSimpleGroundHeight( const Vector &pos, float *height, Vector *normal = NULL ) const;// get the Z coordinate of the ground level directly below the given point + + + /// increase "danger" weights in the given nav area and nearby ones + void IncreaseDangerNearby( int teamID, float amount, CNavArea *area, const Vector &pos, float maxRadius, float dangerLimit = -1.0f ); + void DrawDanger( void ) const; // draw the current danger levels + void DrawPlayerCounts( void ) const; // draw the current player counts for each area + void DrawFuncNavAvoid( void ) const; // draw bot avoidance areas from func_nav_avoid entities + void DrawFuncNavPrefer( void ) const; // draw bot preference areas from func_nav_prefer entities +#ifdef NEXT_BOT + void DrawFuncNavPrerequisite( void ) const; // draw bot prerequisite areas from func_nav_prerequisite entities +#endif + //------------------------------------------------------------------------------------- + // Auto-generation + // + #define INCREMENTAL_GENERATION true + void BeginGeneration( bool incremental = false ); // initiate the generation process + void BeginAnalysis( bool quitWhenFinished = false ); // re-analyze an existing Mesh. Determine Hiding Spots, Encounter Spots, etc. + + bool IsGenerating( void ) const { return m_generationMode != GENERATE_NONE; } // return true while a Navigation Mesh is being generated + const char *GetPlayerSpawnName( void ) const; // return name of player spawn entity + void SetPlayerSpawnName( const char *name ); // define the name of player spawn entities + void AddWalkableSeed( const Vector &pos, const Vector &normal ); // add given walkable position to list of seed positions for map sampling + virtual void AddWalkableSeeds( void ); // adds walkable positions for any/all positions a mod specifies + void ClearWalkableSeeds( void ) { m_walkableSeeds.RemoveAll(); } // erase all walkable seed positions + void MarkStairAreas( void ); + + virtual unsigned int GetGenerationTraceMask( void ) const; // return the mask used by traces when generating the mesh + + + //------------------------------------------------------------------------------------- + // Edit mode + // + unsigned int GetNavPlace( void ) const { return m_navPlace; } + void SetNavPlace( unsigned int place ) { m_navPlace = place; } + + // Edit callbacks from ConCommands + void CommandNavDelete( void ); // delete current area + void CommandNavDeleteMarked( void ); // delete current marked area + + virtual void CommandNavFloodSelect( const CCommand &args ); // select current area and all connected areas, recursively + void CommandNavToggleSelectedSet( void ); // toggles all areas into/out of the selected set + void CommandNavStoreSelectedSet( void ); // stores the current selected set for later + void CommandNavRecallSelectedSet( void ); // restores an older selected set + void CommandNavAddToSelectedSet( void ); // add current area to selected set + void CommandNavAddToSelectedSetByID( const CCommand &args ); // add specified area id to selected set + void CommandNavRemoveFromSelectedSet( void ); // remove current area from selected set + void CommandNavToggleInSelectedSet( void ); // add/remove current area from selected set + void CommandNavClearSelectedSet( void ); // clear the selected set to empty + void CommandNavBeginSelecting( void ); // start continuously selecting areas into the selected set + void CommandNavEndSelecting( void ); // stop continuously selecting areas into the selected set + void CommandNavBeginDragSelecting( void ); // start dragging a selection area + void CommandNavEndDragSelecting( void ); // stop dragging a selection area + void CommandNavBeginDragDeselecting( void ); // start dragging a deselection area + void CommandNavEndDragDeselecting( void ); // stop dragging a deselection area + void CommandNavRaiseDragVolumeMax( void ); // raise the top of the drag volume + void CommandNavLowerDragVolumeMax( void ); // lower the top of the drag volume + void CommandNavRaiseDragVolumeMin( void ); // raise the bottom of the drag volume + void CommandNavLowerDragVolumeMin( void ); // lower the bottom of the drag volume + void CommandNavToggleSelecting( bool playSound = true ); // start/stop continuously selecting areas into the selected set + void CommandNavBeginDeselecting( void ); // start continuously de-selecting areas from the selected set + void CommandNavEndDeselecting( void ); // stop continuously de-selecting areas from the selected set + void CommandNavToggleDeselecting( bool playSound = true ); // start/stop continuously de-selecting areas from the selected set + void CommandNavSelectInvalidAreas( void ); // adds invalid areas to the selected set + void CommandNavSelectBlockedAreas( void ); // adds blocked areas to the selected set + void CommandNavSelectObstructedAreas( void ); // adds obstructed areas to the selected set + void CommandNavSelectDamagingAreas( void ); // adds damaging areas to the selected set + void CommandNavSelectHalfSpace( const CCommand &args ); // selects all areas that intersect the half-space + void CommandNavSelectStairs( void ); // adds stairs areas to the selected set + void CommandNavSelectOrphans( void ); // adds areas not connected to mesh to the selected set + + void CommandNavSplit( void ); // split current area + void CommandNavMerge( void ); // merge adjacent areas + void CommandNavMark( const CCommand &args ); // mark an area for further operations + void CommandNavUnmark( void ); // removes the mark + + void CommandNavBeginArea( void ); // begin creating a new nav area + void CommandNavEndArea( void ); // end creation of the new nav area + + void CommandNavBeginShiftXY( void ); // begin shifting selected set in the XY plane + void CommandNavEndShiftXY( void ); // end shifting selected set in the XY plane + + void CommandNavConnect( void ); // connect marked area to selected area + void CommandNavDisconnect( void ); // disconnect marked area from selected area + void CommandNavDisconnectOutgoingOneWays( void ); // disconnect all outgoing one-way connects from each area in the selected set + void CommandNavSplice( void ); // create new area in between marked and selected areas + void CommandNavCrouch( void ); // toggle crouch attribute on current area + void CommandNavTogglePlaceMode( void ); // switch between normal and place editing + void CommandNavSetPlaceMode( void ); // switch between normal and place editing + void CommandNavPlaceFloodFill( void ); // floodfill areas out from current area + void CommandNavPlaceSet( void ); // sets the Place for the selected set + void CommandNavPlacePick( void ); // "pick up" the place at the current area + void CommandNavTogglePlacePainting( void ); // switch between "painting" places onto areas + void CommandNavMarkUnnamed( void ); // mark an unnamed area for further operations + void CommandNavCornerSelect( void ); // select a corner on the current area + void CommandNavCornerRaise( const CCommand &args ); // raise a corner on the current area + void CommandNavCornerLower( const CCommand &args ); // lower a corner on the current area + void CommandNavCornerPlaceOnGround( const CCommand &args ); // position a corner on the current area at ground height + void CommandNavWarpToMark( void ); // warp a spectating local player to the selected mark + void CommandNavLadderFlip( void ); // Flips the direction a ladder faces + void CommandNavToggleAttribute( NavAttributeType attribute ); // toggle an attribute on current area + void CommandNavMakeSniperSpots( void ); // cuts up the marked area into individual areas suitable for sniper spots + void CommandNavBuildLadder( void ); // builds a nav ladder on the climbable surface under the cursor + void CommandNavRemoveJumpAreas( void ); // removes jump areas, replacing them with connections + void CommandNavSubdivide( const CCommand &args ); // subdivide each nav area in X and Y to create 4 new areas - limit min size + void CommandNavSaveSelected( const CCommand &args ); // Save selected set to disk + void CommandNavMergeMesh( const CCommand &args ); // Merge a saved selected set into the current mesh + void CommandNavMarkWalkable( void ); + + void AddToDragSelectionSet( CNavArea *pArea ); + void RemoveFromDragSelectionSet( CNavArea *pArea ); + void ClearDragSelectionSet( void ); + + CNavArea *GetMarkedArea( void ) const; // return area marked by user in edit mode + CNavLadder *GetMarkedLadder( void ) const { return m_markedLadder; } // return ladder marked by user in edit mode + + CNavArea *GetSelectedArea( void ) const { return m_selectedArea; } // return area user is pointing at in edit mode + CNavLadder *GetSelectedLadder( void ) const { return m_selectedLadder; } // return ladder user is pointing at in edit mode + void SetMarkedLadder( CNavLadder *ladder ); // mark ladder for further edit operations + void SetMarkedArea( CNavArea *area ); // mark area for further edit operations + + bool IsContinuouslySelecting( void ) const + { + return m_isContinuouslySelecting; + } + + bool IsContinuouslyDeselecting( void ) const + { + return m_isContinuouslyDeselecting; + } + + void CreateLadder( const Vector &mins, const Vector &maxs, float maxHeightAboveTopArea ); + void CreateLadder( const Vector &top, const Vector &bottom, float width, const Vector2D &ladderDir, float maxHeightAboveTopArea ); + + float SnapToGrid( float x, bool forceGrid = false ) const; // snap given coordinate to generation grid boundary + Vector SnapToGrid( const Vector& in, bool snapX = true, bool snapY = true, bool forceGrid = false ) const; // snap given vector's X & Y coordinates to generation grid boundary + + const Vector &GetEditCursorPosition( void ) const { return m_editCursorPos; } // return position of edit cursor + void StripNavigationAreas( void ); + const char *GetFilename( void ) const; // return the filename for this map's "nav" file + + /// @todo Remove old select code and make all commands use this selected set + void AddToSelectedSet( CNavArea *area ); // add area to the currently selected set + void RemoveFromSelectedSet( CNavArea *area ); // remove area from the currently selected set + void ClearSelectedSet( void ); // clear the currently selected set to empty + bool IsSelectedSetEmpty( void ) const; // return true if the selected set is empty + bool IsInSelectedSet( const CNavArea *area ) const; // return true if the given area is in the selected set + int GetSelecteSetSize( void ) const; + const NavAreaVector &GetSelectedSet( void ) const; // return the selected set + + /** + * Apply the functor to all navigation areas in the Selected Set, + * or the current selected area. + * If functor returns false, stop processing and return false. + */ + template < typename Functor > + bool ForAllSelectedAreas( Functor &func ) + { + if (IsSelectedSetEmpty()) + { + CNavArea *area = GetSelectedArea(); + + if (area) + { + if (func( area ) == false) + return false; + } + } + else + { + FOR_EACH_VEC( m_selectedSet, it ) + { + CNavArea *area = m_selectedSet[ it ]; + + if (func( area ) == false) + return false; + } + } + + return true; + } + + //------------------------------------------------------------------------------------- + /** + * Apply the functor to all navigation areas. + * If functor returns false, stop processing and return false. + */ + template < typename Functor > + bool ForAllAreas( Functor &func ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if (func( area ) == false) + return false; + } + + return true; + } + + // const version of the above + template < typename Functor > + bool ForAllAreas( Functor &func ) const + { + FOR_EACH_VEC( TheNavAreas, it ) + { + const CNavArea *area = TheNavAreas[ it ]; + + if (func( area ) == false) + return false; + } + + return true; + } + + //------------------------------------------------------------------------------------- + /** + * Apply the functor to all navigation areas that overlap the given extent. + * If functor returns false, stop processing and return false. + */ + template < typename Functor > + bool ForAllAreasOverlappingExtent( Functor &func, const Extent &extent ) + { + if ( !m_grid.Count() ) + { +#if _DEBUG + Warning("Query before nav mesh is loaded! %d\n", TheNavAreas.Count() ); +#endif + return true; + } + static unsigned int searchMarker = RandomInt(0, 1024*1024 ); + if ( ++searchMarker == 0 ) + { + ++searchMarker; + } + + Extent areaExtent; + + // get list in cell that contains position + int startX = WorldToGridX( extent.lo.x ); + int endX = WorldToGridX( extent.hi.x ); + int startY = WorldToGridY( extent.lo.y ); + int endY = WorldToGridY( extent.hi.y ); + + for( int x = startX; x <= endX; ++x ) + { + for( int y = startY; y <= endY; ++y ) + { + int iGrid = x + y*m_gridSizeX; + if ( iGrid >= m_grid.Count() ) + { + ExecuteNTimes( 10, Warning( "** Walked off of the CNavMesh::m_grid in ForAllAreasOverlappingExtent()\n" ) ); + return true; + } + + NavAreaVector *areaVector = &m_grid[ iGrid ]; + + // find closest area in this cell + FOR_EACH_VEC( (*areaVector), it ) + { + CNavArea *area = (*areaVector)[ it ]; + + // skip if we've already visited this area + if ( area->m_nearNavSearchMarker == searchMarker ) + continue; + + // mark as visited + area->m_nearNavSearchMarker = searchMarker; + area->GetExtent( &areaExtent ); + + if ( extent.IsOverlapping( areaExtent ) ) + { + if ( func( area ) == false ) + return false; + } + } + } + } + return true; + } + + //------------------------------------------------------------------------------------- + /** + * Populate the given vector with all navigation areas that overlap the given extent. + */ + template< typename NavAreaType > + void CollectAreasOverlappingExtent( const Extent &extent, CUtlVector< NavAreaType * > *outVector ) + { + if ( !m_grid.Count() ) + { + return; + } + + static unsigned int searchMarker = RandomInt( 0, 1024*1024 ); + if ( ++searchMarker == 0 ) + { + ++searchMarker; + } + + Extent areaExtent; + + // get list in cell that contains position + int startX = WorldToGridX( extent.lo.x ); + int endX = WorldToGridX( extent.hi.x ); + int startY = WorldToGridY( extent.lo.y ); + int endY = WorldToGridY( extent.hi.y ); + + for( int x = startX; x <= endX; ++x ) + { + for( int y = startY; y <= endY; ++y ) + { + int iGrid = x + y*m_gridSizeX; + if ( iGrid >= m_grid.Count() ) + { + ExecuteNTimes( 10, Warning( "** Walked off of the CNavMesh::m_grid in CollectAreasOverlappingExtent()\n" ) ); + return; + } + + NavAreaVector *areaVector = &m_grid[ iGrid ]; + + // find closest area in this cell + for( int v=0; vCount(); ++v ) + { + CNavArea *area = areaVector->Element( v ); + + // skip if we've already visited this area + if ( area->m_nearNavSearchMarker == searchMarker ) + continue; + + // mark as visited + area->m_nearNavSearchMarker = searchMarker; + area->GetExtent( &areaExtent ); + + if ( extent.IsOverlapping( areaExtent ) ) + { + outVector->AddToTail( (NavAreaType *)area ); + } + } + } + } + } + + + template < typename Functor > + bool ForAllAreasInRadius( Functor &func, const Vector &pos, float radius ) + { + // use a unique marker for this method, so it can be used within a SearchSurroundingArea() call + static unsigned int searchMarker = RandomInt(0, 1024*1024 ); + + ++searchMarker; + + if ( searchMarker == 0 ) + { + ++searchMarker; + } + + + // get list in cell that contains position + int originX = WorldToGridX( pos.x ); + int originY = WorldToGridY( pos.y ); + int shiftLimit = ceil( radius / m_gridCellSize ); + float radiusSq = radius * radius; + if ( radius == 0.0f ) + { + shiftLimit = MAX( m_gridSizeX, m_gridSizeY ); // range 0 means all areas + } + + for( int x = originX - shiftLimit; x <= originX + shiftLimit; ++x ) + { + if ( x < 0 || x >= m_gridSizeX ) + continue; + + for( int y = originY - shiftLimit; y <= originY + shiftLimit; ++y ) + { + if ( y < 0 || y >= m_gridSizeY ) + continue; + + NavAreaVector *areaVector = &m_grid[ x + y*m_gridSizeX ]; + + // find closest area in this cell + FOR_EACH_VEC( (*areaVector), it ) + { + CNavArea *area = (*areaVector)[ it ]; + + // skip if we've already visited this area + if ( area->m_nearNavSearchMarker == searchMarker ) + continue; + + // mark as visited + area->m_nearNavSearchMarker = searchMarker; + + float distSq = ( area->GetCenter() - pos ).LengthSqr(); + + if ( ( distSq <= radiusSq ) || ( radiusSq == 0 ) ) + { + if ( func( area ) == false ) + return false; + } + } + } + } + return true; + } + + //--------------------------------------------------------------------------------------------------------------- + /* + * Step through nav mesh along line between startArea and endArea. + * Return true if enumeration reached endArea, false if doesn't reach it (no mesh between, bad connection, etc) + */ + template < typename Functor > + bool ForAllAreasAlongLine( Functor &func, CNavArea *startArea, CNavArea *endArea ) + { + if ( !startArea || !endArea ) + return false; + + if ( startArea == endArea ) + { + func( startArea ); + return true; + } + + Vector start = startArea->GetCenter(); + Vector end = endArea->GetCenter(); + + Vector to = end - start; + float range = to.NormalizeInPlace(); + + const float epsilon = 0.00001f; + + if ( range < epsilon ) + { + func( startArea ); + return true; + } + + if ( abs( to.x ) < epsilon ) + { + NavDirType dir = ( to.y < 0.0f ) ? NORTH : SOUTH; + + CNavArea *area = startArea; + while( area ) + { + func( area ); + + if ( area == endArea ) + return true; + + const NavConnectVector *adjVector = area->GetAdjacentAreas( dir ); + + area = NULL; + + for( int i=0; iCount(); ++i ) + { + CNavArea *adjArea = adjVector->Element(i).area; + + const Vector &adjOrigin = adjArea->GetCorner( NORTH_WEST ); + + if ( adjOrigin.x <= start.x && adjOrigin.x + adjArea->GetSizeX() >= start.x ) + { + area = adjArea; + break; + } + } + } + + return false; + } + else if ( abs( to.y ) < epsilon ) + { + NavDirType dir = ( to.x < 0.0f ) ? WEST : EAST; + + CNavArea *area = startArea; + while( area ) + { + func( area ); + + if ( area == endArea ) + return true; + + const NavConnectVector *adjVector = area->GetAdjacentAreas( dir ); + + area = NULL; + + for( int i=0; iCount(); ++i ) + { + CNavArea *adjArea = adjVector->Element(i).area; + + const Vector &adjOrigin = adjArea->GetCorner( NORTH_WEST ); + + if ( adjOrigin.y <= start.y && adjOrigin.y + adjArea->GetSizeY() >= start.y ) + { + area = adjArea; + break; + } + } + } + + return false; + } + + + CNavArea *area = startArea; + + while( area ) + { + func( area ); + + if ( area == endArea ) + return true; + + const Vector &origin = area->GetCorner( NORTH_WEST ); + float xMin = origin.x; + float xMax = xMin + area->GetSizeX(); + float yMin = origin.y; + float yMax = yMin + area->GetSizeY(); + + // clip ray to area + Vector exit; + NavDirType edge = NUM_DIRECTIONS; + + if ( to.x < 0.0f ) + { + // find Y at west edge intersection + float t = ( xMin - start.x ) / ( end.x - start.x ); + if ( t > 0.0f && t < 1.0f ) + { + float y = start.y + t * ( end.y - start.y ); + if ( y >= yMin && y <= yMax ) + { + // intersects this edge + exit.x = xMin; + exit.y = y; + edge = WEST; + } + } + } + else + { + // find Y at east edge intersection + float t = ( xMax - start.x ) / ( end.x - start.x ); + if ( t > 0.0f && t < 1.0f ) + { + float y = start.y + t * ( end.y - start.y ); + if ( y >= yMin && y <= yMax ) + { + // intersects this edge + exit.x = xMax; + exit.y = y; + edge = EAST; + } + } + } + + if ( edge == NUM_DIRECTIONS ) + { + if ( to.y < 0.0f ) + { + // find X at north edge intersection + float t = ( yMin - start.y ) / ( end.y - start.y ); + if ( t > 0.0f && t < 1.0f ) + { + float x = start.x + t * ( end.x - start.x ); + if ( x >= xMin && x <= xMax ) + { + // intersects this edge + exit.x = x; + exit.y = yMin; + edge = NORTH; + } + } + } + else + { + // find X at south edge intersection + float t = ( yMax - start.y ) / ( end.y - start.y ); + if ( t > 0.0f && t < 1.0f ) + { + float x = start.x + t * ( end.x - start.x ); + if ( x >= xMin && x <= xMax ) + { + // intersects this edge + exit.x = x; + exit.y = yMax; + edge = SOUTH; + } + } + } + } + + if ( edge == NUM_DIRECTIONS ) + break; + + const NavConnectVector *adjVector = area->GetAdjacentAreas( edge ); + + area = NULL; + + for( int i=0; iCount(); ++i ) + { + CNavArea *adjArea = adjVector->Element(i).area; + + const Vector &adjOrigin = adjArea->GetCorner( NORTH_WEST ); + + if ( edge == NORTH || edge == SOUTH ) + { + if ( adjOrigin.x <= exit.x && adjOrigin.x + adjArea->GetSizeX() >= exit.x ) + { + area = adjArea; + break; + } + } + else + { + if ( adjOrigin.y <= exit.y && adjOrigin.y + adjArea->GetSizeY() >= exit.y ) + { + area = adjArea; + break; + } + } + } + } + + return false; + } + + + //------------------------------------------------------------------------------------- + /** + * Apply the functor to all navigation ladders. + * If functor returns false, stop processing and return false. + */ + template < typename Functor > + bool ForAllLadders( Functor &func ) + { + for ( int i=0; i + bool ForAllLadders( Functor &func ) const + { + for ( int i=0; i void StitchAreaIntoMesh( CNavArea *area, NavDirType dir, Functor &func ); + + //------------------------------------------------------------------------------------- + /** + * Use the functor to test if an area is needing stitching into the existing nav mesh. + * The functor is different from how we normally use functors - it does no processing, + * and it's return value is true if the area is in the new set to be stiched, and false + * if it's a pre-existing area. + */ + template < typename Functor > + bool StitchMesh( Functor &func ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( func( area ) ) + { + StitchAreaIntoMesh( area, NORTH, func ); + StitchAreaIntoMesh( area, SOUTH, func ); + StitchAreaIntoMesh( area, EAST, func ); + StitchAreaIntoMesh( area, WEST, func ); + } + } + + return true; + } + + NavLadderVector& GetLadders( void ) { return m_ladders; } // Returns the list of ladders + CNavLadder *GetLadderByID( unsigned int id ) const; + + CUtlVector< CNavArea * >& GetTransientAreas( void ) { return m_transientAreas; } + + enum EditModeType + { + NORMAL, // normal mesh editing + PLACE_PAINTING, // in place painting mode + CREATING_AREA, // creating a new nav area + CREATING_LADDER, // creating a nav ladder + DRAG_SELECTING, // drag selecting a set of areas + SHIFTING_XY, // shifting selected set in XY plane + SHIFTING_Z, // shifting selected set in Z plane + }; + EditModeType GetEditMode( void ) const; // return the current edit mode + void SetEditMode( EditModeType mode ); // change the edit mode + bool IsEditMode( EditModeType mode ) const; // return true if current mode matches given mode + + bool FindNavAreaOrLadderAlongRay( const Vector &start, const Vector &end, CNavArea **area, CNavLadder **ladder, CNavArea *ignore = NULL ); + + void PostProcessCliffAreas(); + void SimplifySelectedAreas( void ); // Simplifies the selected set by reducing to 1x1 areas and re-merging them up with loosened tolerances + +protected: + virtual void PostCustomAnalysis( void ) { } // invoked when custom analysis step is complete + bool FindActiveNavArea( void ); // Finds the area or ladder the local player is currently pointing at. Returns true if a surface was hit by the traceline. + virtual void RemoveNavArea( CNavArea *area ); // remove an area from the grid + bool FindGroundForNode( Vector *pos, Vector *normal ); + void GenerateNodes( const Extent &bounds ); + void RemoveNodes( void ); + +private: + friend class CNavArea; + friend class CNavNode; + friend class CNavUIBasePanel; + + mutable CUtlVector m_grid; + float m_gridCellSize; // the width/height of a grid cell for spatially partitioning nav areas for fast access + int m_gridSizeX; + int m_gridSizeY; + float m_minX; + float m_minY; + unsigned int m_areaCount; // total number of nav areas + + bool m_isLoaded; // true if a Navigation Mesh has been loaded + bool m_isOutOfDate; // true if the Navigation Mesh is older than the actual BSP + bool m_isAnalyzed; // true if the Navigation Mesh needs analysis + + enum { HASH_TABLE_SIZE = 256 }; + CNavArea *m_hashTable[ HASH_TABLE_SIZE ]; // hash table to optimize lookup by ID + int ComputeHashKey( unsigned int id ) const; // returns a hash key for the given nav area ID + + int WorldToGridX( float wx ) const; // given X component, return grid index + int WorldToGridY( float wy ) const; // given Y component, return grid index + void AllocateGrid( float minX, float maxX, float minY, float maxY ); // clear and reset the grid to the given extents + void GridToWorld( int gridX, int gridY, Vector *pos ) const; + + void AddNavArea( CNavArea *area ); // add an area to the grid + + void DestroyNavigationMesh( bool incremental = false ); // free all resources of the mesh and reset it to empty state + void DestroyHidingSpots( void ); + + void ComputeBattlefrontAreas( void ); // determine areas where rushing teams will first meet + + //---------------------------------------------------------------------------------- + // Place directory + // + char **m_placeName; // master directory of place names (ie: "places") + unsigned int m_placeCount; // number of "places" defined in placeName[] + void LoadPlaceDatabase( void ); // load the place names from a file + + //---------------------------------------------------------------------------------- + // Edit mode + // + EditModeType m_editMode; // the current edit mode + bool m_isEditing; // true if in edit mode + + unsigned int m_navPlace; // current navigation place for editing + void OnEditModeStart( void ); // called when edit mode has just been enabled + void DrawEditMode( void ); // draw navigation areas + void OnEditModeEnd( void ); // called when edit mode has just been disabled + void UpdateDragSelectionSet( void ); // update which areas are overlapping the drag selected bounds + Vector m_editCursorPos; // current position of the cursor + CNavArea *m_markedArea; // currently marked area for edit operations + CNavArea *m_selectedArea; // area that is selected this frame + CNavArea *m_lastSelectedArea; // area that was selected last frame + NavCornerType m_markedCorner; // currently marked corner for edit operations + Vector m_anchor; // first corner of an area being created + bool m_isPlacePainting; // if true, we set an area's place by pointing at it + bool m_splitAlongX; // direction the selected nav area would be split + float m_splitEdge; // location of the possible split + + bool m_climbableSurface; // if true, the cursor is pointing at a climable surface + Vector m_surfaceNormal; // Normal of the surface the cursor is pointing at + Vector m_ladderAnchor; // first corner of a ladder being created + Vector m_ladderNormal; // Normal of the surface of the ladder being created + CNavLadder *m_selectedLadder; // ladder that is selected this frame + CNavLadder *m_lastSelectedLadder; // ladder that was selected last frame + CNavLadder *m_markedLadder; // currently marked ladder for edit operations + + bool FindLadderCorners( Vector *c1, Vector *c2, Vector *c3 ); // computes the other corners of a ladder given m_ladderAnchor, m_editCursorPos, and m_ladderNormal + + void GetEditVectors( Vector *pos, Vector *forward ); // Gets the eye position and view direction of the editing player + + CountdownTimer m_showAreaInfoTimer; // Timer that controls how long area info is displayed + + NavAreaVector m_selectedSet; // all currently selected areas + NavAreaVector m_dragSelectionSet; // all areas in the current drag selection + bool m_isContinuouslySelecting; // if true, we are continuously adding to the selected set + bool m_isContinuouslyDeselecting; // if true, we are continuously removing from the selected set + + bool m_bIsDragDeselecting; + int m_nDragSelectionVolumeZMax; + int m_nDragSelectionVolumeZMin; + + void DoToggleAttribute( CNavArea *area, NavAttributeType attribute ); // toggle an attribute on given area + + + //---------------------------------------------------------------------------------- + // Auto-generation + // + bool UpdateGeneration( float maxTime = 0.25f ); // process the auto-generation for 'maxTime' seconds. return false if generation is complete. + + virtual void BeginCustomAnalysis( bool bIncremental ) {} + virtual void EndCustomAnalysis() {} + + CNavNode *m_currentNode; // the current node we are sampling from + NavDirType m_generationDir; + CNavNode *AddNode( const Vector &destPos, const Vector &destNormal, NavDirType dir, CNavNode *source, bool isOnDisplacement, float obstacleHeight, float flObstacleStartDist, float flObstacleEndDist ); // add a nav node and connect it, update current node + + NavLadderVector m_ladders; // list of ladder navigation representations + void BuildLadders( void ); + void DestroyLadders( void ); + + bool SampleStep( void ); // sample the walkable areas of the map + void CreateNavAreasFromNodes( void ); // cover all of the sampled nodes with nav areas + + bool TestArea( CNavNode *node, int width, int height ); // check if an area of size (width, height) can fit, starting from node as upper left corner + int BuildArea( CNavNode *node, int width, int height ); // create a CNavArea of size (width, height) starting fom node at upper left corner + bool CheckObstacles( CNavNode *node, int width, int height, int x, int y ); + + void MarkPlayerClipAreas( void ); + void MarkJumpAreas( void ); + void StichAndRemoveJumpAreas( void ); + void RemoveJumpAreas( void ); + void SquareUpAreas( void ); + void MergeGeneratedAreas( void ); + void ConnectGeneratedAreas( void ); + void FixUpGeneratedAreas( void ); + void FixCornerOnCornerAreas( void ); + void FixConnections( void ); + void SplitAreasUnderOverhangs( void ); + void ValidateNavAreaConnections( void ); + void StitchGeneratedAreas( void ); // Stitches incrementally-generated areas into the existing mesh + void StitchAreaSet( CUtlVector< CNavArea * > *areas ); // Stitches an arbitrary set of areas into the existing mesh + void HandleObstacleTopAreas( void ); // Handles fixing/generating areas on top of slim obstacles such as fences and railings + void RaiseAreasWithInternalObstacles(); + void CreateObstacleTopAreas(); + bool CreateObstacleTopAreaIfNecessary( CNavArea *area, CNavArea *areaOther, NavDirType dir, bool bMultiNode ); + void RemoveOverlappingObstacleTopAreas(); + + + enum GenerationStateType + { + SAMPLE_WALKABLE_SPACE, + CREATE_AREAS_FROM_SAMPLES, + FIND_HIDING_SPOTS, + FIND_ENCOUNTER_SPOTS, + FIND_SNIPER_SPOTS, + FIND_EARLIEST_OCCUPY_TIMES, + FIND_LIGHT_INTENSITY, + COMPUTE_MESH_VISIBILITY, + CUSTOM, // mod-specific generation step + SAVE_NAV_MESH, + + NUM_GENERATION_STATES + } + m_generationState; // the state of the generation process + enum GenerationModeType + { + GENERATE_NONE, + GENERATE_FULL, + GENERATE_INCREMENTAL, + GENERATE_SIMPLIFY, + GENERATE_ANALYSIS_ONLY, + } + m_generationMode; // true while a Navigation Mesh is being generated + int m_generationIndex; // used for iterating nav areas during generation process + int m_sampleTick; // counter for displaying pseudo-progress while sampling walkable space + bool m_bQuitWhenFinished; + float m_generationStartTime; + Extent m_simplifyGenerationExtent; + + char *m_spawnName; // name of player spawn entity, used to initiate sampling + + struct WalkableSeedSpot + { + Vector pos; + Vector normal; + }; + CUtlVector< WalkableSeedSpot > m_walkableSeeds; // list of walkable seed spots for sampling + + CNavNode *GetNextWalkableSeedNode( void ); // return the next walkable seed as a node + int m_seedIdx; + int m_hostThreadModeRestoreValue; // stores the value of host_threadmode before we changed it + + void BuildTransientAreaList( void ); + CUtlVector< CNavArea * > m_transientAreas; + + void UpdateAvoidanceObstacleAreas( void ); + CUtlVector< CNavArea * > m_avoidanceObstacleAreas; + CUtlVector< INavAvoidanceObstacle * > m_avoidanceObstacles; + + void UpdateBlockedAreas( void ); + CUtlVector< CNavArea * > m_blockedAreas; + + CUtlVector< int > m_storedSelectedSet; // "Stored" selected set, so we can do some editing and then restore the old selected set. Done by ID, so we don't have to worry about split/delete/etc. + + void BeginVisibilityComputations( void ); + void EndVisibilityComputations( void ); + + void TestAllAreasForBlockedStatus( void ); // Used to update blocked areas after a round restart. Need to delay so the map logic has all fired. + CountdownTimer m_updateBlockedAreasTimer; +}; + +// the global singleton interface +extern CNavMesh *TheNavMesh; + +// factory for creating the Navigation Mesh +extern CNavMesh *NavMeshFactory( void ); + +// for debugging the A* algorithm, if nonzero, show debug display and decrement for each pathfind +extern int g_DebugPathfindCounter; + + +//-------------------------------------------------------------------------------------------------------------- +inline bool CNavMesh::IsEditMode( EditModeType mode ) const +{ + return m_editMode == mode; +} + +//-------------------------------------------------------------------------------------------------------------- +inline CNavMesh::EditModeType CNavMesh::GetEditMode( void ) const +{ + return m_editMode; +} + +//-------------------------------------------------------------------------------------------------------------- +inline unsigned int CNavMesh::GetSubVersionNumber( void ) const +{ + return 0; +} + + +//-------------------------------------------------------------------------------------------------------------- +inline CNavArea *CNavMesh::CreateArea( void ) const +{ + return new CNavArea; +} + +//-------------------------------------------------------------------------------------------------------------- +inline void CNavMesh::DestroyArea( CNavArea *pArea ) const +{ + delete pArea; +} + +//-------------------------------------------------------------------------------------------------------------- +inline int CNavMesh::ComputeHashKey( unsigned int id ) const +{ + return id & 0xFF; +} + +//-------------------------------------------------------------------------------------------------------------- +inline int CNavMesh::WorldToGridX( float wx ) const +{ + int x = (int)( (wx - m_minX) / m_gridCellSize ); + + if (x < 0) + x = 0; + else if (x >= m_gridSizeX) + x = m_gridSizeX-1; + + return x; +} + +//-------------------------------------------------------------------------------------------------------------- +inline int CNavMesh::WorldToGridY( float wy ) const +{ + int y = (int)( (wy - m_minY) / m_gridCellSize ); + + if (y < 0) + y = 0; + else if (y >= m_gridSizeY) + y = m_gridSizeY-1; + + return y; +} + + +//-------------------------------------------------------------------------------------------------------------- +inline unsigned int CNavMesh::GetGenerationTraceMask( void ) const +{ + return MASK_NPCSOLID_BRUSHONLY; +} + + +//-------------------------------------------------------------------------------------------------------------- +// +// Function prototypes +// + +extern void ApproachAreaAnalysisPrep( void ); +extern void CleanupApproachAreaAnalysisPrep( void ); +extern bool IsHeightDifferenceValid( float test, float other1, float other2, float other3 ); + +#endif // _NAV_MESH_H_ diff --git a/sp/src/game/server/nav_mesh.vpc b/sp/src/game/server/nav_mesh.vpc new file mode 100644 index 00000000..856ce97e --- /dev/null +++ b/sp/src/game/server/nav_mesh.vpc @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +// NAV_MESH.VPC +// +// Project script for navigation mesh files (no NextBot files) +//----------------------------------------------------------------------------- + +$Configuration +{ + $Compiler + { + $PreprocessorDefinitions "$BASE;USE_NAV_MESH" + } +} + +$Project +{ + $Folder "Source Files" + { + $Folder "Navigation Mesh" + { + $File "nav.h" + $File "nav_area.cpp" + $File "nav_area.h" + $File "nav_colors.cpp" + $File "nav_colors.h" + $File "nav_edit.cpp" + $File "nav_entities.cpp" + $File "nav_entities.h" + $File "nav_file.cpp" + $File "nav_generate.cpp" + $File "nav_ladder.cpp" + $File "nav_ladder.h" + $File "nav_merge.cpp" + $File "nav_mesh.cpp" + $File "nav_mesh.h" + $File "nav_mesh_factory.cpp" + $File "nav_node.cpp" + $File "nav_node.h" + $File "nav_pathfind.h" + $File "nav_simplify.cpp" + } + } +} \ No newline at end of file diff --git a/sp/src/game/server/nav_mesh_factory.cpp b/sp/src/game/server/nav_mesh_factory.cpp new file mode 100644 index 00000000..70d9936b --- /dev/null +++ b/sp/src/game/server/nav_mesh_factory.cpp @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// NavMeshFactory.cpp +// Factory to create the NavMesh singleton +// Author: Michael S. Booth, June 2005 + +#include "cbase.h" +#include "nav_mesh.h" + +#ifdef TERROR +#include "terror/TerrorNav.h" +#endif + +#ifdef TF_DLL +#include "tf/nav_mesh/tf_nav_mesh.h" +#endif + +#ifdef CSTRIKE_DLL +#include "cstrike/cs_nav_mesh.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CNavMesh *NavMeshFactory( void ) +{ +#ifdef TERROR + return new TerrorNavMesh; +#endif + +#ifdef TF_DLL + return new CTFNavMesh; +#endif + +#ifdef CSTRIKE_DLL + return new CSNavMesh; +#endif + + return new CNavMesh; +} diff --git a/sp/src/game/server/nav_node.cpp b/sp/src/game/server/nav_node.cpp new file mode 100644 index 00000000..eae21071 --- /dev/null +++ b/sp/src/game/server/nav_node.cpp @@ -0,0 +1,488 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_node.cpp +// AI Navigation Nodes +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#include "cbase.h" +#include "nav_node.h" +#include "nav_colors.h" +#include "nav_mesh.h" +#include "tier1/utlhash.h" +#include "tier1/generichash.h" + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + + +NavDirType Opposite[ NUM_DIRECTIONS ] = { SOUTH, WEST, NORTH, EAST }; + +CNavNode *CNavNode::m_list = NULL; +unsigned int CNavNode::m_listLength = 0; +unsigned int CNavNode::m_nextID = 1; + +extern Vector NavTraceMins; +extern Vector NavTraceMaxs; + +//-------------------------------------------------------------------------------------------------------------- +// Node hash + +class CNodeHashFuncs +{ +public: + CNodeHashFuncs( int ) {} + + bool operator()( const CNavNode *pLhs, const CNavNode *pRhs ) const + { + return pRhs->GetPosition()->AsVector2D() == pLhs->GetPosition()->AsVector2D(); + } + + unsigned int operator()( const CNavNode *pItem ) const + { + return Hash8( &pItem->GetPosition()->AsVector2D() ); + } +}; + +CUtlHash *g_pNavNodeHash; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor + */ +CNavNode::CNavNode( const Vector &pos, const Vector &normal, CNavNode *parent, bool isOnDisplacement ) +{ + m_pos = pos; + m_normal = normal; + + m_id = m_nextID++; + + int i; + for( i=0; i( 16*1024 ); + } + + bool bDidInsert; + UtlHashHandle_t hHash = g_pNavNodeHash->Insert( this, &bDidInsert ); + if ( !bDidInsert ) + { + CNavNode *pExistingNode = g_pNavNodeHash->Element( hHash ); + m_nextAtXY = pExistingNode; + g_pNavNodeHash->Element( hHash ) = this; + } + else + { + m_nextAtXY = NULL; + } +} + +CNavNode::~CNavNode() +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +void CNavNode::CleanupGeneration() +{ + delete g_pNavNodeHash; + g_pNavNodeHash = NULL; + + CNavNode *node, *next; + for( node = CNavNode::m_list; node; node = next ) + { + next = node->m_next; + delete node; + } + CNavNode::m_list = NULL; + CNavNode::m_listLength = 0; + CNavNode::m_nextID = 1; +} + +//-------------------------------------------------------------------------------------------------------------- +#if DEBUG_NAV_NODES +ConVar nav_show_nodes( "nav_show_nodes", "0", FCVAR_CHEAT ); +ConVar nav_show_node_id( "nav_show_node_id", "0", FCVAR_CHEAT ); +ConVar nav_test_node( "nav_test_node", "0", FCVAR_CHEAT ); +ConVar nav_test_node_crouch( "nav_test_node_crouch", "0", FCVAR_CHEAT ); +ConVar nav_test_node_crouch_dir( "nav_test_node_crouch_dir", "4", FCVAR_CHEAT ); +ConVar nav_show_node_grid( "nav_show_node_grid", "0", FCVAR_CHEAT ); +#endif // DEBUG_NAV_NODES + + +//-------------------------------------------------------------------------------------------------------------- +void CNavNode::Draw( void ) +{ +#if DEBUG_NAV_NODES + + if ( !nav_show_nodes.GetBool() ) + return; + + int r = 0, g = 0, b = 0; + + if ( m_isCovered ) + { + if ( GetAttributes() & NAV_MESH_CROUCH ) + { + b = 255; + } + else + { + r = 255; + } + } + else + { + if ( GetAttributes() & NAV_MESH_CROUCH ) + { + b = 255; + } + g = 255; + } + + NDebugOverlay::Cross3D( m_pos, 2, r, g, b, true, 0.1f ); + + if ( (!m_isCovered && nav_show_node_id.GetBool()) || (m_isCovered && nav_show_node_id.GetInt() < 0) ) + { + char text[16]; + Q_snprintf( text, sizeof( text ), "%d", m_id ); + NDebugOverlay::Text( m_pos, text, true, 0.1f ); + } + + if ( (unsigned int)(nav_test_node.GetInt()) == m_id ) + { + TheNavMesh->TestArea( this, 1, 1 ); + nav_test_node.SetValue( 0 ); + } + + if ( (unsigned int)(nav_test_node_crouch.GetInt()) == m_id ) + { + CheckCrouch(); + nav_test_node_crouch.SetValue( 0 ); + } + + if ( GetAttributes() & NAV_MESH_CROUCH ) + { + int i; + for( i=0; iGetPosition(), 255, 255, 0, false, 0.1f ); + + float obstacleHeight = m_obstacleHeight[i]; + if ( obstacleHeight > 0 ) + { + float z = GetPosition()->z + obstacleHeight; + Vector from = *GetPosition(); + Vector to = from; + AddDirectionVector( &to, (NavDirType) i, m_obstacleStartDist[i] ); + NDebugOverlay::Line( from, to, 255, 0, 255, false, 0.1f ); + from = to; + to.z = z; + NDebugOverlay::Line( from, to, 255, 0, 255, false, 0.1f ); + from = to; + to = *GetPosition(); + to.z = z; + AddDirectionVector( &to, (NavDirType) i, m_obstacleEndDist[i] ); + NDebugOverlay::Line( from, to, 255, 0, 255, false, 0.1f ); + } + } + } + } + + +#endif // DEBUG_NAV_NODES +} + + +//-------------------------------------------------------------------------------------------------------- +// return ground height above node in given corner direction (NUM_CORNERS for highest in any direction) +float CNavNode::GetGroundHeightAboveNode( NavCornerType cornerType ) const +{ + if ( cornerType >= 0 && cornerType < NUM_CORNERS ) + return m_groundHeightAboveNode[ cornerType ]; + + float blockedHeight = 0.0f; + for ( int i=0; i 0 ) + { + maxs.x = HalfHumanWidth; + } + if ( cornerVec.y < 0 ) + { + mins.y = -HalfHumanWidth; + } + else if ( cornerVec.y > 0 ) + { + maxs.y = HalfHumanWidth; + } + maxs.z = HumanHeight; + + // now make sure that mins is smaller than maxs + for ( int j=0; j<3; ++j ) + { + if ( mins[j] > maxs[j] ) + { + float tmp = mins[j]; + mins[j] = maxs[j]; + maxs[j] = tmp; + } + } + + if ( !TestForCrouchArea( corner, mins, maxs, &m_groundHeightAboveNode[i] ) ) + { + SetAttributes( NAV_MESH_CROUCH ); + m_crouch[corner] = true; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Create a connection FROM this node TO the given node, in the given direction + */ +void CNavNode::ConnectTo( CNavNode *node, NavDirType dir, float obstacleHeight, float obstacleStartDist, float obstacleEndDist ) +{ + Assert( obstacleStartDist >= 0 && obstacleStartDist <= GenerationStepSize ); + Assert( obstacleEndDist >= 0 && obstacleStartDist <= GenerationStepSize ); + Assert( obstacleStartDist < obstacleEndDist ); + + m_to[ dir ] = node; + m_obstacleHeight[ dir ] = obstacleHeight; + m_obstacleStartDist[ dir ] = obstacleStartDist; + m_obstacleEndDist[ dir ] = obstacleEndDist; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return node at given position. + * @todo Need a hash table to make this lookup fast + */ +CNavNode *CNavNode::GetNode( const Vector &pos ) +{ + const float tolerance = 0.45f * GenerationStepSize; // 1.0f + CNavNode *pNode = NULL; + if ( g_pNavNodeHash ) + { + static CNavNode lookup; + lookup.m_pos = pos; + UtlHashHandle_t hNode = g_pNavNodeHash->Find( &lookup ); + + if ( hNode != g_pNavNodeHash->InvalidHandle() ) + { + for( pNode = g_pNavNodeHash->Element( hNode ); pNode; pNode = pNode->m_nextAtXY ) + { + float dz = fabs( pNode->m_pos.z - pos.z ); + + if (dz < tolerance) + { + break; + } + } + } + } + +#ifdef DEBUG_NODE_HASH + CNavNode *pTestNode = NULL; + for( CNavNode *node = m_list; node; node = node->m_next ) + { + float dx = fabs( node->m_pos.x - pos.x ); + float dy = fabs( node->m_pos.y - pos.y ); + float dz = fabs( node->m_pos.z - pos.z ); + + if (dx < tolerance && dy < tolerance && dz < tolerance) + { + pTestNode = node; + break; + } + } + AssertFatal( pTestNode == pNode ); +#endif + + return pNode; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this node is bidirectionally linked to + * another node in the given direction + */ +BOOL CNavNode::IsBiLinked( NavDirType dir ) const +{ + if (m_to[ dir ] && m_to[ dir ]->m_to[ Opposite[dir] ] == this) + { + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this node is the NW corner of a quad of nodes + * that are all bidirectionally linked. + */ +BOOL CNavNode::IsClosedCell( void ) const +{ + if (IsBiLinked( SOUTH ) && + IsBiLinked( EAST ) && + m_to[ EAST ]->IsBiLinked( SOUTH ) && + m_to[ SOUTH ]->IsBiLinked( EAST ) && + m_to[ EAST ]->m_to[ SOUTH ] == m_to[ SOUTH ]->m_to[ EAST ]) + { + return true; + } + + return false; +} + diff --git a/sp/src/game/server/nav_node.h b/sp/src/game/server/nav_node.h new file mode 100644 index 00000000..9bbafe70 --- /dev/null +++ b/sp/src/game/server/nav_node.h @@ -0,0 +1,151 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_node.h +// Navigation Nodes are used when generating a Navigation Mesh by point sampling the map +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#ifndef _NAV_NODE_H_ +#define _NAV_NODE_H_ + +#include "nav.h" + +// If DEBUG_NAV_NODES is true, nav_show_nodes controls drawing node positions, and +// nav_show_node_id allows you to show the IDs of nodes that didn't get used to create areas. +#define DEBUG_NAV_NODES 1 + +//-------------------------------------------------------------------------------------------------------------- +/** + * Navigation Nodes. + * These Nodes encapsulate world locations, and ways to get from one location to an adjacent one. + * Note that these links are not necessarily commutative (falling off of a ledge, for example). + */ +class CNavNode +{ +public: + CNavNode( const Vector &pos, const Vector &normal, CNavNode *parent, bool onDisplacement ); + ~CNavNode(); + + static CNavNode *GetNode( const Vector &pos ); ///< return navigation node at the position, or NULL if none exists + static void CleanupGeneration(); + + CNavNode *GetConnectedNode( NavDirType dir ) const; ///< get navigation node connected in given direction, or NULL if cant go that way + const Vector *GetPosition( void ) const; + const Vector *GetNormal( void ) const { return &m_normal; } + unsigned int GetID( void ) const { return m_id; } + + static CNavNode *GetFirst( void ) { return m_list; } + static unsigned int GetListLength( void ) { return m_listLength; } + CNavNode *GetNext( void ) { return m_next; } + + void Draw( void ); + + void ConnectTo( CNavNode *node, NavDirType dir, float obstacleHeight, float flObstacleStartDist, float flObstacleEndDist ); ///< create a connection FROM this node TO the given node, in the given direction + CNavNode *GetParent( void ) const; + + void MarkAsVisited( NavDirType dir ); ///< mark the given direction as having been visited + BOOL HasVisited( NavDirType dir ); ///< return TRUE if the given direction has already been searched + BOOL IsBiLinked( NavDirType dir ) const; ///< node is bidirectionally linked to another node in the given direction + BOOL IsClosedCell( void ) const; ///< node is the NW corner of a bi-linked quad of nodes + + void Cover( void ) { m_isCovered = true; } ///< @todo Should pass in area that is covering + BOOL IsCovered( void ) const { return m_isCovered; } ///< return true if this node has been covered by an area + + void AssignArea( CNavArea *area ); ///< assign the given area to this node + CNavArea *GetArea( void ) const; ///< return associated area + + void SetAttributes( int bits ) { m_attributeFlags = bits; } + int GetAttributes( void ) const { return m_attributeFlags; } + float GetGroundHeightAboveNode( NavCornerType cornerType ) const; ///< return ground height above node in given corner direction (NUM_CORNERS for highest in any direction) + bool IsBlockedInAnyDirection( void) const; ///< return true if the node is blocked in any direction + + bool IsOnDisplacement( void ) const { return m_isOnDisplacement; } + +private: + CNavNode() {} // constructor used only for hash lookup + friend class CNavMesh; + + bool TestForCrouchArea( NavCornerType cornerNum, const Vector& mins, const Vector& maxs, float *groundHeightAboveNode ); + void CheckCrouch( void ); + + Vector m_pos; ///< position of this node in the world + Vector m_normal; ///< surface normal at this location + CNavNode *m_to[ NUM_DIRECTIONS ]; ///< links to north, south, east, and west. NULL if no link + float m_obstacleHeight[ NUM_DIRECTIONS ]; ///< obstacle height (delta from nav node z position) that must be climbed to reach next node in this direction + float m_obstacleStartDist[ NUM_DIRECTIONS ]; ///< distance along this direction to reach the beginning of the obstacle + float m_obstacleEndDist[ NUM_DIRECTIONS ]; ///< distance along this direction to reach the end of the obstacle + unsigned int m_id; ///< unique ID of this node + int m_attributeFlags; ///< set of attribute bit flags (see NavAttributeType) + + static CNavNode *m_list; ///< the master list of all nodes for this map + static unsigned int m_listLength; + static unsigned int m_nextID; + CNavNode *m_next; ///< next link in master list + CNavNode *m_nextAtXY; ///< next link at a particular position + + // below are only needed when generating + unsigned char m_visited; ///< flags for automatic node generation. If direction bit is clear, that direction hasn't been explored yet. + CNavNode *m_parent; ///< the node prior to this in the search, which we pop back to when this node's search is done (a stack) + bool m_isCovered; ///< true when this node is "covered" by a CNavArea + CNavArea *m_area; ///< the area this node is contained within + + bool m_isBlocked[ NUM_CORNERS ]; + bool m_crouch[ NUM_CORNERS ]; + float m_groundHeightAboveNode[ NUM_CORNERS ]; + bool m_isOnDisplacement; +}; + +//-------------------------------------------------------------------------------------------------------------- +// +// Inlines +// + +inline CNavNode *CNavNode::GetConnectedNode( NavDirType dir ) const +{ + return m_to[ dir ]; +} + +inline const Vector *CNavNode::GetPosition( void ) const +{ + return &m_pos; +} + +inline CNavNode *CNavNode::GetParent( void ) const +{ + return m_parent; +} + +inline void CNavNode::MarkAsVisited( NavDirType dir ) +{ + m_visited |= (1 << dir); +} + +inline BOOL CNavNode::HasVisited( NavDirType dir ) +{ + if (m_visited & (1 << dir)) + return true; + + return false; +} + +inline void CNavNode::AssignArea( CNavArea *area ) +{ + m_area = area; +} + +inline CNavArea *CNavNode::GetArea( void ) const +{ + return m_area; +} + +inline bool CNavNode::IsBlockedInAnyDirection( void ) const +{ + return m_isBlocked[ SOUTH_EAST ] || m_isBlocked[ SOUTH_WEST ] || m_isBlocked[ NORTH_EAST ] || m_isBlocked[ NORTH_WEST ]; +} + + +#endif // _NAV_NODE_H_ diff --git a/sp/src/game/server/nav_pathfind.h b/sp/src/game/server/nav_pathfind.h new file mode 100644 index 00000000..b06b46a0 --- /dev/null +++ b/sp/src/game/server/nav_pathfind.h @@ -0,0 +1,986 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_pathfind.h +// Path-finding mechanisms using the Navigation Mesh +// Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 + +#ifndef _NAV_PATHFIND_H_ +#define _NAV_PATHFIND_H_ + +#include "tier0/vprof.h" +#include "mathlib/ssemath.h" +#include "nav_area.h" + +extern int g_DebugPathfindCounter; + + +//------------------------------------------------------------------------------------------------------------------- +/** + * Used when building a path to determine the kind of path to build + */ +enum RouteType +{ + DEFAULT_ROUTE, + FASTEST_ROUTE, + SAFEST_ROUTE, + RETREAT_ROUTE, +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor used with NavAreaBuildPath() + */ +class ShortestPathCost +{ +public: + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) + { + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + // if this is a "crouch" area, add penalty + if ( area->GetAttributes() & NAV_MESH_CROUCH ) + { + const float crouchPenalty = 20.0f; // 10 + cost += crouchPenalty * dist; + } + + // if this is a "jump" area, add penalty + if ( area->GetAttributes() & NAV_MESH_JUMP ) + { + const float jumpPenalty = 5.0f; + cost += jumpPenalty * dist; + } + + return cost; + } + } +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find path from startArea to goalArea via an A* search, using supplied cost heuristic. + * If cost functor returns -1 for an area, that area is considered a dead end. + * This doesn't actually build a path, but the path is defined by following parent + * pointers back from goalArea to startArea. + * If 'closestArea' is non-NULL, the closest area to the goal is returned (useful if the path fails). + * If 'goalArea' is NULL, will compute a path as close as possible to 'goalPos'. + * If 'goalPos' is NULL, will use the center of 'goalArea' as the goal position. + * If 'maxPathLength' is nonzero, path building will stop when this length is reached. + * Returns true if a path exists. + */ +#define IGNORE_NAV_BLOCKERS true +template< typename CostFunctor > +bool NavAreaBuildPath( CNavArea *startArea, CNavArea *goalArea, const Vector *goalPos, CostFunctor &costFunc, CNavArea **closestArea = NULL, float maxPathLength = 0.0f, int teamID = TEAM_ANY, bool ignoreNavBlockers = false ) +{ + VPROF_BUDGET( "NavAreaBuildPath", "NextBotSpiky" ); + + if ( closestArea ) + { + *closestArea = startArea; + } + + bool isDebug = ( g_DebugPathfindCounter-- > 0 ); + + if (startArea == NULL) + return false; + + startArea->SetParent( NULL ); + + if (goalArea != NULL && goalArea->IsBlocked( teamID, ignoreNavBlockers )) + goalArea = NULL; + + if (goalArea == NULL && goalPos == NULL) + return false; + + // if we are already in the goal area, build trivial path + if (startArea == goalArea) + { + return true; + } + + // determine actual goal position + Vector actualGoalPos = (goalPos) ? *goalPos : goalArea->GetCenter(); + + // start search + CNavArea::ClearSearchLists(); + + // compute estimate of path length + /// @todo Cost might work as "manhattan distance" + startArea->SetTotalCost( (startArea->GetCenter() - actualGoalPos).Length() ); + + float initCost = costFunc( startArea, NULL, NULL, NULL, -1.0f ); + if (initCost < 0.0f) + return false; + startArea->SetCostSoFar( initCost ); + startArea->SetPathLengthSoFar( 0.0 ); + + startArea->AddToOpenList(); + + // keep track of the area we visit that is closest to the goal + float closestAreaDist = startArea->GetTotalCost(); + + // do A* search + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CNavArea *area = CNavArea::PopOpenList(); + + if ( isDebug ) + { + area->DrawFilled( 0, 255, 0, 128, 30.0f ); + } + + // don't consider blocked areas + if ( area->IsBlocked( teamID, ignoreNavBlockers ) ) + continue; + + // check if we have found the goal area or position + if (area == goalArea || (goalArea == NULL && goalPos && area->Contains( *goalPos ))) + { + if (closestArea) + { + *closestArea = area; + } + + return true; + } + + // search adjacent areas + enum SearchType + { + SEARCH_FLOOR, SEARCH_LADDERS, SEARCH_ELEVATORS + }; + SearchType searchWhere = SEARCH_FLOOR; + int searchIndex = 0; + + int dir = NORTH; + const NavConnectVector *floorList = area->GetAdjacentAreas( NORTH ); + + bool ladderUp = true; + const NavLadderConnectVector *ladderList = NULL; + enum { AHEAD = 0, LEFT, RIGHT, BEHIND, NUM_TOP_DIRECTIONS }; + int ladderTopDir = AHEAD; + bool bHaveMaxPathLength = ( maxPathLength > 0.0f ); + float length = -1; + + while( true ) + { + CNavArea *newArea = NULL; + NavTraverseType how; + const CNavLadder *ladder = NULL; + const CFuncElevator *elevator = NULL; + + // + // Get next adjacent area - either on floor or via ladder + // + if ( searchWhere == SEARCH_FLOOR ) + { + // if exhausted adjacent connections in current direction, begin checking next direction + if ( searchIndex >= floorList->Count() ) + { + ++dir; + + if ( dir == NUM_DIRECTIONS ) + { + // checked all directions on floor - check ladders next + searchWhere = SEARCH_LADDERS; + + ladderList = area->GetLadders( CNavLadder::LADDER_UP ); + searchIndex = 0; + ladderTopDir = AHEAD; + } + else + { + // start next direction + floorList = area->GetAdjacentAreas( (NavDirType)dir ); + searchIndex = 0; + } + + continue; + } + + const NavConnect &floorConnect = floorList->Element( searchIndex ); + newArea = floorConnect.area; + length = floorConnect.length; + how = (NavTraverseType)dir; + ++searchIndex; + + if ( IsX360() && searchIndex < floorList->Count() ) + { + PREFETCH360( floorList->Element( searchIndex ).area, 0 ); + } + } + else if ( searchWhere == SEARCH_LADDERS ) + { + if ( searchIndex >= ladderList->Count() ) + { + if ( !ladderUp ) + { + // checked both ladder directions - check elevators next + searchWhere = SEARCH_ELEVATORS; + searchIndex = 0; + ladder = NULL; + } + else + { + // check down ladders + ladderUp = false; + ladderList = area->GetLadders( CNavLadder::LADDER_DOWN ); + searchIndex = 0; + } + continue; + } + + if ( ladderUp ) + { + ladder = ladderList->Element( searchIndex ).ladder; + + // do not use BEHIND connection, as its very hard to get to when going up a ladder + if ( ladderTopDir == AHEAD ) + { + newArea = ladder->m_topForwardArea; + } + else if ( ladderTopDir == LEFT ) + { + newArea = ladder->m_topLeftArea; + } + else if ( ladderTopDir == RIGHT ) + { + newArea = ladder->m_topRightArea; + } + else + { + ++searchIndex; + ladderTopDir = AHEAD; + continue; + } + + how = GO_LADDER_UP; + ++ladderTopDir; + } + else + { + newArea = ladderList->Element( searchIndex ).ladder->m_bottomArea; + how = GO_LADDER_DOWN; + ladder = ladderList->Element(searchIndex).ladder; + ++searchIndex; + } + + if ( newArea == NULL ) + continue; + + length = -1.0f; + } + else // if ( searchWhere == SEARCH_ELEVATORS ) + { + const NavConnectVector &elevatorAreas = area->GetElevatorAreas(); + + elevator = area->GetElevator(); + + if ( elevator == NULL || searchIndex >= elevatorAreas.Count() ) + { + // done searching connected areas + elevator = NULL; + break; + } + + newArea = elevatorAreas[ searchIndex++ ].area; + if ( newArea->GetCenter().z > area->GetCenter().z ) + { + how = GO_ELEVATOR_UP; + } + else + { + how = GO_ELEVATOR_DOWN; + } + + length = -1.0f; + } + + + // don't backtrack + Assert( newArea ); + if ( newArea == area->GetParent() ) + continue; + if ( newArea == area ) // self neighbor? + continue; + + // don't consider blocked areas + if ( newArea->IsBlocked( teamID, ignoreNavBlockers ) ) + continue; + + float newCostSoFar = costFunc( newArea, area, ladder, elevator, length ); + + // check if cost functor says this area is a dead-end + if ( newCostSoFar < 0.0f ) + continue; + + // Safety check against a bogus functor. The cost of the path + // A...B, C should always be at least as big as the path A...B. + Assert( newCostSoFar >= area->GetCostSoFar() ); + + // And now that we've asserted, let's be a bit more defensive. + // Make sure that any jump to a new area incurs some pathfinsing + // cost, to avoid us spinning our wheels over insignificant cost + // benefit, floating point precision bug, or busted cost functor. + float minNewCostSoFar = area->GetCostSoFar() * 1.00001 + 0.00001; + newCostSoFar = Max( newCostSoFar, minNewCostSoFar ); + + // stop if path length limit reached + if ( bHaveMaxPathLength ) + { + // keep track of path length so far + float deltaLength = ( newArea->GetCenter() - area->GetCenter() ).Length(); + float newLengthSoFar = area->GetPathLengthSoFar() + deltaLength; + if ( newLengthSoFar > maxPathLength ) + continue; + + newArea->SetPathLengthSoFar( newLengthSoFar ); + } + + if ( ( newArea->IsOpen() || newArea->IsClosed() ) && newArea->GetCostSoFar() <= newCostSoFar ) + { + // this is a worse path - skip it + continue; + } + else + { + // compute estimate of distance left to go + float distSq = ( newArea->GetCenter() - actualGoalPos ).LengthSqr(); + float newCostRemaining = ( distSq > 0.0 ) ? FastSqrt( distSq ) : 0.0 ; + + // track closest area to goal in case path fails + if ( closestArea && newCostRemaining < closestAreaDist ) + { + *closestArea = newArea; + closestAreaDist = newCostRemaining; + } + + newArea->SetCostSoFar( newCostSoFar ); + newArea->SetTotalCost( newCostSoFar + newCostRemaining ); + + if ( newArea->IsClosed() ) + { + newArea->RemoveFromClosedList(); + } + + if ( newArea->IsOpen() ) + { + // area already on open list, update the list order to keep costs sorted + newArea->UpdateOnOpenList(); + } + else + { + newArea->AddToOpenList(); + } + + newArea->SetParent( area, how ); + } + } + + // we have searched this area + area->AddToClosedList(); + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute distance between two areas. Return -1 if can't reach 'endArea' from 'startArea'. + */ +template< typename CostFunctor > +float NavAreaTravelDistance( CNavArea *startArea, CNavArea *endArea, CostFunctor &costFunc, float maxPathLength = 0.0f ) +{ + if (startArea == NULL) + return -1.0f; + + if (endArea == NULL) + return -1.0f; + + if (startArea == endArea) + return 0.0f; + + // compute path between areas using given cost heuristic + if (NavAreaBuildPath( startArea, endArea, NULL, costFunc, NULL, maxPathLength ) == false) + return -1.0f; + + // compute distance along path + float distance = 0.0f; + for( CNavArea *area = endArea; area->GetParent(); area = area->GetParent() ) + { + distance += (area->GetCenter() - area->GetParent()->GetCenter()).Length(); + } + + return distance; +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do a breadth-first search, invoking functor on each area. + * If functor returns 'true', continue searching from this area. + * If functor returns 'false', the area's adjacent areas are not explored (dead end). + * If 'maxRange' is 0 or less, no range check is done (all areas will be examined). + * + * NOTE: Returns all areas that overlap range, even partially + * + * @todo Use ladder connections + */ + +// helper function +inline void AddAreaToOpenList( CNavArea *area, CNavArea *parent, const Vector &startPos, float maxRange ) +{ + if (area == NULL) + return; + + if (!area->IsMarked()) + { + area->Mark(); + area->SetTotalCost( 0.0f ); + area->SetParent( parent ); + + if (maxRange > 0.0f) + { + // make sure this area overlaps range + Vector closePos; + area->GetClosestPointOnArea( startPos, &closePos ); + if ((closePos - startPos).AsVector2D().IsLengthLessThan( maxRange )) + { + // compute approximate distance along path to limit travel range, too + float distAlong = parent->GetCostSoFar(); + distAlong += (area->GetCenter() - parent->GetCenter()).Length(); + area->SetCostSoFar( distAlong ); + + // allow for some fudge due to large size areas + if (distAlong <= 1.5f * maxRange) + area->AddToOpenList(); + } + } + else + { + // infinite range + area->AddToOpenList(); + } + } +} + + +/**************************************************************** + * DEPRECATED: Use filter-based SearchSurroundingAreas below + ****************************************************************/ +#define INCLUDE_INCOMING_CONNECTIONS 0x1 +#define INCLUDE_BLOCKED_AREAS 0x2 +#define EXCLUDE_OUTGOING_CONNECTIONS 0x4 +#define EXCLUDE_ELEVATORS 0x8 +template < typename Functor > +void SearchSurroundingAreas( CNavArea *startArea, const Vector &startPos, Functor &func, float maxRange = -1.0f, unsigned int options = 0, int teamID = TEAM_ANY ) +{ + if (startArea == NULL) + return; + + CNavArea::MakeNewMarker(); + CNavArea::ClearSearchLists(); + + startArea->AddToOpenList(); + startArea->SetTotalCost( 0.0f ); + startArea->SetCostSoFar( 0.0f ); + startArea->SetParent( NULL ); + startArea->Mark(); + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CNavArea *area = CNavArea::PopOpenList(); + + // don't use blocked areas + if ( area->IsBlocked( teamID ) && !(options & INCLUDE_BLOCKED_AREAS) ) + continue; + + // invoke functor on area + if (func( area )) + { + // explore adjacent floor areas + for( int dir=0; dirGetAdjacentCount( (NavDirType)dir ); + for( int i=0; iGetAdjacentArea( (NavDirType)dir, i ); + if ( options & EXCLUDE_OUTGOING_CONNECTIONS ) + { + if ( !adjArea->IsConnected( area, NUM_DIRECTIONS ) ) + { + continue; // skip this outgoing connection + } + } + + AddAreaToOpenList( adjArea, area, startPos, maxRange ); + } + } + + // potentially include areas that connect TO this area via a one-way link + if (options & INCLUDE_INCOMING_CONNECTIONS) + { + for( int dir=0; dirGetIncomingConnections( (NavDirType)dir ); + + FOR_EACH_VEC( (*list), it ) + { + NavConnect connect = (*list)[ it ]; + + AddAreaToOpenList( connect.area, area, startPos, maxRange ); + } + } + } + + + // explore adjacent areas connected by ladders + + // check up ladders + const NavLadderConnectVector *ladderList = area->GetLadders( CNavLadder::LADDER_UP ); + if (ladderList) + { + FOR_EACH_VEC( (*ladderList), it ) + { + const CNavLadder *ladder = (*ladderList)[ it ].ladder; + + // do not use BEHIND connection, as its very hard to get to when going up a ladder + AddAreaToOpenList( ladder->m_topForwardArea, area, startPos, maxRange ); + AddAreaToOpenList( ladder->m_topLeftArea, area, startPos, maxRange ); + AddAreaToOpenList( ladder->m_topRightArea, area, startPos, maxRange ); + } + } + + // check down ladders + ladderList = area->GetLadders( CNavLadder::LADDER_DOWN ); + if (ladderList) + { + FOR_EACH_VEC( (*ladderList), it ) + { + const CNavLadder *ladder = (*ladderList)[ it ].ladder; + + AddAreaToOpenList( ladder->m_bottomArea, area, startPos, maxRange ); + } + } + + if ( (options & EXCLUDE_ELEVATORS) == 0 ) + { + const NavConnectVector &elevatorList = area->GetElevatorAreas(); + FOR_EACH_VEC( elevatorList, it ) + { + CNavArea *elevatorArea = elevatorList[ it ].area; + AddAreaToOpenList( elevatorArea, area, startPos, maxRange ); + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Derive your own custom search functor from this interface method for use with SearchSurroundingAreas below. + */ +class ISearchSurroundingAreasFunctor +{ +public: + virtual ~ISearchSurroundingAreasFunctor() { } + + /** + * Perform user-defined action on area. + * Return 'false' to end the search (ie: you found what you were looking for) + */ + virtual bool operator() ( CNavArea *area, CNavArea *priorArea, float travelDistanceSoFar ) = 0; + + // return true if 'adjArea' should be included in the ongoing search + virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) + { + return !adjArea->IsBlocked( TEAM_ANY ); + } + + /** + * Collect adjacent areas to continue the search by calling 'IncludeInSearch' on each + */ + virtual void IterateAdjacentAreas( CNavArea *area, CNavArea *priorArea, float travelDistanceSoFar ) + { + // search adjacent outgoing connections + for( int dir=0; dirGetAdjacentCount( (NavDirType)dir ); + for( int i=0; iGetAdjacentArea( (NavDirType)dir, i ); + + if ( ShouldSearch( adjArea, area, travelDistanceSoFar ) ) + { + IncludeInSearch( adjArea, area ); + } + } + } + } + + // Invoked after the search has completed + virtual void PostSearch( void ) { } + + // consider 'area' in upcoming search steps + void IncludeInSearch( CNavArea *area, CNavArea *priorArea ) + { + if ( area == NULL ) + return; + + if ( !area->IsMarked() ) + { + area->Mark(); + area->SetTotalCost( 0.0f ); + area->SetParent( priorArea ); + + // compute approximate travel distance from start area of search + if ( priorArea ) + { + float distAlong = priorArea->GetCostSoFar(); + distAlong += ( area->GetCenter() - priorArea->GetCenter() ).Length(); + area->SetCostSoFar( distAlong ); + } + else + { + area->SetCostSoFar( 0.0f ); + } + + // adding an area to the open list also marks it + area->AddToOpenList(); + } + } +}; + + +/** + * Do a breadth-first search starting from 'startArea' and continuing outward based on + * adjacent areas that pass the given filter + */ +inline void SearchSurroundingAreas( CNavArea *startArea, ISearchSurroundingAreasFunctor &func, float travelDistanceLimit = -1.0f ) +{ + if ( startArea ) + { + CNavArea::MakeNewMarker(); + CNavArea::ClearSearchLists(); + + startArea->AddToOpenList(); + startArea->SetTotalCost( 0.0f ); + startArea->SetCostSoFar( 0.0f ); + startArea->SetParent( NULL ); + startArea->Mark(); + + CUtlVector< CNavArea * > adjVector; + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CNavArea *area = CNavArea::PopOpenList(); + + if ( travelDistanceLimit > 0.0f && area->GetCostSoFar() > travelDistanceLimit ) + continue; + + if ( func( area, area->GetParent(), area->GetCostSoFar() ) ) + { + func.IterateAdjacentAreas( area, area->GetParent(), area->GetCostSoFar() ); + } + else + { + // search aborted + break; + } + } + } + + func.PostSearch(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Starting from 'startArea', collect adjacent areas via a breadth-first search continuing outward until + * 'travelDistanceLimit' is reached. + * Areas in the collection will be "marked", returning true for IsMarked(). + * Each area in the collection's GetCostSoFar() will be approximate travel distance from 'startArea'. + */ +inline void CollectSurroundingAreas( CUtlVector< CNavArea * > *nearbyAreaVector, CNavArea *startArea, float travelDistanceLimit = 1500.0f, float maxStepUpLimit = StepHeight, float maxDropDownLimit = 100.0f ) +{ + nearbyAreaVector->RemoveAll(); + + if ( startArea ) + { + CNavArea::MakeNewMarker(); + CNavArea::ClearSearchLists(); + + startArea->AddToOpenList(); + startArea->SetTotalCost( 0.0f ); + startArea->SetCostSoFar( 0.0f ); + startArea->SetParent( NULL ); + startArea->Mark(); + + CUtlVector< CNavArea * > adjVector; + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CNavArea *area = CNavArea::PopOpenList(); + + if ( travelDistanceLimit > 0.0f && area->GetCostSoFar() > travelDistanceLimit ) + continue; + + if ( area->GetParent() ) + { + float deltaZ = area->GetParent()->ComputeAdjacentConnectionHeightChange( area ); + + if ( deltaZ > maxStepUpLimit ) + continue; + + if ( deltaZ < -maxDropDownLimit ) + continue; + } + + nearbyAreaVector->AddToTail( area ); + + // mark here to ensure all marked areas are also valid areas that are in the collection + area->Mark(); + + // search adjacent outgoing connections + for( int dir=0; dirGetAdjacentCount( (NavDirType)dir ); + for( int i=0; iGetAdjacentArea( (NavDirType)dir, i ); + + if ( adjArea->IsBlocked( TEAM_ANY ) ) + { + continue; + } + + if ( !adjArea->IsMarked() ) + { + adjArea->SetTotalCost( 0.0f ); + adjArea->SetParent( area ); + + // compute approximate travel distance from start area of search + float distAlong = area->GetCostSoFar(); + distAlong += ( adjArea->GetCenter() - area->GetCenter() ).Length(); + adjArea->SetCostSoFar( distAlong ); + adjArea->AddToOpenList(); + } + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor that returns lowest cost for farthest away areas + * For use with FindMinimumCostArea() + */ +class FarAwayFunctor +{ +public: + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder ) + { + if (area == fromArea) + return 9999999.9f; + + return 1.0f/(fromArea->GetCenter() - area->GetCenter()).Length(); + } +}; + +/** + * Functor that returns lowest cost for areas farthest from given position + * For use with FindMinimumCostArea() + */ +class FarAwayFromPositionFunctor +{ +public: + FarAwayFromPositionFunctor( const Vector &pos ) : m_pos( pos ) + { + } + + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder ) + { + return 1.0f/(m_pos - area->GetCenter()).Length(); + } + +private: + const Vector &m_pos; +}; + + +/** + * Pick a low-cost area of "decent" size + */ +template< typename CostFunctor > +CNavArea *FindMinimumCostArea( CNavArea *startArea, CostFunctor &costFunc ) +{ + const float minSize = 150.0f; + + // collect N low-cost areas of a decent size + enum { NUM_CHEAP_AREAS = 32 }; + struct + { + CNavArea *area; + float cost; + } + cheapAreaSet[ NUM_CHEAP_AREAS ] = {}; + int cheapAreaSetCount = 0; + + FOR_EACH_VEC( TheNavAreas, iter ) + { + CNavArea *area = TheNavAreas[iter]; + + // skip the small areas + if ( area->GetSizeX() < minSize || area->GetSizeY() < minSize) + continue; + + // compute cost of this area + + // HPE_FIX[pfreese]: changed this to only pass three parameters, in accord with the two functors above + float cost = costFunc( area, startArea, NULL ); + + if (cheapAreaSetCount < NUM_CHEAP_AREAS) + { + cheapAreaSet[ cheapAreaSetCount ].area = area; + cheapAreaSet[ cheapAreaSetCount++ ].cost = cost; + } + else + { + // replace most expensive cost if this is cheaper + int expensive = 0; + for( int i=1; i cheapAreaSet[expensive].cost) + expensive = i; + + if (cheapAreaSet[expensive].cost > cost) + { + cheapAreaSet[expensive].area = area; + cheapAreaSet[expensive].cost = cost; + } + } + } + + if (cheapAreaSetCount) + { + // pick one of the areas at random + return cheapAreaSet[ RandomInt( 0, cheapAreaSetCount-1 ) ].area; + } + else + { + // degenerate case - no decent sized areas - pick a random area + int numAreas = TheNavAreas.Count(); + int which = RandomInt( 0, numAreas-1 ); + + FOR_EACH_VEC( TheNavAreas, iter ) + { + if (which-- == 0) + return TheNavAreas[iter]; + } + + } + return cheapAreaSet[ RandomInt( 0, cheapAreaSetCount-1 ) ].area; +} + + +//-------------------------------------------------------------------------------------------------------- +// +// Given a vector of CNavAreas (or derived types), 'inVector', populate 'outVector' with a randomly shuffled set +// of 'maxCount' areas that are at least 'minSeparation' travel distance apart from each other. +// +template< typename T > +void SelectSeparatedShuffleSet( int maxCount, float minSeparation, const CUtlVector< T * > &inVector, CUtlVector< T * > *outVector ) +{ + if ( !outVector ) + return; + + outVector->RemoveAll(); + + CUtlVector< T * > shuffledVector; + + int i, j; + + for( i=0; i 1 ) + { + int k = RandomInt( 0, n-1 ); + n--; + + T *tmp = shuffledVector[n]; + shuffledVector[n] = shuffledVector[k]; + shuffledVector[k] = tmp; + } + + // enforce minSeparation between shuffled areas + for( i=0; i nearVector; + CollectSurroundingAreas( &nearVector, area, minSeparation, 2.0f * StepHeight, 2.0f * StepHeight ); + + for( j=0; jAddToTail( area ); + + if ( outVector->Count() >= maxCount ) + return; + } + } +} + + +#endif // _NAV_PATHFIND_H_ diff --git a/sp/src/game/server/nav_simplify.cpp b/sp/src/game/server/nav_simplify.cpp new file mode 100644 index 00000000..1e04cf43 --- /dev/null +++ b/sp/src/game/server/nav_simplify.cpp @@ -0,0 +1,285 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_simplify.cpp + +#include "cbase.h" +#include "nav_mesh.h" +#include "nav_node.h" + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + +extern ConVar nav_snap_to_grid; +extern ConVar nav_split_place_on_ground; +extern ConVar nav_coplanar_slope_limit; +extern ConVar nav_coplanar_slope_limit_displacement; + +//-------------------------------------------------------------------------------------------------------- +static bool ReduceToComponentAreas( CNavArea *area, bool addToSelectedSet ) +{ + if ( !area ) + return false; + + bool splitAlongX; + float splitEdge; + + const float minSplitSize = 2.0f; // ensure the first split is larger than this + + float sizeX = area->GetSizeX(); + float sizeY = area->GetSizeY(); + + CNavArea *first = NULL; + CNavArea *second = NULL; + CNavArea *third = NULL; + CNavArea *fourth = NULL; + + bool didSplit = false; + + if ( sizeX > GenerationStepSize ) + { + splitEdge = RoundToUnits( area->GetCorner( NORTH_WEST ).x, GenerationStepSize ); + if ( splitEdge < area->GetCorner( NORTH_WEST ).x + minSplitSize ) + splitEdge += GenerationStepSize; + splitAlongX = false; + + didSplit = area->SplitEdit( splitAlongX, splitEdge, &first, &second ); + } + + if ( sizeY > GenerationStepSize ) + { + splitEdge = RoundToUnits( area->GetCorner( NORTH_WEST ).y, GenerationStepSize ); + if ( splitEdge < area->GetCorner( NORTH_WEST ).y + minSplitSize ) + splitEdge += GenerationStepSize; + splitAlongX = true; + + if ( didSplit ) + { + didSplit = first->SplitEdit( splitAlongX, splitEdge, &third, &fourth ); + didSplit = second->SplitEdit( splitAlongX, splitEdge, &first, &second ); + } + else + { + didSplit = area->SplitEdit( splitAlongX, splitEdge, &first, &second ); + } + } + + if ( !didSplit ) + return false; + + if ( addToSelectedSet ) + { + TheNavMesh->AddToSelectedSet( first ); + TheNavMesh->AddToSelectedSet( second ); + TheNavMesh->AddToSelectedSet( third ); + TheNavMesh->AddToSelectedSet( fourth ); + } + + ReduceToComponentAreas( first, addToSelectedSet ); + ReduceToComponentAreas( second, addToSelectedSet ); + ReduceToComponentAreas( third, addToSelectedSet ); + ReduceToComponentAreas( fourth, addToSelectedSet ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_chop_selected, "Chops all selected areas into their component 1x1 areas", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() || engine->IsDedicatedServer() ) + return; + + TheNavMesh->StripNavigationAreas(); + TheNavMesh->SetMarkedArea( NULL ); + + NavAreaCollector collector; + TheNavMesh->ForAllSelectedAreas( collector ); + + for ( int i=0; iGetSelecteSetSize() ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::RemoveNodes( void ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + TheNavAreas[ it ]->ResetNodes(); + } + + // destroy navigation nodes created during map generation + CNavNode::CleanupGeneration(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNavMesh::GenerateNodes( const Extent &bounds ) +{ + m_simplifyGenerationExtent = bounds; + m_seedIdx = 0; + + Assert( m_generationMode == GENERATE_SIMPLIFY ); + while ( SampleStep() ) + { + // do nothing + } +} + + +//-------------------------------------------------------------------------------------------------------- +// Simplifies the selected set by reducing to 1x1 areas and re-merging them up with loosened tolerances +void CNavMesh::SimplifySelectedAreas( void ) +{ + // Save off somve cvars: we need to place nodes on ground, we need snap to grid set, and we loosen slope tolerances + m_generationMode = GENERATE_SIMPLIFY; + bool savedSplitPlaceOnGround = nav_split_place_on_ground.GetBool(); + nav_split_place_on_ground.SetValue( 1 ); + + float savedCoplanarSlopeDisplacementLimit = nav_coplanar_slope_limit_displacement.GetFloat(); + nav_coplanar_slope_limit_displacement.SetValue( MIN( 0.5f, savedCoplanarSlopeDisplacementLimit ) ); + + float savedCoplanarSlopeLimit = nav_coplanar_slope_limit.GetFloat(); + nav_coplanar_slope_limit.SetValue( MIN( 0.5f, savedCoplanarSlopeLimit ) ); + + int savedGrid = nav_snap_to_grid.GetInt(); + nav_snap_to_grid.SetValue( 1 ); + + StripNavigationAreas(); + SetMarkedArea( NULL ); + + NavAreaCollector collector; + ForAllSelectedAreas( collector ); + + // Select walkable seeds and re-generate nodes in the bounds + ClearWalkableSeeds(); + + Extent bounds; + bounds.lo.Init( FLT_MAX, FLT_MAX, FLT_MAX ); + bounds.hi.Init( -FLT_MAX, -FLT_MAX, -FLT_MAX ); + for ( int i=0; iGetExtent( &areaExtent ); + areaExtent.lo.z -= HalfHumanHeight; + areaExtent.hi.z += 2 * HumanHeight; + bounds.Encompass( areaExtent ); + + Vector center = area->GetCenter(); + center.x = SnapToGrid( center.x ); + center.y = SnapToGrid( center.y ); + + Vector normal; + if ( FindGroundForNode( ¢er, &normal ) ) + { + AddWalkableSeed( center, normal ); + + center.z += HumanHeight; + bounds.Encompass( center ); + } + } + RemoveNodes(); + GenerateNodes( bounds ); + ClearWalkableSeeds(); + + // Split nav areas up into 1x1 component areas + for ( int i=0; iGetCorner( NORTH_EAST ); + Vector normal; + if ( FindGroundForNode( &corner, &normal ) ) + { + area->m_node[ NORTH_EAST ] = CNavNode::GetNode( corner ); + if ( area->m_node[ NORTH_EAST ] ) + { + area->m_node[ NORTH_WEST ] = area->m_node[ NORTH_EAST ]->GetConnectedNode( WEST ); + area->m_node[ SOUTH_EAST ] = area->m_node[ NORTH_EAST ]->GetConnectedNode( SOUTH ); + if ( area->m_node[ SOUTH_EAST ] ) + { + area->m_node[ SOUTH_WEST ] = area->m_node[ SOUTH_EAST ]->GetConnectedNode( WEST ); + + if ( area->m_node[ NORTH_WEST ] && area->m_node[ SOUTH_WEST ] ) + { + area->AssignNodes( area ); + } + } + } + } + + Assert ( area->m_node[ NORTH_EAST ] && area->m_node[ NORTH_WEST ] && area->m_node[ SOUTH_EAST ] && area->m_node[ SOUTH_WEST ] ); + if ( !( area->m_node[ NORTH_EAST ] && area->m_node[ NORTH_WEST ] && area->m_node[ SOUTH_EAST ] && area->m_node[ SOUTH_WEST ] ) ) + { + Warning( "Area %d didn't get any nodes!\n", area->GetID() ); + } + } + + // Run a subset of incremental generation on the component areas + MergeGeneratedAreas(); + SquareUpAreas(); + MarkJumpAreas(); + SplitAreasUnderOverhangs(); + MarkStairAreas(); + StichAndRemoveJumpAreas(); + HandleObstacleTopAreas(); + FixUpGeneratedAreas(); + + // Re-select the new areas + ClearSelectedSet(); + FOR_EACH_VEC( TheNavAreas, i ) + { + CNavArea *area = TheNavAreas[i]; + if ( area->HasNodes() ) + { + AddToSelectedSet( area ); + } + } + +#ifndef _DEBUG + // leave nodes in debug for testing + RemoveNodes(); +#endif + + m_generationMode = GENERATE_NONE; + nav_split_place_on_ground.SetValue( savedSplitPlaceOnGround ); + nav_coplanar_slope_limit_displacement.SetValue( savedCoplanarSlopeDisplacementLimit ); + nav_coplanar_slope_limit.SetValue( savedCoplanarSlopeLimit ); + nav_snap_to_grid.SetValue( savedGrid ); +} + + +//-------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( nav_simplify_selected, "Chops all selected areas into their component 1x1 areas and re-merges them together into larger areas", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() || engine->IsDedicatedServer() ) + return; + + int selectedSetSize = TheNavMesh->GetSelecteSetSize(); + if ( selectedSetSize == 0 ) + { + Msg( "nav_simplify_selected only works on the selected set\n" ); + return; + } + + TheNavMesh->SimplifySelectedAreas(); + + Msg( "%d areas simplified - %d remain\n", selectedSetSize, TheNavMesh->GetSelecteSetSize() ); +} + diff --git a/sp/src/game/server/ndebugoverlay.cpp b/sp/src/game/server/ndebugoverlay.cpp new file mode 100644 index 00000000..fa152ba3 --- /dev/null +++ b/sp/src/game/server/ndebugoverlay.cpp @@ -0,0 +1,269 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Namespace for functions dealing with Debug Overlays +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "mathlib/mathlib.h" +#include "player.h" +#include "ndebugoverlay.h" +#include "wcedit.h" + +#ifdef POSIX +#include "ai_basenpc.h" +#include "ai_network.h" +#include "ai_networkmanager.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +#define NUM_DEBUG_OVERLAY_LINES 20 + +int m_nDebugOverlayIndex = -1; +OverlayLine_t* m_debugOverlayLine[NUM_DEBUG_OVERLAY_LINES]; + +//----------------------------------------------------------------------------- +// Purpose: Return the old overlay line from the overlay pool +//----------------------------------------------------------------------------- +OverlayLine_t* GetDebugOverlayLine(void) +{ + // Make casing pool if not initialized + if (m_nDebugOverlayIndex == -1) + { + for (int i=0;inoDepthTest = true; + m_debugOverlayLine[i]->draw = false; + } + m_nDebugOverlayIndex = 0; + } + + int id; + + m_nDebugOverlayIndex++; + if (m_nDebugOverlayIndex == NUM_DEBUG_OVERLAY_LINES) + { + m_nDebugOverlayIndex = 0; + id = NUM_DEBUG_OVERLAY_LINES-1; + + } + else + { + id = m_nDebugOverlayIndex-1; + } + return m_debugOverlayLine[id]; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a debug line to be drawn on the screen +// Input : If testLOS is true, color is based on line of sight test +// Output : +//----------------------------------------------------------------------------- +void UTIL_AddDebugLine(const Vector &startPos, const Vector &endPos, bool noDepthTest, bool testLOS) +{ + OverlayLine_t* debugLine = GetDebugOverlayLine(); + + debugLine->origin = startPos; + debugLine->dest = endPos; + debugLine->noDepthTest = noDepthTest; + debugLine->draw = true; + + if (testLOS) + { + trace_t tr; + UTIL_TraceLine ( debugLine->origin, debugLine->dest, MASK_BLOCKLOS, NULL, COLLISION_GROUP_NONE, &tr ); + if (tr.startsolid || tr.fraction < 1.0) + { + debugLine->r = 255; + debugLine->g = 0; + debugLine->b = 0; + return; + } + } + + debugLine->r = 255; + debugLine->g = 255; + debugLine->b = 255; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns z value of floor below given point (up to 384 inches below) +//----------------------------------------------------------------------------- +float GetLongFloorZ(const Vector &origin) +{ + // trace to the ground, then pop up 8 units and place node there to make it + // easier for them to connect (think stairs, chairs, and bumps in the floor). + // After the routing is done, push them back down. + // + trace_t tr; + UTIL_TraceLine ( origin, + origin - Vector ( 0, 0, 2048 ), + MASK_NPCSOLID_BRUSHONLY, + NULL, + COLLISION_GROUP_NONE, + &tr ); + + // This trace is ONLY used if we hit an entity flagged with FL_WORLDBRUSH + trace_t trEnt; + UTIL_TraceLine ( origin, + origin - Vector ( 0, 0, 2048 ), + MASK_NPCSOLID, + NULL, + COLLISION_GROUP_NONE, + &trEnt ); + + + // Did we hit something closer than the floor? + if ( trEnt.fraction < tr.fraction ) + { + // If it was a world brush entity, copy the node location + if ( trEnt.m_pEnt ) + { + CBaseEntity *e = trEnt.m_pEnt; + if ( e && (e->GetFlags() & FL_WORLDBRUSH) ) + { + tr.endpos = trEnt.endpos; + } + } + } + + return tr.endpos.z; +} + +//------------------------------------------------------------------------------ +// Purpose : Draws a large crosshair at flCrossDistance from the debug player +// with tick marks +//------------------------------------------------------------------------------ +void UTIL_DrawPositioningOverlay( float flCrossDistance ) +{ + CBasePlayer* pPlayer = UTIL_PlayerByIndex(CBaseEntity::m_nDebugPlayer); + + if (!pPlayer) + { + return; + } + + Vector pRight; + pPlayer->EyeVectors( NULL, &pRight, NULL ); + +#ifdef _WIN32 + Vector topPos = NWCEdit::AirNodePlacementPosition(); +#else + Vector pForward; + pPlayer->EyeVectors( &pForward ); + + Vector floorVec = pForward; + floorVec.z = 0; + VectorNormalize( floorVec ); + VectorNormalize( pForward ); + + float cosAngle = DotProduct(floorVec,pForward); + + float lookDist = g_pAINetworkManager->GetEditOps()->m_flAirEditDistance/cosAngle; + Vector topPos = pPlayer->EyePosition()+pForward * lookDist; +#endif + + Vector bottomPos = topPos; + bottomPos.z = GetLongFloorZ(bottomPos); + + // Make sure we can see the target pos + trace_t tr; + Vector endPos; + UTIL_TraceLine(pPlayer->EyePosition(), topPos, MASK_NPCSOLID_BRUSHONLY, pPlayer, COLLISION_GROUP_NONE, &tr); + if (tr.fraction == 1.0) + { + Vector rightTrace = topPos + pRight*400; + float traceLen = (topPos - rightTrace).Length(); + UTIL_TraceLine(topPos, rightTrace, MASK_NPCSOLID_BRUSHONLY, pPlayer, COLLISION_GROUP_NONE, &tr); + endPos = topPos+(pRight*traceLen*tr.fraction); + NDebugOverlay::DrawTickMarkedLine(topPos, endPos, 24.0, 5, 255,0,0,false,0); + + Vector leftTrace = topPos - pRight*400; + traceLen = (topPos - leftTrace).Length(); + UTIL_TraceLine(topPos, leftTrace, MASK_NPCSOLID_BRUSHONLY, pPlayer, COLLISION_GROUP_NONE, &tr); + endPos = topPos-(pRight*traceLen*tr.fraction); + NDebugOverlay::DrawTickMarkedLine(topPos, endPos, 24.0, 5, 255,0,0,false,0); + + Vector upTrace = topPos + Vector(0,0,1)*400; + traceLen = (topPos - upTrace).Length(); + UTIL_TraceLine(topPos, upTrace, MASK_NPCSOLID_BRUSHONLY, pPlayer, COLLISION_GROUP_NONE, &tr); + endPos = topPos+(Vector(0,0,1)*traceLen*tr.fraction); + NDebugOverlay::DrawTickMarkedLine(bottomPos, endPos, 24.0, 5, 255,0,0,false,0); + + // Draw white cross at center + NDebugOverlay::Cross3D(topPos, Vector(-2,-2,-2), Vector(2,2,2), 255,255,255, true, 0); + } + else + { + // Draw warning cross at center + NDebugOverlay::Cross3D(topPos, Vector(-2,-2,-2), Vector(2,2,2), 255,100,100, true, 0 ); + } + /* Don't show distance for now. + char text[25]; + Q_snprintf(text,sizeof(text),"%3.0f",flCrossDistance/12.0); + Vector textPos = topPos - pRight*16 + Vector(0,0,10); + NDebugOverlay::Text( textPos, text, true, 0 ); + */ +} + +//------------------------------------------------------------------------------ +// Purpose : Draw all overlay lines in the list +//------------------------------------------------------------------------------ +void UTIL_DrawOverlayLines(void) +{ + if (m_nDebugOverlayIndex != -1) + { + for (int i=0;idraw) + { + NDebugOverlay::Line(m_debugOverlayLine[i]->origin, + m_debugOverlayLine[i]->dest, + m_debugOverlayLine[i]->r, + m_debugOverlayLine[i]->g, + m_debugOverlayLine[i]->b, + m_debugOverlayLine[i]->noDepthTest,0); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Add an overlay line with padding on the start and end +//----------------------------------------------------------------------------- +void DebugDrawLine( const Vector& vecAbsStart, const Vector& vecAbsEnd, int r, int g, int b, bool test, float duration ) +{ + NDebugOverlay::Line( vecAbsStart + Vector( 0,0,0.1), vecAbsEnd + Vector( 0,0,0.1), r,g,b, test, duration ); +} + +//----------------------------------------------------------------------------- +// Purpose: Allow all debug overlays to be cleared at once +//----------------------------------------------------------------------------- +CON_COMMAND( clear_debug_overlays, "clears debug overlays" ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CBaseEntity *pEntity = gEntList.FirstEnt(); + + // Clear all entities of their debug overlays + while ( pEntity ) + { + pEntity->m_debugOverlays = 0; + // UNDONE: Clear out / expire timed overlays? + pEntity = gEntList.NextEnt( pEntity ); + } + + // Clear all engine overlays + if ( debugoverlay ) + { + debugoverlay->ClearAllOverlays(); + } +} diff --git a/sp/src/game/server/ndebugoverlay.h b/sp/src/game/server/ndebugoverlay.h new file mode 100644 index 00000000..7b0a64a9 --- /dev/null +++ b/sp/src/game/server/ndebugoverlay.h @@ -0,0 +1,34 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Namespace for functions dealing with Debug Overlays +// +// $NoKeywords: $ +//=============================================================================// +#ifndef NDEBUGOVERLAY_H +#define NDEBUGOVERLAY_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "debugoverlay_shared.h" + +// An overlay line +struct OverlayLine_t +{ + Vector origin; + Vector dest; + int r; + int g; + int b; + bool noDepthTest; + bool draw; +}; + +extern void UTIL_AddDebugLine( const Vector &startPos, const Vector &endPos, bool noDepthTest, bool testLOS ); +extern void UTIL_DrawPositioningOverlay( float flCrossDistance ); +extern void UTIL_DrawOverlayLines( void ); + +extern void DebugDrawLine( const Vector& vecAbsStart, const Vector& vecAbsEnd, int r, int g, int b, bool test, float duration ); + +#endif // NDEBUGOVERLAY_H diff --git a/sp/src/game/server/networkstringtable_gamedll.h b/sp/src/game/server/networkstringtable_gamedll.h new file mode 100644 index 00000000..903c7c6c --- /dev/null +++ b/sp/src/game/server/networkstringtable_gamedll.h @@ -0,0 +1,46 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef NETWORKSTRINGTABLE_GAMEDLL_H +#define NETWORKSTRINGTABLE_GAMEDLL_H +#ifdef _WIN32 +#pragma once +#endif + +#include "networkstringtabledefs.h" + +class CStringTableSaveRestoreOps; + +// String tables used by the game DLL +#define MAX_VGUI_SCREEN_STRING_BITS 8 +#define MAX_VGUI_SCREEN_STRINGS ( 1 << MAX_VGUI_SCREEN_STRING_BITS ) +#define VGUI_SCREEN_INVALID_STRING ( MAX_VGUI_SCREEN_STRINGS - 1 ) + +#define MAX_MATERIAL_STRING_BITS 10 +#define MAX_MATERIAL_STRINGS ( 1 << MAX_MATERIAL_STRING_BITS ) +#define OVERLAY_MATERIAL_INVALID_STRING ( MAX_MATERIAL_STRINGS - 1 ) + +#define MAX_CHOREO_SCENES_STRING_BITS 12 +#define MAX_CHOREO_SCENES_STRINGS ( 1 << MAX_CHOREO_SCENES_STRING_BITS ) +#define CHOREO_SCENES_INVALID_STRING ( MAX_CHOREO_SCENES_STRINGS - 1 ) + +#define MAX_PARTICLESYSTEMS_STRING_BITS 11 +#define MAX_PARTICLESYSTEMS_STRINGS ( 1 << MAX_PARTICLESYSTEMS_STRING_BITS ) +#define PARTICLESYSTEMS_INVALID_STRING ( MAX_PARTICLESYSTEMS_STRINGS - 1 ) + +extern INetworkStringTableContainer *networkstringtable; +extern INetworkStringTable *g_pStringTableVguiScreen; +extern INetworkStringTable *g_pStringTableEffectDispatch; +extern INetworkStringTable *g_pStringTableClientSideChoreoScenes; + +#define MAX_INFOPANEL_STRINGS 128 + +// save/load +extern CStringTableSaveRestoreOps g_VguiScreenStringOps; + + +#endif // NETWORKSTRINGTABLE_GAMEDLL_H diff --git a/sp/src/game/server/npc_talker.cpp b/sp/src/game/server/npc_talker.cpp new file mode 100644 index 00000000..ebb94583 --- /dev/null +++ b/sp/src/game/server/npc_talker.cpp @@ -0,0 +1,1357 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" + +#include "npc_talker.h" +#include "npcevent.h" +#include "scriptevent.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + + +BEGIN_SIMPLE_DATADESC( CNPCSimpleTalkerExpresser ) + // m_pSink (reconnected on load) + DEFINE_AUTO_ARRAY( m_szMonologSentence, FIELD_CHARACTER ), + DEFINE_FIELD( m_iMonologIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_fMonologSuspended, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hMonologTalkTarget, FIELD_EHANDLE ), +END_DATADESC() + +BEGIN_DATADESC( CNPCSimpleTalker ) + DEFINE_FIELD( m_useTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextIdleSpeechTime, FIELD_TIME ), + DEFINE_FIELD( m_nSpeak, FIELD_INTEGER ), + DEFINE_FIELD( m_iszUse, FIELD_STRING ), + DEFINE_FIELD( m_iszUnUse, FIELD_STRING ), + // m_FollowBehavior (auto saved by AI) + // Function Pointers + DEFINE_USEFUNC( FollowerUse ), + +END_DATADESC() + +// array of friend names +char *CNPCSimpleTalker::m_szFriends[TLK_CFRIENDS] = +{ + "NPC_barney", + "NPC_scientist", + "NPC_sitting_scientist", + NULL, +}; + +bool CNPCSimpleTalker::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "UseSentence")) + { + m_iszUse = AllocPooledString(szValue); + } + else if (FStrEq(szKeyName, "UnUseSentence")) + { + m_iszUnUse = AllocPooledString(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +void CNPCSimpleTalker::Precache( void ) +{ + /* + // FIXME: Need to figure out how to hook these... + if ( m_iszUse != NULL_STRING ) + GetExpresser()->ModifyConcept( TLK_STARTFOLLOW, STRING( m_iszUse ) ); + if ( m_iszUnUse != NULL_STRING ) + GetExpresser()->ModifyConcept( TLK_STOPFOLLOW, STRING( m_iszUnUse ) ); + + */ + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Allows for modification of the interrupt mask for the current schedule. +// In the most cases the base implementation should be called first. +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::BuildScheduleTestBits( void ) +{ + BaseClass::BuildScheduleTestBits(); + + // Assume that if I move from the player, I can respond to a question + if ( ConditionInterruptsCurSchedule( COND_PLAYER_PUSHING ) || ConditionInterruptsCurSchedule( COND_PROVOKED ) ) + { + SetCustomInterruptCondition( COND_TALKER_RESPOND_TO_QUESTION ); + } +} + +void CNPCSimpleTalker::PrescheduleThink( void ) +{ + BaseClass::PrescheduleThink(); + + (assert_cast(GetExpresser()))->SpeakMonolog(); +} + +bool CNPCSimpleTalker::ShouldSuspendMonolog( void ) +{ + float flDist; + + flDist = ((assert_cast(GetExpresser()))->GetMonologueTarget()->GetAbsOrigin() - GetAbsOrigin()).Length(); + + if( flDist >= 384 ) + { + return true; + } + + return false; +} + +bool CNPCSimpleTalker::ShouldResumeMonolog( void ) +{ + float flDist; + + if( HasCondition( COND_SEE_PLAYER ) ) + { + flDist = ((assert_cast(GetExpresser()))->GetMonologueTarget()->GetAbsOrigin() - GetAbsOrigin()).Length(); + + if( flDist <= 256 ) + { + return true; + } + } + + return false; +} + +int CNPCSimpleTalker::SelectSchedule( void ) +{ + if ( !HasCondition(COND_RECEIVED_ORDERS) ) + { + if ( GetState() == NPC_STATE_IDLE ) + { + // if never seen player, try to greet him + // Filter might be preventing us from ever greeting the player + if ( HasCondition( COND_SEE_PLAYER ) && CanSayHello()) + { + return SCHED_TALKER_IDLE_HELLO; + } + } + } + + return BaseClass::SelectSchedule(); +} + +void CNPCSimpleTalker::StartTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_TALKER_WAIT_FOR_SEMAPHORE: + if ( GetExpresser()->SemaphoreIsAvailable( this ) ) + TaskComplete(); + break; + + case TASK_TALKER_SPEAK: + // ask question or make statement + FIdleSpeak(); + TaskComplete(); + break; + + case TASK_TALKER_RESPOND: + // respond to question + IdleRespond(); + TaskComplete(); + break; + + case TASK_TALKER_HELLO: + // greet player + FIdleHello(); + TaskComplete(); + break; + + case TASK_TALKER_STARE: + // let the player know I know he's staring at me. + FIdleStare(); + TaskComplete(); + break; + + case TASK_TALKER_LOOK_AT_CLIENT: + case TASK_TALKER_CLIENT_STARE: + // track head to the client for a while. + SetWait( pTask->flTaskData ); + break; + + case TASK_TALKER_EYECONTACT: + break; + + case TASK_TALKER_IDEALYAW: + if (GetSpeechTarget() != NULL) + { + GetMotor()->SetIdealYawToTarget( GetSpeechTarget()->GetAbsOrigin() ); + } + TaskComplete(); + break; + + case TASK_TALKER_HEADRESET: + // reset head position after looking at something + SetSpeechTarget( NULL ); + TaskComplete(); + break; + + case TASK_TALKER_BETRAYED: + Speak( TLK_BETRAYED ); + TaskComplete(); + break; + + case TASK_TALKER_STOPSHOOTING: + // tell player to stop shooting + Speak( TLK_NOSHOOT ); + TaskComplete(); + break; + default: + BaseClass::StartTask( pTask ); + } +} + +void CNPCSimpleTalker::RunTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_TALKER_WAIT_FOR_SEMAPHORE: + if ( GetExpresser()->SemaphoreIsAvailable( this ) ) + TaskComplete(); + break; + + case TASK_TALKER_CLIENT_STARE: + case TASK_TALKER_LOOK_AT_CLIENT: + + if ( pTask->iTask == TASK_TALKER_CLIENT_STARE && AI_IsSinglePlayer() ) + { + // Get edict for one player + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + Assert( pPlayer ); + + // fail out if the player looks away or moves away. + if ( ( pPlayer->GetAbsOrigin() - GetAbsOrigin() ).Length2D() > TALKER_STARE_DIST ) + { + // player moved away. + TaskFail("Player moved away"); + } + + Vector forward; + AngleVectors( pPlayer->GetLocalAngles(), &forward ); + if ( UTIL_DotPoints( pPlayer->GetAbsOrigin(), GetAbsOrigin(), forward ) < m_flFieldOfView ) + { + // player looked away + TaskFail("Player looked away"); + } + } + + if ( IsWaitFinished() ) + { + TaskComplete(); + } + break; + + case TASK_TALKER_EYECONTACT: + if (IsMoving() || !GetExpresser()->IsSpeaking() || GetSpeechTarget() == NULL) + { + TaskComplete(); + } + break; + + case TASK_WAIT_FOR_MOVEMENT: + FIdleSpeakWhileMoving(); + BaseClass::RunTask( pTask ); + break; + + default: + BaseClass::RunTask( pTask ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ + +Activity CNPCSimpleTalker::NPC_TranslateActivity( Activity eNewActivity ) +{ + if ((eNewActivity == ACT_IDLE) && + (GetExpresser()->IsSpeaking()) && + (SelectWeightedSequence ( ACT_SIGNAL3 ) != ACTIVITY_NOT_AVAILABLE) ) + { + return ACT_SIGNAL3; + } + else if ((eNewActivity == ACT_SIGNAL3) && + (SelectWeightedSequence ( ACT_SIGNAL3 ) == ACTIVITY_NOT_AVAILABLE) ) + { + return ACT_IDLE; + } + return BaseClass::NPC_TranslateActivity( eNewActivity ); +} + + +void CNPCSimpleTalker::Event_Killed( const CTakeDamageInfo &info ) +{ + AlertFriends( info.GetAttacker() ); + if ( info.GetAttacker()->GetFlags() & FL_CLIENT ) + { + LimitFollowers( info.GetAttacker(), 0 ); + } + BaseClass::Event_Killed( info ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CNPCSimpleTalker::EnumFriends( CBaseEntity *pPrevious, int listNumber, bool bTrace ) +{ + CBaseEntity *pFriend = pPrevious; + char *pszFriend; + trace_t tr; + Vector vecCheck; + + pszFriend = m_szFriends[ FriendNumber(listNumber) ]; + while ( pszFriend != NULL && ((pFriend = gEntList.FindEntityByClassname( pFriend, pszFriend )) != NULL) ) + { + if (pFriend == this || !pFriend->IsAlive()) + // don't talk to self or dead people + continue; + + if ( bTrace ) + { + Vector vecCheck; + pFriend->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 1.0f ), &vecCheck ); + UTIL_TraceLine( GetAbsOrigin(), vecCheck, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + } + else + { + tr.fraction = 1.0; + } + + if (tr.fraction == 1.0) + { + return pFriend; + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pKiller - +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::AlertFriends( CBaseEntity *pKiller ) +{ + CBaseEntity *pFriend = NULL; + int i; + + // for each friend in this bsp... + for ( i = 0; i < TLK_CFRIENDS; i++ ) + { + while ((pFriend = EnumFriends( pFriend, i, true )) != NULL ) + { + CAI_BaseNPC *pNPC = pFriend->MyNPCPointer(); + if ( pNPC->IsAlive() ) + { + // If a client killed me, make everyone else mad/afraid of him + if ( pKiller->GetFlags() & FL_CLIENT ) + { + CNPCSimpleTalker*pTalkNPC = (CNPCSimpleTalker *)pFriend; + + if (pTalkNPC && pTalkNPC->IsOkToCombatSpeak()) + { + // FIXME: need to check CanSpeakConcept? + pTalkNPC->Speak( TLK_BETRAYED ); + } + } + else + { +#ifdef MAPBASE + if( IRelationType(pKiller) <= D_FR) +#else + if( IRelationType(pKiller) == D_HT) +#endif + { + // Killed by an enemy!!! + CNPCSimpleTalker *pAlly = (CNPCSimpleTalker *)pNPC; + + if( pAlly && pAlly->GetExpresser()->CanSpeakConcept( TLK_ALLY_KILLED ) ) + { + pAlly->Speak( TLK_ALLY_KILLED ); + } + } + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::ShutUpFriends( void ) +{ + CBaseEntity *pFriend = NULL; + int i; + + // for each friend in this bsp... + for ( i = 0; i < TLK_CFRIENDS; i++ ) + { + while ((pFriend = EnumFriends( pFriend, i, true )) != NULL) + { + CAI_BaseNPC *pNPC = pFriend->MyNPCPointer(); + if ( pNPC ) + { + pNPC->SentenceStop(); + } + } + } +} + + +// UNDONE: Keep a follow time in each follower, make a list of followers in this function and do LRU +// UNDONE: Check this in Restore to keep restored NPCs from joining a full list of followers +void CNPCSimpleTalker::LimitFollowers( CBaseEntity *pPlayer, int maxFollowers ) +{ + CBaseEntity *pFriend = NULL; + int i, count; + + count = 0; + // for each friend in this bsp... + for ( i = 0; i < TLK_CFRIENDS; i++ ) + { + while ((pFriend = EnumFriends( pFriend, i, false )) != NULL) + { + CAI_BaseNPC *pNPC = pFriend->MyNPCPointer(); + CNPCSimpleTalker *pTalker; + if ( pNPC ) + { + if ( pNPC->GetTarget() == pPlayer ) + { + count++; + if ( count > maxFollowers && (pTalker = dynamic_cast( pNPC ) ) != NULL ) + pTalker->StopFollowing(); + } + } + } + } +} + +//========================================================= +// HandleAnimEvent - catches the NPC-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CNPCSimpleTalker::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case SCRIPT_EVENT_SENTENCE_RND1: // Play a named sentence group 25% of the time + if (random->RandomInt(0,99) < 75) + break; + // fall through... + case SCRIPT_EVENT_SENTENCE: // Play a named sentence group + ShutUpFriends(); + PlaySentence( pEvent->options, random->RandomFloat(2.8, 3.4) ); + //Msg( "script event speak\n"); + break; + + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Scan for nearest, visible friend. If fPlayer is true, look for nearest player +//----------------------------------------------------------------------------- +bool CNPCSimpleTalker::IsValidSpeechTarget( int flags, CBaseEntity *pEntity ) +{ + return BaseClass::IsValidSpeechTarget( flags, pEntity ); +} + +CBaseEntity *CNPCSimpleTalker::FindNearestFriend(bool fPlayer) +{ + return FindSpeechTarget( (fPlayer) ? AIST_PLAYERS : AIST_NPCS ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// Purpose: Respond to a previous question +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::IdleRespond( void ) +{ + if (!IsOkToSpeak()) + return; + + // play response + SpeakAnswerFriend( GetSpeechTarget() ); + + DeferAllIdleSpeech( random->RandomFloat( TALKER_DEFER_IDLE_SPEAK_MIN, TALKER_DEFER_IDLE_SPEAK_MAX ) ); +} + +bool CNPCSimpleTalker::IsOkToSpeak( void ) +{ + if ( m_flNextIdleSpeechTime > gpGlobals->curtime ) + return false; + + return BaseClass::IsOkToSpeak(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Find a nearby friend to stare at +//----------------------------------------------------------------------------- +int CNPCSimpleTalker::FIdleStare( void ) +{ + // Don't idly speak if our speech filter is preventing us + if ( GetSpeechFilter() && GetSpeechFilter()->GetIdleModifier() == 0 ) + return true; + + SpeakIfAllowed( TLK_STARE ); + + SetSpeechTarget( FindNearestFriend( true ) ); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Try to greet player first time he's seen +// Output : int +//----------------------------------------------------------------------------- +int CNPCSimpleTalker::FIdleHello( void ) +{ + // Filter might be preventing us from ever greeting the player + if ( !CanSayHello() ) + return false; + + // get a player + CBaseEntity *pPlayer = FindNearestFriend(true); + + if (pPlayer) + { + if (FInViewCone(pPlayer) && FVisible(pPlayer)) + { + SayHelloToPlayer( pPlayer ); + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Say hello to the specified player +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::SayHelloToPlayer( CBaseEntity *pPlayer ) +{ + Assert( !GetExpresser()->SpokeConcept(TLK_HELLO) ); + + SetSpeechTarget( pPlayer ); + + Speak( TLK_HELLO ); + DeferAllIdleSpeech( random->RandomFloat( 5, 10 ) ); + + CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs(); + CAI_PlayerAlly *pTalker; + for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) + { + pTalker = dynamic_cast(ppAIs[i]); + + if( pTalker && FVisible( pTalker ) ) + { + // Tell this guy he's already said hello to the player, too. + pTalker->GetExpresser()->SetSpokeConcept( TLK_HELLO, NULL ); + } + } +} + + +//--------------------------------------------------------- +// Stop all allies from idle speech for a fixed amount +// of time. Mostly filthy hack to hold us over until +// acting comes online. +//--------------------------------------------------------- +void CNPCSimpleTalker::DeferAllIdleSpeech( float flDelay, CAI_BaseNPC *pIgnore ) +{ + // Brute force. Just plow through NPC list looking for talkers. + CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs(); + CNPCSimpleTalker *pTalker; + + float flTime = gpGlobals->curtime + flDelay; + + for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) + { + if( ppAIs[i] != pIgnore ) + { + pTalker = dynamic_cast(ppAIs[i]); + + if( pTalker ) + { + pTalker->m_flNextIdleSpeechTime = flTime; + } + } + } + + BaseClass::DeferAllIdleSpeech( flDelay, pIgnore ); +} + +//========================================================= +// FIdleSpeak +// ask question of nearby friend, or make statement +//========================================================= +int CNPCSimpleTalker::FIdleSpeak( void ) +{ + // try to start a conversation, or make statement + int pitch; + + if (!IsOkToSpeak()) + return false; + + Assert( GetExpresser()->SemaphoreIsAvailable( this ) ); + + pitch = GetExpresser()->GetVoicePitch(); + + // player using this entity is alive and wounded? + CBaseEntity *pTarget = GetTarget(); + + if ( pTarget != NULL ) + { + if ( pTarget->IsPlayer() ) + { + if ( pTarget->IsAlive() ) + { + SetSpeechTarget( GetTarget() ); + if (GetExpresser()->CanSpeakConcept( TLK_PLHURT3) && + (GetTarget()->m_iHealth <= GetTarget()->m_iMaxHealth / 8)) + { + Speak( TLK_PLHURT3 ); + return true; + } + else if (GetExpresser()->CanSpeakConcept( TLK_PLHURT2) && + (GetTarget()->m_iHealth <= GetTarget()->m_iMaxHealth / 4)) + { + Speak( TLK_PLHURT2 ); + return true; + } + else if (GetExpresser()->CanSpeakConcept( TLK_PLHURT1) && + (GetTarget()->m_iHealth <= GetTarget()->m_iMaxHealth / 2)) + { + Speak( TLK_PLHURT1 ); + return true; + } + } + else + { + //!!!KELLY - here's a cool spot to have the talkNPC talk about the dead player if we want. + // "Oh dear, Gordon Freeman is dead!" -Scientist + // "Damn, I can't do this without you." -Barney + } + } + } + + // ROBIN: Disabled idle question & answer for now + /* + // if there is a friend nearby to speak to, play sentence, set friend's response time, return + CBaseEntity *pFriend = FindNearestFriend(false); + + // 75% chance of talking to another citizen if one is available. + if (pFriend && !(pFriend->IsMoving()) && random->RandomInt( 0, 3 ) != 0 ) + { + if ( SpeakQuestionFriend( pFriend ) ) + { + // force friend to answer + CAI_PlayerAlly *pTalkNPC = dynamic_cast(pFriend); + if (pTalkNPC && !pTalkNPC->HasSpawnFlags(SF_NPC_GAG) && !pTalkNPC->IsInAScript() ) + { + SetSpeechTarget( pFriend ); + pTalkNPC->SetAnswerQuestion( this ); + pTalkNPC->GetExpresser()->BlockSpeechUntil( GetExpresser()->GetTimeSpeechComplete() ); + + m_nSpeak++; + } + + // Don't let anyone else butt in. + DeferAllIdleSpeech( random->RandomFloat( TALKER_DEFER_IDLE_SPEAK_MIN, TALKER_DEFER_IDLE_SPEAK_MAX ), pTalkNPC ); + return true; + } + } + */ + + // Otherwise, play an idle statement, try to face client when making a statement. + CBaseEntity *pFriend = FindNearestFriend(true); + if ( pFriend ) + { + SetSpeechTarget( pFriend ); + + // If we're about to talk to the player, and we've never said hello, say hello first + if ( !GetSpeechFilter() || !GetSpeechFilter()->NeverSayHello() ) + { + if ( GetExpresser()->CanSpeakConcept( TLK_HELLO ) && !GetExpresser()->SpokeConcept( TLK_HELLO ) ) + { + SayHelloToPlayer( pFriend ); + return true; + } + } + + if ( Speak( TLK_IDLE ) ) + { + DeferAllIdleSpeech( random->RandomFloat( TALKER_DEFER_IDLE_SPEAK_MIN, TALKER_DEFER_IDLE_SPEAK_MAX ) ); + m_nSpeak++; + } + else + { + // We failed to speak. Don't try again for a bit. + m_flNextIdleSpeechTime = gpGlobals->curtime + 3; + } + + return true; + } + + // didn't speak + m_flNextIdleSpeechTime = gpGlobals->curtime + 3; + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Speak the right question based upon who we're asking +//----------------------------------------------------------------------------- +bool CNPCSimpleTalker::SpeakQuestionFriend( CBaseEntity *pFriend ) +{ + return Speak( TLK_QUESTION ); +} + +//----------------------------------------------------------------------------- +// Purpose: Speak the right answer based upon who we're answering +//----------------------------------------------------------------------------- +bool CNPCSimpleTalker::SpeakAnswerFriend( CBaseEntity *pFriend ) +{ + return Speak( TLK_ANSWER ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::FIdleSpeakWhileMoving( void ) +{ + if ( GetExpresser()->CanSpeak() ) + { + if (!GetExpresser()->IsSpeaking() || GetSpeechTarget() == NULL) + { + // override so that during walk, a scientist may talk and greet player + FIdleHello(); + + if ( ShouldSpeakRandom( m_nSpeak * 20, GetSpeechFilter() ? GetSpeechFilter()->GetIdleModifier() : 1.0 ) ) + { + FIdleSpeak(); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CNPCSimpleTalker::PlayScriptedSentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, bool bConcurrent, CBaseEntity *pListener ) +{ + if ( !bConcurrent ) + ShutUpFriends(); + + int sentenceIndex = BaseClass::PlayScriptedSentence( pszSentence, delay, volume, soundlevel, bConcurrent, pListener ); + delay += engine->SentenceLength( sentenceIndex ); + if ( delay < 0 ) + delay = 0; + m_useTime = gpGlobals->curtime + delay; + + // Stop all idle speech until after the sentence has completed + DeferAllIdleSpeech( delay + random->RandomInt( 3.0f, 5.0f ) ); + + return sentenceIndex; +} + +//----------------------------------------------------------------------------- +// Purpose: Tell this NPC to answer a question from another NPC +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::SetAnswerQuestion( CNPCSimpleTalker *pSpeaker ) +{ + if ( !m_hCine ) + { + SetCondition( COND_TALKER_RESPOND_TO_QUESTION ); + } + + SetSpeechTarget( (CAI_BaseNPC *)pSpeaker ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CNPCSimpleTalker::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + CTakeDamageInfo subInfo = info; + + // if player damaged this entity, have other friends talk about it. + if (subInfo.GetAttacker() && (subInfo.GetAttacker()->GetFlags() & FL_CLIENT) && subInfo.GetDamage() < GetHealth() ) + { + CBaseEntity *pFriend = FindNearestFriend(false); + + if (pFriend && pFriend->IsAlive()) + { + // only if not dead or dying! + CNPCSimpleTalker *pTalkNPC = (CNPCSimpleTalker *)pFriend; + + if (pTalkNPC && pTalkNPC->IsOkToCombatSpeak()) + { + pTalkNPC->Speak( TLK_NOSHOOT ); + } + } + } + return BaseClass::OnTakeDamage_Alive( subInfo ); +} + +int CNPCSimpleTalker::SelectNonCombatSpeechSchedule() +{ + if ( !IsOkToSpeak() ) + return SCHED_NONE; + + // talk about world + if ( ShouldSpeakRandom( m_nSpeak * 2, GetSpeechFilter() ? GetSpeechFilter()->GetIdleModifier() : 1.0 ) ) + { + //Msg("standing idle speak\n" ); + return SCHED_TALKER_IDLE_SPEAK; + } + + // failed to speak, so look at the player if he's around + if ( AI_IsSinglePlayer() && GetExpresser()->CanSpeak() && HasCondition ( COND_SEE_PLAYER ) && random->RandomInt( 0, 6 ) == 0 ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + Assert( pPlayer ); + + if ( pPlayer ) + { + // watch the client. + Vector forward; + AngleVectors( pPlayer->GetLocalAngles(), &forward ); + if ( ( pPlayer->GetAbsOrigin() - GetAbsOrigin() ).Length2D() < TALKER_STARE_DIST && + UTIL_DotPoints( pPlayer->GetAbsOrigin(), GetAbsOrigin(), forward ) >= m_flFieldOfView ) + { + // go into the special STARE schedule if the player is close, and looking at me too. + return SCHED_TALKER_IDLE_WATCH_CLIENT_STARE; + } + + return SCHED_TALKER_IDLE_WATCH_CLIENT; + } + } + else + { + // look at who we're talking to + if ( GetSpeechTarget() && GetExpresser()->IsSpeaking() ) + return SCHED_TALKER_IDLE_EYE_CONTACT; + } + return SCHED_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPCSimpleTalker::CanSayHello( void ) +{ +#ifndef HL1_DLL + if ( Classify() == CLASS_PLAYER_ALLY_VITAL ) + return false; +#endif + + if ( GetSpeechFilter() && GetSpeechFilter()->NeverSayHello() ) + return false; + + if ( !GetExpresser()->CanSpeakConcept(TLK_HELLO) || GetExpresser()->SpokeConcept(TLK_HELLO) ) + return false; + + if ( !IsOkToSpeak() ) + return false; + + return true; +} + +void CNPCSimpleTalker::OnStartingFollow( CBaseEntity *pTarget ) +{ + GetExpresser()->SetSpokeConcept( TLK_HELLO, NULL ); // Don't say hi after you've started following + if ( IsOkToSpeak() ) // don't speak if idle talk is blocked. player commanded/use follow will always speak + Speak( TLK_STARTFOLLOW ); + SetSpeechTarget( GetTarget() ); + ClearCondition( COND_PLAYER_PUSHING ); +} + +void CNPCSimpleTalker::OnStoppingFollow( CBaseEntity *pTarget ) +{ + if ( !(m_afMemory & bits_MEMORY_PROVOKED) ) + { + if ( IsOkToCombatSpeak() ) + { + if ( pTarget == NULL ) + Speak( TLK_STOPFOLLOW ); + else + Speak( TLK_STOP ); + } + SetSpeechTarget( FindNearestFriend(true) ); + } +} + +void CNPCSimpleTalker::FollowerUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // Don't allow use during a scripted_sentence + if ( m_useTime > gpGlobals->curtime ) + return; + + if ( pCaller != NULL && pCaller->IsPlayer() ) + { + if ( !m_FollowBehavior.GetFollowTarget() && IsInterruptable() ) + { +#if TOML_TODO + LimitFollowers( pCaller , 1 ); +#endif + + if ( m_afMemory & bits_MEMORY_PROVOKED ) + Msg( "I'm not following you, you evil person!\n" ); + else + { + StartFollowing( pCaller ); + } + } + else + { + StopFollowing(); + } + } +} + +//----------------------------------------------------------------------------- +void CNPCSimpleTalker::InputIdleRespond( inputdata_t &inputdata ) +{ + // We've been told to respond. Check combat speak, not isoktospeak, because + // we don't want to check the idle speech time. + if (!IsOkToCombatSpeak()) + return; + + IdleRespond(); +} + +int CNPCSimpleTalkerExpresser::SpeakRawSentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, CBaseEntity *pListener ) +{ + char szSpecificSentence[1024]; + int sentenceIndex = -1; + + if ( !pszSentence ) + return sentenceIndex; + + if ( pszSentence[0] == AI_SP_START_MONOLOG ) + { + // this sentence command will start this NPC speaking + // lengthy monolog from smaller sentences. + BeginMonolog( (char *)pszSentence, pListener ); + return -1; + } + else if ( pszSentence[0] == AI_SP_MONOLOG_LINE ) + { + Q_strncpy(szSpecificSentence, pszSentence, sizeof(szSpecificSentence) ); + szSpecificSentence[0] = AI_SP_SPECIFIC_SENTENCE; + pszSentence = szSpecificSentence; + } + else + { + // this bit of speech is interrupting my monolog! + SuspendMonolog( 0 ); + } + + return CAI_Expresser::SpeakRawSentence( pszSentence, delay, volume, soundlevel, pListener ); +} + +//------------------------------------- + +void CNPCSimpleTalkerExpresser::BeginMonolog( char *pszSentenceName, CBaseEntity *pListener ) +{ + if( pListener ) + { + m_hMonologTalkTarget = pListener; + } + else + { + Warning( "NULL Listener in BeginMonolog()!\n" ); + Assert(0); + EndMonolog(); + return; + } + + Q_strncpy( m_szMonologSentence, pszSentenceName ,sizeof(m_szMonologSentence)); + + // change the "AI_SP_START_MONOLOG" to an "AI_SP_MONOLOG_LINE". m_sMonologSentence is now the + // string we'll tack numbers onto to play sentences from this group in + // sequential order. + m_szMonologSentence[0] = AI_SP_MONOLOG_LINE; + + m_fMonologSuspended = false; + + m_iMonologIndex = 0; +} + +//------------------------------------- + +void CNPCSimpleTalkerExpresser::EndMonolog( void ) +{ + m_szMonologSentence[0] = 0; + m_iMonologIndex = -1; + m_fMonologSuspended = false; + m_hMonologTalkTarget = NULL; +} + +//------------------------------------- + +void CNPCSimpleTalkerExpresser::SpeakMonolog( void ) +{ + int i; + char szSentence[ MONOLOGNAME_LEN ]; + + if( !HasMonolog() ) + { + return; + } + + if( CanSpeak() ) + { + if( m_fMonologSuspended ) + { + if ( GetOuter()->ShouldResumeMonolog() ) + { + ResumeMonolog(); + } + + return; + } + + Q_snprintf( szSentence,sizeof(szSentence), "%s%d", m_szMonologSentence, m_iMonologIndex ); + m_iMonologIndex++; + + i = SpeakRawSentence( szSentence, 0, VOL_NORM ); + + if ( i == -1 ) + { + EndMonolog(); + } + } + else + { + if( GetOuter()->ShouldSuspendMonolog() ) + { + SuspendMonolog( 0 ); + } + } +} + +//------------------------------------- + +void CNPCSimpleTalkerExpresser::SuspendMonolog( float flInterval ) +{ + if( HasMonolog() ) + { + m_fMonologSuspended = true; + } + + // free up other characters to speak. + if ( GetSink()->UseSemaphore() ) + { + GetSpeechSemaphore( GetOuter() )->Release(); + } +} + +//------------------------------------- + +void CNPCSimpleTalkerExpresser::ResumeMonolog( void ) +{ + if( m_iMonologIndex > 0 ) + { + // back up and repeat what I was saying + // when interrupted. + m_iMonologIndex--; + } + + GetOuter()->OnResumeMonolog(); + m_fMonologSuspended = false; +} + +// try to smell something +void CNPCSimpleTalker::TrySmellTalk( void ) +{ + if ( !IsOkToSpeak() ) + return; + + if ( HasCondition( COND_SMELL ) && GetExpresser()->CanSpeakConcept( TLK_SMELL ) ) + Speak( TLK_SMELL ); +} + +void CNPCSimpleTalker::OnChangeRunningBehavior( CAI_BehaviorBase *pOldBehavior, CAI_BehaviorBase *pNewBehavior ) +{ + BaseClass::OnChangeRunningBehavior( pOldBehavior, pNewBehavior ); + + CAI_FollowBehavior *pFollowBehavior; + if ( ( pFollowBehavior = dynamic_cast(pNewBehavior) ) != NULL ) + { + OnStartingFollow( pFollowBehavior->GetFollowTarget() ); + } + else if ( ( pFollowBehavior = dynamic_cast(pOldBehavior) ) != NULL ) + { + OnStoppingFollow( pFollowBehavior->GetFollowTarget() ); + } +} + + +bool CNPCSimpleTalker::OnBehaviorChangeStatus( CAI_BehaviorBase *pBehavior, bool fCanFinishSchedule ) +{ + bool interrupt = BaseClass::OnBehaviorChangeStatus( pBehavior, fCanFinishSchedule ); + if ( !interrupt ) + { + interrupt = ( dynamic_cast(pBehavior) != NULL && ( m_NPCState == NPC_STATE_IDLE || m_NPCState == NPC_STATE_ALERT ) ); + } + return interrupt; + +} +//----------------------------------------------------------------------------- +// Purpose: Return true if I should speak based on the chance & the speech filter's modifier +//----------------------------------------------------------------------------- +bool CNPCSimpleTalker::ShouldSpeakRandom( int iChance, float flModifier ) +{ + if ( flModifier != 1.0 ) + { + // Avoid divide by zero + if ( !flModifier ) + return false; + + iChance = floor( (float)iChance / flModifier ); + } + + return (random->RandomInt(0,iChance) == 0); +} + + +AI_BEGIN_CUSTOM_NPC(talk_monster,CNPCSimpleTalker) + DECLARE_USES_SCHEDULE_PROVIDER( CAI_FollowBehavior ) + + DECLARE_TASK(TASK_TALKER_RESPOND) + DECLARE_TASK(TASK_TALKER_SPEAK) + DECLARE_TASK(TASK_TALKER_HELLO) + DECLARE_TASK(TASK_TALKER_BETRAYED) + DECLARE_TASK(TASK_TALKER_HEADRESET) + DECLARE_TASK(TASK_TALKER_STOPSHOOTING) + DECLARE_TASK(TASK_TALKER_STARE) + DECLARE_TASK(TASK_TALKER_LOOK_AT_CLIENT) + DECLARE_TASK(TASK_TALKER_CLIENT_STARE) + DECLARE_TASK(TASK_TALKER_EYECONTACT) + DECLARE_TASK(TASK_TALKER_IDEALYAW) + DECLARE_TASK(TASK_TALKER_WAIT_FOR_SEMAPHORE) + + //========================================================= + // > SCHED_TALKER_IDLE_RESPONSE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_IDLE_RESPONSE, + + " Tasks" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // Stop and listen + " TASK_WAIT 0.5" // Wait until sure it's me they are talking to + " TASK_TALKER_IDEALYAW 0" // look at who I'm talking to + " TASK_FACE_IDEAL 0" + " TASK_TALKER_EYECONTACT 0" // Wait until speaker is done + " TASK_TALKER_WAIT_FOR_SEMAPHORE 0" + " TASK_TALKER_EYECONTACT 0" // Wait until speaker is done + " TASK_TALKER_RESPOND 0" // Wait and then say my response + " TASK_TALKER_IDEALYAW 0" // look at who I'm talking to + " TASK_FACE_IDEAL 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_SIGNAL3" + " TASK_TALKER_EYECONTACT 0" // Wait until speaker is done + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_PLAYER_PUSHING" + " COND_GIVE_WAY" + ) + + //========================================================= + // > SCHED_TALKER_IDLE_SPEAK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_IDLE_SPEAK, + + " Tasks" + " TASK_TALKER_SPEAK 0" // question or remark + " TASK_TALKER_IDEALYAW 0" // look at who I'm talking to + " TASK_FACE_IDEAL 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_SIGNAL3" + " TASK_TALKER_EYECONTACT 0" + " TASK_WAIT_RANDOM 0.5" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_PLAYER_PUSHING" + " COND_GIVE_WAY" + ) + + //========================================================= + // > SCHED_TALKER_IDLE_HELLO + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_IDLE_HELLO, + + " Tasks" + " TASK_SET_ACTIVITY ACTIVITY:ACT_SIGNAL3" // Stop and talk + " TASK_TALKER_HELLO 0" // Try to say hello to player + " TASK_TALKER_EYECONTACT 0" + " TASK_WAIT 0.5" // wait a bit + " TASK_TALKER_HELLO 0" // Try to say hello to player + " TASK_TALKER_EYECONTACT 0" + " TASK_WAIT 0.5" // wait a bit + " TASK_TALKER_HELLO 0" // Try to say hello to player + " TASK_TALKER_EYECONTACT 0" + " TASK_WAIT 0.5" // wait a bit + " TASK_TALKER_HELLO 0" // Try to say hello to player + " TASK_TALKER_EYECONTACT 0" + " TASK_WAIT 0.5 " // wait a bit + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + " COND_HEAR_COMBAT" + " COND_HEAR_DANGER" + " COND_PLAYER_PUSHING" + " COND_GIVE_WAY" + ) + + //========================================================= + // > SCHED_TALKER_IDLE_STOP_SHOOTING + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_IDLE_STOP_SHOOTING, + + " Tasks" + " TASK_TALKER_STOPSHOOTING 0" // tell player to stop shooting friend + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // Scold the player before attacking. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_BETRAYED, + + " Tasks" + " TASK_TALKER_BETRAYED 0" + " TASK_WAIT 0.5" + "" + " Interrupts" + " COND_HEAR_DANGER" + ) + + //========================================================= + // > SCHED_TALKER_IDLE_WATCH_CLIENT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_IDLE_WATCH_CLIENT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_TALKER_LOOK_AT_CLIENT 6" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + " COND_HEAR_COMBAT" // sound flags - change these and you'll break the talking code. + " COND_HEAR_DANGER" + " COND_SMELL" + " COND_PLAYER_PUSHING" + " COND_TALKER_CLIENTUNSEEN" + " COND_GIVE_WAY" + " COND_IDLE_INTERRUPT" + ) + + //========================================================= + // > SCHED_TALKER_IDLE_WATCH_CLIENT_STARE + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_IDLE_WATCH_CLIENT_STARE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_TALKER_CLIENT_STARE 6" + " TASK_TALKER_STARE 0" + " TASK_TALKER_IDEALYAW 0" // look at who I'm talking to + " TASK_FACE_IDEAL 0 " + " TASK_SET_ACTIVITY ACTIVITY:ACT_SIGNAL3" + " TASK_TALKER_EYECONTACT 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + " COND_HEAR_COMBAT" // sound flags - change these and you'll break the talking code. + " COND_HEAR_DANGER" + " COND_SMELL" + " COND_PLAYER_PUSHING" + " COND_TALKER_CLIENTUNSEEN" + " COND_GIVE_WAY" + " COND_IDLE_INTERRUPT" + ) + + //========================================================= + // > SCHED_TALKER_IDLE_EYE_CONTACT + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_TALKER_IDLE_EYE_CONTACT, + + " Tasks" + " TASK_TALKER_IDEALYAW 0" // look at who I'm talking to + " TASK_FACE_IDEAL 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_SIGNAL3" + " TASK_TALKER_EYECONTACT 0" // Wait until speaker is done + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_PLAYER_PUSHING" + " COND_GIVE_WAY" + " COND_IDLE_INTERRUPT" + ) + +AI_END_CUSTOM_NPC() diff --git a/sp/src/game/server/npc_talker.h b/sp/src/game/server/npc_talker.h new file mode 100644 index 00000000..c8654177 --- /dev/null +++ b/sp/src/game/server/npc_talker.h @@ -0,0 +1,254 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef TALKNPC_H +#define TALKNPC_H + +#ifdef POSIX +#undef time +#include +#endif + +#ifndef _XBOX +#undef min +#undef max +#pragma warning(push) +#include +#pragma warning(pop) +#endif + +#ifdef _WIN32 +#pragma once +#endif + +// the include monkey's with the MAX() define, unbreak it +#undef MINMAX_H +#include "minmax.h" + +#include "ai_playerally.h" + +#include "soundflags.h" + +#include "ai_task.h" +#include "ai_schedule.h" +#include "ai_default.h" +#include "ai_speech.h" +#include "ai_basenpc.h" +#include "ai_behavior.h" +#include "ai_behavior_follow.h" + +#include "tier0/memdbgon.h" + +//========================================================= +// Talking NPC base class +// Used for scientists and barneys +//========================================================= + +#define TLK_CFRIENDS 4 + +//============================================================================= +// >> CNPCSimpleTalker +//============================================================================= + +#define MONOLOGNAME_LEN 16 // sentence names passed as monolog may be no longer than this. +#define AI_SP_START_MONOLOG '~' +#define AI_SP_MONOLOG_LINE '@' + +class CNPCSimpleTalker; + +class CNPCSimpleTalkerExpresser : public CAI_ComponentWithOuter +{ +public: + CNPCSimpleTalkerExpresser( CNPCSimpleTalker *pOuter ) + : CAI_ComponentWithOuter( pOuter ) + { + EndMonolog(); + } + + virtual int SpeakRawSentence( const char *pszSentence, float delay, float volume = VOL_NORM, soundlevel_t soundlevel = SNDLVL_TALKING, CBaseEntity *pListener = NULL ); + + // -------------------------------- + // + // Monologue operations + // + + bool HasMonolog( void ) { return m_iMonologIndex != -1; }; + void BeginMonolog( char *pszSentenceName, CBaseEntity *pListener ); + void EndMonolog( void ); + void SpeakMonolog( void ); + + void SuspendMonolog( float flInterval ); + void ResumeMonolog( void ); + + CBaseEntity *GetMonologueTarget() { return m_hMonologTalkTarget.Get(); } + + // -------------------------------- + // + // Monologue data + // + char m_szMonologSentence[MONOLOGNAME_LEN]; // The name of the sentence group for the monolog I'm speaking. + int m_iMonologIndex; // Which sentence from the group I should be speaking. + bool m_fMonologSuspended; + EHANDLE m_hMonologTalkTarget; // Who I'm trying to deliver my monolog to. + + DECLARE_SIMPLE_DATADESC(); +}; + +//------------------------------------- + +class CNPCSimpleTalker : public CAI_PlayerAlly +{ + DECLARE_CLASS( CNPCSimpleTalker, CAI_PlayerAlly ); +public: + void Precache( void ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + + virtual CAI_Expresser *CreateExpresser() { return new CNPCSimpleTalkerExpresser(this); } + + virtual void StartFollowing( CBaseEntity *pLeader ) { m_FollowBehavior.SetFollowTarget( pLeader ); DeferSchedulingToBehavior( &m_FollowBehavior ); } + virtual void StopFollowing( ) { m_FollowBehavior.SetFollowTarget( NULL ); DeferSchedulingToBehavior( NULL ); } + CBaseEntity *GetFollowTarget( void ) { return m_FollowBehavior.GetFollowTarget(); } + + virtual void OnChangeRunningBehavior( CAI_BehaviorBase *pOldBehavior, CAI_BehaviorBase *pNewBehavior ); + bool OnBehaviorChangeStatus( CAI_BehaviorBase *pBehavior, bool fCanFinishSchedule ); + + int PlayScriptedSentence( const char *pszSentence, float delay, float volume, soundlevel_t soundlevel, bool bConcurrent, CBaseEntity *pListener ); + virtual void FollowerUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void Event_Killed( const CTakeDamageInfo &info ); + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + bool CreateBehaviors() + { + AddBehavior( &m_FollowBehavior ); + return BaseClass::CreateBehaviors(); + } + + void BuildScheduleTestBits( void ); + void PrescheduleThink( void ); + virtual int SelectSchedule( void ); + virtual int SelectNonCombatSpeechSchedule(); + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + void HandleAnimEvent( animevent_t *pEvent ); + Activity NPC_TranslateActivity( Activity newActivity ); + + virtual void OnStartingFollow( CBaseEntity *pTarget ); + virtual void OnStoppingFollow( CBaseEntity *pTarget ); + + virtual void DeferAllIdleSpeech( float flDelay, CAI_BaseNPC *pIgnore = NULL ); + bool ShouldSpeakRandom( int iChance, float flModifier ); + + // For following + virtual void DeclineFollowing( void ) {} + void LimitFollowers( CBaseEntity *pPlayer, int maxFollowers ); + + float GetUseTime() const { return m_useTime; } + + //========================================================= + // TalkNPC schedules + //========================================================= + enum + { + SCHED_TALKER_IDLE_RESPONSE = BaseClass::NEXT_SCHEDULE, + SCHED_TALKER_IDLE_SPEAK, + SCHED_TALKER_IDLE_HELLO, + SCHED_TALKER_IDLE_STOP_SHOOTING, + SCHED_TALKER_IDLE_WATCH_CLIENT, + SCHED_TALKER_IDLE_WATCH_CLIENT_STARE, + SCHED_TALKER_IDLE_EYE_CONTACT, + SCHED_TALKER_BETRAYED, + + // !ALWAYS LAST! + NEXT_SCHEDULE, + }; + + //========================================================= + // TalkNPC tasks + //========================================================= + enum + { + TASK_TALKER_RESPOND = BaseClass::NEXT_TASK, // say my response + TASK_TALKER_SPEAK, // question or remark + TASK_TALKER_HELLO, // Try to say hello to player + TASK_TALKER_BETRAYED, // Player killed an ally + TASK_TALKER_HEADRESET, // reset head position + TASK_TALKER_STOPSHOOTING, // tell player to stop shooting friend + TASK_TALKER_STARE, // let the player know I know he's staring at me. + TASK_TALKER_LOOK_AT_CLIENT,// faces player if not moving and not talking and in idle. + TASK_TALKER_CLIENT_STARE, // same as look at client, but says something if the player stares. + TASK_TALKER_EYECONTACT, // maintain eyecontact with person who I'm talking to + TASK_TALKER_IDEALYAW, // set ideal yaw to face who I'm talking to + TASK_FIND_LOCK_HINTNODE_HEALTH, // Find & lock a nearby healthkit hintnode to heal myself at + TASK_TALKER_WAIT_FOR_SEMAPHORE, + + // !ALWAYS LAST! + NEXT_TASK, + }; + +//private: + virtual bool IsValidSpeechTarget( int flags, CBaseEntity *pEntity ); + + CBaseEntity *FindNearestFriend(bool fPlayer); + + bool IsOkToSpeak( void ); + + void SayHelloToPlayer( CBaseEntity *pPlayer ); + virtual bool CanSayHello( void ); + virtual int FIdleHello( void ); + + // Inputs + void InputIdleRespond( inputdata_t &inputdata ); + + // Conversations / communication + void IdleRespond( void ); + int FIdleSpeak( void ); + void FIdleSpeakWhileMoving( void ); + int FIdleStare( void ); + bool SpeakQuestionFriend( CBaseEntity *pFriend ); + bool SpeakAnswerFriend( CBaseEntity *pFriend ); + void TrySmellTalk( void ); + + virtual void SetAnswerQuestion( CNPCSimpleTalker *pSpeaker ); + + bool ShouldSuspendMonolog( void ); + bool ShouldResumeMonolog( void ); + void OnResumeMonolog() { Speak( TLK_RESUME ); } + + int m_nSpeak; // number of times initiated talking + float m_flNextIdleSpeechTime; + + static char *m_szFriends[TLK_CFRIENDS]; // array of friend names + CBaseEntity *EnumFriends( CBaseEntity *pentPrevious, int listNumber, bool bTrace ); + + virtual int FriendNumber( int arrayNumber ) { return arrayNumber; } + void ShutUpFriends( void ); + void AlertFriends( CBaseEntity *pKiller ); + + string_t m_iszUse; // Custom +USE sentence group (follow) + string_t m_iszUnUse; // Custom +USE sentence group (stop following) + +protected: + CAI_FollowBehavior m_FollowBehavior; + float m_useTime; // Don't allow +USE until this time + + //--------------------------------- + + DECLARE_DATADESC(); +#ifndef _XBOX + DEFINE_CUSTOM_AI; +#else +public: + DEFINE_CUSTOM_AI; +private: +#endif +}; + +#include "tier0/memdbgoff.h" + +#endif //TALKNPC_H diff --git a/sp/src/game/server/npc_vehicledriver.cpp b/sp/src/game/server/npc_vehicledriver.cpp new file mode 100644 index 00000000..8e38d769 --- /dev/null +++ b/sp/src/game/server/npc_vehicledriver.cpp @@ -0,0 +1,1196 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "ai_network.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_node.h" +#include "ai_task.h" +#include "ai_senses.h" +#include "ai_navigator.h" +#include "ai_route.h" +#include "entitylist.h" +#include "soundenvelope.h" +#include "gamerules.h" +#include "ndebugoverlay.h" +#include "soundflags.h" +#include "trains.h" +#include "globalstate.h" +#include "vehicle_base.h" +#include "npc_vehicledriver.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define DRIVER_DEBUG_PATH 1 +#define DRIVER_DEBUG_PATH_SPLINE 2 + +//------------------------------------ +// +//------------------------------------ +ConVar g_debug_vehicledriver( "g_debug_vehicledriver", "0", FCVAR_CHEAT ); + +BEGIN_DATADESC( CNPC_VehicleDriver ) + DEFINE_KEYFIELD( m_iszVehicleName, FIELD_STRING, "vehicle" ), +// DEFINE_FIELD( m_hVehicle, FIELD_EHANDLE ), + // DEFINE_FIELD( m_pVehicleInterface, FIELD_POINTER ), + DEFINE_FIELD( m_hVehicleEntity, FIELD_EHANDLE ), + // DEFINE_FIELD( m_Waypoints, FIELD_???? ), + // DEFINE_FIELD( m_pCurrentWaypoint, FIELD_POINTER ), + // DEFINE_FIELD( m_pNextWaypoint, FIELD_POINTER ), + DEFINE_FIELD( m_vecDesiredVelocity, FIELD_VECTOR ), + DEFINE_FIELD( m_vecDesiredPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecPrevPoint, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecPrevPrevPoint, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecPostPoint, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecPostPostPoint, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_flDistanceAlongSpline, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flDriversMaxSpeed, FIELD_FLOAT, "drivermaxspeed" ), + DEFINE_KEYFIELD( m_flDriversMinSpeed, FIELD_FLOAT, "driverminspeed" ), + DEFINE_FIELD( m_flMaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flGoalSpeed, FIELD_FLOAT ), + //DEFINE_KEYFIELD( m_flInitialSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flSteering, FIELD_FLOAT ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDriversMaxSpeed", InputSetDriversMaxSpeed ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDriversMinSpeed", InputSetDriversMinSpeed ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartForward", InputStartForward ), + DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartFiring", InputStartFiring ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopFiring", InputStopFiring ), + DEFINE_INPUTFUNC( FIELD_STRING, "GotoPathCorner", InputGotoPathCorner ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( npc_vehicledriver, CNPC_VehicleDriver ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CNPC_VehicleDriver::CNPC_VehicleDriver( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CNPC_VehicleDriver::~CNPC_VehicleDriver( void ) +{ + ClearWaypoints(); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CNPC_VehicleDriver::Spawn( void ) +{ + Precache( ); + + BaseClass::Spawn(); + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_MOVE_GROUND ); + CapabilitiesAdd( bits_CAP_MOVE_SHOOT ); + + SetModel( "models/roller_vehicledriver.mdl" ); + SetHullType(HULL_LARGE); + SetHullSizeNormal(); + m_iMaxHealth = m_iHealth = 1; + m_flFieldOfView = VIEW_FIELD_FULL; + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); + AddEffects( EF_NODRAW ); + + m_lifeState = LIFE_ALIVE; + SetCycle( 0 ); + ResetSequenceInfo(); + + AddFlag( FL_NPC ); + + m_flMaxSpeed = 0; + m_flGoalSpeed = m_flInitialSpeed; + + m_vecDesiredVelocity = vec3_origin; + m_vecPrevPoint = vec3_origin; + m_vecPrevPrevPoint = vec3_origin; + m_vecPostPoint = vec3_origin; + m_vecPostPostPoint = vec3_origin; + m_vecDesiredPosition = vec3_origin; + m_flSteering = 45; + m_flDistanceAlongSpline = 0.2; + m_pCurrentWaypoint = m_pNextWaypoint = NULL; + + GetNavigator()->SetPathcornerPathfinding( false ); + + NPCInit(); + + m_takedamage = DAMAGE_NO; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::Precache( void ) +{ + PrecacheModel( "models/roller_vehicledriver.mdl" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::Activate( void ) +{ + BaseClass::Activate(); + + // Restore doesn't need to do this + if ( m_hVehicleEntity ) + return; + + // Make sure we've got a vehicle + if ( m_iszVehicleName == NULL_STRING ) + { + Warning( "npc_vehicledriver %s has no vehicle to drive.\n", STRING(GetEntityName()) ); + UTIL_Remove( this ); + return; + } + + m_hVehicleEntity = (gEntList.FindEntityByName( NULL, STRING(m_iszVehicleName) )); + if ( !m_hVehicleEntity ) + { + Warning( "npc_vehicledriver %s couldn't find his vehicle named %s.\n", STRING(GetEntityName()), STRING(m_iszVehicleName) ); + UTIL_Remove( this ); + return; + } + + m_pVehicleInterface = m_hVehicleEntity->GetServerVehicle(); + Assert( m_pVehicleInterface ); + if ( !m_pVehicleInterface->NPC_CanDrive() ) + { + Warning( "npc_vehicledriver %s doesn't know how to drive vehicle %s.\n", STRING(GetEntityName()), STRING(m_hVehicleEntity->GetEntityName()) ); + UTIL_Remove( this ); + return; + } + + // We've found our vehicle. Move to it and start following it. + SetAbsOrigin( m_hVehicleEntity->WorldSpaceCenter() ); + m_pVehicleInterface->NPC_SetDriver( this ); + + RecalculateSpeeds(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::OnRestore( void ) +{ + BaseClass::OnRestore(); + if ( m_hVehicleEntity ) + { + m_pVehicleInterface = m_hVehicleEntity->GetServerVehicle(); + Assert( m_pVehicleInterface ); + } +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::UpdateOnRemove( void ) +{ + // Leave our vehicle + if ( m_pVehicleInterface ) + { + m_pVehicleInterface->NPC_SetDriver( NULL ); + } + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::PrescheduleThink( void ) +{ + if ( !m_hVehicleEntity ) + { + m_pVehicleInterface = NULL; + UTIL_Remove( this ); + return; + } + + // Keep up with my vehicle + SetAbsOrigin( m_hVehicleEntity->WorldSpaceCenter() ); + SetAbsAngles( m_hVehicleEntity->GetAbsAngles() ); + + BaseClass::PrescheduleThink(); + + if ( m_NPCState == NPC_STATE_IDLE ) + { + m_pVehicleInterface->NPC_Brake(); + return; + } + + // If we've been picked up by something (dropship probably), abort. + if ( m_hVehicleEntity->GetParent() ) + { + SetState( NPC_STATE_IDLE ); + ClearWaypoints(); + SetGoalEnt( NULL ); + return; + } + + DriveVehicle(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CNPC_VehicleDriver::SelectSchedule( void ) +{ + // Vehicle driver hangs in the air inside the vehicle, so we never need to fall to ground + ClearCondition( COND_FLOATING_OFF_GROUND ); + + if ( HasSpawnFlags(SF_VEHICLEDRIVER_INACTIVE) ) + { + SetState( NPC_STATE_IDLE ); + return SCHED_VEHICLEDRIVER_INACTIVE; + } + + if ( GetGoalEnt() ) + return SCHED_VEHICLEDRIVER_DRIVE_PATH; + + switch ( m_NPCState ) + { + case NPC_STATE_IDLE: + break; + + case NPC_STATE_ALERT: + break; + + case NPC_STATE_COMBAT: + { + if ( HasCondition(COND_NEW_ENEMY) || HasCondition( COND_ENEMY_DEAD ) ) + return BaseClass::SelectSchedule(); + + if ( HasCondition(COND_SEE_ENEMY) ) + { + // we can see the enemy + if ( HasCondition(COND_CAN_RANGE_ATTACK2) ) + return SCHED_RANGE_ATTACK2; + if ( HasCondition(COND_CAN_RANGE_ATTACK1) ) + return SCHED_RANGE_ATTACK1; + + // What to do here? Not necessarily easy to face enemy. + //if ( HasCondition(COND_NOT_FACING_ATTACK) ) + //return SCHED_COMBAT_FACE; + } + + // We can see him, but can't shoot him. Just wait and hope he comes closer. + return SCHED_VEHICLEDRIVER_COMBAT_WAIT; + } + break; + } + + return BaseClass::SelectSchedule(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CNPC_VehicleDriver::RangeAttack1Conditions( float flDot, float flDist ) +{ + // Vehicle not ready to fire again yet? + if ( m_pVehicleInterface->Weapon_PrimaryCanFireAt() > gpGlobals->curtime ) + return 0; + + // Check weapon range + float flMinRange, flMaxRange; + m_pVehicleInterface->Weapon_PrimaryRanges( &flMinRange, &flMaxRange ); + if ( flDist < flMinRange ) + return COND_TOO_CLOSE_TO_ATTACK; + if ( flDist > flMaxRange ) + return COND_TOO_FAR_TO_ATTACK; + + // Don't shoot backwards + Vector vecForward; + Vector vecToTarget = (GetEnemy()->GetAbsOrigin() - GetAbsOrigin()); + VectorNormalize(vecToTarget); + m_hVehicleEntity->GetVectors( &vecForward, NULL, NULL ); + float flForwardDot = DotProduct( vecForward, vecToTarget ); + if ( flForwardDot < 0 && fabs(flDot) < 0.5 ) + return COND_NOT_FACING_ATTACK; + + return COND_CAN_RANGE_ATTACK1; +} + +//========================================================= +// RangeAttack2Conditions +//========================================================= +int CNPC_VehicleDriver::RangeAttack2Conditions( float flDot, float flDist ) +{ + // Vehicle not ready to fire again yet? + if ( m_pVehicleInterface->Weapon_SecondaryCanFireAt() > gpGlobals->curtime ) + return 0; + + // Check weapon range + float flMinRange, flMaxRange; + m_pVehicleInterface->Weapon_SecondaryRanges( &flMinRange, &flMaxRange ); + if ( flDist < flMinRange ) + return COND_TOO_CLOSE_TO_ATTACK; + if ( flDist > flMaxRange ) + return COND_TOO_FAR_TO_ATTACK; + + return COND_CAN_RANGE_ATTACK2; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CNPC_VehicleDriver::TranslateSchedule( int scheduleType ) +{ + switch ( scheduleType ) + { + case SCHED_COMBAT_FACE: + { + // Vehicles can't rotate, so don't try and face + return TranslateSchedule( SCHED_CHASE_ENEMY ); + } + break; + + case SCHED_ALERT_FACE: + { + // Vehicles can't rotate, so don't try and face + return SCHED_ALERT_STAND; + } + break; + + case SCHED_CHASE_ENEMY_FAILED: + case SCHED_FAIL: + { + return SCHED_FAIL; + } + break; + } + + return BaseClass::TranslateSchedule(scheduleType); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pTask - +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_RUN_PATH: + case TASK_WALK_PATH: + TaskComplete(); + break; + + case TASK_FACE_IDEAL: + case TASK_FACE_ENEMY: + { + // Vehicle ignores face commands, since it can't rotate on the spot. + TaskComplete(); + } + break; + + case TASK_VEHICLEDRIVER_GET_PATH: + { + if ( !GetGoalEnt() ) + { + TaskFail( FAIL_NO_TARGET ); + return; + } + + CheckForTeleport(); + + if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH ) + { + NDebugOverlay::Box( GetGoalEnt()->GetAbsOrigin(), -Vector(50,50,50), Vector(50,50,50), 255,255,255, true, 5); + } + + AI_NavGoal_t goal( GOALTYPE_PATHCORNER, GetGoalEnt()->GetLocalOrigin(), ACT_WALK, AIN_DEF_TOLERANCE, AIN_YAW_TO_DEST); + if ( !GetNavigator()->SetGoal( goal ) ) + { + TaskFail( FAIL_NO_ROUTE ); + return; + } + + TaskComplete(); + } + break; + + case TASK_WAIT_FOR_MOVEMENT: + { + if (GetNavigator()->GetGoalType() == GOALTYPE_NONE) + { + TaskComplete(); + GetNavigator()->StopMoving(); // Stop moving + } + else if (!GetNavigator()->IsGoalActive()) + { + SetIdealActivity( GetStoppedActivity() ); + } + else + { + // Check validity of goal type + ValidateNavGoal(); + } + } + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::RunTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_RANGE_ATTACK1: + { + // Vehicle driver has no animations, so fire a burst at the target + CBaseEntity *pEnemy = GetEnemy(); + if ( pEnemy ) + { + // TODO: Get a bodytarget from the firing point of the gun in the vehicle + Vector vecTarget = GetEnemy()->BodyTarget( GetAbsOrigin(), false ); + m_pVehicleInterface->NPC_AimPrimaryWeapon( vecTarget ); + m_pVehicleInterface->NPC_PrimaryFire(); + TaskComplete(); + } + else + { + TaskFail(FAIL_NO_ENEMY); + return; + } + } + break; + + case TASK_RANGE_ATTACK2: + { + // Vehicle driver has no animations, so fire a burst at the target + CBaseEntity *pEnemy = GetEnemy(); + if ( pEnemy ) + { + // TODO: Get a bodytarget from the firing point of the gun in the vehicle + Vector vecTarget = GetEnemy()->BodyTarget( GetAbsOrigin(), false ); + m_pVehicleInterface->NPC_AimSecondaryWeapon( vecTarget ); + m_pVehicleInterface->NPC_SecondaryFire(); + TaskComplete(); + } + else + { + TaskFail(FAIL_NO_ENEMY); + return; + } + } + break; + + case TASK_WAIT_FOR_MOVEMENT: + { + BaseClass::RunTask( pTask ); + + if ( HasCondition(COND_SEE_ENEMY) ) + { + // we can see the enemy + if ( HasCondition(COND_CAN_RANGE_ATTACK2) ) + { + ChainRunTask( TASK_RANGE_ATTACK2, pTask->flTaskData ); + } + if ( HasCondition(COND_CAN_RANGE_ATTACK1) ) + { + ChainRunTask( TASK_RANGE_ATTACK1, pTask->flTaskData ); + } + } + } + break; + + default: + BaseClass::RunTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::GatherEnemyConditions( CBaseEntity *pEnemy ) +{ + BaseClass::GatherEnemyConditions(pEnemy); +} + +//----------------------------------------------------------------------------- +// Purpose: Overridden because if the player is a criminal, we hate them. +//----------------------------------------------------------------------------- +Disposition_t CNPC_VehicleDriver::IRelationType(CBaseEntity *pTarget) +{ + // If it's the player and they are a criminal, we hate them. + if ( pTarget && pTarget->Classify() == CLASS_PLAYER) + { + if (GlobalEntity_GetState("gordon_precriminal") == GLOBAL_ON) + { + return(D_NU); + } + } + + return(BaseClass::IRelationType(pTarget)); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_VehicleDriver::OverrideMove( float flInterval ) +{ + if ( !m_hVehicleEntity ) + return true; + + // If we don't have a maxspeed, we've been stopped, so abort early + // Or we've been picked up by something (dropship probably). + if ( !m_flMaxSpeed || m_hVehicleEntity->GetParent() ) + { + m_pVehicleInterface->NPC_Brake(); + return true; + } + + // ----------------------------------------------------------------- + // If I have a route, keep it updated and move toward target + // ------------------------------------------------------------------ + if (GetNavigator()->IsGoalActive()) + { + if ( OverridePathMove( flInterval ) ) + return true; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::CalculatePostPoints( void ) +{ + m_vecPostPoint = m_vecDesiredPosition; + m_vecPostPostPoint = m_vecDesiredPosition; + + // If we have a waypoint beyond our current, use it instead. + if ( !GetNavigator()->CurWaypointIsGoal() ) + { + AI_Waypoint_t *pCurWaypoint = GetNavigator()->GetPath()->GetCurWaypoint(); + m_vecPostPoint = pCurWaypoint->GetNext()->GetPos(); + if ( pCurWaypoint->GetNext()->GetNext() ) + { + m_vecPostPostPoint = pCurWaypoint->GetNext()->GetNext()->GetPos(); + } + else + { + m_vecPostPostPoint = m_vecPostPoint; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Destroy our current waypoints +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::ClearWaypoints( void ) +{ + m_vecDesiredPosition = vec3_origin; + if ( m_pCurrentWaypoint ) + { + delete m_pCurrentWaypoint; + m_pCurrentWaypoint = NULL; + } + if ( m_pNextWaypoint ) + { + delete m_pNextWaypoint; + m_pNextWaypoint = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: We've hit a waypoint. Handle it, and return true if this is the +// end of the path. +//----------------------------------------------------------------------------- +bool CNPC_VehicleDriver::WaypointReached( void ) +{ + // We reached our current waypoint. + m_vecPrevPrevPoint = m_vecPrevPoint; + m_vecPrevPoint = GetAbsOrigin(); + + // If we've got to our goal, we're done here. + if ( GetNavigator()->CurWaypointIsGoal() ) + { + // Necessary for InPass outputs to be fired, is a no-op otherwise + GetNavigator()->AdvancePath(); + + // Stop pathing + ClearWaypoints(); + TaskComplete(); + SetGoalEnt( NULL ); + return true; + } + + AI_Waypoint_t *pCurWaypoint = GetNavigator()->GetPath()->GetCurWaypoint(); + if ( !pCurWaypoint ) + return false; + + // Check to see if the waypoint wants us to change speed + if ( pCurWaypoint->Flags() & bits_WP_TO_PATHCORNER ) + { + CBaseEntity *pEntity = pCurWaypoint->hPathCorner; + if ( pEntity ) + { + if ( pEntity->m_flSpeed > 0 ) + { + if ( pEntity->m_flSpeed <= 1.0 ) + { + m_flDriversMaxSpeed = pEntity->m_flSpeed; + RecalculateSpeeds(); + } + else + { + Warning("path_track %s tried to tell the npc_vehicledriver to set speed to %.3f. npc_vehicledriver only accepts values between 0 and 1.\n", STRING(pEntity->GetEntityName()), pEntity->m_flSpeed ); + } + } + } + } + + // Get the waypoints for the next part of the path + GetNavigator()->AdvancePath(); + if ( !GetNavigator()->GetPath()->GetCurWaypoint() ) + { + ClearWaypoints(); + TaskComplete(); + SetGoalEnt( NULL ); + return true; + } + + m_vecDesiredPosition = GetNavigator()->GetCurWaypointPos(); + CalculatePostPoints(); + + // Move to the next waypoint + delete m_pCurrentWaypoint; + m_pCurrentWaypoint = m_pNextWaypoint; + m_Waypoints[1] = new CVehicleWaypoint( m_vecPrevPoint, m_vecDesiredPosition, m_vecPostPoint, m_vecPostPostPoint ); + m_pNextWaypoint = m_Waypoints[1]; + + // Drop the spline marker back + m_flDistanceAlongSpline = MAX( 0, m_flDistanceAlongSpline - 1.0 ); + + CheckForTeleport(); + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_VehicleDriver::OverridePathMove( float flInterval ) +{ + // Setup our initial path data if we've just started running a path + if ( !m_pCurrentWaypoint ) + { + m_vecPrevPoint = GetAbsOrigin(); + m_vecPrevPrevPoint = GetAbsOrigin(); + m_vecDesiredPosition = GetNavigator()->GetCurWaypointPos(); + CalculatePostPoints(); + + // Init our two waypoints + m_Waypoints[0] = new CVehicleWaypoint( m_vecPrevPrevPoint, m_vecPrevPoint, m_vecDesiredPosition, m_vecPostPoint ); + m_Waypoints[1] = new CVehicleWaypoint( m_vecPrevPoint, m_vecDesiredPosition, m_vecPostPoint, m_vecPostPostPoint ); + m_pCurrentWaypoint = m_Waypoints[0]; + m_pNextWaypoint = m_Waypoints[1]; + + m_flDistanceAlongSpline = 0.2; + } + + // Have we reached our target? See if we've passed the current waypoint's plane. + Vector vecAbsMins, vecAbsMaxs; +#ifdef MAPBASE + vecAbsMins = m_hVehicleEntity->CollisionProp()->OBBMins(); + vecAbsMaxs = m_hVehicleEntity->CollisionProp()->OBBMaxs(); + m_hVehicleEntity->CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); +#else + CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); +#endif + if ( BoxOnPlaneSide( vecAbsMins, vecAbsMaxs, &m_pCurrentWaypoint->planeWaypoint ) == 3 ) + { + if ( WaypointReached() ) + return true; + } + + // Did we bypass it and reach the next one already? + if ( m_pNextWaypoint && BoxOnPlaneSide( vecAbsMins, vecAbsMaxs, &m_pNextWaypoint->planeWaypoint ) == 3 ) + { + if ( WaypointReached() ) + return true; + } + + // We may have just teleported, so check to make sure we have a waypoint + if ( !m_pCurrentWaypoint || !m_pNextWaypoint ) + return false; + + // Figure out which spline we're trucking along + CVehicleWaypoint *pCurrentSplineBeingTraversed = m_pCurrentWaypoint; + if ( m_flDistanceAlongSpline > 1 ) + { + pCurrentSplineBeingTraversed = m_pNextWaypoint; + } + + // Get our current speed, and check it against the length of the spline to know how far to advance our marker + AngularImpulse angVel; + Vector vecVelocity; + IPhysicsObject *pVehiclePhysics = m_hVehicleEntity->VPhysicsGetObject(); + + if( !pVehiclePhysics ) + { + // I think my vehicle has been destroyed. + return false; + } + + pVehiclePhysics->GetVelocity( &vecVelocity, &angVel ); + float flSpeed = vecVelocity.Length(); + float flIncTime = gpGlobals->curtime - GetLastThink(); + float flIncrement = flIncTime * (flSpeed / pCurrentSplineBeingTraversed->GetLength()); + + // Now advance our point along the spline + m_flDistanceAlongSpline = clamp( m_flDistanceAlongSpline + flIncrement, 0.f, 2.f); + if ( m_flDistanceAlongSpline > 1 ) + { + // We crossed the spline boundary + pCurrentSplineBeingTraversed = m_pNextWaypoint; + } + + Vector vSplinePoint = pCurrentSplineBeingTraversed->GetPointAt( m_flDistanceAlongSpline > 1 ? m_flDistanceAlongSpline-1 : m_flDistanceAlongSpline ); + Vector vSplineTangent = pCurrentSplineBeingTraversed->GetTangentAt( m_flDistanceAlongSpline > 1 ? m_flDistanceAlongSpline-1 : m_flDistanceAlongSpline ); + + // Now that we've got the target spline point & tangent, use it to decide what our desired velocity is. + // If we're close to the tangent, just use the tangent. Otherwise, Lerp towards it. + Vector vecToDesired = (vSplinePoint - GetAbsOrigin()); + float flDistToDesired = VectorNormalize( vecToDesired ); + float flTangentLength = VectorNormalize( vSplineTangent ); + + if ( flDistToDesired > (flTangentLength * 0.75) ) + { + m_vecDesiredVelocity = vecToDesired * flTangentLength; + } + else + { + VectorLerp( vSplineTangent, vecToDesired * flTangentLength, (flDistToDesired / (flTangentLength * 0.5)), m_vecDesiredVelocity ); + } + + // Decrease speed according to the turn we're trying to make + Vector vecRight; + m_hVehicleEntity->GetVectors( NULL, &vecRight, NULL ); + Vector vecNormVel = m_vecDesiredVelocity; + VectorNormalize( vecNormVel ); + float flDotRight = DotProduct( vecRight, vecNormVel ); + flSpeed = (1.0 - fabs(flDotRight)); + // Don't go slower than we've been told to go + if ( flSpeed < m_flDriversMinSpeed ) + { + flSpeed = m_flDriversMinSpeed; + } + m_vecDesiredVelocity = vecNormVel * (flSpeed * m_flMaxSpeed); + + // Bunch o'debug + if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH ) + { + NDebugOverlay::Box( m_vecPrevPrevPoint, -Vector(15,15,15), Vector(15,15,15), 192,0,0, true, 0.1); + NDebugOverlay::Box( m_vecPrevPoint, -Vector(20,20,20), Vector(20,20,20), 255,0,0, true, 0.1); + NDebugOverlay::Box( m_vecPostPoint, -Vector(20,20,20), Vector(20,20,20), 0,192,0, true, 0.1); + NDebugOverlay::Box( m_vecPostPostPoint, -Vector(20,20,20), Vector(20,20,20), 0,128,0, true, 0.1); + NDebugOverlay::Box( vSplinePoint, -Vector(10,10,10), Vector(10,10,10), 0,0,255, true, 0.1); + NDebugOverlay::Line( vSplinePoint, vSplinePoint + (vSplineTangent * 40), 0,0,255, true, 0.1); + + //NDebugOverlay::HorzArrow( pCurrentSplineBeingTraversed->splinePoints[0], pCurrentSplineBeingTraversed->splinePoints[1], 30, 255,255,255,0, false, 0.1f ); + //NDebugOverlay::HorzArrow( pCurrentSplineBeingTraversed->splinePoints[1], pCurrentSplineBeingTraversed->splinePoints[2], 20, 255,255,255,0, false, 0.1f ); + //NDebugOverlay::HorzArrow( pCurrentSplineBeingTraversed->splinePoints[2], pCurrentSplineBeingTraversed->splinePoints[3], 10, 255,255,255,0, false, 0.1f ); + + // Draw the plane we're checking against for waypoint passing + Vector vecPlaneRight; + CrossProduct( m_pCurrentWaypoint->planeWaypoint.normal, Vector(0,0,1), vecPlaneRight ); + Vector vecPlane = m_pCurrentWaypoint->splinePoints[2]; + NDebugOverlay::Line( vecPlane + (vecPlaneRight * -100), vecPlane + (vecPlaneRight * 100), 255,0,0, true, 0.1); + + // Draw the next plane too + CrossProduct( m_pNextWaypoint->planeWaypoint.normal, Vector(0,0,1), vecPlaneRight ); + vecPlane = m_pNextWaypoint->splinePoints[2]; + NDebugOverlay::Line( vecPlane + (vecPlaneRight * -100), vecPlane + (vecPlaneRight * 100), 192,0,0, true, 0.1); + } + + if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH_SPLINE ) + { + for ( int i = 0; i < 10; i++ ) + { + Vector vecTarget = m_pCurrentWaypoint->GetPointAt( 0.1 * i ); + Vector vecTangent = m_pCurrentWaypoint->GetTangentAt( 0.1 * i ); + VectorNormalize(vecTangent); + NDebugOverlay::Box( vecTarget, -Vector(10,10,10), Vector(10,10,10), 255,0,0, true, 0.1 ); + NDebugOverlay::Line( vecTarget, vecTarget + (vecTangent * 10), 255,255,0, true, 0.1); + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: This takes the current place the NPC's trying to get to, figures out +// what keys to press to get the vehicle to go there, and then sends +// them to the vehicle. +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::DriveVehicle( void ) +{ + AngularImpulse angVel; + Vector vecVelocity; + IPhysicsObject *pVehiclePhysics = m_hVehicleEntity->VPhysicsGetObject(); + if ( !pVehiclePhysics ) + return; + pVehiclePhysics->GetVelocity( &vecVelocity, &angVel ); + float flSpeed = VectorNormalize( vecVelocity ); + + // If we have no target position to drive to, brake to a halt + if ( !m_flMaxSpeed || m_vecDesiredPosition == vec3_origin ) + { + if ( flSpeed > 1 ) + { + m_pVehicleInterface->NPC_Brake(); + } + return; + } + + if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH ) + { + NDebugOverlay::Box(m_vecDesiredPosition, -Vector(20,20,20), Vector(20,20,20), 0,255,0, true, 0.1); + NDebugOverlay::Line(GetAbsOrigin(), GetAbsOrigin() + m_vecDesiredVelocity, 0,255,0, true, 0.1); + } + + m_flGoalSpeed = VectorNormalize(m_vecDesiredVelocity); + + // Is our target in front or behind us? + Vector vecForward, vecRight; + m_hVehicleEntity->GetVectors( &vecForward, &vecRight, NULL ); + float flDot = DotProduct( vecForward, m_vecDesiredVelocity ); + bool bBehind = ( flDot < 0 ); + float flVelDot = DotProduct( vecVelocity, m_vecDesiredVelocity ); + bool bGoingWrongWay = ( flVelDot < 0 ); + + // Figure out whether we should accelerate / decelerate + if ( bGoingWrongWay || (flSpeed < m_flGoalSpeed) ) + { + // If it's behind us, go backwards not forwards + if ( bBehind ) + { + m_pVehicleInterface->NPC_ThrottleReverse(); + } + else + { + m_pVehicleInterface->NPC_ThrottleForward(); + } + } + else + { + // Brake if we're go significantly too fast + if ( (flSpeed - 200) > m_flGoalSpeed ) + { + m_pVehicleInterface->NPC_Brake(); + } + else + { + m_pVehicleInterface->NPC_ThrottleCenter(); + } + } + + // Do we need to turn? + float flDotRight = DotProduct( vecRight, m_vecDesiredVelocity ); + if ( bBehind ) + { + // If we're driving backwards, flip our turning + flDotRight *= -1; + } + // Map it to the vehicle's steering + flDotRight *= (m_flSteering / 90); + + if ( flDotRight < 0 ) + { + // Turn left + m_pVehicleInterface->NPC_TurnLeft( -flDotRight ); + } + else if ( flDotRight > 0 ) + { + // Turn right + m_pVehicleInterface->NPC_TurnRight( flDotRight ); + } + else + { + m_pVehicleInterface->NPC_TurnCenter(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Check to see if we should teleport to the current path corner +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::CheckForTeleport( void ) +{ + if ( !GetGoalEnt() ) + return; + + CPathTrack *pTrack = dynamic_cast( GetGoalEnt() ); + if ( !pTrack ) + return; + + // Does it have the teleport flag set? + if ( pTrack->HasSpawnFlags( SF_PATH_TELEPORT ) ) + { + IncrementInterpolationFrame(); + + // Teleport the vehicle to the pathcorner + Vector vecMins, vecMaxs; + vecMins = m_hVehicleEntity->CollisionProp()->OBBMins(); + vecMaxs = m_hVehicleEntity->CollisionProp()->OBBMaxs(); + Vector vecTarget = pTrack->GetAbsOrigin() - (vecMins + vecMaxs) * 0.5; + vecTarget.z += ((vecMaxs.z - vecMins.z) * 0.5) + 8; // Safety buffer + + // Orient it to face the next point + QAngle vecAngles = pTrack->GetAbsAngles(); + Vector vecToTarget = vec3_origin; + if ( pTrack->GetNext() ) + { + vecToTarget = (pTrack->GetNext()->GetAbsOrigin() - pTrack->GetAbsOrigin()); + VectorNormalize( vecToTarget ); + + // Vehicles are rotated 90 degrees + VectorAngles( vecToTarget, vecAngles ); + vecAngles[YAW] -= 90; + } + m_hVehicleEntity->Teleport( &vecTarget, &vecAngles, &vec3_origin ); + + // Teleport the driver + SetAbsOrigin( m_hVehicleEntity->WorldSpaceCenter() ); + SetAbsAngles( m_hVehicleEntity->GetAbsAngles() ); + + m_vecPrevPoint = pTrack->GetAbsOrigin(); + + // Move to the next waypoint, we've reached this one + if ( GetNavigator()->GetPath() ) + { + WaypointReached(); + } + + // Clear our waypoints, because the next waypoint is certainly invalid now. + ClearWaypoints(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CNPC_VehicleDriver::GetDefaultNavGoalTolerance() +{ + return 48; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::RecalculateSpeeds( void ) +{ + // Get data from the vehicle + const vehicleparams_t *pParams = m_pVehicleInterface->GetVehicleParams(); + if ( pParams ) + { + m_flMaxSpeed = pParams->engine.maxSpeed * m_flDriversMaxSpeed; + m_flSteering = pParams->steering.degreesSlow; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::InputSetDriversMaxSpeed( inputdata_t &inputdata ) +{ + m_flDriversMaxSpeed = inputdata.value.Float(); + + RecalculateSpeeds(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::InputSetDriversMinSpeed( inputdata_t &inputdata ) +{ + m_flDriversMinSpeed = inputdata.value.Float(); + + RecalculateSpeeds(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::InputStartForward( inputdata_t &inputdata ) +{ + CLEARBITS( m_spawnflags, SF_VEHICLEDRIVER_INACTIVE ); + if ( m_NPCState == NPC_STATE_IDLE ) + { + SetState( NPC_STATE_ALERT ); + } + SetCondition( COND_PROVOKED ); + + RecalculateSpeeds(); +} + +//----------------------------------------------------------------------------- +// Purpose: Tell the driver to stop moving +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::InputStop( inputdata_t &inputdata ) +{ + m_flMaxSpeed = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Tell the driver to start firing at targets +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::InputStartFiring( inputdata_t &inputdata ) +{ + CLEARBITS( m_spawnflags, SF_VEHICLEDRIVER_INACTIVE ); + SetCondition( COND_PROVOKED ); + + float flMinRange, flMaxRange; + // If the vehicle has a weapon, set our capability + if ( m_pVehicleInterface->NPC_HasPrimaryWeapon() ) + { + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 ); + m_pVehicleInterface->Weapon_PrimaryRanges( &flMinRange, &flMaxRange ); + + // Ensure the look distances is long enough + if ( m_flDistTooFar < flMaxRange || GetSenses()->GetDistLook() < flMaxRange ) + { + m_flDistTooFar = flMaxRange; + SetDistLook( flMaxRange ); + } + } + + if ( m_pVehicleInterface->NPC_HasSecondaryWeapon() ) + { + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK2 ); + m_pVehicleInterface->Weapon_SecondaryRanges( &flMinRange, &flMaxRange ); + + // Ensure the look distances is long enough + if ( m_flDistTooFar < flMaxRange || GetSenses()->GetDistLook() < flMaxRange ) + { + m_flDistTooFar = flMaxRange; + SetDistLook( flMaxRange ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Tell the driver to stop firing at targets +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::InputStopFiring( inputdata_t &inputdata ) +{ + // If the vehicle has a weapon, set our capability + CapabilitiesRemove( bits_CAP_INNATE_RANGE_ATTACK1 ); + CapabilitiesRemove( bits_CAP_INNATE_RANGE_ATTACK2 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_VehicleDriver::InputGotoPathCorner( inputdata_t &inputdata ) +{ + string_t iszPathName = inputdata.value.StringID(); + if ( iszPathName != NULL_STRING ) + { + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, iszPathName ); + if ( !pEntity ) + { + Warning("npc_vehicledriver %s couldn't find entity named %s\n", STRING(GetEntityName()), STRING(iszPathName) ); + return; + } + + ClearWaypoints(); + + // Drive to the point + SetGoalEnt( pEntity ); + if ( m_NPCState == NPC_STATE_IDLE ) + { + SetState( NPC_STATE_ALERT ); + } + SetCondition( COND_PROVOKED ); + + // Force him to start forward + InputStartForward( inputdata ); + } +} + +//----------------------------------------------------------------------------- +// +// Schedules +// +//----------------------------------------------------------------------------- + +AI_BEGIN_CUSTOM_NPC( npc_vehicledriver, CNPC_VehicleDriver ) + + //Tasks + DECLARE_TASK( TASK_VEHICLEDRIVER_GET_PATH ) + + // Schedules + DEFINE_SCHEDULE + ( + SCHED_VEHICLEDRIVER_INACTIVE, + + " Tasks" + " TASK_WAIT_INDEFINITE 0" + "" + " Interrupts" + " COND_PROVOKED" + ) + + DEFINE_SCHEDULE + ( + SCHED_VEHICLEDRIVER_COMBAT_WAIT, + + " Tasks" + " TASK_WAIT 5" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_PROVOKED" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + ) + + DEFINE_SCHEDULE + ( + SCHED_VEHICLEDRIVER_DRIVE_PATH, + + " Tasks" + " TASK_VEHICLEDRIVER_GET_PATH 0" + " TASK_WALK_PATH 9999" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_WAIT_PVS 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_PROVOKED" + ) + +AI_END_CUSTOM_NPC() diff --git a/sp/src/game/server/npc_vehicledriver.h b/sp/src/game/server/npc_vehicledriver.h new file mode 100644 index 00000000..d59b4433 --- /dev/null +++ b/sp/src/game/server/npc_vehicledriver.h @@ -0,0 +1,197 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: NPC that drives vehicles +// +//=============================================================================// + +#ifndef NPC_VEHICLEDRIVER_H +#define NPC_VEHICLEDRIVER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "ai_basenpc.h" + +class CPropVehicleDriveable; + +//------------------------------------ +// Spawnflags +//------------------------------------ +#define SF_VEHICLEDRIVER_INACTIVE (1 << 16) + +//========================================================= +// Custom schedules +//========================================================= +enum +{ + SCHED_VEHICLEDRIVER_INACTIVE = LAST_SHARED_SCHEDULE, + SCHED_VEHICLEDRIVER_COMBAT_WAIT, + SCHED_VEHICLEDRIVER_DRIVE_PATH, + + LAST_VEHICLEDRIVER_SCHED, +}; + +//========================================================= +// Custom tasks +//========================================================= +enum +{ + TASK_VEHICLEDRIVER_GET_PATH = LAST_SHARED_TASK, + + LAST_VEHICLEDRIVER_TASK, +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CVehicleWaypoint +{ +public: + CVehicleWaypoint( Vector &pPrevPoint, Vector &pCurPoint, Vector &pNextPoint, Vector &pNextNextPoint ) + { + splinePoints[0] = pPrevPoint; + splinePoints[1] = pCurPoint; + splinePoints[2] = pNextPoint; + splinePoints[3] = pNextNextPoint; + + RecalculateSpline(); + } + + void RecalculateSpline( void ) + { + planeWaypoint.normal = (splinePoints[2] - splinePoints[1]); + VectorNormalize( planeWaypoint.normal ); + planeWaypoint.type = PLANE_ANYZ; + planeWaypoint.dist = DotProduct( planeWaypoint.normal, splinePoints[2] ); + planeWaypoint.signbits = SignbitsForPlane(&planeWaypoint); + // TODO: Use the vehicle's absbox + iInitialPlaneSide = BoxOnPlaneSide( -Vector(32,32,32), Vector(32,32,32), &planeWaypoint ); + + // Hackily calculate a length for the spline. Subdivide & measure. + flSplineLength = 0; + Vector vecPrev = splinePoints[1]; + const int iDivs = 10; + for ( int i = 1; i <= iDivs; i++ ) + { + Vector vecCurr; + float flT = (float)i / (float)iDivs; + Catmull_Rom_Spline( splinePoints[0], splinePoints[1], splinePoints[2], splinePoints[3], flT, vecCurr ); + flSplineLength += (vecCurr - vecPrev).Length(); + vecPrev = vecCurr; + } + } + + Vector GetPointAt( float flT ) + { + Vector vecCurr(0,0,0); + Catmull_Rom_Spline( splinePoints[0], splinePoints[1], splinePoints[2], splinePoints[3], flT, vecCurr ); + return vecCurr; + } + + Vector GetTangentAt( float flT ) + { + Vector vecCurr(0,0,0); + Catmull_Rom_Spline_Tangent( splinePoints[0], splinePoints[1], splinePoints[2], splinePoints[3], flT, vecCurr ); + return vecCurr; + } + + float GetLength( void ) + { + return flSplineLength; + } + +public: + int iInitialPlaneSide; + float flSplineLength; + Vector splinePoints[4]; + cplane_t planeWaypoint; +}; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CNPC_VehicleDriver : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_VehicleDriver, CAI_BaseNPC ); +public: + DECLARE_DATADESC(); + DEFINE_CUSTOM_AI; + + CNPC_VehicleDriver( void ); + ~CNPC_VehicleDriver( void ); + + virtual void Spawn( void ); + virtual void Precache( void ); + virtual void Activate( void ); + virtual void OnRestore(); + virtual void UpdateOnRemove( void ); + + // AI + void UpdateEfficiency( bool bInPVS ) { SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); SetMoveEfficiency( AIME_NORMAL ); } + virtual void PrescheduleThink( void ); + virtual int TranslateSchedule( int scheduleType ); + virtual int SelectSchedule( void ); + virtual void StartTask( const Task_t *pTask ); + virtual void RunTask( const Task_t *pTask ); + virtual void GatherEnemyConditions( CBaseEntity *pEnemy ); + virtual int RangeAttack1Conditions( float flDot, float flDist ); + virtual int RangeAttack2Conditions( float flDot, float flDist ); + + // Driving + virtual void DriveVehicle( void ); + virtual bool OverrideMove( float flInterval ); + bool OverridePathMove( float flInterval ); + void CalculatePostPoints( void ); + bool WaypointReached( void ); + float GetDefaultNavGoalTolerance(); + void RecalculateSpeeds( void ); + void ClearWaypoints( void ); + void CheckForTeleport( void ); + + int BloodColor( void ) { return DONT_BLEED; } + +#ifdef HL2_DLL + Class_T Classify( void ) { return CLASS_METROPOLICE; } +#else + Class_T Classify( void ) { return CLASS_NONE; } +#endif + + Disposition_t IRelationType( CBaseEntity *pTarget ); + + // Inputs + void InputSetDriversMaxSpeed( inputdata_t &inputdata ); + void InputSetDriversMinSpeed( inputdata_t &inputdata ); + void InputStartForward( inputdata_t &inputdata ); + void InputStop( inputdata_t &inputdata ); + void InputStartFiring( inputdata_t &inputdata ); + void InputStopFiring( inputdata_t &inputdata ); + void InputGotoPathCorner( inputdata_t &inputdata ); + +public: + string_t m_iszVehicleName; + IServerVehicle *m_pVehicleInterface; + EHANDLE m_hVehicleEntity; + + // Path driving + CVehicleWaypoint *m_Waypoints[2]; + CVehicleWaypoint *m_pCurrentWaypoint; + CVehicleWaypoint *m_pNextWaypoint; + Vector m_vecDesiredVelocity; + Vector m_vecDesiredPosition; + Vector m_vecPrevPoint; + Vector m_vecPrevPrevPoint; + Vector m_vecPostPoint; + Vector m_vecPostPostPoint; + float m_flDistanceAlongSpline; + float m_flDriversMaxSpeed; + float m_flDriversMinSpeed; + + // Speed + float m_flMaxSpeed; // Maximum speed this driver will go + float m_flGoalSpeed; // Desired speed + float m_flInitialSpeed; + float m_flSteering; +}; + +#endif // NPC_VEHICLEDRIVER_H diff --git a/sp/src/game/server/particle_fire.cpp b/sp/src/game/server/particle_fire.cpp new file mode 100644 index 00000000..4181b983 --- /dev/null +++ b/sp/src/game/server/particle_fire.cpp @@ -0,0 +1,43 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" + +#include "particle_fire.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CParticleFire, DT_ParticleFire) + SendPropVector(SENDINFO(m_vOrigin), 0, SPROP_COORD), + SendPropVector(SENDINFO(m_vDirection), 0, SPROP_NOSCALE) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_particlefire, CParticleFire ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CParticleFire ) + + DEFINE_FIELD( m_vOrigin, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vDirection, FIELD_VECTOR ), + +END_DATADESC() + + +CParticleFire::CParticleFire() +{ +#ifdef _DEBUG + m_vOrigin.Init(); + m_vDirection.Init(); +#endif +} + + + diff --git a/sp/src/game/server/particle_fire.h b/sp/src/game/server/particle_fire.h new file mode 100644 index 00000000..33ecce29 --- /dev/null +++ b/sp/src/game/server/particle_fire.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef PARTICLE_FIRE_H +#define PARTICLE_FIRE_H + + +#include "baseparticleentity.h" + + +class CParticleFire : public CBaseParticleEntity +{ + DECLARE_DATADESC(); + +public: + CParticleFire(); + + DECLARE_CLASS( CParticleFire, CBaseParticleEntity ); + + DECLARE_SERVERCLASS(); + + // The client shoots a ray out and starts creating fire where it hits. + CNetworkVector( m_vOrigin ); + CNetworkVector( m_vDirection ); +}; + + +#endif + + + diff --git a/sp/src/game/server/particle_light.cpp b/sp/src/game/server/particle_light.cpp new file mode 100644 index 00000000..1430d2d2 --- /dev/null +++ b/sp/src/game/server/particle_light.cpp @@ -0,0 +1,41 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "particle_light.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( env_particlelight, CParticleLight ); + + +//Save/restore +BEGIN_DATADESC( CParticleLight ) + + //Keyvalue fields + DEFINE_KEYFIELD( m_flIntensity, FIELD_FLOAT, "Intensity" ), + DEFINE_KEYFIELD( m_vColor, FIELD_VECTOR, "Color" ), + DEFINE_KEYFIELD( m_PSName, FIELD_STRING, "PSName" ), + DEFINE_KEYFIELD( m_bDirectional, FIELD_BOOLEAN, "Directional" ) + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after key values have been set. +//----------------------------------------------------------------------------- +CParticleLight::CParticleLight() +{ + m_flIntensity = 5000; + m_vColor.Init( 1, 0, 0 ); + m_PSName = NULL_STRING; + m_bDirectional = false; +} + + diff --git a/sp/src/game/server/particle_light.h b/sp/src/game/server/particle_light.h new file mode 100644 index 00000000..cc7d9281 --- /dev/null +++ b/sp/src/game/server/particle_light.h @@ -0,0 +1,41 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef PARTICLE_LIGHT_H +#define PARTICLE_LIGHT_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" + + +//================================================== +// CParticleLight. These are tied to +//================================================== + +#define PARTICLELIGHT_ENTNAME "env_particlelight" + +class CParticleLight : public CServerOnlyPointEntity +{ +public: + DECLARE_CLASS( CParticleLight, CServerOnlyPointEntity ); + DECLARE_DATADESC(); + + CParticleLight(); + + +public: + float m_flIntensity; + Vector m_vColor; // 0-255 + string_t m_PSName; // Name of the particle system entity this light affects. + bool m_bDirectional; +}; + + +#endif // PARTICLE_LIGHT_H diff --git a/sp/src/game/server/particle_smokegrenade.cpp b/sp/src/game/server/particle_smokegrenade.cpp new file mode 100644 index 00000000..57493d0e --- /dev/null +++ b/sp/src/game/server/particle_smokegrenade.cpp @@ -0,0 +1,75 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "particle_smokegrenade.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +IMPLEMENT_SERVERCLASS_ST(ParticleSmokeGrenade, DT_ParticleSmokeGrenade) + SendPropTime(SENDINFO(m_flSpawnTime) ), + SendPropFloat(SENDINFO(m_FadeStartTime), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_FadeEndTime), 0, SPROP_NOSCALE), + SendPropInt(SENDINFO(m_CurrentStage), 1, SPROP_UNSIGNED), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_particlesmokegrenade, ParticleSmokeGrenade ); + +BEGIN_DATADESC( ParticleSmokeGrenade ) + + DEFINE_FIELD( m_CurrentStage, FIELD_CHARACTER ), + DEFINE_FIELD( m_FadeStartTime, FIELD_TIME ), + DEFINE_FIELD( m_FadeEndTime, FIELD_TIME ), + DEFINE_FIELD( m_flSpawnTime, FIELD_TIME ), + +END_DATADESC() + + +ParticleSmokeGrenade::ParticleSmokeGrenade() +{ + m_CurrentStage = 0; + m_FadeStartTime = 17; + m_FadeEndTime = 22; + + m_flSpawnTime = gpGlobals->curtime; +} + + +// Smoke grenade particles should always transmitted to clients. If not, a client who +// enters the PVS late will see the smoke start billowing from then, allowing better vision. +int ParticleSmokeGrenade::UpdateTransmitState( void ) +{ + if ( IsEffectActive( EF_NODRAW ) ) + return SetTransmitState( FL_EDICT_DONTSEND ); + + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +void ParticleSmokeGrenade::FillVolume() +{ + m_CurrentStage = 1; + CollisionProp()->SetCollisionBounds( Vector( -50, -50, -50 ), Vector( 50, 50, 50 ) ); +} + + +void ParticleSmokeGrenade::SetFadeTime(float startTime, float endTime) +{ + m_FadeStartTime = startTime; + m_FadeEndTime = endTime; +} + +// Fade start and end are relative to current time +void ParticleSmokeGrenade::SetRelativeFadeTime(float startTime, float endTime) +{ + float flCurrentTime = gpGlobals->curtime - m_flSpawnTime; + + m_FadeStartTime = flCurrentTime + startTime; + m_FadeEndTime = flCurrentTime + endTime; +} diff --git a/sp/src/game/server/particle_smokegrenade.h b/sp/src/game/server/particle_smokegrenade.h new file mode 100644 index 00000000..925b11d9 --- /dev/null +++ b/sp/src/game/server/particle_smokegrenade.h @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#ifndef PARTICLE_SMOKEGRENADE_H +#define PARTICLE_SMOKEGRENADE_H + + +#include "baseparticleentity.h" + + +#define PARTICLESMOKEGRENADE_ENTITYNAME "env_particlesmokegrenade" + + +class ParticleSmokeGrenade : public CBaseParticleEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( ParticleSmokeGrenade, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + ParticleSmokeGrenade(); + + virtual int UpdateTransmitState( void ); + +public: + + // Tell the client entity to start filling the volume. + void FillVolume(); + + // Set the times it fades out at. + void SetFadeTime(float startTime, float endTime); + + // Set time to fade out relative to current time + void SetRelativeFadeTime(float startTime, float endTime); + + +public: + + // Stage 0 (default): make a smoke trail that follows the entity it's following. + // Stage 1 : fill a volume with smoke. + CNetworkVar( unsigned char, m_CurrentStage ); + + CNetworkVar( float, m_flSpawnTime ); + + // When to fade in and out. + CNetworkVar( float, m_FadeStartTime ); + CNetworkVar( float, m_FadeEndTime ); +}; + + +#endif + + diff --git a/sp/src/game/server/particle_system.cpp b/sp/src/game/server/particle_system.cpp new file mode 100644 index 00000000..6ec0cb96 --- /dev/null +++ b/sp/src/game/server/particle_system.cpp @@ -0,0 +1,276 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: An entity that spawns and controls a particle system +// +//============================================================================= + +#include "cbase.h" +#include "particles/particles.h" +#include "networkstringtable_gamedll.h" +#include "particle_system.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern void SendProxy_Origin( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); +extern void SendProxy_Angles( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); + +// Stripped down CBaseEntity send table +IMPLEMENT_SERVERCLASS_ST_NOBASE(CParticleSystem, DT_ParticleSystem) + SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_COORD|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ), + SendPropEHandle (SENDINFO(m_hOwnerEntity)), + SendPropEHandle (SENDINFO_NAME(m_hMoveParent, moveparent)), + SendPropInt (SENDINFO(m_iParentAttachment), NUM_PARENTATTACHMENT_BITS, SPROP_UNSIGNED), + SendPropQAngles (SENDINFO(m_angRotation), 13, SPROP_CHANGES_OFTEN, SendProxy_Angles ), + + SendPropInt( SENDINFO(m_iEffectIndex), MAX_PARTICLESYSTEMS_STRING_BITS, SPROP_UNSIGNED ), + SendPropBool( SENDINFO(m_bActive) ), +#ifdef MAPBASE + SendPropBool( SENDINFO(m_bDestroyImmediately) ), +#endif + SendPropFloat( SENDINFO(m_flStartTime) ), + + SendPropArray3( SENDINFO_ARRAY3(m_hControlPointEnts), SendPropEHandle( SENDINFO_ARRAY(m_hControlPointEnts) ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iControlPointParents), SendPropInt( SENDINFO_ARRAY(m_iControlPointParents), 3, SPROP_UNSIGNED ) ), + SendPropBool( SENDINFO(m_bWeatherEffect) ), +END_SEND_TABLE() + +BEGIN_DATADESC( CParticleSystem ) + DEFINE_KEYFIELD( m_bStartActive, FIELD_BOOLEAN, "start_active" ), + DEFINE_KEYFIELD( m_bWeatherEffect, FIELD_BOOLEAN, "flag_as_weather" ), + DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), +#ifdef MAPBASE + DEFINE_FIELD( m_bDestroyImmediately, FIELD_BOOLEAN ), +#endif + DEFINE_FIELD( m_flStartTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_iszEffectName, FIELD_STRING, "effect_name" ), + //DEFINE_FIELD( m_iEffectIndex, FIELD_INTEGER ), // Don't save. Refind after loading. + + DEFINE_KEYFIELD( m_iszControlPointNames[0], FIELD_STRING, "cpoint1" ), + DEFINE_KEYFIELD( m_iszControlPointNames[1], FIELD_STRING, "cpoint2" ), + DEFINE_KEYFIELD( m_iszControlPointNames[2], FIELD_STRING, "cpoint3" ), + DEFINE_KEYFIELD( m_iszControlPointNames[3], FIELD_STRING, "cpoint4" ), + DEFINE_KEYFIELD( m_iszControlPointNames[4], FIELD_STRING, "cpoint5" ), + DEFINE_KEYFIELD( m_iszControlPointNames[5], FIELD_STRING, "cpoint6" ), + DEFINE_KEYFIELD( m_iszControlPointNames[6], FIELD_STRING, "cpoint7" ), + DEFINE_KEYFIELD( m_iszControlPointNames[7], FIELD_STRING, "cpoint8" ), + DEFINE_KEYFIELD( m_iszControlPointNames[8], FIELD_STRING, "cpoint9" ), + DEFINE_KEYFIELD( m_iszControlPointNames[9], FIELD_STRING, "cpoint10" ), + DEFINE_KEYFIELD( m_iszControlPointNames[10], FIELD_STRING, "cpoint11" ), + DEFINE_KEYFIELD( m_iszControlPointNames[11], FIELD_STRING, "cpoint12" ), + DEFINE_KEYFIELD( m_iszControlPointNames[12], FIELD_STRING, "cpoint13" ), + DEFINE_KEYFIELD( m_iszControlPointNames[13], FIELD_STRING, "cpoint14" ), + DEFINE_KEYFIELD( m_iszControlPointNames[14], FIELD_STRING, "cpoint15" ), + DEFINE_KEYFIELD( m_iszControlPointNames[15], FIELD_STRING, "cpoint16" ), + DEFINE_KEYFIELD( m_iszControlPointNames[16], FIELD_STRING, "cpoint17" ), + DEFINE_KEYFIELD( m_iszControlPointNames[17], FIELD_STRING, "cpoint18" ), + DEFINE_KEYFIELD( m_iszControlPointNames[18], FIELD_STRING, "cpoint19" ), + DEFINE_KEYFIELD( m_iszControlPointNames[19], FIELD_STRING, "cpoint20" ), + DEFINE_KEYFIELD( m_iszControlPointNames[20], FIELD_STRING, "cpoint21" ), + DEFINE_KEYFIELD( m_iszControlPointNames[21], FIELD_STRING, "cpoint22" ), + DEFINE_KEYFIELD( m_iszControlPointNames[22], FIELD_STRING, "cpoint23" ), + DEFINE_KEYFIELD( m_iszControlPointNames[23], FIELD_STRING, "cpoint24" ), + DEFINE_KEYFIELD( m_iszControlPointNames[24], FIELD_STRING, "cpoint25" ), + DEFINE_KEYFIELD( m_iszControlPointNames[25], FIELD_STRING, "cpoint26" ), + DEFINE_KEYFIELD( m_iszControlPointNames[26], FIELD_STRING, "cpoint27" ), + DEFINE_KEYFIELD( m_iszControlPointNames[27], FIELD_STRING, "cpoint28" ), + DEFINE_KEYFIELD( m_iszControlPointNames[28], FIELD_STRING, "cpoint29" ), + DEFINE_KEYFIELD( m_iszControlPointNames[29], FIELD_STRING, "cpoint30" ), + DEFINE_KEYFIELD( m_iszControlPointNames[30], FIELD_STRING, "cpoint31" ), + DEFINE_KEYFIELD( m_iszControlPointNames[31], FIELD_STRING, "cpoint32" ), + DEFINE_KEYFIELD( m_iszControlPointNames[32], FIELD_STRING, "cpoint33" ), + DEFINE_KEYFIELD( m_iszControlPointNames[33], FIELD_STRING, "cpoint34" ), + DEFINE_KEYFIELD( m_iszControlPointNames[34], FIELD_STRING, "cpoint35" ), + DEFINE_KEYFIELD( m_iszControlPointNames[35], FIELD_STRING, "cpoint36" ), + DEFINE_KEYFIELD( m_iszControlPointNames[36], FIELD_STRING, "cpoint37" ), + DEFINE_KEYFIELD( m_iszControlPointNames[37], FIELD_STRING, "cpoint38" ), + DEFINE_KEYFIELD( m_iszControlPointNames[38], FIELD_STRING, "cpoint39" ), + DEFINE_KEYFIELD( m_iszControlPointNames[39], FIELD_STRING, "cpoint40" ), + DEFINE_KEYFIELD( m_iszControlPointNames[40], FIELD_STRING, "cpoint41" ), + DEFINE_KEYFIELD( m_iszControlPointNames[41], FIELD_STRING, "cpoint42" ), + DEFINE_KEYFIELD( m_iszControlPointNames[42], FIELD_STRING, "cpoint43" ), + DEFINE_KEYFIELD( m_iszControlPointNames[43], FIELD_STRING, "cpoint44" ), + DEFINE_KEYFIELD( m_iszControlPointNames[44], FIELD_STRING, "cpoint45" ), + DEFINE_KEYFIELD( m_iszControlPointNames[45], FIELD_STRING, "cpoint46" ), + DEFINE_KEYFIELD( m_iszControlPointNames[46], FIELD_STRING, "cpoint47" ), + DEFINE_KEYFIELD( m_iszControlPointNames[47], FIELD_STRING, "cpoint48" ), + DEFINE_KEYFIELD( m_iszControlPointNames[48], FIELD_STRING, "cpoint49" ), + DEFINE_KEYFIELD( m_iszControlPointNames[49], FIELD_STRING, "cpoint50" ), + DEFINE_KEYFIELD( m_iszControlPointNames[50], FIELD_STRING, "cpoint51" ), + DEFINE_KEYFIELD( m_iszControlPointNames[51], FIELD_STRING, "cpoint52" ), + DEFINE_KEYFIELD( m_iszControlPointNames[52], FIELD_STRING, "cpoint53" ), + DEFINE_KEYFIELD( m_iszControlPointNames[53], FIELD_STRING, "cpoint54" ), + DEFINE_KEYFIELD( m_iszControlPointNames[54], FIELD_STRING, "cpoint55" ), + DEFINE_KEYFIELD( m_iszControlPointNames[55], FIELD_STRING, "cpoint56" ), + DEFINE_KEYFIELD( m_iszControlPointNames[56], FIELD_STRING, "cpoint57" ), + DEFINE_KEYFIELD( m_iszControlPointNames[57], FIELD_STRING, "cpoint58" ), + DEFINE_KEYFIELD( m_iszControlPointNames[58], FIELD_STRING, "cpoint59" ), + DEFINE_KEYFIELD( m_iszControlPointNames[59], FIELD_STRING, "cpoint60" ), + DEFINE_KEYFIELD( m_iszControlPointNames[60], FIELD_STRING, "cpoint61" ), + DEFINE_KEYFIELD( m_iszControlPointNames[61], FIELD_STRING, "cpoint62" ), + DEFINE_KEYFIELD( m_iszControlPointNames[62], FIELD_STRING, "cpoint63" ), + + DEFINE_KEYFIELD( m_iControlPointParents[0], FIELD_CHARACTER, "cpoint1_parent" ), + DEFINE_KEYFIELD( m_iControlPointParents[1], FIELD_CHARACTER, "cpoint2_parent" ), + DEFINE_KEYFIELD( m_iControlPointParents[2], FIELD_CHARACTER, "cpoint3_parent" ), + DEFINE_KEYFIELD( m_iControlPointParents[3], FIELD_CHARACTER, "cpoint4_parent" ), + DEFINE_KEYFIELD( m_iControlPointParents[4], FIELD_CHARACTER, "cpoint5_parent" ), + DEFINE_KEYFIELD( m_iControlPointParents[5], FIELD_CHARACTER, "cpoint6_parent" ), + DEFINE_KEYFIELD( m_iControlPointParents[6], FIELD_CHARACTER, "cpoint7_parent" ), + + DEFINE_AUTO_ARRAY( m_hControlPointEnts, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Start", InputStart ), + DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "DestroyImmediately", InputDestroyImmediately ), +#endif + + DEFINE_THINKFUNC( StartParticleSystemThink ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( info_particle_system, CParticleSystem ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CParticleSystem::CParticleSystem() +{ + m_bWeatherEffect = false; +} + +//----------------------------------------------------------------------------- +// Precache +//----------------------------------------------------------------------------- +void CParticleSystem::Precache( void ) +{ + const char *pParticleSystemName = STRING( m_iszEffectName ); + if ( pParticleSystemName == NULL || pParticleSystemName[0] == '\0' ) + { + Warning( "info_particle_system (%s) has no particle system name specified!\n", GetEntityName().ToCStr() ); + } + + PrecacheParticleSystem( pParticleSystemName ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::Spawn( void ) +{ + BaseClass::Spawn(); + + Precache(); + m_iEffectIndex = -1; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::Activate( void ) +{ + BaseClass::Activate(); + + // Find our particle effect index + m_iEffectIndex = GetParticleSystemIndex( STRING(m_iszEffectName) ); + + if ( m_bStartActive ) + { + m_bStartActive = false; + StartParticleSystem(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::StartParticleSystemThink( void ) +{ + StartParticleSystem(); +} + +//----------------------------------------------------------------------------- +// Purpose: Always transmitted to clients +//----------------------------------------------------------------------------- +int CParticleSystem::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::StartParticleSystem( void ) +{ + if ( m_bActive == false ) + { + m_flStartTime = gpGlobals->curtime; + m_bActive = true; +#ifdef MAPBASE + m_bDestroyImmediately = false; +#endif + + // Setup our control points at this time (in case our targets weren't around at spawn time) + ReadControlPointEnts(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::StopParticleSystem( void ) +{ + m_bActive = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::InputStart( inputdata_t &inputdata ) +{ + StartParticleSystem(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::InputStop( inputdata_t &inputdata ) +{ + StopParticleSystem(); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticleSystem::InputDestroyImmediately( inputdata_t &inputdata ) +{ + m_bDestroyImmediately = true; + StopParticleSystem(); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Find each entity referred to by m_iszControlPointNames and +// resolve it into the corresponding slot in m_hControlPointEnts +//----------------------------------------------------------------------------- +void CParticleSystem::ReadControlPointEnts( void ) +{ + for ( int i = 0 ; i < kMAXCONTROLPOINTS; ++i ) + { + if ( m_iszControlPointNames[i] == NULL_STRING ) + continue; + + CBaseEntity *pPointEnt = gEntList.FindEntityGeneric( NULL, STRING( m_iszControlPointNames[i] ), this ); + Assert( pPointEnt != NULL ); + if ( pPointEnt == NULL ) + { + Warning("Particle system %s could not find control point entity (%s)\n", GetEntityName().ToCStr(), m_iszControlPointNames[i].ToCStr() ); + continue; + } + + m_hControlPointEnts.Set( i, pPointEnt ); + } +} diff --git a/sp/src/game/server/particle_system.h b/sp/src/game/server/particle_system.h new file mode 100644 index 00000000..6ce9a9c9 --- /dev/null +++ b/sp/src/game/server/particle_system.h @@ -0,0 +1,65 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef PARTICLE_SYSTEM_H +#define PARTICLE_SYSTEM_H +#ifdef _WIN32 +#pragma once +#endif + +#include "cbase.h" + +//----------------------------------------------------------------------------- +// Purpose: An entity that spawns and controls a particle system +//----------------------------------------------------------------------------- +class CParticleSystem : public CBaseEntity +{ + DECLARE_CLASS( CParticleSystem, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CParticleSystem(); + + virtual void Precache( void ); + virtual void Spawn( void ); + virtual void Activate( void ); + virtual int UpdateTransmitState(void); + + void StartParticleSystem( void ); + void StopParticleSystem( void ); + + void InputStart( inputdata_t &inputdata ); + void InputStop( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputDestroyImmediately( inputdata_t &inputdata ); +#endif + void StartParticleSystemThink( void ); + + enum { kMAXCONTROLPOINTS = 63 }; ///< actually one less than the total number of cpoints since 0 is assumed to be me + +protected: + + /// Load up and resolve the entities that are supposed to be the control points + void ReadControlPointEnts( void ); + + bool m_bStartActive; + string_t m_iszEffectName; + + CNetworkVar( bool, m_bActive ); +#ifdef MAPBASE + CNetworkVar( bool, m_bDestroyImmediately ); +#endif + CNetworkVar( int, m_iEffectIndex ) + CNetworkVar( float, m_flStartTime ); // Time at which this effect was started. This is used after restoring an active effect. + + string_t m_iszControlPointNames[kMAXCONTROLPOINTS]; + CNetworkArray( EHANDLE, m_hControlPointEnts, kMAXCONTROLPOINTS ); + CNetworkArray( unsigned char, m_iControlPointParents, kMAXCONTROLPOINTS ); + CNetworkVar( bool, m_bWeatherEffect ); +}; + +#endif // PARTICLE_SYSTEM_H diff --git a/sp/src/game/server/pathcorner.cpp b/sp/src/game/server/pathcorner.cpp new file mode 100644 index 00000000..977ce8ba --- /dev/null +++ b/sp/src/game/server/pathcorner.cpp @@ -0,0 +1,142 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Used to create a path that can be followed by NPCs and trains. +// +//=============================================================================// + +#include "cbase.h" +#include "trains.h" +#include "entitylist.h" +#include "ndebugoverlay.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CPathCorner : public CPointEntity +{ + DECLARE_CLASS( CPathCorner, CPointEntity ); +public: + + void Spawn( ); + float GetDelay( void ) { return m_flWait; } + int DrawDebugTextOverlays(void); + void DrawDebugGeometryOverlays(void); + + // Input handlers + void InputSetNextPathCorner( inputdata_t &inputdata ); + void InputInPass( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + float m_flWait; + COutputEvent m_OnPass; +}; + +LINK_ENTITY_TO_CLASS( path_corner, CPathCorner ); + + +class CPathCornerCrash : public CPathCorner +{ + DECLARE_CLASS( CPathCornerCrash, CPathCorner ); +}; + +LINK_ENTITY_TO_CLASS( path_corner_crash, CPathCornerCrash ); + + +BEGIN_DATADESC( CPathCorner ) + + DEFINE_KEYFIELD( m_flWait, FIELD_FLOAT, "wait" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetNextPathCorner", InputSetNextPathCorner), + + // Internal inputs - not exposed in the FGD + DEFINE_INPUTFUNC( FIELD_VOID, "InPass", InputInPass ), + + // Outputs + DEFINE_OUTPUT( m_OnPass, "OnPass"), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathCorner::Spawn( void ) +{ + ASSERTSZ(GetEntityName() != NULL_STRING, "path_corner without a targetname"); +} + + +//------------------------------------------------------------------------------ +// Purpose: Sets the next path corner by name. +// Input : String ID name of next path corner. +//----------------------------------------------------------------------------- +void CPathCorner::InputSetNextPathCorner( inputdata_t &inputdata ) +{ + m_target = inputdata.value.StringID(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Fired by path followers as they pass the path corner. +//----------------------------------------------------------------------------- +void CPathCorner::InputInPass( inputdata_t &inputdata ) +{ + m_OnPass.FireOutput( inputdata.pActivator, inputdata.pCaller, 0); +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CPathCorner::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + // -------------- + // Print Target + // -------------- + char tempstr[255]; + if (m_target!=NULL_STRING) + { + Q_snprintf(tempstr,sizeof(tempstr),"Target: %s",STRING(m_target)); + } + else + { + Q_strncpy(tempstr,"Target: - ",sizeof(tempstr)); + } + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Override base class to add display of paths +//----------------------------------------------------------------------------- +void CPathCorner::DrawDebugGeometryOverlays(void) +{ + // ---------------------------------------------- + // Draw line to next target is bbox is selected + // ---------------------------------------------- + if (m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_ABSBOX_BIT)) + { + NDebugOverlay::Box(GetAbsOrigin(), Vector(-10,-10,-10), Vector(10,10,10), 255, 100, 100, 0 ,0); + + if (m_target != NULL_STRING) + { + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_target ); + if (pTarget) + { + NDebugOverlay::Line(GetAbsOrigin(),pTarget->GetAbsOrigin(),255,100,100,true,0.0); + } + } + } + BaseClass::DrawDebugGeometryOverlays(); +} diff --git a/sp/src/game/server/pathtrack.cpp b/sp/src/game/server/pathtrack.cpp new file mode 100644 index 00000000..2a77666d --- /dev/null +++ b/sp/src/game/server/pathtrack.cpp @@ -0,0 +1,592 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Used to create a path that can be followed by NPCs and trains. +// +//=============================================================================// + +#include "cbase.h" +#include "pathtrack.h" +#include "entitylist.h" +#include "ndebugoverlay.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CPathTrack ) + +#ifdef MAPBASE + DEFINE_FIELD( m_pnext, FIELD_EHANDLE ), + DEFINE_FIELD( m_pprevious, FIELD_EHANDLE ), + DEFINE_FIELD( m_paltpath, FIELD_EHANDLE ), +#else + DEFINE_FIELD( m_pnext, FIELD_CLASSPTR ), + DEFINE_FIELD( m_pprevious, FIELD_CLASSPTR ), + DEFINE_FIELD( m_paltpath, FIELD_CLASSPTR ), +#endif + + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + DEFINE_FIELD( m_length, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_altName, FIELD_STRING, "altpath" ), + DEFINE_KEYFIELD( m_eOrientationType, FIELD_INTEGER, "orientationtype" ), +// DEFINE_FIELD( m_nIterVal, FIELD_INTEGER ), + + DEFINE_INPUTFUNC( FIELD_VOID, "InPass", InputPass ), + DEFINE_INPUTFUNC( FIELD_VOID, "InTeleport", InputTeleport ), + + DEFINE_INPUTFUNC( FIELD_VOID, "EnableAlternatePath", InputEnableAlternatePath ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableAlternatePath", InputDisableAlternatePath ), + DEFINE_INPUTFUNC( FIELD_VOID, "ToggleAlternatePath", InputToggleAlternatePath ), + + DEFINE_INPUTFUNC( FIELD_VOID, "EnablePath", InputEnablePath ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisablePath", InputDisablePath ), + DEFINE_INPUTFUNC( FIELD_VOID, "TogglePath", InputTogglePath ), + + // Outputs + DEFINE_OUTPUT(m_OnPass, "OnPass"), + DEFINE_OUTPUT(m_OnTeleport, "OnTeleport"), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( path_track, CPathTrack ); + + +//----------------------------------------------------------------------------- +// Finds circular paths +//----------------------------------------------------------------------------- +int CPathTrack::s_nCurrIterVal = 0; +bool CPathTrack::s_bIsIterating = false; + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CPathTrack::CPathTrack() +{ + m_nIterVal = -1; + m_eOrientationType = TrackOrientation_FacePath; +} + + +//----------------------------------------------------------------------------- +// Spawn! +//----------------------------------------------------------------------------- +void CPathTrack::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + UTIL_SetSize(this, Vector(-8, -8, -8), Vector(8, 8, 8)); +} + + +//----------------------------------------------------------------------------- +// Activate! +//----------------------------------------------------------------------------- +void CPathTrack::Activate( void ) +{ + BaseClass::Activate(); + + if ( GetEntityName() != NULL_STRING ) // Link to next, and back-link + { + Link(); + } +} + + +//----------------------------------------------------------------------------- +// Connects up the previous + next pointers +//----------------------------------------------------------------------------- +void CPathTrack::Link( void ) +{ + CBaseEntity *pTarget; + + if ( m_target != NULL_STRING ) + { + pTarget = gEntList.FindEntityByName( NULL, m_target ); + + if ( pTarget == this) + { + Warning("ERROR: path_track (%s) refers to itself as a target!\n", GetDebugName()); + + //FIXME: Why were we removing this? If it was already connected to, we weren't updating the other linked + // end, causing problems with walking through bogus memory links! -- jdw + + //UTIL_Remove(this); + //return; + } + else if ( pTarget ) + { + m_pnext = dynamic_cast( pTarget ); + + if ( m_pnext ) // If no next pointer, this is the end of a path + { + m_pnext->SetPrevious( this ); + } + } + else + { + Warning("Dead end link: %s\n", STRING( m_target ) ); + } + } + + // Find "alternate" path + if ( m_altName != NULL_STRING ) + { + pTarget = gEntList.FindEntityByName( NULL, m_altName ); + if ( pTarget ) + { + m_paltpath = dynamic_cast( pTarget ); + m_paltpath->SetPrevious( this ); + } + } +} + + +//----------------------------------------------------------------------------- +// Circular path checking +//----------------------------------------------------------------------------- +void CPathTrack::BeginIteration() +{ + Assert( !s_bIsIterating ); + ++s_nCurrIterVal; + s_bIsIterating = true; +} + +void CPathTrack::EndIteration() +{ + Assert( s_bIsIterating ); + s_bIsIterating = false; +} + +void CPathTrack::Visit() +{ + m_nIterVal = s_nCurrIterVal; +} + +bool CPathTrack::HasBeenVisited() const +{ + return ( m_nIterVal == s_nCurrIterVal ); +} + + +//----------------------------------------------------------------------------- +// Do we have an alternate path? +//----------------------------------------------------------------------------- +bool CPathTrack::HasAlternathPath() const +{ + return ( m_paltpath != NULL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Toggles the track to or from its alternate path +//----------------------------------------------------------------------------- +void CPathTrack::ToggleAlternatePath( void ) +{ + // Use toggles between two paths + if ( m_paltpath != NULL ) + { + if ( FBitSet( m_spawnflags, SF_PATH_ALTERNATE ) == false ) + { + EnableAlternatePath(); + } + else + { + DisableAlternatePath(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathTrack::EnableAlternatePath( void ) +{ + if ( m_paltpath != NULL ) + { + SETBITS( m_spawnflags, SF_PATH_ALTERNATE ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathTrack::DisableAlternatePath( void ) +{ + if ( m_paltpath != NULL ) + { + CLEARBITS( m_spawnflags, SF_PATH_ALTERNATE ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPathTrack::InputEnableAlternatePath( inputdata_t &inputdata ) +{ + EnableAlternatePath(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPathTrack::InputDisableAlternatePath( inputdata_t &inputdata ) +{ + DisableAlternatePath(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPathTrack::InputToggleAlternatePath( inputdata_t &inputdata ) +{ + ToggleAlternatePath(); +} + +//----------------------------------------------------------------------------- +// Purpose: Toggles the track to or from its alternate path +//----------------------------------------------------------------------------- +void CPathTrack::TogglePath( void ) +{ + // Use toggles between two paths + if ( FBitSet( m_spawnflags, SF_PATH_DISABLED ) ) + { + EnablePath(); + } + else + { + DisablePath(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathTrack::EnablePath( void ) +{ + CLEARBITS( m_spawnflags, SF_PATH_DISABLED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathTrack::DisablePath( void ) +{ + SETBITS( m_spawnflags, SF_PATH_DISABLED ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPathTrack::InputEnablePath( inputdata_t &inputdata ) +{ + EnablePath(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPathTrack::InputDisablePath( inputdata_t &inputdata ) +{ + DisablePath(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPathTrack::InputTogglePath( inputdata_t &inputdata ) +{ + TogglePath(); +} + + +void CPathTrack::DrawDebugGeometryOverlays() +{ + // ---------------------------------------------- + // Draw line to next target is bbox is selected + // ---------------------------------------------- + if (m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_ABSBOX_BIT)) + { + if (m_pnext) + { + NDebugOverlay::Line(GetAbsOrigin(),m_pnext->GetAbsOrigin(),255,100,100,true,0.0); + } + } + BaseClass::DrawDebugGeometryOverlays(); +} + +CPathTrack *CPathTrack::ValidPath( CPathTrack *ppath, int testFlag ) +{ + if ( !ppath ) + return NULL; + + if ( testFlag && FBitSet( ppath->m_spawnflags, SF_PATH_DISABLED ) ) + return NULL; + + return ppath; +} + + +void CPathTrack::Project( CPathTrack *pstart, CPathTrack *pend, Vector &origin, float dist ) +{ + if ( pstart && pend ) + { + Vector dir = (pend->GetLocalOrigin() - pstart->GetLocalOrigin()); + VectorNormalize( dir ); + origin = pend->GetLocalOrigin() + dir * dist; + } +} + +CPathTrack *CPathTrack::GetNext( void ) +{ + if ( m_paltpath && FBitSet( m_spawnflags, SF_PATH_ALTERNATE ) && !FBitSet( m_spawnflags, SF_PATH_ALTREVERSE ) ) + { + Assert( !m_paltpath.IsValid() || m_paltpath.Get() != NULL ); + return m_paltpath; + } + + // The paths shouldn't normally be getting deleted so assert that if it was set, it's valid. + Assert( !m_pnext.IsValid() || m_pnext.Get() != NULL ); + return m_pnext; +} + + + +CPathTrack *CPathTrack::GetPrevious( void ) +{ + if ( m_paltpath && FBitSet( m_spawnflags, SF_PATH_ALTERNATE ) && FBitSet( m_spawnflags, SF_PATH_ALTREVERSE ) ) + { + Assert( !m_paltpath.IsValid() || m_paltpath.Get() != NULL ); + return m_paltpath; + } + + Assert( !m_pprevious.IsValid() || m_pprevious.Get() != NULL ); + return m_pprevious; +} + + + +void CPathTrack::SetPrevious( CPathTrack *pprev ) +{ + // Only set previous if this isn't my alternate path + if ( pprev && !FStrEq( STRING(pprev->GetEntityName()), STRING(m_altName) ) ) + m_pprevious = pprev; +} + + +CPathTrack *CPathTrack::GetNextInDir( bool bForward ) +{ + if ( bForward ) + return GetNext(); + + return GetPrevious(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Assumes this is ALWAYS enabled +// Input : origin - position along path to look ahead from +// dist - distance to look ahead, negative values look backward +// move - +// Output : Returns the track that we will be PAST in 'dist' units. +//----------------------------------------------------------------------------- +CPathTrack *CPathTrack::LookAhead( Vector &origin, float dist, int move, CPathTrack **pNextNext ) +{ + CPathTrack *pcurrent = this; + float originalDist = dist; + Vector currentPos = origin; + + bool bForward = true; + if ( dist < 0 ) + { + // Travelling backwards along the path. + dist = -dist; + bForward = false; + } + + // Move along the path, until we've gone 'dist' units or run out of path. + while ( dist > 0 ) + { + // If there is no next path track, or it's disabled, we're done. + if ( !ValidPath( pcurrent->GetNextInDir( bForward ), move ) ) + { + if ( !move ) + { + Project( pcurrent->GetNextInDir( !bForward ), pcurrent, origin, dist ); + } + + return NULL; + } + + // The next path track is valid. How far are we from it? + Vector dir = pcurrent->GetNextInDir( bForward )->GetLocalOrigin() - currentPos; + float length = dir.Length(); + + // If we are at the next node and there isn't one beyond it, return the next node. + if ( !length && !ValidPath( pcurrent->GetNextInDir( bForward )->GetNextInDir( bForward ), move ) ) + { + if ( pNextNext ) + { + *pNextNext = NULL; + } + + if ( dist == originalDist ) + { + // Didn't move at all, must be in a dead end. + return NULL; + } + + return pcurrent->GetNextInDir( bForward ); + } + + // If we don't hit the next path track within the distance remaining, we're done. + if ( length > dist ) + { + origin = currentPos + ( dir * ( dist / length ) ); + if ( pNextNext ) + { + *pNextNext = pcurrent->GetNextInDir( bForward ); + } + + return pcurrent; + } + + // We hit the next path track, advance to it. + dist -= length; + currentPos = pcurrent->GetNextInDir( bForward )->GetLocalOrigin(); + pcurrent = pcurrent->GetNextInDir( bForward ); + origin = currentPos; + } + + // We consumed all of the distance, and exactly landed on a path track. + if ( pNextNext ) + { + *pNextNext = pcurrent->GetNextInDir( bForward ); + } + + return pcurrent; +} + + +// Assumes this is ALWAYS enabled +CPathTrack *CPathTrack::Nearest( const Vector &origin ) +{ + int deadCount; + float minDist, dist; + Vector delta; + CPathTrack *ppath, *pnearest; + + + delta = origin - GetLocalOrigin(); + delta.z = 0; + minDist = delta.Length(); + pnearest = this; + ppath = GetNext(); + + // Hey, I could use the old 2 racing pointers solution to this, but I'm lazy :) + deadCount = 0; + while ( ppath && ppath != this ) + { + deadCount++; + if ( deadCount > 9999 ) + { + Warning( "Bad sequence of path_tracks from %s\n", GetDebugName() ); + Assert(0); + return NULL; + } + delta = origin - ppath->GetLocalOrigin(); + delta.z = 0; + dist = delta.Length(); + if ( dist < minDist ) + { + minDist = dist; + pnearest = ppath; + } + ppath = ppath->GetNext(); + } + return pnearest; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns how the path follower should orient itself when moving +// through this path track. +//----------------------------------------------------------------------------- +TrackOrientationType_t CPathTrack::GetOrientationType() +{ + return m_eOrientationType; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +QAngle CPathTrack::GetOrientation( bool bForwardDir ) +{ + TrackOrientationType_t eOrient = GetOrientationType(); + if ( eOrient == TrackOrientation_FacePathAngles ) + { + return GetLocalAngles(); + } + + CPathTrack *pPrev = this; + CPathTrack *pNext = GetNextInDir( bForwardDir ); + + if ( !pNext ) + { pPrev = GetNextInDir( !bForwardDir ); + pNext = this; + } + + Vector vecDir = pNext->GetLocalOrigin() - pPrev->GetLocalOrigin(); + + QAngle angDir; + VectorAngles( vecDir, angDir ); + return angDir; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pent - +// Output : CPathTrack +//----------------------------------------------------------------------------- +CPathTrack *CPathTrack::Instance( edict_t *pent ) +{ + CBaseEntity *pEntity = CBaseEntity::Instance( pent ); + if ( FClassnameIs( pEntity, "path_track" ) ) + return (CPathTrack *)pEntity; + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathTrack::InputPass( inputdata_t &inputdata ) +{ + m_OnPass.FireOutput( inputdata.pActivator, this ); + +#ifdef TF_DLL + IGameEvent * event = gameeventmanager->CreateEvent( "path_track_passed" ); + if ( event ) + { + event->SetInt( "index", entindex() ); + gameeventmanager->FireEvent( event, true ); + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPathTrack::InputTeleport( inputdata_t &inputdata ) +{ + m_OnTeleport.FireOutput( inputdata.pActivator, this ); +} \ No newline at end of file diff --git a/sp/src/game/server/pathtrack.h b/sp/src/game/server/pathtrack.h new file mode 100644 index 00000000..b88e44c4 --- /dev/null +++ b/sp/src/game/server/pathtrack.h @@ -0,0 +1,158 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PATHTRACK_H +#define PATHTRACK_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "entityoutput.h" +#include "shareddefs.h" + + +//----------------------------------------------------------------------------- +// Spawnflag for CPathTrack +//----------------------------------------------------------------------------- +#define SF_PATH_DISABLED 0x00000001 +//#define SF_PATH_FIREONCE 0x00000002 +#define SF_PATH_ALTREVERSE 0x00000004 +#define SF_PATH_DISABLE_TRAIN 0x00000008 +#define SF_PATH_TELEPORT 0x00000010 +#define SF_PATH_UPHILL 0x00000020 +#define SF_PATH_DOWNHILL 0x00000040 +#define SF_PATH_ALTERNATE 0x00008000 + + +enum TrackOrientationType_t +{ + TrackOrientation_Fixed = 0, + TrackOrientation_FacePath, + TrackOrientation_FacePathAngles, +}; + + +//----------------------------------------------------------------------------- +// Paths! +//----------------------------------------------------------------------------- +class CPathTrack : public CPointEntity +{ + DECLARE_CLASS( CPathTrack, CPointEntity ); + +public: + CPathTrack(); + + void Spawn( void ); + void Activate( void ); + void DrawDebugGeometryOverlays(); + + void ToggleAlternatePath( void ); + void EnableAlternatePath( void ); + void DisableAlternatePath( void ); + bool HasAlternathPath() const; + + void TogglePath( void ); + void EnablePath( void ); + void DisablePath( void ); + + static CPathTrack *ValidPath( CPathTrack *ppath, int testFlag = true ); // Returns ppath if enabled, NULL otherwise + + CPathTrack *GetNextInDir( bool bForward ); + CPathTrack *GetNext( void ); + CPathTrack *GetPrevious( void ); + + CPathTrack *Nearest( const Vector &origin ); + //CPathTrack *LookAhead( Vector &origin, float dist, int move ); + CPathTrack *LookAhead( Vector &origin, float dist, int move, CPathTrack **pNextNext = NULL ); + + TrackOrientationType_t GetOrientationType(); + QAngle GetOrientation( bool bForwardDir ); + + CHandle m_pnext; + CHandle m_pprevious; + CHandle m_paltpath; + + float GetRadius() const { return m_flRadius; } + + // These four methods help for circular path checking. Call BeginIteration + // before iterating, EndInteration afterwards. Call Visit on each path in the + // list. Then you can use HasBeenVisited to see if you've visited the node + // already, which means you've got a circular or lasso path. You can use the + // macro BEGIN_PATH_TRACK_ITERATION below to simplify the calls to + // BeginInteration + EndIteration. + static void BeginIteration(); + static void EndIteration(); + void Visit(); + bool HasBeenVisited() const; + + bool IsUpHill(){ return ( FBitSet( m_spawnflags, SF_PATH_UPHILL ) ) ? true : false; } + bool IsDownHill(){ return ( FBitSet( m_spawnflags, SF_PATH_DOWNHILL ) ) ? true : false; } + int GetHillType() + { + int iRetVal = HILL_TYPE_NONE; + if ( IsUpHill() ) + { + iRetVal = HILL_TYPE_UPHILL; + } + else if ( IsDownHill() ) + { + iRetVal = HILL_TYPE_DOWNHILL; + } + + return iRetVal; + } + + bool IsDisabled( void ){ return FBitSet( m_spawnflags, SF_PATH_DISABLED ); } + + void InputPass( inputdata_t &inputdata ); + void InputTeleport( inputdata_t &inputdata ); + + void InputToggleAlternatePath( inputdata_t &inputdata ); + void InputEnableAlternatePath( inputdata_t &inputdata ); + void InputDisableAlternatePath( inputdata_t &inputdata ); + + void InputTogglePath( inputdata_t &inputdata ); + void InputEnablePath( inputdata_t &inputdata ); + void InputDisablePath( inputdata_t &inputdata ); + +private: + void Project( CPathTrack *pstart, CPathTrack *pend, Vector &origin, float dist ); + void SetPrevious( CPathTrack *pprevious ); + void Link( void ); + + static CPathTrack *Instance( edict_t *pent ); + + DECLARE_DATADESC(); + + float m_flRadius; + float m_length; + string_t m_altName; + int m_nIterVal; + TrackOrientationType_t m_eOrientationType; + + COutputEvent m_OnPass; + COutputEvent m_OnTeleport; + + static int s_nCurrIterVal; + static bool s_bIsIterating; +}; + +//----------------------------------------------------------------------------- +// Used to make sure circular iteration works all nice +//----------------------------------------------------------------------------- +#define BEGIN_PATH_TRACK_ITERATION() CPathTrackVisitor _visit + +class CPathTrackVisitor +{ +public: + CPathTrackVisitor() { CPathTrack::BeginIteration(); } + ~CPathTrackVisitor() { CPathTrack::EndIteration(); } +}; + + + +#endif // PATHTRACK_H diff --git a/sp/src/game/server/phys_controller.cpp b/sp/src/game/server/phys_controller.cpp new file mode 100644 index 00000000..f5e2221a --- /dev/null +++ b/sp/src/game/server/phys_controller.cpp @@ -0,0 +1,1078 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "entitylist.h" +#include "physics.h" +#include "vphysics/constraints.h" +#include "physics_saverestore.h" +#include "phys_controller.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_THRUST_STARTACTIVE 0x0001 +#define SF_THRUST_FORCE 0x0002 +#define SF_THRUST_TORQUE 0x0004 +#define SF_THRUST_LOCAL_ORIENTATION 0x0008 +#define SF_THRUST_MASS_INDEPENDENT 0x0010 +#define SF_THRUST_IGNORE_POS 0x0020 + +class CPhysThruster; + +//----------------------------------------------------------------------------- +// Purpose: This class only implements the IMotionEvent-specific behavior +// It keeps track of the forces so they can be integrated +//----------------------------------------------------------------------------- +class CConstantForceController : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); + +public: + void Init( IMotionEvent::simresult_e controlType ) + { + m_controlType = controlType; + } + + void SetConstantForce( const Vector &linear, const AngularImpulse &angular ); + void ScaleConstantForce( float scale ); + + IMotionEvent::simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + IMotionEvent::simresult_e m_controlType; + Vector m_linear; + AngularImpulse m_angular; + Vector m_linearSave; + AngularImpulse m_angularSave; +}; + +BEGIN_SIMPLE_DATADESC( CConstantForceController ) + DEFINE_FIELD( m_controlType, FIELD_INTEGER ), + DEFINE_FIELD( m_linear, FIELD_VECTOR ), + DEFINE_FIELD( m_angular, FIELD_VECTOR ), + DEFINE_FIELD( m_linearSave, FIELD_VECTOR ), + DEFINE_FIELD( m_angularSave, FIELD_VECTOR ), +END_DATADESC() + + +void CConstantForceController::SetConstantForce( const Vector &linear, const AngularImpulse &angular ) +{ + m_linear = linear; + m_angular = angular; + // cache these for scaling later + m_linearSave = linear; + m_angularSave = angular; +} + +void CConstantForceController::ScaleConstantForce( float scale ) +{ + m_linear = m_linearSave * scale; + m_angular = m_angularSave * scale; +} + + +IMotionEvent::simresult_e CConstantForceController::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + linear = m_linear; + angular = m_angular; + + return m_controlType; +} + +// UNDONE: Make these logical entities +//----------------------------------------------------------------------------- +// Purpose: This is a general entity that has a force/motion controller that +// simply integrates a constant linear/angular acceleration +//----------------------------------------------------------------------------- +abstract_class CPhysForce : public CPointEntity +{ +public: + DECLARE_CLASS( CPhysForce, CPointEntity ); + + CPhysForce(); + ~CPhysForce(); + + DECLARE_DATADESC(); + + virtual void OnRestore( ); + void Spawn( void ); + void Activate( void ); + + void ForceOn( void ); + void ForceOff( void ); + void ActivateForce( void ); + + // Input handlers + void InputActivate( inputdata_t &inputdata ); + void InputDeactivate( inputdata_t &inputdata ); + void InputForceScale( inputdata_t &inputdata ); + + void SaveForce( void ); + void ScaleForce( float scale ); + + // MUST IMPLEMENT THIS IN DERIVED CLASS + virtual void SetupForces( IPhysicsObject *pPhys, Vector &linear, AngularImpulse &angular ) = 0; + + // optional + virtual void OnActivate( void ) {} + +protected: + IPhysicsMotionController *m_pController; + + string_t m_nameAttach; + float m_force; + float m_forceTime; + EHANDLE m_attachedObject; + bool m_wasRestored; + + CConstantForceController m_integrator; +}; + +BEGIN_DATADESC( CPhysForce ) + + DEFINE_PHYSPTR( m_pController ), + DEFINE_KEYFIELD( m_nameAttach, FIELD_STRING, "attach1" ), + DEFINE_KEYFIELD( m_force, FIELD_FLOAT, "force" ), + DEFINE_KEYFIELD( m_forceTime, FIELD_FLOAT, "forcetime" ), + + DEFINE_FIELD( m_attachedObject, FIELD_EHANDLE ), + //DEFINE_FIELD( m_wasRestored, FIELD_BOOLEAN ), // NOTE: DO NOT save/load this - it's used to detect loads + DEFINE_EMBEDDED( m_integrator ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "scale", InputForceScale ), + + // Function Pointers + DEFINE_FUNCTION( ForceOff ), + +END_DATADESC() + + +CPhysForce::CPhysForce( void ) +{ + m_pController = NULL; + m_wasRestored = false; +} + +CPhysForce::~CPhysForce() +{ + if ( m_pController ) + { + physenv->DestroyMotionController( m_pController ); + } +} + +void CPhysForce::Spawn( void ) +{ + if ( m_spawnflags & SF_THRUST_LOCAL_ORIENTATION ) + { + m_integrator.Init( IMotionEvent::SIM_LOCAL_ACCELERATION ); + } + else + { + m_integrator.Init( IMotionEvent::SIM_GLOBAL_ACCELERATION ); + } +} + +void CPhysForce::OnRestore( ) +{ + BaseClass::OnRestore(); + + if ( m_pController ) + { + m_pController->SetEventHandler( &m_integrator ); + } + m_wasRestored = true; +} + +void CPhysForce::Activate( void ) +{ + BaseClass::Activate(); + + if ( m_pController ) + { + m_pController->WakeObjects(); + } + if ( m_wasRestored ) + return; + + if ( m_attachedObject == NULL ) + { + m_attachedObject = gEntList.FindEntityByName( NULL, m_nameAttach ); + } + + // Let the derived class set up before we throw the switch + OnActivate(); + + if ( m_spawnflags & SF_THRUST_STARTACTIVE ) + { + ForceOn(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysForce::InputActivate( inputdata_t &inputdata ) +{ + ForceOn(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysForce::InputDeactivate( inputdata_t &inputdata ) +{ + ForceOff(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysForce::InputForceScale( inputdata_t &inputdata ) +{ + ScaleForce( inputdata.value.Float() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysForce::ForceOn( void ) +{ + if ( m_pController ) + return; + + ActivateForce(); + if ( m_forceTime ) + { + SetNextThink( gpGlobals->curtime + m_forceTime ); + SetThink( &CPhysForce::ForceOff ); + } +} + + +void CPhysForce::ActivateForce( void ) +{ + IPhysicsObject *pPhys = NULL; + if ( m_attachedObject ) + { + pPhys = m_attachedObject->VPhysicsGetObject(); + } + + if ( !pPhys ) + return; + + Vector linear; + AngularImpulse angular; + + SetupForces( pPhys, linear, angular ); + + m_integrator.SetConstantForce( linear, angular ); + m_pController = physenv->CreateMotionController( &m_integrator ); + m_pController->AttachObject( pPhys, true ); + // Make sure the object is simulated + pPhys->Wake(); +} + + +void CPhysForce::ForceOff( void ) +{ + if ( !m_pController ) + return; + + physenv->DestroyMotionController( m_pController ); + m_pController = NULL; + SetThink( NULL ); + SetNextThink( TICK_NEVER_THINK ); + IPhysicsObject *pPhys = NULL; + if ( m_attachedObject ) + { + pPhys = m_attachedObject->VPhysicsGetObject(); + if ( pPhys ) + { + pPhys->Wake(); + } + } +} + + +void CPhysForce::ScaleForce( float scale ) +{ + if ( !m_pController ) + ForceOn(); + + m_integrator.ScaleConstantForce( scale ); + m_pController->WakeObjects(); +} + + +//----------------------------------------------------------------------------- +// Purpose: A rocket-engine/thruster based on the force controller above +// Calculate the force (and optional torque) that the engine would create +//----------------------------------------------------------------------------- +class CPhysThruster : public CPhysForce +{ + DECLARE_CLASS( CPhysThruster, CPhysForce ); +public: + DECLARE_DATADESC(); + + virtual void OnActivate( void ); + virtual void SetupForces( IPhysicsObject *pPhys, Vector &linear, AngularImpulse &angular ); + +private: + Vector m_localOrigin; +}; + +LINK_ENTITY_TO_CLASS( phys_thruster, CPhysThruster ); + +BEGIN_DATADESC( CPhysThruster ) + + DEFINE_FIELD( m_localOrigin, FIELD_VECTOR ), + +END_DATADESC() + + + +void CPhysThruster::OnActivate( void ) +{ + if ( m_attachedObject != NULL ) + { + matrix3x4_t worldToAttached, thrusterToAttached; + MatrixInvert( m_attachedObject->EntityToWorldTransform(), worldToAttached ); + ConcatTransforms( worldToAttached, EntityToWorldTransform(), thrusterToAttached ); + MatrixGetColumn( thrusterToAttached, 3, m_localOrigin ); + + if ( HasSpawnFlags( SF_THRUST_LOCAL_ORIENTATION ) ) + { + QAngle angles; + MatrixAngles( thrusterToAttached, angles ); + SetLocalAngles( angles ); + } + // maintain the local relationship with this entity + // it may move before the thruster is activated + if ( HasSpawnFlags( SF_THRUST_IGNORE_POS ) ) + { + m_localOrigin.Init(); + } + } +} + +// utility function to duplicate this call in local space +void CalculateVelocityOffsetLocal( IPhysicsObject *pPhys, const Vector &forceLocal, const Vector &positionLocal, Vector &outVelLocal, AngularImpulse &outAngular ) +{ + Vector posWorld, forceWorld; + pPhys->LocalToWorld( &posWorld, positionLocal ); + pPhys->LocalToWorldVector( &forceWorld, forceLocal ); + Vector velWorld; + pPhys->CalculateVelocityOffset( forceWorld, posWorld, &velWorld, &outAngular ); + pPhys->WorldToLocalVector( &outVelLocal, velWorld ); +} + +void CPhysThruster::SetupForces( IPhysicsObject *pPhys, Vector &linear, AngularImpulse &angular ) +{ + Vector thrustVector; + AngleVectors( GetLocalAngles(), &thrustVector ); + thrustVector *= m_force; + + // multiply the force by mass (it's actually just an acceleration) + if ( m_spawnflags & SF_THRUST_MASS_INDEPENDENT ) + { + thrustVector *= pPhys->GetMass(); + } + if ( m_spawnflags & SF_THRUST_LOCAL_ORIENTATION ) + { + CalculateVelocityOffsetLocal( pPhys, thrustVector, m_localOrigin, linear, angular ); + } + else + { + Vector position; + VectorTransform( m_localOrigin, m_attachedObject->EntityToWorldTransform(), position ); + pPhys->CalculateVelocityOffset( thrustVector, position, &linear, &angular ); + } + + if ( !(m_spawnflags & SF_THRUST_FORCE) ) + { + // clear out force + linear.Init(); + } + + if ( !(m_spawnflags & SF_THRUST_TORQUE) ) + { + // clear out torque + angular.Init(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: A controllable motor - exerts torque +//----------------------------------------------------------------------------- +class CPhysTorque : public CPhysForce +{ + DECLARE_CLASS( CPhysTorque, CPhysForce ); +public: + DECLARE_DATADESC(); + void Spawn( void ); + virtual void SetupForces( IPhysicsObject *pPhys, Vector &linear, AngularImpulse &angular ); +private: + Vector m_axis; +}; + +BEGIN_DATADESC( CPhysTorque ) + + DEFINE_KEYFIELD( m_axis, FIELD_VECTOR, "axis" ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( phys_torque, CPhysTorque ); + +void CPhysTorque::Spawn( void ) +{ + // force spawnflags to agree with implementation of this class + m_spawnflags |= SF_THRUST_TORQUE | SF_THRUST_MASS_INDEPENDENT; + m_spawnflags &= ~SF_THRUST_FORCE; + + m_axis -= GetAbsOrigin(); + VectorNormalize(m_axis); + UTIL_SnapDirectionToAxis( m_axis ); + BaseClass::Spawn(); +} + +void CPhysTorque::SetupForces( IPhysicsObject *pPhys, Vector &linear, AngularImpulse &angular ) +{ + // clear out force + linear.Init(); + + matrix3x4_t matrix; + pPhys->GetPositionMatrix( &matrix ); + + // transform motor axis to local space + Vector axis_ls; + VectorIRotate( m_axis, matrix, axis_ls ); + + // Set torque to be around selected axis + angular = axis_ls * m_force; +} + + + +//----------------------------------------------------------------------------- +// Purpose: This class only implements the IMotionEvent-specific behavior +// It keeps track of the forces so they can be integrated +//----------------------------------------------------------------------------- +class CMotorController : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); + +public: + IMotionEvent::simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + float m_speed; + float m_maxTorque; + Vector m_axis; + float m_inertiaFactor; + + float m_lastSpeed; + float m_lastAcceleration; + float m_lastForce; + float m_restistanceDamping; +}; + +BEGIN_SIMPLE_DATADESC( CMotorController ) + + DEFINE_FIELD( m_speed, FIELD_FLOAT ), + DEFINE_FIELD( m_maxTorque, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_axis, FIELD_VECTOR, "axis" ), + DEFINE_KEYFIELD( m_inertiaFactor, FIELD_FLOAT, "inertiafactor" ), + DEFINE_FIELD( m_lastSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_lastAcceleration, FIELD_FLOAT ), + DEFINE_FIELD( m_lastForce, FIELD_FLOAT ), + DEFINE_FIELD( m_restistanceDamping, FIELD_FLOAT ), + +END_DATADESC() + + +IMotionEvent::simresult_e CMotorController::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + linear = vec3_origin; + angular = vec3_origin; + + if ( m_speed == 0 ) + return SIM_NOTHING; + + matrix3x4_t matrix; + pObject->GetPositionMatrix( &matrix ); + AngularImpulse currentRotAxis; + + // currentRotAxis is in local space + pObject->GetVelocity( NULL, ¤tRotAxis ); + // transform motor axis to local space + Vector motorAxis_ls; + VectorIRotate( m_axis, matrix, motorAxis_ls ); + float currentSpeed = DotProduct( currentRotAxis, motorAxis_ls ); + + float inertia = DotProductAbs( pObject->GetInertia(), motorAxis_ls ); + + // compute absolute acceleration, don't integrate over the timestep + float accel = m_speed - currentSpeed; + float rotForce = accel * inertia * m_inertiaFactor; + + // BUGBUG: This heuristic is a little flaky + // UNDONE: Make a better heuristic for speed control + if ( fabsf(m_lastAcceleration) > 0 ) + { + float deltaSpeed = currentSpeed - m_lastSpeed; + // make sure they are going the same way + if ( deltaSpeed * accel > 0 ) + { + float factor = deltaSpeed / m_lastAcceleration; + factor = 1 - clamp( factor, 0.f, 1.f ); + rotForce += m_lastForce * factor * m_restistanceDamping; + } + else + { + if ( currentSpeed != 0 ) + { + // have we reached a steady state that isn't our target? + float increase = deltaSpeed / m_lastAcceleration; + if ( fabsf(increase) < 0.05 ) + { + rotForce += m_lastForce * m_restistanceDamping; + } + } + } + } + // ------------------------------------------------------- + + + if ( m_maxTorque != 0 ) + { + if ( rotForce > m_maxTorque ) + { + rotForce = m_maxTorque; + } + else if ( rotForce < -m_maxTorque ) + { + rotForce = -m_maxTorque; + } + } + + m_lastForce = rotForce; + m_lastAcceleration = (rotForce / inertia); + m_lastSpeed = currentSpeed; + + // this is in local space + angular = motorAxis_ls * rotForce; + + return SIM_LOCAL_FORCE; +} + +#define SF_MOTOR_START_ON 0x0001 // starts on by default +#define SF_MOTOR_NOCOLLIDE 0x0002 // don't collide with world geometry +#define SF_MOTOR_HINGE 0x0004 // motor also acts as a hinge constraining the object to this axis +// NOTE: THIS DOESN'T WORK YET +#define SF_MOTOR_LOCAL 0x0008 // Maintain local relationship with the attached object + +class CPhysMotor : public CLogicalEntity +{ + DECLARE_CLASS( CPhysMotor, CLogicalEntity ); +public: + ~CPhysMotor(); + DECLARE_DATADESC(); + void Spawn( void ); + void Activate( void ); + void Think( void ); + + void TurnOn( void ); + void TargetSpeedChanged( void ); + void OnRestore(); + + void InputSetTargetSpeed( inputdata_t &inputdata ); + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void CalculateAcceleration(); + + string_t m_nameAttach; + EHANDLE m_attachedObject; + float m_spinUp; + float m_additionalAcceleration; + float m_angularAcceleration; + float m_lastTime; + // FIXME: can we remove m_flSpeed from CBaseEntity? + //float m_flSpeed; + + IPhysicsConstraint *m_pHinge; + IPhysicsMotionController *m_pController; + CMotorController m_motor; +}; + + +BEGIN_DATADESC( CPhysMotor ) + + DEFINE_KEYFIELD( m_nameAttach, FIELD_STRING, "attach1" ), + DEFINE_FIELD( m_attachedObject, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_spinUp, FIELD_FLOAT, "spinup" ), + DEFINE_KEYFIELD( m_additionalAcceleration, FIELD_FLOAT, "addangaccel" ), + DEFINE_FIELD( m_angularAcceleration, FIELD_FLOAT ), + DEFINE_FIELD( m_lastTime, FIELD_TIME ), + DEFINE_PHYSPTR( m_pHinge ), + DEFINE_PHYSPTR( m_pController ), + + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetTargetSpeed ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + + DEFINE_EMBEDDED( m_motor ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( phys_motor, CPhysMotor ); + + +void CPhysMotor::CalculateAcceleration() +{ + if ( m_spinUp ) + { + m_angularAcceleration = fabsf(m_flSpeed / m_spinUp); + } + else + { + m_angularAcceleration = fabsf(m_flSpeed); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler that sets a speed to spin up or down to. +//----------------------------------------------------------------------------- +void CPhysMotor::InputSetTargetSpeed( inputdata_t &inputdata ) +{ + if ( m_flSpeed == inputdata.value.Float() ) + return; + + m_flSpeed = inputdata.value.Float(); + TargetSpeedChanged(); + CalculateAcceleration(); +} + + +void CPhysMotor::TargetSpeedChanged( void ) +{ + SetNextThink( gpGlobals->curtime ); + m_lastTime = gpGlobals->curtime; + m_pController->WakeObjects(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler that turns the motor on. +//------------------------------------------------------------------------------ +void CPhysMotor::InputTurnOn( inputdata_t &inputdata ) +{ + TurnOn(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler that turns the motor off. +//------------------------------------------------------------------------------ +void CPhysMotor::InputTurnOff( inputdata_t &inputdata ) +{ + m_motor.m_speed = 0; + SetNextThink( TICK_NEVER_THINK ); +} + + +CPhysMotor::~CPhysMotor() +{ + if ( m_attachedObject && m_pHinge ) + { + IPhysicsObject *pPhys = m_attachedObject->VPhysicsGetObject(); + if ( pPhys ) + { + PhysClearGameFlags(pPhys, FVPHYSICS_NO_PLAYER_PICKUP); + } + } + + physenv->DestroyConstraint( m_pHinge ); + physenv->DestroyMotionController( m_pController ); +} + + +void CPhysMotor::Spawn( void ) +{ + m_motor.m_axis -= GetLocalOrigin(); + float axisLength = VectorNormalize(m_motor.m_axis); + // double check that the axis is at least a unit long. If not, warn and self-destruct. + if ( axisLength > 1.0f ) + { + UTIL_SnapDirectionToAxis( m_motor.m_axis ); + } + else + { + Warning("phys_motor %s does not have a valid axis helper, and self-destructed!\n", GetDebugName()); + + m_motor.m_speed = 0; + SetNextThink( TICK_NEVER_THINK ); + + UTIL_Remove(this); + } +} + + +void CPhysMotor::TurnOn( void ) +{ + CBaseEntity *pAttached = m_attachedObject; + if ( !pAttached ) + return; + + IPhysicsObject *pPhys = pAttached->VPhysicsGetObject(); + if ( pPhys ) + { + m_pController->WakeObjects(); + // If the current speed is zero, the objects can run a tick without getting torque'd and go back to sleep + // so force a think now and have some acceleration happen before the controller gets called. + m_lastTime = gpGlobals->curtime - TICK_INTERVAL; + Think(); + } +} + + +void CPhysMotor::Activate( void ) +{ + BaseClass::Activate(); + + // This gets called after all objects spawn and after all objects restore + if ( m_attachedObject == NULL ) + { + CBaseEntity *pAttach = gEntList.FindEntityByName( NULL, m_nameAttach ); + if ( pAttach && pAttach->GetMoveType() == MOVETYPE_VPHYSICS ) + { + m_attachedObject = pAttach; + IPhysicsObject *pPhys = m_attachedObject->VPhysicsGetObject(); + CalculateAcceleration(); + matrix3x4_t matrix; + pPhys->GetPositionMatrix( &matrix ); + Vector motorAxis_ls; + VectorIRotate( m_motor.m_axis, matrix, motorAxis_ls ); + float inertia = DotProductAbs( pPhys->GetInertia(), motorAxis_ls ); + m_motor.m_maxTorque = inertia * m_motor.m_inertiaFactor * (m_angularAcceleration + m_additionalAcceleration); + m_motor.m_restistanceDamping = 1.0f; + } + } + + if ( m_attachedObject ) + { + IPhysicsObject *pPhys = m_attachedObject->VPhysicsGetObject(); + + // create a hinge constraint for this object? + if ( m_spawnflags & SF_MOTOR_HINGE ) + { + // UNDONE: Don't do this on restore? + if ( !m_pHinge ) + { + constraint_hingeparams_t hingeParams; + hingeParams.Defaults(); + hingeParams.worldAxisDirection = m_motor.m_axis; + hingeParams.worldPosition = GetLocalOrigin(); + + m_pHinge = physenv->CreateHingeConstraint( g_PhysWorldObject, pPhys, NULL, hingeParams ); + m_pHinge->SetGameData( (void *)this ); + // can't grab this object + PhysSetGameFlags(pPhys, FVPHYSICS_NO_PLAYER_PICKUP); + } + + if ( m_spawnflags & SF_MOTOR_NOCOLLIDE ) + { + PhysDisableEntityCollisions( g_PhysWorldObject, pPhys ); + } + } + else + { + m_pHinge = NULL; + } + + // NOTE: On restore, this path isn't run because m_pController will not be NULL + if ( !m_pController ) + { + m_pController = physenv->CreateMotionController( &m_motor ); + m_pController->AttachObject( m_attachedObject->VPhysicsGetObject(), false ); + + if ( m_spawnflags & SF_MOTOR_START_ON ) + { + TurnOn(); + } + } + } +} + +void CPhysMotor::OnRestore() +{ + BaseClass::OnRestore(); + // Need to do this on restore since there's no good way to save this + if ( m_pController ) + { + m_pController->SetEventHandler( &m_motor ); + } +} + +void CPhysMotor::Think( void ) +{ + // angular acceleration is always positive - it should be treated as a magnitude - the controller + // will apply it in the proper direction + Assert(m_angularAcceleration>=0); + + m_motor.m_speed = UTIL_Approach( m_flSpeed, m_motor.m_speed, m_angularAcceleration*(gpGlobals->curtime-m_lastTime) ); + m_lastTime = gpGlobals->curtime; + if ( m_motor.m_speed != m_flSpeed ) + { + SetNextThink( gpGlobals->curtime ); + } +} + +//====================================================================================== +// KEEPUPRIGHT CONTROLLER +//====================================================================================== + +class CKeepUpright : public CPointEntity, public IMotionEvent +{ + DECLARE_CLASS( CKeepUpright, CPointEntity ); +public: + DECLARE_DATADESC(); + + CKeepUpright(); + ~CKeepUpright(); + void Spawn(); + void Activate(); + + // IMotionEvent + virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + + // Inputs + void InputTurnOn( inputdata_t &inputdata ) + { + m_bActive = true; + } + void InputTurnOff( inputdata_t &inputdata ) + { + m_bActive = false; + } + + void InputSetAngularLimit( inputdata_t &inputdata ) + { + m_angularLimit = inputdata.value.Float(); + } + +private: + friend CBaseEntity *CreateKeepUpright( const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner, float flAngularLimit, bool bActive ); + + Vector m_worldGoalAxis; + Vector m_localTestAxis; + IPhysicsMotionController *m_pController; + string_t m_nameAttach; + EHANDLE m_attachedObject; + float m_angularLimit; + bool m_bActive; + bool m_bDampAllRotation; +}; + +#define SF_KEEPUPRIGHT_START_INACTIVE 0x0001 + +LINK_ENTITY_TO_CLASS( phys_keepupright, CKeepUpright ); + +BEGIN_DATADESC( CKeepUpright ) + + DEFINE_FIELD( m_worldGoalAxis, FIELD_VECTOR ), + DEFINE_FIELD( m_localTestAxis, FIELD_VECTOR ), + DEFINE_PHYSPTR( m_pController ), + DEFINE_KEYFIELD( m_nameAttach, FIELD_STRING, "attach1" ), + DEFINE_FIELD( m_attachedObject, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_angularLimit, FIELD_FLOAT, "angularlimit" ), + DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bDampAllRotation, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetAngularLimit", InputSetAngularLimit ), + +END_DATADESC() + +CKeepUpright::CKeepUpright() +{ + // by default, recover from up to 15 degrees / sec angular velocity + m_angularLimit = 15; + m_attachedObject = NULL; + m_bDampAllRotation = false; +} + +CKeepUpright::~CKeepUpright() +{ + if ( m_pController ) + { + physenv->DestroyMotionController( m_pController ); + m_pController = NULL; + } +} + +void CKeepUpright::Spawn() +{ + // align the object's local Z axis + m_localTestAxis.Init( 0, 0, 1 ); + // Use our Up axis so mapmakers can orient us arbitrarily + GetVectors( NULL, NULL, &m_worldGoalAxis ); + + SetMoveType( MOVETYPE_NONE ); + + if ( m_spawnflags & SF_KEEPUPRIGHT_START_INACTIVE ) + { + m_bActive = false; + } + else + { + m_bActive = true; + } +} + +void CKeepUpright::Activate() +{ + BaseClass::Activate(); + + if ( !m_pController ) + { + // This case occurs when spawning + IPhysicsObject *pPhys; + if ( m_attachedObject ) + { + pPhys = m_attachedObject->VPhysicsGetObject(); + } + else + { + pPhys = FindPhysicsObjectByName( STRING(m_nameAttach), this ); + } + + if ( !pPhys ) + { + UTIL_Remove(this); + return; + } + // HACKHACK: Due to changes in the vehicle simulator the keepupright controller used in coast_01 is unstable + // force it to have perfect damping to compensate. + // detect it using the hack of angular limit == 150, attached to a vehicle + // Fixing it in the code is the simplest course of action presently +#ifdef HL2_DLL + if ( m_angularLimit == 150.0f ) + { + CBaseEntity *pEntity = static_cast(pPhys->GetGameData()); + if ( pEntity && pEntity->GetServerVehicle() && Q_stristr( gpGlobals->mapname.ToCStr(), "d2_coast_01" ) ) + { + m_bDampAllRotation = true; + } + } +#endif + + m_pController = physenv->CreateMotionController( (IMotionEvent *)this ); + m_pController->AttachObject( pPhys, false ); + } + else + { + // This case occurs when restoring + m_pController->SetEventHandler( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Use this to spawn a keepupright controller via code instead of map-placed +//----------------------------------------------------------------------------- +CBaseEntity *CreateKeepUpright( const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner, float flAngularLimit, bool bActive ) +{ + CKeepUpright *pKeepUpright = (CKeepUpright*)CBaseEntity::Create( "phys_keepupright", vecOrigin, vecAngles, pOwner ); + if ( pKeepUpright ) + { + pKeepUpright->m_attachedObject = pOwner; + pKeepUpright->m_angularLimit = flAngularLimit; + if ( !bActive ) + { + pKeepUpright->AddSpawnFlags( SF_KEEPUPRIGHT_START_INACTIVE ); + } + pKeepUpright->Spawn(); + pKeepUpright->Activate(); + } + + return pKeepUpright; +} + +IMotionEvent::simresult_e CKeepUpright::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + if ( !m_bActive ) + return SIM_NOTHING; + + linear.Init(); + + AngularImpulse angVel; + pObject->GetVelocity( NULL, &angVel ); + + matrix3x4_t matrix; + // get the object's local to world transform + pObject->GetPositionMatrix( &matrix ); + + // Get the alignment axis in object space + Vector currentLocalTargetAxis; + VectorIRotate( m_worldGoalAxis, matrix, currentLocalTargetAxis ); + + float invDeltaTime = (1/deltaTime); + + if ( m_bDampAllRotation ) + { + angular = ComputeRotSpeedToAlignAxes( m_localTestAxis, currentLocalTargetAxis, angVel, 0, invDeltaTime, m_angularLimit ); + angular -= angVel; + angular *= invDeltaTime; + return SIM_LOCAL_ACCELERATION; + } + + angular = ComputeRotSpeedToAlignAxes( m_localTestAxis, currentLocalTargetAxis, angVel, 1.0, invDeltaTime, m_angularLimit ); + angular *= invDeltaTime; + +#if 0 + Vector position, out, worldAxis; + MatrixGetColumn( matrix, 3, position ); + out = angular * 0.1; + VectorRotate( m_localTestAxis, matrix, worldAxis ); + NDebugOverlay::Line( position, position + worldAxis * 100, 255, 0, 0, 0, 0 ); + NDebugOverlay::Line( position, position + m_worldGoalAxis * 100, 255, 0, 0, 0, 0 ); + NDebugOverlay::Line( position, position + out, 255, 255, 0, 0, 0 ); +#endif + + return SIM_LOCAL_ACCELERATION; +} + + +// computes the torque necessary to align testAxis with alignAxis +AngularImpulse ComputeRotSpeedToAlignAxes( const Vector &testAxis, const Vector &alignAxis, const AngularImpulse ¤tSpeed, float damping, float scale, float maxSpeed ) +{ + Vector rotationAxis = CrossProduct( testAxis, alignAxis ); + + // atan2() is well defined, so do a Dot & Cross instead of asin(Cross) + float cosine = DotProduct( testAxis, alignAxis ); + float sine = VectorNormalize( rotationAxis ); + float angle = atan2( sine, cosine ); + + angle = RAD2DEG(angle); + AngularImpulse angular = rotationAxis * scale * angle; + angular -= rotationAxis * damping * DotProduct( currentSpeed, rotationAxis ); + + float len = VectorNormalize( angular ); + + if ( len > maxSpeed ) + { + len = maxSpeed; + } + + return angular * len; +} + diff --git a/sp/src/game/server/phys_controller.h b/sp/src/game/server/phys_controller.h new file mode 100644 index 00000000..1492a501 --- /dev/null +++ b/sp/src/game/server/phys_controller.h @@ -0,0 +1,19 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PHYS_CONTROLLER_H +#define PHYS_CONTROLLER_H +#ifdef _WIN32 +#pragma once +#endif + + +CBaseEntity *CreateKeepUpright( const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner, float flAngularLimit, bool bActive ); + +AngularImpulse ComputeRotSpeedToAlignAxes( const Vector &testAxis, const Vector &alignAxis, const AngularImpulse ¤tSpeed, + float damping, float scale, float maxSpeed ); + +#endif // PHYS_CONTROLLER_H diff --git a/sp/src/game/server/physconstraint.cpp b/sp/src/game/server/physconstraint.cpp new file mode 100644 index 00000000..467c7cfb --- /dev/null +++ b/sp/src/game/server/physconstraint.cpp @@ -0,0 +1,1912 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Physics constraint entities +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "physics.h" +#include "entityoutput.h" +#include "engine/IEngineSound.h" +#include "igamesystem.h" +#include "physics_saverestore.h" +#include "vcollide_parse.h" +#include "positionwatcher.h" +#include "fmtstr.h" +#include "physics_prop_ragdoll.h" + +#define HINGE_NOTIFY HL2_EPISODIC +#if HINGE_NOTIFY +#include "physconstraint_sounds.h" +#endif + +#include "physconstraint.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_CONSTRAINT_DISABLE_COLLISION 0x0001 +#define SF_SLIDE_LIMIT_ENDS 0x0002 +#define SF_PULLEY_RIGID 0x0002 +#define SF_LENGTH_RIGID 0x0002 +#define SF_RAGDOLL_FREEMOVEMENT 0x0002 +#define SF_CONSTRAINT_START_INACTIVE 0x0004 +#define SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY 0x0008 +#define SF_CONSTRAINT_NO_CONNECT_UNTIL_ACTIVATED 0x0010 // Will only check the two attached entities at activation + + +ConVar g_debug_constraint_sounds ( "g_debug_constraint_sounds", "0", FCVAR_CHEAT, "Enable debug printing about constraint sounds."); + +struct constraint_anchor_t +{ + Vector localOrigin; + EHANDLE hEntity; + int parentAttachment; + string_t name; + float massScale; +}; + +class CAnchorList : public CAutoGameSystem +{ +public: + CAnchorList( char const *name ) : CAutoGameSystem( name ) + { + } + void LevelShutdownPostEntity() + { + m_list.Purge(); + } + + void AddToList( CBaseEntity *pEntity, float massScale ) + { + int index = m_list.AddToTail(); + constraint_anchor_t *pAnchor = &m_list[index]; + + pAnchor->hEntity = pEntity->GetParent(); + pAnchor->parentAttachment = pEntity->GetParentAttachment(); + pAnchor->name = pEntity->GetEntityName(); + pAnchor->localOrigin = pEntity->GetLocalOrigin(); + pAnchor->massScale = massScale; + } + + constraint_anchor_t *Find( string_t name ) + { + for ( int i = m_list.Count()-1; i >=0; i-- ) + { + if ( FStrEq( STRING(m_list[i].name), STRING(name) ) ) + { + return &m_list[i]; + } + } + return NULL; + } + +private: + CUtlVector m_list; +}; + +static CAnchorList g_AnchorList( "CAnchorList" ); + +class CConstraintAnchor : public CPointEntity +{ + DECLARE_CLASS( CConstraintAnchor, CPointEntity ); +public: + CConstraintAnchor() + { + m_massScale = 1.0f; + } + void Spawn( void ) + { + if ( GetParent() ) + { + g_AnchorList.AddToList( this, m_massScale ); + UTIL_Remove( this ); + } + } + DECLARE_DATADESC(); + + float m_massScale; +}; + +BEGIN_DATADESC( CConstraintAnchor ) + DEFINE_KEYFIELD( m_massScale, FIELD_FLOAT, "massScale" ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( info_constraint_anchor, CConstraintAnchor ); + +class CPhysConstraintSystem : public CLogicalEntity +{ + DECLARE_CLASS( CPhysConstraintSystem, CLogicalEntity ); +public: + + void Spawn(); + IPhysicsConstraintGroup *GetVPhysicsGroup() { return m_pMachine; } + + DECLARE_DATADESC(); +private: + IPhysicsConstraintGroup *m_pMachine; + int m_additionalIterations; +}; + +BEGIN_DATADESC( CPhysConstraintSystem ) + DEFINE_PHYSPTR( m_pMachine ), + DEFINE_KEYFIELD( m_additionalIterations, FIELD_INTEGER, "additionaliterations" ), + +END_DATADESC() + + +void CPhysConstraintSystem::Spawn() +{ + constraint_groupparams_t group; + group.Defaults(); + group.additionalIterations = m_additionalIterations; + m_pMachine = physenv->CreateConstraintGroup( group ); +} + +LINK_ENTITY_TO_CLASS( phys_constraintsystem, CPhysConstraintSystem ); + +void PhysTeleportConstrainedEntity( CBaseEntity *pTeleportSource, IPhysicsObject *pObject0, IPhysicsObject *pObject1, const Vector &prevPosition, const QAngle &prevAngles, bool physicsRotate ) +{ + // teleport the other object + CBaseEntity *pEntity0 = static_cast (pObject0->GetGameData()); + CBaseEntity *pEntity1 = static_cast (pObject1->GetGameData()); + if ( !pEntity0 || !pEntity1 ) + return; + + // figure out which entity needs to be fixed up (the one that isn't pTeleportSource) + CBaseEntity *pFixup = pEntity1; + // teleport the other object + if ( pTeleportSource != pEntity0 ) + { + if ( pTeleportSource != pEntity1 ) + { + Msg("Bogus teleport notification!!\n"); + return; + } + pFixup = pEntity0; + } + + // constraint doesn't move this entity + if ( pFixup->GetMoveType() != MOVETYPE_VPHYSICS ) + return; + + if ( !pFixup->VPhysicsGetObject() || !pFixup->VPhysicsGetObject()->IsMoveable() ) + return; + + QAngle oldAngles = prevAngles; + + if ( !physicsRotate ) + { + oldAngles = pTeleportSource->GetAbsAngles(); + } + + matrix3x4_t startCoord, startInv, endCoord, xform; + AngleMatrix( oldAngles, prevPosition, startCoord ); + MatrixInvert( startCoord, startInv ); + ConcatTransforms( pTeleportSource->EntityToWorldTransform(), startInv, xform ); + QAngle fixupAngles; + Vector fixupPos; + + ConcatTransforms( xform, pFixup->EntityToWorldTransform(), endCoord ); + MatrixAngles( endCoord, fixupAngles, fixupPos ); + pFixup->Teleport( &fixupPos, &fixupAngles, NULL ); +} + +static void DrawPhysicsBounds( IPhysicsObject *pObject, int r, int g, int b, int a ) +{ + const CPhysCollide *pCollide = pObject->GetCollide(); + Vector pos; + QAngle angles; + pObject->GetPosition( &pos, &angles ); + Vector mins, maxs; + physcollision->CollideGetAABB( &mins, &maxs, pCollide, vec3_origin, vec3_angle ); + // don't fight the z-buffer + mins -= Vector(1,1,1); + maxs += Vector(1,1,1); + NDebugOverlay::BoxAngles( pos, mins, maxs, angles, r, g, b, a, 0 ); +} + +static void DrawConstraintObjectsAxes(CBaseEntity *pConstraintEntity, IPhysicsConstraint *pConstraint) +{ + if ( !pConstraint || !pConstraintEntity ) + return; + matrix3x4_t xformRef, xformAtt; + bool bXform = pConstraint->GetConstraintTransform( &xformRef, &xformAtt ); + IPhysicsObject *pRef = pConstraint->GetReferenceObject(); + + if ( pRef && !pRef->IsStatic() ) + { + if ( bXform ) + { + Vector pos, posWorld; + QAngle angles; + MatrixAngles( xformRef, angles, pos ); + pRef->LocalToWorld( &posWorld, pos ); + NDebugOverlay::Axis( posWorld, vec3_angle, 12, false, 0 ); + } + DrawPhysicsBounds( pRef, 0, 255, 0, 12 ); + } + IPhysicsObject *pAttach = pConstraint->GetAttachedObject(); + if ( pAttach && !pAttach->IsStatic() ) + { + if ( bXform ) + { + Vector pos, posWorld; + QAngle angles; + MatrixAngles( xformAtt, angles, pos ); + pAttach->LocalToWorld( &posWorld, pos ); + NDebugOverlay::Axis( posWorld, vec3_angle, 12, false, 0 ); + } + DrawPhysicsBounds( pAttach, 255, 0, 0, 12 ); + } +} + +void CPhysConstraint::ClearStaticFlag( IPhysicsObject *pObj ) +{ + if ( !pObj ) + return; + PhysClearGameFlags( pObj, FVPHYSICS_CONSTRAINT_STATIC ); +} + +void CPhysConstraint::Deactivate() +{ + if ( !m_pConstraint ) + return; + m_pConstraint->Deactivate(); + ClearStaticFlag( m_pConstraint->GetReferenceObject() ); + ClearStaticFlag( m_pConstraint->GetAttachedObject() ); + if ( m_spawnflags & SF_CONSTRAINT_DISABLE_COLLISION ) + { + // constraint may be getting deactivated because an object got deleted, so check them here. + IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); + IPhysicsObject *pAtt = m_pConstraint->GetAttachedObject(); + if ( pRef && pAtt ) + { + PhysEnableEntityCollisions( pRef, pAtt ); + } + } +} + +void CPhysConstraint::OnBreak( void ) +{ + Deactivate(); + if ( m_breakSound != NULL_STRING ) + { + CPASAttenuationFilter filter( this, ATTN_STATIC ); + + Vector origin = GetAbsOrigin(); + Vector refPos = origin, attachPos = origin; + + IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); + if ( pRef && (pRef != g_PhysWorldObject) ) + { + pRef->GetPosition( &refPos, NULL ); + attachPos = refPos; + } + IPhysicsObject *pAttach = m_pConstraint->GetAttachedObject(); + if ( pAttach && (pAttach != g_PhysWorldObject) ) + { + pAttach->GetPosition( &attachPos, NULL ); + if ( !pRef || (pRef == g_PhysWorldObject) ) + { + refPos = attachPos; + } + } + + VectorAdd( refPos, attachPos, origin ); + origin *= 0.5f; + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = STRING(m_breakSound); + ep.m_flVolume = VOL_NORM; + ep.m_SoundLevel = ATTN_TO_SNDLVL( ATTN_STATIC ); + ep.m_pOrigin = &origin; + + EmitSound( filter, entindex(), ep ); + } + m_OnBreak.FireOutput( this, this ); + // queue this up to be deleted at the end of physics + // The Deactivate() call should make sure we don't get more of these callbacks. + PhysCallbackRemove( this->NetworkProp() ); +} + +void CPhysConstraint::InputBreak( inputdata_t &inputdata ) +{ + if ( m_pConstraint ) + m_pConstraint->Deactivate(); + + OnBreak(); +} + +void CPhysConstraint::InputOnBreak( inputdata_t &inputdata ) +{ + OnBreak(); +} + +void CPhysConstraint::InputTurnOn( inputdata_t &inputdata ) +{ + if ( HasSpawnFlags( SF_CONSTRAINT_NO_CONNECT_UNTIL_ACTIVATED ) ) + { + ActivateConstraint(); + } + + if ( !m_pConstraint || !m_pConstraint->GetReferenceObject() || !m_pConstraint->GetAttachedObject() ) + return; + + m_pConstraint->Activate(); + m_pConstraint->GetReferenceObject()->Wake(); + m_pConstraint->GetAttachedObject()->Wake(); +} + +void CPhysConstraint::InputTurnOff( inputdata_t &inputdata ) +{ + Deactivate(); +} + +int CPhysConstraint::DrawDebugTextOverlays() +{ + int pos = BaseClass::DrawDebugTextOverlays(); + if ( m_pConstraint && (m_debugOverlays & OVERLAY_TEXT_BIT) ) + { + constraint_breakableparams_t params; + Q_memset(¶ms,0,sizeof(params)); + m_pConstraint->GetConstraintParams( ¶ms ); + + if ( (params.bodyMassScale[0] != 1.0f && params.bodyMassScale[0] != 0.0f) || (params.bodyMassScale[1] != 1.0f && params.bodyMassScale[1] != 0.0f) ) + { + CFmtStr str("mass ratio %.4f:%.4f\n", params.bodyMassScale[0], params.bodyMassScale[1] ); + NDebugOverlay::EntityTextAtPosition( GetAbsOrigin(), pos, str.Access(), 0, 255, 255, 0, 255 ); + } + pos++; + } + return pos; +} + +void CPhysConstraint::DrawDebugGeometryOverlays() +{ + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + DrawConstraintObjectsAxes(this, m_pConstraint); + } + BaseClass::DrawDebugGeometryOverlays(); +} + +void CPhysConstraint::GetBreakParams( constraint_breakableparams_t ¶ms, const hl_constraint_info_t &info ) +{ + params.Defaults(); + params.forceLimit = lbs2kg(m_forceLimit); + params.torqueLimit = lbs2kg(m_torqueLimit); + params.isActive = HasSpawnFlags( SF_CONSTRAINT_START_INACTIVE ) ? false : true; + params.bodyMassScale[0] = info.massScale[0]; + params.bodyMassScale[1] = info.massScale[1]; +} + +BEGIN_DATADESC( CPhysConstraint ) + + DEFINE_PHYSPTR( m_pConstraint ), + + DEFINE_KEYFIELD( m_nameSystem, FIELD_STRING, "constraintsystem" ), + DEFINE_KEYFIELD( m_nameAttach1, FIELD_STRING, "attach1" ), + DEFINE_KEYFIELD( m_nameAttach2, FIELD_STRING, "attach2" ), + DEFINE_KEYFIELD( m_breakSound, FIELD_SOUNDNAME, "breaksound" ), + DEFINE_KEYFIELD( m_forceLimit, FIELD_FLOAT, "forcelimit" ), + DEFINE_KEYFIELD( m_torqueLimit, FIELD_FLOAT, "torquelimit" ), + DEFINE_KEYFIELD( m_minTeleportDistance, FIELD_FLOAT, "teleportfollowdistance" ), +// DEFINE_FIELD( m_teleportTick, FIELD_INTEGER ), + + DEFINE_OUTPUT( m_OnBreak, "OnBreak" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Break", InputBreak ), + DEFINE_INPUTFUNC( FIELD_VOID, "ConstraintBroken", InputOnBreak ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + +END_DATADESC() + + +CPhysConstraint::CPhysConstraint( void ) +{ + m_pConstraint = NULL; + m_nameAttach1 = NULL_STRING; + m_nameAttach2 = NULL_STRING; + m_forceLimit = 0; + m_torqueLimit = 0; + m_teleportTick = 0xFFFFFFFF; + m_minTeleportDistance = 0.0f; +} + +CPhysConstraint::~CPhysConstraint() +{ + Deactivate(); + physenv->DestroyConstraint( m_pConstraint ); +} + +void CPhysConstraint::Precache( void ) +{ + if ( m_breakSound != NULL_STRING ) + { + PrecacheScriptSound( STRING(m_breakSound) ); + } +} + +void CPhysConstraint::Spawn( void ) +{ + BaseClass::Spawn(); + + Precache(); +} + +// debug function - slow, uses dynamic_cast<> - use this to query the attached objects +// physics_debug_entity toggles the constraint system for an object using this +bool GetConstraintAttachments( CBaseEntity *pEntity, CBaseEntity *pAttachOut[2], IPhysicsObject *pAttachVPhysics[2] ) +{ + CPhysConstraint *pConstraintEntity = dynamic_cast(pEntity); + if ( pConstraintEntity ) + { + IPhysicsConstraint *pConstraint = pConstraintEntity->GetPhysConstraint(); + if ( pConstraint ) + { + IPhysicsObject *pRef = pConstraint->GetReferenceObject(); + pAttachVPhysics[0] = pRef; + pAttachOut[0] = pRef ? static_cast(pRef->GetGameData()) : NULL; + IPhysicsObject *pAttach = pConstraint->GetAttachedObject(); + pAttachVPhysics[1] = pAttach; + pAttachOut[1] = pAttach ? static_cast(pAttach->GetGameData()) : NULL; + return true; + } + } + return false; +} + +void DebugConstraint(CBaseEntity *pEntity) +{ + CPhysConstraint *pConstraintEntity = dynamic_cast(pEntity); + if ( pConstraintEntity ) + { + IPhysicsConstraint *pConstraint = pConstraintEntity->GetPhysConstraint(); + if ( pConstraint ) + { + pConstraint->OutputDebugInfo(); + } + } +} + + +void FindPhysicsAnchor( string_t name, hl_constraint_info_t &info, int index, CBaseEntity *pErrorEntity ) +{ + constraint_anchor_t *pAnchor = g_AnchorList.Find( name ); + if ( pAnchor ) + { + CBaseEntity *pEntity = pAnchor->hEntity; + if ( pEntity ) + { + info.massScale[index] = pAnchor->massScale; + bool bWroteAttachment = false; + if ( pAnchor->parentAttachment > 0 ) + { + CBaseAnimating *pAnim = pAnchor->hEntity->GetBaseAnimating(); + if ( pAnim ) + { + IPhysicsObject *list[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int listCount = pAnchor->hEntity->VPhysicsGetObjectList( list, ARRAYSIZE(list) ); + int iPhysicsBone = pAnim->GetPhysicsBone( pAnim->GetAttachmentBone( pAnchor->parentAttachment ) ); + if ( iPhysicsBone < listCount ) + { + Vector pos; + info.pObjects[index] = list[iPhysicsBone]; + pAnim->GetAttachment( pAnchor->parentAttachment, pos ); + list[iPhysicsBone]->WorldToLocal( &info.anchorPosition[index], pos ); + bWroteAttachment = true; + } + } + } + if ( !bWroteAttachment ) + { + info.anchorPosition[index] = pAnchor->localOrigin; + info.pObjects[index] = pAnchor->hEntity->VPhysicsGetObject(); + } + } + else + { + pAnchor = NULL; + } + } + if ( !pAnchor ) + { + info.anchorPosition[index] = vec3_origin; + info.pObjects[index] = FindPhysicsObjectByName( STRING(name), pErrorEntity ); + info.massScale[index] = 1.0f; + } +} + +void CPhysConstraint::OnConstraintSetup( hl_constraint_info_t &info ) +{ + if ( info.pObjects[0] && info.pObjects[1] ) + { + SetupTeleportationHandling( info ); + } + if ( m_spawnflags & SF_CONSTRAINT_DISABLE_COLLISION ) + { + PhysDisableEntityCollisions( info.pObjects[0], info.pObjects[1] ); + } +} + +void CPhysConstraint::SetupTeleportationHandling( hl_constraint_info_t &info ) +{ + CBaseEntity *pEntity0 = (CBaseEntity *)info.pObjects[0]->GetGameData(); + if ( pEntity0 ) + { + g_pNotify->AddEntity( this, pEntity0 ); + } + + CBaseEntity *pEntity1 = (CBaseEntity *)info.pObjects[1]->GetGameData(); + if ( pEntity1 ) + { + g_pNotify->AddEntity( this, pEntity1 ); + } +} + +static IPhysicsConstraintGroup *GetRagdollConstraintGroup( IPhysicsObject *pObj ) +{ + if ( pObj ) + { + CBaseEntity *pEntity = static_cast(pObj->GetGameData()); + ragdoll_t *pRagdoll = Ragdoll_GetRagdoll(pEntity); + if ( pRagdoll ) + return pRagdoll->pGroup; + } + return NULL; +} + +void CPhysConstraint::GetConstraintObjects( hl_constraint_info_t &info ) +{ + FindPhysicsAnchor( m_nameAttach1, info, 0, this ); + FindPhysicsAnchor( m_nameAttach2, info, 1, this ); + + // Missing one object, assume the world instead + if ( info.pObjects[0] == NULL && info.pObjects[1] ) + { + // This brokens hanging lamps in hl2mp +#if !defined ( HL2MP ) + if ( Q_strlen(STRING(m_nameAttach1)) ) + { + Warning("Bogus constraint %s (attaches ENTITY NOT FOUND:%s to %s)\n", GetDebugName(), STRING(m_nameAttach1), STRING(m_nameAttach2)); +#ifdef HL2_EPISODIC + info.pObjects[0] = info.pObjects[1] = NULL; + return; +#endif // HL2_EPISODIC + } +#endif + info.pObjects[0] = g_PhysWorldObject; + info.massScale[0] = info.massScale[1] = 1.0f; // no mass scale on world constraint + + } + else if ( info.pObjects[0] && !info.pObjects[1] ) + { + // This brokens hanging lamps in hl2mp +#if !defined ( HL2MP ) + if ( Q_strlen(STRING(m_nameAttach2)) ) + { + Warning("Bogus constraint %s (attaches %s to ENTITY NOT FOUND:%s)\n", GetDebugName(), STRING(m_nameAttach1), STRING(m_nameAttach2)); +#ifdef HL2_EPISODIC + info.pObjects[0] = info.pObjects[1] = NULL; + return; +#endif // HL2_EPISODIC + } +#endif + info.pObjects[1] = info.pObjects[0]; + info.pObjects[0] = g_PhysWorldObject; // Try to make the world object consistently object0 for ease of implementation + info.massScale[0] = info.massScale[1] = 1.0f; // no mass scale on world constraint + info.swapped = true; + } + + info.pGroup = GetRagdollConstraintGroup(info.pObjects[0]); + if ( !info.pGroup ) + { + info.pGroup = GetRagdollConstraintGroup(info.pObjects[1]); + } +} + +void CPhysConstraint::Activate( void ) +{ + BaseClass::Activate(); + + if ( HasSpawnFlags( SF_CONSTRAINT_NO_CONNECT_UNTIL_ACTIVATED ) == false ) + { + if ( !ActivateConstraint() ) + { + UTIL_Remove(this); + } + } +} + +IPhysicsConstraintGroup *GetConstraintGroup( string_t systemName ) +{ + CBaseEntity *pMachine = gEntList.FindEntityByName( NULL, systemName ); + + if ( pMachine ) + { + CPhysConstraintSystem *pGroup = dynamic_cast(pMachine); + if ( pGroup ) + { + return pGroup->GetVPhysicsGroup(); + } + } + return NULL; +} + +bool CPhysConstraint::ActivateConstraint( void ) +{ + // A constraint attaches two objects to each other. + // The constraint is specified in the coordinate frame of the "reference" object + // and constrains the "attached" object + hl_constraint_info_t info; + if ( m_pConstraint ) + { + // already have a constraint, don't make a new one + info.pObjects[0] = m_pConstraint->GetReferenceObject(); + info.pObjects[1] = m_pConstraint->GetAttachedObject(); + OnConstraintSetup(info); + return true; + } + + GetConstraintObjects( info ); + if ( !info.pObjects[0] && !info.pObjects[1] ) + return false; + + if ( info.pObjects[0]->IsStatic() && info.pObjects[1]->IsStatic() ) + { + Warning("Constraint (%s) attached to two static objects (%s and %s)!!!\n", STRING(GetEntityName()), STRING(m_nameAttach1), m_nameAttach2 == NULL_STRING ? "world" : STRING(m_nameAttach2) ); + return false; + } + + if ( info.pObjects[0]->GetShadowController() && info.pObjects[1]->GetShadowController() ) + { + Warning("Constraint (%s) attached to two shadow objects (%s and %s)!!!\n", STRING(GetEntityName()), STRING(m_nameAttach1), m_nameAttach2 == NULL_STRING ? "world" : STRING(m_nameAttach2) ); + return false; + } + IPhysicsConstraintGroup *pGroup = GetConstraintGroup( m_nameSystem ); + if ( !pGroup ) + { + pGroup = info.pGroup; + } + m_pConstraint = CreateConstraint( pGroup, info ); + if ( !m_pConstraint ) + return false; + + m_pConstraint->SetGameData( (void *)this ); + + if ( pGroup ) + { + pGroup->Activate(); + } + + OnConstraintSetup(info); + + return true; +} + +void CPhysConstraint::NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ) +{ + // don't recurse + if ( eventType != NOTIFY_EVENT_TELEPORT || (unsigned int)gpGlobals->tickcount == m_teleportTick ) + return; + + float distance = (params.pTeleport->prevOrigin - pNotify->GetAbsOrigin()).Length(); + + // no need to follow a small teleport + if ( distance <= m_minTeleportDistance ) + return; + + m_teleportTick = gpGlobals->tickcount; + + PhysTeleportConstrainedEntity( pNotify, m_pConstraint->GetReferenceObject(), m_pConstraint->GetAttachedObject(), params.pTeleport->prevOrigin, params.pTeleport->prevAngles, params.pTeleport->physicsRotate ); +} + +class CPhysHinge : public CPhysConstraint, public IVPhysicsWatcher +{ + DECLARE_CLASS( CPhysHinge, CPhysConstraint ); + +public: + void Spawn( void ); + IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) + { + if ( m_hinge.worldAxisDirection == vec3_origin ) + { + DevMsg("ERROR: Hinge with bad data!!!\n" ); + return NULL; + } + GetBreakParams( m_hinge.constraint, info ); + m_hinge.constraint.strength = 1.0; + // BUGBUG: These numbers are very hard to edit + // Scale by 1000 to make things easier + // CONSIDER: Unify the units of torque around something other + // than HL units (kg * in^2 / s ^2) + m_hinge.hingeAxis.SetAxisFriction( 0, 0, m_hingeFriction * 1000 ); + + int hingeAxis; + if ( IsWorldHinge( info, &hingeAxis ) ) + { + info.pObjects[1]->BecomeHinged( hingeAxis ); + } + else + { + RemoveSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ); + } + + return physenv->CreateHingeConstraint( info.pObjects[0], info.pObjects[1], pGroup, m_hinge ); + } + + void DrawDebugGeometryOverlays() + { + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + NDebugOverlay::Line(m_hinge.worldPosition, m_hinge.worldPosition + 48 * m_hinge.worldAxisDirection, 0, 255, 0, false, 0 ); + } + BaseClass::DrawDebugGeometryOverlays(); + } + + void InputSetVelocity( inputdata_t &inputdata ) + { + if ( !m_pConstraint || !m_pConstraint->GetReferenceObject() || !m_pConstraint->GetAttachedObject() ) + return; + + float speed = inputdata.value.Float(); + float massLoad = 1; + int numMasses = 0; + if ( m_pConstraint->GetReferenceObject()->IsMoveable() ) + { + massLoad = m_pConstraint->GetReferenceObject()->GetInertia().Length(); + numMasses++; + m_pConstraint->GetReferenceObject()->Wake(); + } + if ( m_pConstraint->GetAttachedObject()->IsMoveable() ) + { + massLoad += m_pConstraint->GetAttachedObject()->GetInertia().Length(); + numMasses++; + m_pConstraint->GetAttachedObject()->Wake(); + } + if ( numMasses > 0 ) + { + massLoad /= (float)numMasses; + } + + float loadscale = m_systemLoadScale != 0 ? m_systemLoadScale : 1; + m_pConstraint->SetAngularMotor( speed, speed * loadscale * massLoad * loadscale * (1.0/TICK_INTERVAL) ); + } + + void InputSetHingeFriction( inputdata_t &inputdata ) + { + m_hingeFriction = inputdata.value.Float(); + Msg("Setting hinge friction to %f\n", m_hingeFriction ); + m_hinge.hingeAxis.SetAxisFriction( 0, 0, m_hingeFriction * 1000 ); + } + + virtual void Deactivate() + { + if ( HasSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ) ) + { + if ( m_pConstraint && m_pConstraint->GetAttachedObject() ) + { + // NOTE: RemoveHinged() is always safe + m_pConstraint->GetAttachedObject()->RemoveHinged(); + } + } + + BaseClass::Deactivate(); + } + + void NotifyVPhysicsStateChanged( IPhysicsObject *pPhysics, CBaseEntity *pEntity, bool bAwake ) + { +#if HINGE_NOTIFY + Assert(m_pConstraint); + if (!m_pConstraint) + return; + + // if something woke up, start thinking. If everything is asleep, stop thinking. + if ( bAwake ) + { + // Did something wake up when I was not thinking? + if ( GetNextThink() == TICK_NEVER_THINK ) + { + m_soundInfo.StartThinking(this, + VelocitySampler::GetRelativeAngularVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()) , + m_hinge.worldAxisDirection + ); + + SetThink(&CPhysHinge::SoundThink); + SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); + } + } + else + { + // Is everything asleep? If so, stop thinking. + if ( GetNextThink() != TICK_NEVER_THINK && + m_pConstraint->GetAttachedObject()->IsAsleep() && + m_pConstraint->GetReferenceObject()->IsAsleep() ) + { + m_soundInfo.StopThinking(this); + SetNextThink(TICK_NEVER_THINK); + } + } +#endif + } + + +#if HINGE_NOTIFY + virtual void OnConstraintSetup( hl_constraint_info_t &info ) + { + CBaseEntity *pEntity0 = info.pObjects[0] ? static_cast(info.pObjects[0]->GetGameData()) : NULL; + if ( pEntity0 && !info.pObjects[0]->IsStatic() ) + { + WatchVPhysicsStateChanges( this, pEntity0 ); + } + CBaseEntity *pEntity1 = info.pObjects[1] ? static_cast(info.pObjects[1]->GetGameData()) : NULL; + if ( pEntity1 && !info.pObjects[1]->IsStatic() ) + { + WatchVPhysicsStateChanges( this, pEntity1 ); + } + BaseClass::OnConstraintSetup(info); + } + + void SoundThink( void ); + // void Spawn( void ); + void Activate( void ); + void Precache( void ); +#endif + + DECLARE_DATADESC(); + + +#if HINGE_NOTIFY +protected: + ConstraintSoundInfo m_soundInfo; +#endif + +private: + constraint_hingeparams_t m_hinge; + float m_hingeFriction; + float m_systemLoadScale; + bool IsWorldHinge( const hl_constraint_info_t &info, int *pAxisOut ); +}; + +BEGIN_DATADESC( CPhysHinge ) + +// Quiet down classcheck +// DEFINE_FIELD( m_hinge, FIELD_??? ), + + DEFINE_KEYFIELD( m_hingeFriction, FIELD_FLOAT, "hingefriction" ), + DEFINE_FIELD( m_hinge.worldPosition, FIELD_POSITION_VECTOR ), + DEFINE_KEYFIELD( m_hinge.worldAxisDirection, FIELD_VECTOR, "hingeaxis" ), + DEFINE_KEYFIELD( m_systemLoadScale, FIELD_FLOAT, "systemloadscale" ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetAngularVelocity", InputSetVelocity ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetHingeFriction", InputSetHingeFriction ), + +#if HINGE_NOTIFY + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_THRESHOLD] , FIELD_FLOAT, "minSoundThreshold" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_FULL] , FIELD_FLOAT, "maxSoundThreshold" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundFwd, FIELD_SOUNDNAME, "slidesoundfwd" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundBack, FIELD_SOUNDNAME, "slidesoundback" ), + + DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[0], FIELD_SOUNDNAME, "reversalsoundSmall" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[1], FIELD_SOUNDNAME, "reversalsoundMedium" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[2], FIELD_SOUNDNAME, "reversalsoundLarge" ), + + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[0] , FIELD_FLOAT, "reversalsoundthresholdSmall" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[1], FIELD_FLOAT, "reversalsoundthresholdMedium" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[2] , FIELD_FLOAT, "reversalsoundthresholdLarge" ), + + DEFINE_THINKFUNC( SoundThink ), +#endif + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( phys_hinge, CPhysHinge ); + + +void CPhysHinge::Spawn( void ) +{ + m_hinge.worldPosition = GetLocalOrigin(); + m_hinge.worldAxisDirection -= GetLocalOrigin(); + VectorNormalize(m_hinge.worldAxisDirection); + UTIL_SnapDirectionToAxis( m_hinge.worldAxisDirection ); + + m_hinge.hingeAxis.SetAxisFriction( 0, 0, 0 ); + + if ( HasSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ) ) + { + masscenteroverride_t params; + if ( m_nameAttach1 == NULL_STRING ) + { + params.SnapToAxis( m_nameAttach2, m_hinge.worldPosition, m_hinge.worldAxisDirection ); + PhysSetMassCenterOverride( params ); + } + else if ( m_nameAttach2 == NULL_STRING ) + { + params.SnapToAxis( m_nameAttach1, m_hinge.worldPosition, m_hinge.worldAxisDirection ); + PhysSetMassCenterOverride( params ); + } + else + { + RemoveSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ); + } + } + + Precache(); +} + +#if HINGE_NOTIFY +void CPhysHinge::Activate( void ) +{ + BaseClass::Activate(); + + m_soundInfo.OnActivate(this); + if (m_pConstraint) + { + m_soundInfo.StartThinking(this, + VelocitySampler::GetRelativeAngularVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()) , + m_hinge.worldAxisDirection + ); + + SetThink(&CPhysHinge::SoundThink); + SetNextThink( gpGlobals->curtime + m_soundInfo.getThinkRate() ); + } +} + +void CPhysHinge::Precache( void ) +{ + BaseClass::Precache(); + return m_soundInfo.OnPrecache(this); +} + +#endif + + +static int GetUnitAxisIndex( const Vector &axis ) +{ + bool valid = false; + int index = -1; + + for ( int i = 0; i < 3; i++ ) + { + if ( axis[i] != 0 ) + { + if ( fabs(axis[i]) == 1 ) + { + if ( index < 0 ) + { + index = i; + valid = true; + continue; + } + } + valid = false; + } + } + return valid ? index : -1; +} + +bool CPhysHinge::IsWorldHinge( const hl_constraint_info_t &info, int *pAxisOut ) +{ + if ( HasSpawnFlags( SF_CONSTRAINT_ASSUME_WORLD_GEOMETRY ) && info.pObjects[0] == g_PhysWorldObject ) + { + Vector localHinge; + info.pObjects[1]->WorldToLocalVector( &localHinge, m_hinge.worldAxisDirection ); + UTIL_SnapDirectionToAxis( localHinge ); + int hingeAxis = GetUnitAxisIndex( localHinge ); + if ( hingeAxis >= 0 ) + { + *pAxisOut = hingeAxis; + return true; + } + } + return false; +} + + +#if HINGE_NOTIFY +void CPhysHinge::SoundThink( void ) +{ + Assert(m_pConstraint); + if (!m_pConstraint) + return; + + IPhysicsObject * pAttached = m_pConstraint->GetAttachedObject(), *pReference = m_pConstraint->GetReferenceObject(); + Assert( pAttached && pReference ); + if (pAttached && pReference) + { + Vector relativeVel = VelocitySampler::GetRelativeAngularVelocity(pAttached,pReference); + if (g_debug_constraint_sounds.GetBool()) + { + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + (relativeVel), 255, 255, 0, true, 0.1f ); + } + m_soundInfo.OnThink( this, relativeVel ); + + SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); + } +} +#endif + +class CPhysBallSocket : public CPhysConstraint +{ +public: + DECLARE_CLASS( CPhysBallSocket, CPhysConstraint ); + + IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) + { + constraint_ballsocketparams_t ballsocket; + + ballsocket.Defaults(); + + for ( int i = 0; i < 2; i++ ) + { + info.pObjects[i]->WorldToLocal( &ballsocket.constraintPosition[i], GetAbsOrigin() ); + // HACKHACK - the mapper forgot to put in some sane physics damping + float damping, adamping; + info.pObjects[i]->GetDamping(&damping, &adamping); + if (damping < .2f) { + damping = .2f; + } + if (adamping < .2f) { + adamping = .2f; + } + info.pObjects[i]->SetDamping(&damping, &adamping); + } + GetBreakParams( ballsocket.constraint, info ); + ballsocket.constraint.torqueLimit = 0; + + return physenv->CreateBallsocketConstraint( info.pObjects[0], info.pObjects[1], pGroup, ballsocket ); + } +}; + +LINK_ENTITY_TO_CLASS( phys_ballsocket, CPhysBallSocket ); + +class CPhysSlideConstraint : public CPhysConstraint, public IVPhysicsWatcher +{ +public: + DECLARE_CLASS( CPhysSlideConstraint, CPhysConstraint ); + + DECLARE_DATADESC(); + IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); + void InputSetVelocity( inputdata_t &inputdata ) + { + if ( !m_pConstraint || !m_pConstraint->GetReferenceObject() || !m_pConstraint->GetAttachedObject() ) + return; + + float speed = inputdata.value.Float(); + float massLoad = 1; + int numMasses = 0; + if ( m_pConstraint->GetReferenceObject()->IsMoveable() ) + { + massLoad = m_pConstraint->GetReferenceObject()->GetMass(); + numMasses++; + m_pConstraint->GetReferenceObject()->Wake(); + } + if ( m_pConstraint->GetAttachedObject()->IsMoveable() ) + { + massLoad += m_pConstraint->GetAttachedObject()->GetMass(); + numMasses++; + m_pConstraint->GetAttachedObject()->Wake(); + } + if ( numMasses > 0 ) + { + massLoad /= (float)numMasses; + } + float loadscale = m_systemLoadScale != 0 ? m_systemLoadScale : 1; + m_pConstraint->SetLinearMotor( speed, speed * loadscale * massLoad * (1.0/TICK_INTERVAL) ); + } + + void DrawDebugGeometryOverlays() + { + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + NDebugOverlay::Box( GetAbsOrigin(), -Vector(8,8,8), Vector(8,8,8), 0, 255, 0, 0, 0 ); + NDebugOverlay::Box( m_axisEnd, -Vector(4,4,4), Vector(4,4,4), 0, 0, 255, 0, 0 ); + NDebugOverlay::Line( GetAbsOrigin(), m_axisEnd, 255, 255, 0, false, 0 ); + } + BaseClass::DrawDebugGeometryOverlays(); + } + + void NotifyVPhysicsStateChanged( IPhysicsObject *pPhysics, CBaseEntity *pEntity, bool bAwake ) + { +#if HINGE_NOTIFY + Assert(m_pConstraint); + if (!m_pConstraint) + return; + + // if something woke up, start thinking. If everything is asleep, stop thinking. + if ( bAwake ) + { + // Did something wake up when I was not thinking? + if ( GetNextThink() == TICK_NEVER_THINK ) + { + Vector axisDirection = m_axisEnd - GetAbsOrigin(); + VectorNormalize( axisDirection ); + UTIL_SnapDirectionToAxis( axisDirection ); + + m_soundInfo.StartThinking(this, + VelocitySampler::GetRelativeVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()), + axisDirection + ); + SetThink(&CPhysSlideConstraint::SoundThink); + SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); + } + } + else + { + // Is everything asleep? If so, stop thinking. + if ( GetNextThink() != TICK_NEVER_THINK && + m_pConstraint->GetAttachedObject()->IsAsleep() && + m_pConstraint->GetReferenceObject()->IsAsleep() ) + { + m_soundInfo.StopThinking(this); + SetNextThink(TICK_NEVER_THINK); + } + } +#endif + } + + +#if HINGE_NOTIFY + virtual void OnConstraintSetup( hl_constraint_info_t &info ) + { + CBaseEntity *pEntity0 = info.pObjects[0] ? static_cast(info.pObjects[0]->GetGameData()) : NULL; + if ( pEntity0 && !info.pObjects[0]->IsStatic() ) + { + WatchVPhysicsStateChanges( this, pEntity0 ); + } + CBaseEntity *pEntity1 = info.pObjects[1] ? static_cast(info.pObjects[1]->GetGameData()) : NULL; + if ( pEntity1 && !info.pObjects[1]->IsStatic() ) + { + WatchVPhysicsStateChanges( this, pEntity1 ); + } + BaseClass::OnConstraintSetup(info); + } + + + void SoundThink( void ); + // void Spawn( void ); + void Activate( void ); + void Precache( void ); +#endif + + Vector m_axisEnd; + float m_slideFriction; + float m_systemLoadScale; + +#if HINGE_NOTIFY +protected: + ConstraintSoundInfo m_soundInfo; +#endif +}; + +LINK_ENTITY_TO_CLASS( phys_slideconstraint, CPhysSlideConstraint ); + +BEGIN_DATADESC( CPhysSlideConstraint ) + + DEFINE_KEYFIELD( m_axisEnd, FIELD_POSITION_VECTOR, "slideaxis" ), + DEFINE_KEYFIELD( m_slideFriction, FIELD_FLOAT, "slidefriction" ), + DEFINE_KEYFIELD( m_systemLoadScale, FIELD_FLOAT, "systemloadscale" ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetVelocity", InputSetVelocity ), +#if HINGE_NOTIFY + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_THRESHOLD] , FIELD_FLOAT, "minSoundThreshold" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_FULL] , FIELD_FLOAT, "maxSoundThreshold" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundFwd, FIELD_SOUNDNAME, "slidesoundfwd" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundBack, FIELD_SOUNDNAME, "slidesoundback" ), + + DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[0], FIELD_SOUNDNAME, "reversalsoundSmall" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[1], FIELD_SOUNDNAME, "reversalsoundMedium" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSounds[2], FIELD_SOUNDNAME, "reversalsoundLarge" ), + + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[0] , FIELD_FLOAT, "reversalsoundthresholdSmall" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[1], FIELD_FLOAT, "reversalsoundthresholdMedium" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThresholds[2] , FIELD_FLOAT, "reversalsoundthresholdLarge" ), + + + DEFINE_THINKFUNC( SoundThink ), +#endif + +END_DATADESC() + + + +IPhysicsConstraint *CPhysSlideConstraint::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) +{ + constraint_slidingparams_t sliding; + sliding.Defaults(); + GetBreakParams( sliding.constraint, info ); + sliding.constraint.strength = 1.0; + + Vector axisDirection = m_axisEnd - GetAbsOrigin(); + VectorNormalize( axisDirection ); + UTIL_SnapDirectionToAxis( axisDirection ); + + sliding.InitWithCurrentObjectState( info.pObjects[0], info.pObjects[1], axisDirection ); + sliding.friction = m_slideFriction; + if ( m_spawnflags & SF_SLIDE_LIMIT_ENDS ) + { + Vector position; + info.pObjects[1]->GetPosition( &position, NULL ); + + sliding.limitMin = DotProduct( axisDirection, GetAbsOrigin() ); + sliding.limitMax = DotProduct( axisDirection, m_axisEnd ); + if ( sliding.limitMax < sliding.limitMin ) + { + ::V_swap( sliding.limitMin, sliding.limitMax ); + } + + // expand limits to make initial position of the attached object valid + float limit = DotProduct( position, axisDirection ); + if ( limit < sliding.limitMin ) + { + sliding.limitMin = limit; + } + else if ( limit > sliding.limitMax ) + { + sliding.limitMax = limit; + } + // offset so that the current position is 0 + sliding.limitMin -= limit; + sliding.limitMax -= limit; + } + + return physenv->CreateSlidingConstraint( info.pObjects[0], info.pObjects[1], pGroup, sliding ); +} + + +#if HINGE_NOTIFY +void CPhysSlideConstraint::SoundThink( void ) +{ + Assert(m_pConstraint); + if (!m_pConstraint) + return; + + IPhysicsObject * pAttached = m_pConstraint->GetAttachedObject(), *pReference = m_pConstraint->GetReferenceObject(); + Assert( pAttached && pReference ); + if (pAttached && pReference) + { + Vector relativeVel = VelocitySampler::GetRelativeVelocity(pAttached,pReference); + // project velocity onto my primary axis.: + + Vector axisDirection = m_axisEnd - GetAbsOrigin(); + relativeVel = m_axisEnd * relativeVel.Dot(m_axisEnd)/m_axisEnd.Dot(m_axisEnd); + + m_soundInfo.OnThink( this, relativeVel ); + + SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); + } + +} + +void CPhysSlideConstraint::Activate( void ) +{ + BaseClass::Activate(); + + m_soundInfo.OnActivate(this); + + Vector axisDirection = m_axisEnd - GetAbsOrigin(); + VectorNormalize( axisDirection ); + UTIL_SnapDirectionToAxis( axisDirection ); + m_soundInfo.StartThinking(this, + VelocitySampler::GetRelativeVelocity(m_pConstraint->GetAttachedObject(), m_pConstraint->GetReferenceObject()), + axisDirection + ); + + SetThink(&CPhysSlideConstraint::SoundThink); + SetNextThink(gpGlobals->curtime + m_soundInfo.getThinkRate()); +} + +void CPhysSlideConstraint::Precache() +{ + m_soundInfo.OnPrecache(this); +} + +#endif + + + +LINK_ENTITY_TO_CLASS( phys_constraint, CPhysFixed ); + +//----------------------------------------------------------------------------- +// Purpose: Activate/create the constraint +//----------------------------------------------------------------------------- +IPhysicsConstraint *CPhysFixed::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) +{ + constraint_fixedparams_t fixed; + fixed.Defaults(); + fixed.InitWithCurrentObjectState( info.pObjects[0], info.pObjects[1] ); + GetBreakParams( fixed.constraint, info ); + + // constraining to the world means object 1 is fixed + if ( info.pObjects[0] == g_PhysWorldObject ) + { + PhysSetGameFlags( info.pObjects[1], FVPHYSICS_CONSTRAINT_STATIC ); + } + + return physenv->CreateFixedConstraint( info.pObjects[0], info.pObjects[1], pGroup, fixed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Breakable pulley w/ropes constraint +//----------------------------------------------------------------------------- +class CPhysPulley : public CPhysConstraint +{ + DECLARE_CLASS( CPhysPulley, CPhysConstraint ); +public: + DECLARE_DATADESC(); + + void DrawDebugGeometryOverlays() + { + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + Vector origin = GetAbsOrigin(); + Vector refPos = origin, attachPos = origin; + IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); + if ( pRef ) + { + matrix3x4_t matrix; + pRef->GetPositionMatrix( &matrix ); + VectorTransform( m_offset[0], matrix, refPos ); + } + IPhysicsObject *pAttach = m_pConstraint->GetAttachedObject(); + if ( pAttach ) + { + matrix3x4_t matrix; + pAttach->GetPositionMatrix( &matrix ); + VectorTransform( m_offset[1], matrix, attachPos ); + } + NDebugOverlay::Line( refPos, origin, 0, 255, 0, false, 0 ); + NDebugOverlay::Line( origin, m_position2, 128, 128, 128, false, 0 ); + NDebugOverlay::Line( m_position2, attachPos, 0, 255, 0, false, 0 ); + NDebugOverlay::Box( origin, -Vector(8,8,8), Vector(8,8,8), 128, 255, 128, 32, 0 ); + NDebugOverlay::Box( m_position2, -Vector(8,8,8), Vector(8,8,8), 255, 128, 128, 32, 0 ); + } + BaseClass::DrawDebugGeometryOverlays(); + } + + IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); + +private: + Vector m_position2; + Vector m_offset[2]; + float m_addLength; + float m_gearRatio; +}; + +BEGIN_DATADESC( CPhysPulley ) + + DEFINE_KEYFIELD( m_position2, FIELD_POSITION_VECTOR, "position2" ), + DEFINE_AUTO_ARRAY( m_offset, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_addLength, FIELD_FLOAT, "addlength" ), + DEFINE_KEYFIELD( m_gearRatio, FIELD_FLOAT, "gearratio" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( phys_pulleyconstraint, CPhysPulley ); + + +//----------------------------------------------------------------------------- +// Purpose: Activate/create the constraint +//----------------------------------------------------------------------------- +IPhysicsConstraint *CPhysPulley::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) +{ + constraint_pulleyparams_t pulley; + pulley.Defaults(); + pulley.pulleyPosition[0] = GetAbsOrigin(); + pulley.pulleyPosition[1] = m_position2; + + matrix3x4_t matrix; + Vector world[2]; + + info.pObjects[0]->GetPositionMatrix( &matrix ); + VectorTransform( info.anchorPosition[0], matrix, world[0] ); + info.pObjects[1]->GetPositionMatrix( &matrix ); + VectorTransform( info.anchorPosition[1], matrix, world[1] ); + + for ( int i = 0; i < 2; i++ ) + { + pulley.objectPosition[i] = info.anchorPosition[i]; + m_offset[i] = info.anchorPosition[i]; + } + + pulley.totalLength = m_addLength + + (world[0] - pulley.pulleyPosition[0]).Length() + + ((world[1] - pulley.pulleyPosition[1]).Length() * m_gearRatio); + + if ( m_gearRatio != 0 ) + { + pulley.gearRatio = m_gearRatio; + } + GetBreakParams( pulley.constraint, info ); + if ( m_spawnflags & SF_PULLEY_RIGID ) + { + pulley.isRigid = true; + } + + return physenv->CreatePulleyConstraint( info.pObjects[0], info.pObjects[1], pGroup, pulley ); +} + +//----------------------------------------------------------------------------- +// Purpose: Breakable rope/length constraint +//----------------------------------------------------------------------------- +class CPhysLength : public CPhysConstraint +{ + DECLARE_CLASS( CPhysLength, CPhysConstraint ); +public: + DECLARE_DATADESC(); + + void DrawDebugGeometryOverlays() + { + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + Vector origin = GetAbsOrigin(); + Vector refPos = origin, attachPos = origin; + IPhysicsObject *pRef = m_pConstraint->GetReferenceObject(); + if ( pRef ) + { + matrix3x4_t matrix; + pRef->GetPositionMatrix( &matrix ); + VectorTransform( m_offset[0], matrix, refPos ); + } + IPhysicsObject *pAttach = m_pConstraint->GetAttachedObject(); + if ( pAttach ) + { + matrix3x4_t matrix; + pAttach->GetPositionMatrix( &matrix ); + VectorTransform( m_offset[1], matrix, attachPos ); + } + Vector dir = attachPos - refPos; + + float len = VectorNormalize(dir); + if ( len > m_totalLength ) + { + Vector mid = refPos + dir * m_totalLength; + NDebugOverlay::Line( refPos, mid, 0, 255, 0, false, 0 ); + NDebugOverlay::Line( mid, attachPos, 255, 0, 0, false, 0 ); + } + else + { + NDebugOverlay::Line( refPos, attachPos, 0, 255, 0, false, 0 ); + } + } + BaseClass::DrawDebugGeometryOverlays(); + } + + IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); + +private: + Vector m_offset[2]; + Vector m_vecAttach; + float m_addLength; + float m_minLength; + float m_totalLength; +}; + +BEGIN_DATADESC( CPhysLength ) + + DEFINE_AUTO_ARRAY( m_offset, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_addLength, FIELD_FLOAT, "addlength" ), + DEFINE_KEYFIELD( m_minLength, FIELD_FLOAT, "minlength" ), + DEFINE_KEYFIELD( m_vecAttach, FIELD_POSITION_VECTOR, "attachpoint" ), + DEFINE_FIELD( m_totalLength, FIELD_FLOAT ), +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( phys_lengthconstraint, CPhysLength ); + + +//----------------------------------------------------------------------------- +// Purpose: Activate/create the constraint +//----------------------------------------------------------------------------- +IPhysicsConstraint *CPhysLength::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) +{ + constraint_lengthparams_t length; + length.Defaults(); + Vector position[2]; + position[0] = GetAbsOrigin(); + position[1] = m_vecAttach; + int index = info.swapped ? 1 : 0; + length.InitWorldspace( info.pObjects[0], info.pObjects[1], position[index], position[!index] ); + length.totalLength += m_addLength; + length.minLength = m_minLength; + m_totalLength = length.totalLength; + if ( HasSpawnFlags(SF_LENGTH_RIGID) ) + { + length.minLength = length.totalLength; + } + + for ( int i = 0; i < 2; i++ ) + { + m_offset[i] = length.objectPosition[i]; + } + GetBreakParams( length.constraint, info ); + + return physenv->CreateLengthConstraint( info.pObjects[0], info.pObjects[1], pGroup, length ); +} + +//----------------------------------------------------------------------------- +// Purpose: Limited ballsocket constraint with toggle-able translation constraints +//----------------------------------------------------------------------------- +class CRagdollConstraint : public CPhysConstraint +{ + DECLARE_CLASS( CRagdollConstraint, CPhysConstraint ); +public: + DECLARE_DATADESC(); +#if 0 + void DrawDebugGeometryOverlays() + { + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + NDebugOverlay::Line( refPos, attachPos, 0, 255, 0, false, 0 ); + } + BaseClass::DrawDebugGeometryOverlays(); + } +#endif + + IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); + +private: + float m_xmin; // constraint limits in degrees + float m_xmax; + float m_ymin; + float m_ymax; + float m_zmin; + float m_zmax; + + float m_xfriction; + float m_yfriction; + float m_zfriction; +}; + +BEGIN_DATADESC( CRagdollConstraint ) + + DEFINE_KEYFIELD( m_xmin, FIELD_FLOAT, "xmin" ), + DEFINE_KEYFIELD( m_xmax, FIELD_FLOAT, "xmax" ), + DEFINE_KEYFIELD( m_ymin, FIELD_FLOAT, "ymin" ), + DEFINE_KEYFIELD( m_ymax, FIELD_FLOAT, "ymax" ), + DEFINE_KEYFIELD( m_zmin, FIELD_FLOAT, "zmin" ), + DEFINE_KEYFIELD( m_zmax, FIELD_FLOAT, "zmax" ), + DEFINE_KEYFIELD( m_xfriction, FIELD_FLOAT, "xfriction" ), + DEFINE_KEYFIELD( m_yfriction, FIELD_FLOAT, "yfriction" ), + DEFINE_KEYFIELD( m_zfriction, FIELD_FLOAT, "zfriction" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( phys_ragdollconstraint, CRagdollConstraint ); + +//----------------------------------------------------------------------------- +// Purpose: Activate/create the constraint +//----------------------------------------------------------------------------- +IPhysicsConstraint *CRagdollConstraint::CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) +{ + constraint_ragdollparams_t ragdoll; + ragdoll.Defaults(); + + matrix3x4_t entityToWorld, worldToEntity; + info.pObjects[0]->GetPositionMatrix( &entityToWorld ); + MatrixInvert( entityToWorld, worldToEntity ); + ConcatTransforms( worldToEntity, EntityToWorldTransform(), ragdoll.constraintToReference ); + + info.pObjects[1]->GetPositionMatrix( &entityToWorld ); + MatrixInvert( entityToWorld, worldToEntity ); + ConcatTransforms( worldToEntity, EntityToWorldTransform(), ragdoll.constraintToAttached ); + + ragdoll.onlyAngularLimits = HasSpawnFlags( SF_RAGDOLL_FREEMOVEMENT ) ? true : false; + + // FIXME: Why are these friction numbers in different units from what the hinge uses? + ragdoll.axes[0].SetAxisFriction( m_xmin, m_xmax, m_xfriction ); + ragdoll.axes[1].SetAxisFriction( m_ymin, m_ymax, m_yfriction ); + ragdoll.axes[2].SetAxisFriction( m_zmin, m_zmax, m_zfriction ); + + if ( HasSpawnFlags( SF_CONSTRAINT_START_INACTIVE ) ) + { + ragdoll.isActive = false; + } + return physenv->CreateRagdollConstraint( info.pObjects[0], info.pObjects[1], pGroup, ragdoll ); +} + + + +class CPhysConstraintEvents : public IPhysicsConstraintEvent +{ + void ConstraintBroken( IPhysicsConstraint *pConstraint ) + { + CBaseEntity *pEntity = (CBaseEntity *)pConstraint->GetGameData(); + if ( pEntity ) + { + IPhysicsConstraintEvent *pConstraintEvent = dynamic_cast( pEntity ); + //Msg("Constraint broken %s\n", pEntity->GetDebugName() ); + if ( pConstraintEvent ) + { + pConstraintEvent->ConstraintBroken( pConstraint ); + } + else + { + variant_t emptyVariant; + pEntity->AcceptInput( "ConstraintBroken", NULL, NULL, emptyVariant, 0 ); + } + } + } +}; + +static CPhysConstraintEvents constraintevents; +// registered in physics.cpp +IPhysicsConstraintEvent *g_pConstraintEvents = &constraintevents; + + + + + +#if HINGE_NOTIFY +//----------------------------------------------------------------------------- +// Code for sampler +//----------------------------------------------------------------------------- + + +/// Call this in spawn(). (Not a constructor because those are difficult to use in entities.) +void VelocitySampler::Initialize(float samplerate) +{ + m_fIdealSampleRate = samplerate; +} + +// This is an old style approach to reversal sounds, from when there was only one. +#if 0 +bool VelocitySampler::HasReversed(const Vector &relativeVelocity, float thresholdAcceleration) +{ + // first, make sure the velocity has reversed (is more than 90deg off) from last time, or is zero now. + // float rVsq = relativeVelocity.LengthSqr(); + float vDot = relativeVelocity.Dot(m_prevSample); + if (vDot <= 0) // there is a reversal in direction. compute the magnitude of acceleration. + { + // find the scalar projection of the relative acceleration this fame onto the previous frame's + // velocity, and compare that to the threshold. + Vector accel = relativeVelocity - m_prevSample; + + float prevSampleLength = m_prevSample.Length(); + float projection = 0; + // divide through by dt to get the accel per sec + if (prevSampleLength) + { + projection = -(accel.Dot(m_prevSample) / prevSampleLength) / (gpGlobals->curtime - m_fPrevSampleTime); + } + else + { + projection = accel.Length() / (gpGlobals->curtime - m_fPrevSampleTime); + } + + if (g_debug_constraint_sounds.GetBool()) + { + Msg("Reversal accel is %f/%f\n",projection,thresholdAcceleration); + } + return ((projection) > thresholdAcceleration); // the scalar projection is negative because the acceleration is against vel + } + else + { + return false; + } +} +#endif + +/// Looks at the force of reversal and compares it to a ladder of thresholds. +/// Returns the index of the highest threshold exceeded by the reversal velocity. +int VelocitySampler::HasReversed(const Vector &relativeVelocity, const float thresholdAcceleration[], const unsigned short numThresholds) +{ + // first, make sure the velocity has reversed (is more than 90deg off) from last time, or is zero now. + // float rVsq = relativeVelocity.LengthSqr(); + float vDot = relativeVelocity.Dot(m_prevSample); + if (vDot <= 0) // there is a reversal in direction. compute the magnitude of acceleration. + { + // find the scalar projection of the relative acceleration this fame onto the previous frame's + // velocity, and compare that to the threshold. + Vector accel = relativeVelocity - m_prevSample; + + float prevSampleLength = m_prevSample.Length(); + float projection = 0; + // divide through by dt to get the accel per sec + if (prevSampleLength) + { + // the scalar projection is negative because the acceleration is against vel + projection = -(accel.Dot(m_prevSample) / prevSampleLength) / (gpGlobals->curtime - m_fPrevSampleTime); + } + else + { + projection = accel.Length() / (gpGlobals->curtime - m_fPrevSampleTime); + } + + if (g_debug_constraint_sounds.GetBool()) + { + Msg("Reversal accel is %f/%f\n", projection, thresholdAcceleration[0]); + } + + + // now find the threshold crossed. + int retval; + for (retval = numThresholds - 1; retval >= 0 ; --retval) + { + if (projection > thresholdAcceleration[retval]) + break; + } + + return retval; + } + else + { + return -1; + } +} + +/// small helper function used just below (technique copy-pasted from sound.cpp) +inline static bool IsEmpty (const string_t &str) +{ + return (!str || strlen(str.ToCStr()) < 1 ); +} + +void ConstraintSoundInfo::OnActivate( CPhysConstraint *pOuter ) +{ + m_pTravelSound = NULL; + m_vSampler.Initialize( getThinkRate() ); + + + ValidateInternals( pOuter ); + + // make sure sound filenames are not empty + m_bPlayTravelSound = !IsEmpty(m_iszTravelSoundFwd) || !IsEmpty(m_iszTravelSoundBack); + m_bPlayReversalSound = false; + for (int i = 0; i < SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE ; ++i) + { + if ( !IsEmpty(m_iszReversalSounds[i]) ) + { + // if there is at least one filled sound field, we should try + // to play reversals + m_bPlayReversalSound = true; + break; + } + } + + + /* + SetThink(&CPhysSlideConstraint::SoundThink); + SetNextThink(gpGlobals->curtime + m_vSampler.getSampleRate()); + */ +} + +/// Maintain consistency of internal datastructures on start +void ConstraintSoundInfo::ValidateInternals( CPhysConstraint *pOuter ) +{ + // Make sure the reversal sound thresholds are strictly increasing. + for (int i = 1 ; i < SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE ; ++i) + { + // if decreases from small to medium, promote small to medium and warn. + if (m_soundProfile.m_reversalSoundThresholds[i] < m_soundProfile.m_reversalSoundThresholds[i-1]) + { + Warning("Constraint reversal sounds for %s are out of order!", pOuter->GetDebugName() ); + m_soundProfile.m_reversalSoundThresholds[i] = m_soundProfile.m_reversalSoundThresholds[i-1]; + m_iszReversalSounds[i] = m_iszReversalSounds[i-1]; + } + } +} + +void ConstraintSoundInfo::OnPrecache( CPhysConstraint *pOuter ) +{ + pOuter->PrecacheScriptSound( m_iszTravelSoundFwd.ToCStr() ); + pOuter->PrecacheScriptSound( m_iszTravelSoundBack.ToCStr() ); + for (int i = 0 ; i < SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE; ++i ) + { + pOuter->PrecacheScriptSound( m_iszReversalSounds[i].ToCStr() ); + } +} + +void ConstraintSoundInfo::OnThink( CPhysConstraint *pOuter, const Vector &relativeVelocity ) +{ + // have we had a hard reversal? + int playReversal = m_vSampler.HasReversed( relativeVelocity, m_soundProfile.m_reversalSoundThresholds, SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE ); + float relativeVelMag = relativeVelocity.Length(); //< magnitude of relative velocity + + CBaseEntity *pChildEntity = static_cast(pOuter->GetPhysConstraint()->GetAttachedObject()->GetGameData()); + + // compute sound level + float soundVol = this->m_soundProfile.GetVolume(relativeVelMag); + + if (g_debug_constraint_sounds.GetBool()) + { + char tempstr[512]; + Q_snprintf(tempstr,sizeof(tempstr),"Velocity: %.3f", relativeVelMag ); + pChildEntity->EntityText( 0, tempstr, m_vSampler.getSampleRate() ); + + Q_snprintf(tempstr,sizeof(tempstr),"Sound volume: %.3f", soundVol ); + pChildEntity->EntityText( 1, tempstr, m_vSampler.getSampleRate() ); + + if (playReversal >= 0) + { + Q_snprintf(tempstr,sizeof(tempstr),"Reversal [%d]", playReversal ); + pChildEntity->EntityText(2,tempstr,m_vSampler.getSampleRate()); + } + } + + // if we loaded a travel sound + if (m_bPlayTravelSound) + { + if (soundVol > 0) + { + // if we want to play a sound... + if ( m_pTravelSound ) + { // if a sound exists, modify it + CSoundEnvelopeController::GetController().SoundChangeVolume( m_pTravelSound, soundVol, 0.1f ); + } + else + { // if a sound does not exist, create it + bool travellingForward = relativeVelocity.Dot(m_forwardAxis) > 0; + + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + CPASAttenuationFilter filter( pChildEntity ); + m_pTravelSound = controller.SoundCreate( filter, pChildEntity->entindex(), + (travellingForward ? m_iszTravelSoundFwd : m_iszTravelSoundBack).ToCStr() ); + controller.Play( m_pTravelSound, soundVol, 100 ); + } + } + else + { + // if we want to not play sound + if ( m_pTravelSound ) + { // and it exists, kill it + CSoundEnvelopeController::GetController().SoundDestroy( m_pTravelSound ); + m_pTravelSound = NULL; + } + } + } + + if (m_bPlayReversalSound && (playReversal >= 0)) + { + pChildEntity->EmitSound(m_iszReversalSounds[playReversal].ToCStr()); + } + + m_vSampler.AddSample( relativeVelocity ); + +} + + +void ConstraintSoundInfo::StartThinking( CPhysConstraint *pOuter, const Vector &relativeVelocity, const Vector &forwardVector ) +{ + m_forwardAxis = forwardVector; + m_vSampler.BeginSampling( relativeVelocity ); + + /* + IPhysicsConstraint *pConstraint = pOuter->GetPhysConstraint(); + Assert(pConstraint); + if (pConstraint) + { + IPhysicsObject * pAttached = pConstraint->GetAttachedObject(), *pReference = pConstraint->GetReferenceObject(); + m_vSampler.BeginSampling( VelocitySampler::GetRelativeVelocity(pAttached,pReference) ); + } + */ +} + +void ConstraintSoundInfo::StopThinking( CPhysConstraint *pOuter ) +{ + DeleteAllSounds(); +} + + +ConstraintSoundInfo::~ConstraintSoundInfo() +{ + DeleteAllSounds(); +} + +// Any sounds envelopes that are active, kill. +void ConstraintSoundInfo::DeleteAllSounds() +{ + if ( m_pTravelSound ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pTravelSound ); + m_pTravelSound = NULL; + } +} + +#endif diff --git a/sp/src/game/server/physconstraint.h b/sp/src/game/server/physconstraint.h new file mode 100644 index 00000000..8031ec2b --- /dev/null +++ b/sp/src/game/server/physconstraint.h @@ -0,0 +1,146 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Physics constraint entities +// +// $NoKeywords: $ +//===========================================================================// + +#ifndef PHYSCONSTRAINT_H +#define PHYSCONSTRAINT_H +#ifdef _WIN32 +#pragma once +#endif + +#include "vphysics/constraints.h" + +struct hl_constraint_info_t +{ + hl_constraint_info_t() + { + pObjects[0] = pObjects[1] = NULL; + pGroup = NULL; + anchorPosition[0].Init(); + anchorPosition[1].Init(); + swapped = false; + massScale[0] = massScale[1] = 1.0f; + } + Vector anchorPosition[2]; + IPhysicsObject *pObjects[2]; + IPhysicsConstraintGroup *pGroup; + float massScale[2]; + bool swapped; +}; + +abstract_class CPhysConstraint : public CLogicalEntity +{ + DECLARE_CLASS( CPhysConstraint, CLogicalEntity ); +public: + + CPhysConstraint(); + ~CPhysConstraint(); + + DECLARE_DATADESC(); + + void Spawn( void ); + void Precache( void ); + void Activate( void ); + + void ClearStaticFlag( IPhysicsObject *pObj ); + + virtual void Deactivate(); + + void OnBreak( void ); + + void InputBreak( inputdata_t &inputdata ); + + void InputOnBreak( inputdata_t &inputdata ); + + void InputTurnOn( inputdata_t &inputdata ); + + void InputTurnOff( inputdata_t &inputdata ); + + int DrawDebugTextOverlays(); + + void DrawDebugGeometryOverlays(); + + void GetBreakParams( constraint_breakableparams_t ¶ms, const hl_constraint_info_t &info ); + + // the notify system calls this on the constrained entities - used to detect & follow teleports + void NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ); + + // gets called at setup time on first init and restore + virtual void OnConstraintSetup( hl_constraint_info_t &info ); + + // return the internal constraint object (used by sound gadgets) + inline IPhysicsConstraint *GetPhysConstraint() { return m_pConstraint; } + + string_t GetNameAttach1( void ){ return m_nameAttach1; } + string_t GetNameAttach2( void ){ return m_nameAttach2; } + +protected: + void GetConstraintObjects( hl_constraint_info_t &info ); + void SetupTeleportationHandling( hl_constraint_info_t &info ); + bool ActivateConstraint( void ); + virtual IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ) = 0; + + IPhysicsConstraint *m_pConstraint; + + // These are "template" values used to construct the hinge + string_t m_nameAttach1; + string_t m_nameAttach2; + string_t m_breakSound; + string_t m_nameSystem; + float m_forceLimit; + float m_torqueLimit; + unsigned int m_teleportTick; + float m_minTeleportDistance; + + COutputEvent m_OnBreak; +}; + +//----------------------------------------------------------------------------- +// Purpose: Fixed breakable constraint +//----------------------------------------------------------------------------- +class CPhysFixed : public CPhysConstraint +{ + DECLARE_CLASS( CPhysFixed, CPhysConstraint ); +public: + IPhysicsConstraint *CreateConstraint( IPhysicsConstraintGroup *pGroup, const hl_constraint_info_t &info ); + + // just for debugging - move to the position of the reference entity + void MoveToRefPosition() + { + if ( m_pConstraint ) + { + matrix3x4_t xformRef; + m_pConstraint->GetConstraintTransform( &xformRef, NULL ); + IPhysicsObject *pObj = m_pConstraint->GetReferenceObject(); + if ( pObj && pObj->IsMoveable() ) + { + Vector pos, posWorld; + MatrixPosition( xformRef, pos ); + pObj->LocalToWorld(&posWorld, pos); + SetAbsOrigin(posWorld); + } + } + } + int DrawDebugTextOverlays() + { + if ( m_debugOverlays & OVERLAY_TEXT_BIT ) + { + MoveToRefPosition(); + } + return BaseClass::DrawDebugTextOverlays(); + } + void DrawDebugGeometryOverlays() + { + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + MoveToRefPosition(); + } + BaseClass::DrawDebugGeometryOverlays(); + } +}; + +#endif // PHYSCONSTRAINT_H + diff --git a/sp/src/game/server/physconstraint_sounds.h b/sp/src/game/server/physconstraint_sounds.h new file mode 100644 index 00000000..71b9a579 --- /dev/null +++ b/sp/src/game/server/physconstraint_sounds.h @@ -0,0 +1,291 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Data types used inside constraints for the purpose of playing sounds +// during movement. +// +//=============================================================================// + +#ifndef PHYSCONSTRAINT_SOUNDS_H +#define PHYSCONSTRAINT_SOUNDS_H +#ifdef _WIN32 +#pragma once +#endif + + +#include +#include "soundenvelope.h" + + +/** \brief Class to store a sampled history of velocity for an object -- used for certain sound calculations + +Although this contains only one sample for now, it exists as an interface +so as to make simpler the possibility of moving to a ring buffer +implementation in the future. + +The "sample rate" variable is not nominal: it should be used to specify +the ClientThink() interval. + +Be sure to use the beginSampling() function for the first sample, and +addSample() thereafter: this will be relevant and necessary for a ring +buffer implementation (which will have to perform certain initialization). +*/ +class VelocitySampler +{ +public: + /* + enum + { + HISTORY_DEPTH_LOG = 3, // < log-base-2 of the sampler's array depth + HISTORY_DEPTH = (1 << VELOCITY_SAMPLER_HISTORY_DEPTH_LOG), + }; + */ + + /// Return the internally stored sample rate. + inline float getSampleRate() + { + return m_fIdealSampleRate; + } + + + /// Store off the first recorded sample for the given object. + inline void BeginSampling(const Vector &relativeVelocity); + + /// Record a sample. Do this LAST, after calling hasReversed() et al. + inline void AddSample(const Vector &relativeVelocity); + + /// Using the sample history, determine if the object has reversed direction + /// with at least the given acceleration (in units/sec^2). + int HasReversed(const Vector &relativeVelocity, const float thresholdAcceleration[], const unsigned short numThresholds); + + /// Call this in spawn(). (Not a constructor because those are difficult to use in entities.) + void Initialize(float samplerate); + + + /// A convenience function for extracting the linear velocity of one object relative to another. + inline static Vector GetRelativeVelocity(IPhysicsObject *pObj, IPhysicsObject *pReferenceFrame); + + /// A convenience function for extracting the angular velocity of one object relative to another. + inline static Vector GetRelativeAngularVelocity(IPhysicsObject *pObj, IPhysicsObject *pReferenceFrame); + + +protected: + Vector m_prevSample; + float m_fPrevSampleTime; + + float m_fIdealSampleRate; + +}; + +struct SimpleConstraintSoundProfile +{ + // define the indices of the sound points: + enum + { + kMIN_THRESHOLD, ///< below this no sound is played + kMIN_FULL, ///< at this velocity sound is at its loudest + + kHIGHWATER, ///< high water mark for this enum + } eKeypoints; + + float m_keyPoints[kHIGHWATER]; + + /// Number of entries in the reversal sound array + enum { kREVERSAL_SOUND_ARRAY_SIZE = 3 }; + + /// Acceleration threshold for playing the hard-reverse sound. Divided into sections. + /// Below the 0th threshold no sound will play. + float m_reversalSoundThresholds[kREVERSAL_SOUND_ARRAY_SIZE]; + + /// Get volume for given velocity [0..1] + float GetVolume(float inVel); +}; + +float SimpleConstraintSoundProfile::GetVolume(float inVel) +{ + // clamped lerp on 0-1 + if (inVel <= m_keyPoints[kMIN_THRESHOLD]) + { + return 0; + } + else if (inVel >= m_keyPoints[kMIN_FULL]) + { + return 1; + } + else // lerp... + { + return (inVel - m_keyPoints[kMIN_THRESHOLD])/(m_keyPoints[kMIN_FULL] - m_keyPoints[kMIN_THRESHOLD]); + } +} + +class CPhysConstraint; +/** This class encapsulates the data and behavior necessary for a constraint to play sounds. + + For the moment I have no easy means of populating this from an entity's datadesc. + You should explicitly fill out the fields with eg + + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_THRESHOLD] , FIELD_FLOAT, "minSoundThreshold" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_keyPoints[SimpleConstraintSoundProfile::kMIN_FULL] , FIELD_FLOAT, "maxSoundThreshold" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundFwd, FIELD_SOUNDNAME, "slidesoundfwd" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszTravelSoundBack, FIELD_SOUNDNAME, "slidesoundback" ), + DEFINE_KEYFIELD( m_soundInfo.m_iszReversalSound, FIELD_SOUNDNAME, "reversalsound" ), + DEFINE_KEYFIELD( m_soundInfo.m_soundProfile.m_reversalSoundThreshold , FIELD_FLOAT, "reversalsoundthreshold" ), + + */ +class ConstraintSoundInfo +{ +public: + // no ctor. + // dtor + ~ConstraintSoundInfo(); + + /// Call from the constraint's Activate() + void OnActivate( CPhysConstraint *pOuter ); + + /// Constraint should have a think function that calls this. It should pass in relative velocity + /// between child and parent. (This need not be linear velocity; it may be angular.) + void OnThink( CPhysConstraint *pOuter, const Vector &relativeVelocity ); + + /// This is how often the think function should be run: + inline float getThinkRate() const { return 0.09f; } + + /// Call this before the first call to OnThink() + void StartThinking( CPhysConstraint *pOuter, const Vector &relativeVelocity, const Vector &forwardVector ); + + /// Call this if you intend to stop calling OnThink(): + void StopThinking( CPhysConstraint *pOuter ); + + /// Call from owner's Precache(). + void OnPrecache( CPhysConstraint *pOuter ); + + + VelocitySampler m_vSampler; + SimpleConstraintSoundProfile m_soundProfile; + + Vector m_forwardAxis; ///< velocity in this direction is forward. The opposite direction is backward. + + string_t m_iszTravelSoundFwd,m_iszTravelSoundBack; // Path/filename of WAV file to play. + CSoundPatch *m_pTravelSound; + bool m_bPlayTravelSound; + + string_t m_iszReversalSounds[SimpleConstraintSoundProfile::kREVERSAL_SOUND_ARRAY_SIZE]; // Path/filename of WAV files to play -- one per entry in threshold. + // CSoundPatch *m_pReversalSound; + bool m_bPlayReversalSound; + +protected: + /// Maintain consistency of internal datastructures on start + void ValidateInternals( CPhysConstraint *pOuter ); + + /// Stop playing any active sounds. + void DeleteAllSounds(); +}; + + +/////////////// INLINE FUNCTIONS + + +/// compute the relative velocity between an object and its parent. Just a convenience. +Vector VelocitySampler::GetRelativeVelocity( IPhysicsObject *pObj, IPhysicsObject *pReferenceFrame ) +{ + Vector childVelocity, parentVelocity; + pObj->GetImplicitVelocity( &childVelocity, NULL ); + pReferenceFrame->GetImplicitVelocity(&parentVelocity, NULL); + + return (childVelocity - parentVelocity); +} + + +Vector VelocitySampler::GetRelativeAngularVelocity( IPhysicsObject *pObj, IPhysicsObject *pReferenceFrame ) +{ + Assert(pObj); + + if ( pReferenceFrame ) + { + Vector childVelocityLocal, parentVelocityLocal, childVelocityWorld, parentVelocityWorld; + pObj->GetImplicitVelocity( NULL, &childVelocityLocal ); + pObj->LocalToWorldVector( &childVelocityWorld, childVelocityLocal ); + pReferenceFrame->GetImplicitVelocity( NULL, &parentVelocityLocal ); + pObj->LocalToWorldVector( &parentVelocityWorld, parentVelocityLocal ); + + return (childVelocityWorld - parentVelocityWorld); + } + else + { + Vector childVelocityLocal, childVelocityWorld; + pObj->GetImplicitVelocity( NULL, &childVelocityLocal ); + pObj->LocalToWorldVector( &childVelocityWorld, childVelocityLocal ); + + return (childVelocityWorld); + } +} + +/************************************************************************/ +// This function is nominal -- it's here as an interface because in the +// future there will need to be special initialization for the first entry +// in a ring buffer. (I made a test implementation of this, then reverted it +// later; this is not an arbitrary assumption.) +/************************************************************************/ +/// Store off the first recorded sample for the given object. +void VelocitySampler::BeginSampling(const Vector &relativeVelocity) +{ + return AddSample(relativeVelocity); +} + +// Record a sample for the given object +void VelocitySampler::AddSample(const Vector &relativeVelocity) +{ + m_prevSample = relativeVelocity; + m_fPrevSampleTime = gpGlobals->curtime; +} + + + +/* // abandoned -- too complicated, no way to set from keyfields +#pragma warning(push) +#pragma warning( disable:4201 ) // C4201: nonstandard extension used: nameless struct/union +/// Stores information used for playing sounds based on +/// constraint movement +class ConstraintSoundProfile +{ +public: +/// Defines a point in the sound profile: volume and pitch for the sound to play. +/// Implicit crossfading between two sounds. Used to map velocity to a sound profile. +struct SoundInfoTuple +{ +float minVelocity; +union { +struct{ +float volume1,pitch1; //< volume and pitch of sound 1 +float volume2,pitch2; //< volume and pitch of sound 2 +}; +fltx4 m_as4; +}; + +inline SoundInfoTuple(float _minVelocity, float _volume1, float _pitch1, float _volume2, float _pitch2) : +minVelocity(_minVelocity), volume1(_volume1), pitch1(_pitch1), volume2(_volume2), pitch2(_pitch2) +{} +}; + +ConstraintSoundProfile(const SoundInfoTuple *soundTable, unsigned int tableSize) +: m_pSoundInfos(soundTable), m_numSoundInfos(tableSize) +{} + + +protected: + +/// A table of sound info structs +const SoundInfoTuple * const m_pSoundInfos; +/// Size of the table +const unsigned int m_numSoundInfos; +}; + +static ConstraintSoundProfile::SoundInfoTuple CSDebugProfileTable[] = +{ +ConstraintSoundProfile::SoundInfoTuple(12,0,0,0,0), +ConstraintSoundProfile::SoundInfoTuple(24,0,0,0,0), + +}; +#pragma warning(pop) +*/ + + +#endif diff --git a/sp/src/game/server/physgun.cpp b/sp/src/game/server/physgun.cpp new file mode 100644 index 00000000..21324bae --- /dev/null +++ b/sp/src/game/server/physgun.cpp @@ -0,0 +1,1523 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "beam_shared.h" +#include "player.h" +#include "gamerules.h" +#include "basecombatweapon.h" +#include "baseviewmodel.h" +#include "vphysics/constraints.h" +#include "physics.h" +#include "in_buttons.h" +#include "IEffects.h" +#include "engine/IEngineSound.h" +#include "ndebugoverlay.h" +#include "physics_saverestore.h" +#include "player_pickup.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar phys_gunmass("phys_gunmass", "200"); +ConVar phys_gunvel("phys_gunvel", "400"); +ConVar phys_gunforce("phys_gunforce", "5e5" ); +ConVar phys_guntorque("phys_guntorque", "100" ); +ConVar phys_gunglueradius("phys_gunglueradius", "128" ); + +static int g_physgunBeam; +#define PHYSGUN_BEAM_SPRITE "sprites/physbeam.vmt" + +#define MAX_PELLETS 16 + +class CWeaponGravityGun; + +class CGravityPellet : public CBaseAnimating +{ + DECLARE_CLASS( CGravityPellet, CBaseAnimating ); +public: + DECLARE_DATADESC(); + + ~CGravityPellet(); + void Precache() + { + SetModelName( MAKE_STRING( "models/weapons/glueblob.mdl" ) ); + PrecacheModel( STRING( GetModelName() ) ); + BaseClass::Precache(); + } + void Spawn() + { + Precache(); + SetModel( STRING( GetModelName() ) ); + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + AddEffects( EF_NOSHADOW ); + SetRenderColor( 255, 0, 0 ); + m_isInert = false; + } + + bool IsInert() + { + return m_isInert; + } + + bool MakeConstraint( CBaseEntity *pObject ) + { + IPhysicsObject *pReference = g_PhysWorldObject; + if ( GetMoveParent() ) + { + pReference = GetMoveParent()->VPhysicsGetObject(); + } + IPhysicsObject *pAttached = pObject->VPhysicsGetObject(); + if ( !pReference || !pAttached ) + { + return false; + } + + constraint_fixedparams_t fixed; + fixed.Defaults(); + fixed.InitWithCurrentObjectState( pReference, pAttached ); + + m_pConstraint = physenv->CreateFixedConstraint( pReference, pAttached, NULL, fixed ); + m_pConstraint->SetGameData( (void *)this ); + + MakeInert(); + return true; + } + + void MakeInert() + { + SetRenderColor( 64, 64, 128 ); + m_isInert = true; + } + + void InputOnBreak( inputdata_t &inputdata ) + { + UTIL_Remove(this); + } + + IPhysicsConstraint *m_pConstraint; + bool m_isInert; +}; + +LINK_ENTITY_TO_CLASS(gravity_pellet, CGravityPellet); +PRECACHE_REGISTER(gravity_pellet); + +BEGIN_DATADESC( CGravityPellet ) + + DEFINE_PHYSPTR( m_pConstraint ), + DEFINE_FIELD( m_isInert, FIELD_BOOLEAN ), + // physics system will fire this input if the constraint breaks due to physics + DEFINE_INPUTFUNC( FIELD_VOID, "ConstraintBroken", InputOnBreak ), + +END_DATADESC() + + +CGravityPellet::~CGravityPellet() +{ + if ( m_pConstraint ) + { + physenv->DestroyConstraint( m_pConstraint ); + } +} + +class CGravControllerPoint : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); + +public: + CGravControllerPoint( void ); + ~CGravControllerPoint( void ); + void AttachEntity( CBaseEntity *pEntity, IPhysicsObject *pPhys, const Vector &position ); + void DetachEntity( void ); + void SetMaxVelocity( float maxVel ) + { + m_maxVel = maxVel; + } + void SetTargetPosition( const Vector &target ) + { + m_targetPosition = target; + if ( m_attachedEntity == NULL ) + { + m_worldPosition = target; + } + m_timeToArrive = gpGlobals->frametime; + } + + void SetAutoAlign( const Vector &localDir, const Vector &localPos, const Vector &worldAlignDir, const Vector &worldAlignPos ) + { + m_align = true; + m_localAlignNormal = -localDir; + m_localAlignPosition = localPos; + m_targetAlignNormal = worldAlignDir; + m_targetAlignPosition = worldAlignPos; + } + + void ClearAutoAlign() + { + m_align = false; + } + + IMotionEvent::simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + Vector m_localPosition; + Vector m_targetPosition; + Vector m_worldPosition; + Vector m_localAlignNormal; + Vector m_localAlignPosition; + Vector m_targetAlignNormal; + Vector m_targetAlignPosition; + bool m_align; + float m_saveDamping; + float m_maxVel; + float m_maxAcceleration; + Vector m_maxAngularAcceleration; + EHANDLE m_attachedEntity; + QAngle m_targetRotation; + float m_timeToArrive; + + IPhysicsMotionController *m_controller; +}; + + +BEGIN_SIMPLE_DATADESC( CGravControllerPoint ) + + DEFINE_FIELD( m_localPosition, FIELD_VECTOR ), + DEFINE_FIELD( m_targetPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_worldPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_localAlignNormal, FIELD_VECTOR ), + DEFINE_FIELD( m_localAlignPosition, FIELD_VECTOR ), + DEFINE_FIELD( m_targetAlignNormal, FIELD_VECTOR ), + DEFINE_FIELD( m_targetAlignPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_align, FIELD_BOOLEAN ), + DEFINE_FIELD( m_saveDamping, FIELD_FLOAT ), + DEFINE_FIELD( m_maxVel, FIELD_FLOAT ), + DEFINE_FIELD( m_maxAcceleration, FIELD_FLOAT ), + DEFINE_FIELD( m_maxAngularAcceleration, FIELD_VECTOR ), + DEFINE_FIELD( m_attachedEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_targetRotation, FIELD_VECTOR ), + DEFINE_FIELD( m_timeToArrive, FIELD_FLOAT ), + + // Physptrs can't be saved in embedded classes... this is to silence classcheck + // DEFINE_PHYSPTR( m_controller ), + +END_DATADESC() + + +CGravControllerPoint::CGravControllerPoint( void ) +{ + m_attachedEntity = NULL; +} + +CGravControllerPoint::~CGravControllerPoint( void ) +{ + DetachEntity(); +} + + +void CGravControllerPoint::AttachEntity( CBaseEntity *pEntity, IPhysicsObject *pPhys, const Vector &position ) +{ + m_attachedEntity = pEntity; + pPhys->WorldToLocal( &m_localPosition, position ); + m_worldPosition = position; + pPhys->GetDamping( NULL, &m_saveDamping ); + float damping = 2; + pPhys->SetDamping( NULL, &damping ); + m_controller = physenv->CreateMotionController( this ); + m_controller->AttachObject( pPhys, true ); + m_controller->SetPriority( IPhysicsMotionController::HIGH_PRIORITY ); + SetTargetPosition( position ); + m_maxAcceleration = phys_gunforce.GetFloat() * pPhys->GetInvMass(); + m_targetRotation = pEntity->GetAbsAngles(); + float torque = phys_guntorque.GetFloat(); + m_maxAngularAcceleration = torque * pPhys->GetInvInertia(); +} + +void CGravControllerPoint::DetachEntity( void ) +{ + CBaseEntity *pEntity = m_attachedEntity; + if ( pEntity ) + { + IPhysicsObject *pPhys = pEntity->VPhysicsGetObject(); + if ( pPhys ) + { + // on the odd chance that it's gone to sleep while under anti-gravity + pPhys->Wake(); + pPhys->SetDamping( NULL, &m_saveDamping ); + } + } + m_attachedEntity = NULL; + physenv->DestroyMotionController( m_controller ); + m_controller = NULL; + + // UNDONE: Does this help the networking? + m_targetPosition = vec3_origin; + m_worldPosition = vec3_origin; +} + +void AxisAngleQAngle( const Vector &axis, float angle, QAngle &outAngles ) +{ + // map back to HL rotation axes + outAngles.z = axis.x * angle; + outAngles.x = axis.y * angle; + outAngles.y = axis.z * angle; +} + +IMotionEvent::simresult_e CGravControllerPoint::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + Vector vel; + AngularImpulse angVel; + + float fracRemainingSimTime = 1.0; + if ( m_timeToArrive > 0 ) + { + fracRemainingSimTime *= deltaTime / m_timeToArrive; + if ( fracRemainingSimTime > 1 ) + { + fracRemainingSimTime = 1; + } + } + + m_timeToArrive -= deltaTime; + if ( m_timeToArrive < 0 ) + { + m_timeToArrive = 0; + } + + float invDeltaTime = (1.0f / deltaTime); + Vector world; + pObject->LocalToWorld( &world, m_localPosition ); + m_worldPosition = world; + pObject->GetVelocity( &vel, &angVel ); + //pObject->GetVelocityAtPoint( world, &vel ); + float damping = 1.0; + world += vel * deltaTime * damping; + Vector delta = (m_targetPosition - world) * fracRemainingSimTime * invDeltaTime; + Vector alignDir; + linear = vec3_origin; + angular = vec3_origin; + + if ( m_align ) + { + QAngle angles; + Vector origin; + Vector axis; + AngularImpulse torque; + + pObject->GetShadowPosition( &origin, &angles ); + // align local normal to target normal + VMatrix tmp = SetupMatrixOrgAngles( origin, angles ); + Vector worldNormal = tmp.VMul3x3( m_localAlignNormal ); + axis = CrossProduct( worldNormal, m_targetAlignNormal ); + float trig = VectorNormalize(axis); + float alignRotation = RAD2DEG(asin(trig)); + axis *= alignRotation; + if ( alignRotation < 10 ) + { + float dot = DotProduct( worldNormal, m_targetAlignNormal ); + // probably 180 degrees off + if ( dot < 0 ) + { + if ( worldNormal.x < 0.5 ) + { + axis.Init(10,0,0); + } + else + { + axis.Init(0,0,10); + } + alignRotation = 10; + } + } + + // Solve for the rotation around the target normal (at the local align pos) that will + // move the grabbed spot to the destination. + Vector worldRotCenter = tmp.VMul4x3( m_localAlignPosition ); + Vector rotSrc = world - worldRotCenter; + Vector rotDest = m_targetPosition - worldRotCenter; + + // Get a basis in the plane perpendicular to m_targetAlignNormal + Vector srcN = rotSrc; + VectorNormalize( srcN ); + Vector tangent = CrossProduct( srcN, m_targetAlignNormal ); + float len = VectorNormalize( tangent ); + + // needs at least ~5 degrees, or forget rotation (0.08 ~= sin(5)) + if ( len > 0.08 ) + { + Vector binormal = CrossProduct( m_targetAlignNormal, tangent ); + + // Now project the src & dest positions into that plane + Vector planeSrc( DotProduct( rotSrc, tangent ), DotProduct( rotSrc, binormal ), 0 ); + Vector planeDest( DotProduct( rotDest, tangent ), DotProduct( rotDest, binormal ), 0 ); + + float rotRadius = VectorNormalize( planeSrc ); + float destRadius = VectorNormalize( planeDest ); + if ( rotRadius > 0.1 ) + { + if ( destRadius < rotRadius ) + { + destRadius = rotRadius; + } + //float ratio = rotRadius / destRadius; + float angleSrc = atan2( planeSrc.y, planeSrc.x ); + float angleDest = atan2( planeDest.y, planeDest.x ); + float angleDiff = angleDest - angleSrc; + angleDiff = RAD2DEG(angleDiff); + axis += m_targetAlignNormal * angleDiff; + //world = m_targetPosition;// + rotDest * (1-ratio); +// NDebugOverlay::Line( worldRotCenter, worldRotCenter-m_targetAlignNormal*50, 255, 0, 0, false, 0.1 ); +// NDebugOverlay::Line( worldRotCenter, worldRotCenter+tangent*50, 0, 255, 0, false, 0.1 ); +// NDebugOverlay::Line( worldRotCenter, worldRotCenter+binormal*50, 0, 0, 255, false, 0.1 ); + } + } + + torque = WorldToLocalRotation( tmp, axis, 1 ); + torque *= fracRemainingSimTime * invDeltaTime; + torque -= angVel * 1.0; // damping + for ( int i = 0; i < 3; i++ ) + { + if ( torque[i] > 0 ) + { + if ( torque[i] > m_maxAngularAcceleration[i] ) + torque[i] = m_maxAngularAcceleration[i]; + } + else + { + if ( torque[i] < -m_maxAngularAcceleration[i] ) + torque[i] = -m_maxAngularAcceleration[i]; + } + } + torque *= invDeltaTime; + angular += torque; + // Calculate an acceleration that pulls the object toward the constraint + // When you're out of alignment, don't pull very hard + float factor = fabsf(alignRotation); + if ( factor < 5 ) + { + factor = clamp( factor, 0, 5 ) * (1/5); + alignDir = m_targetAlignPosition - worldRotCenter; + // Limit movement to the part along m_targetAlignNormal if worldRotCenter is on the backside of + // of the target plane (one inch epsilon)! + float planeForward = DotProduct( alignDir, m_targetAlignNormal ); + if ( planeForward > 1 ) + { + alignDir = m_targetAlignNormal * planeForward; + } + Vector accel = alignDir * invDeltaTime * fracRemainingSimTime * (1-factor) * 0.20 * invDeltaTime; + float mag = accel.Length(); + if ( mag > m_maxAcceleration ) + { + accel *= (m_maxAcceleration/mag); + } + linear += accel; + } + linear -= vel*damping*invDeltaTime; + // UNDONE: Factor in the change in worldRotCenter due to applied torque! + } + else + { + // clamp future velocity to max speed + Vector nextVel = delta + vel; + float nextSpeed = nextVel.Length(); + if ( nextSpeed > m_maxVel ) + { + nextVel *= (m_maxVel / nextSpeed); + delta = nextVel - vel; + } + + delta *= invDeltaTime; + + float linearAccel = delta.Length(); + if ( linearAccel > m_maxAcceleration ) + { + delta *= m_maxAcceleration / linearAccel; + } + + Vector accel; + AngularImpulse angAccel; + pObject->CalculateForceOffset( delta, world, &accel, &angAccel ); + + linear += accel; + angular += angAccel; + } + + return SIM_GLOBAL_ACCELERATION; +} + + +struct pelletlist_t +{ + DECLARE_SIMPLE_DATADESC(); + + Vector localNormal; // normal in parent space + CHandle pellet; + EHANDLE parent; +}; + +class CWeaponGravityGun : public CBaseCombatWeapon +{ + DECLARE_DATADESC(); + +public: + DECLARE_CLASS( CWeaponGravityGun, CBaseCombatWeapon ); + + CWeaponGravityGun(); + void Spawn( void ); + void OnRestore( void ); + void Precache( void ); + + void PrimaryAttack( void ); + void SecondaryAttack( void ); + void WeaponIdle( void ); + void ItemPostFrame( void ); + virtual bool Holster( CBaseCombatWeapon *pSwitchingTo ) + { + EffectDestroy(); + return BaseClass::Holster(); + } + + bool Reload( void ); + void Equip( CBaseCombatCharacter *pOwner ) + { + // add constraint ammo + pOwner->SetAmmoCount( MAX_PELLETS, m_iSecondaryAmmoType ); + BaseClass::Equip( pOwner ); + } + void Drop(const Vector &vecVelocity) + { + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + pOwner->SetAmmoCount( 0, m_iSecondaryAmmoType ); + // destroy all constraints + BaseClass::Drop(vecVelocity); + } + + bool HasAnyAmmo( void ); + + void AttachObject( CBaseEntity *pEdict, const Vector& start, const Vector &end, float distance ); + void DetachObject( void ); + + void EffectCreate( void ); + void EffectUpdate( void ); + void EffectDestroy( void ); + + void SoundCreate( void ); + void SoundDestroy( void ); + void SoundStop( void ); + void SoundStart( void ); + void SoundUpdate( void ); + void AddPellet( CGravityPellet *pPellet, CBaseEntity *pParent, const Vector &surfaceNormal ); + void DeleteActivePellets(); + void SortPelletsForObject( CBaseEntity *pObject ); + void SetObjectPelletsColor( int r, int g, int b ); + void CreatePelletAttraction( float radius, CBaseEntity *pObject ); + IPhysicsObject *GetPelletPhysObject( int pelletIndex ); + void GetPelletWorldCoords( int pelletIndex, Vector *worldPos, Vector *worldNormal ) + { + if ( worldPos ) + { + *worldPos = m_activePellets[pelletIndex].pellet->GetAbsOrigin(); + } + if ( worldNormal ) + { + if ( m_activePellets[pelletIndex].parent ) + { + EntityMatrix tmp; + tmp.InitFromEntity( m_activePellets[pelletIndex].parent ); + *worldNormal = tmp.LocalToWorldRotation( m_activePellets[pelletIndex].localNormal ); + } + else + { + *worldNormal = m_activePellets[pelletIndex].localNormal; + } + } + } + + int ObjectCaps( void ) + { + int caps = BaseClass::ObjectCaps(); + if ( m_active ) + { + caps |= FCAP_DIRECTIONAL_USE; + } + return caps; + } + + CBaseEntity *GetBeamEntity(); + + DECLARE_SERVERCLASS(); + +private: + CNetworkVar( int, m_active ); + bool m_useDown; + EHANDLE m_hObject; + float m_distance; + float m_movementLength; + float m_lastYaw; + int m_soundState; + CNetworkVar( int, m_viewModelIndex ); + Vector m_originalObjectPosition; + + CGravControllerPoint m_gravCallback; + pelletlist_t m_activePellets[MAX_PELLETS]; + int m_pelletCount; + int m_objectPelletCount; + + int m_pelletHeld; + int m_pelletAttract; + float m_glueTime; + CNetworkVar( bool, m_glueTouching ); +}; + +IMPLEMENT_SERVERCLASS_ST( CWeaponGravityGun, DT_WeaponGravityGun ) + SendPropVector( SENDINFO_NAME(m_gravCallback.m_targetPosition, m_targetPosition), -1, SPROP_COORD ), + SendPropVector( SENDINFO_NAME(m_gravCallback.m_worldPosition, m_worldPosition), -1, SPROP_COORD ), + SendPropInt( SENDINFO(m_active), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_glueTouching), 1, SPROP_UNSIGNED ), + SendPropModelIndex( SENDINFO(m_viewModelIndex) ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( weapon_physgun, CWeaponGravityGun ); +PRECACHE_WEAPON_REGISTER(weapon_physgun); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_SIMPLE_DATADESC( pelletlist_t ) + + DEFINE_FIELD( localNormal, FIELD_VECTOR ), + DEFINE_FIELD( pellet, FIELD_EHANDLE ), + DEFINE_FIELD( parent, FIELD_EHANDLE ), + +END_DATADESC() + +BEGIN_DATADESC( CWeaponGravityGun ) + + DEFINE_FIELD( m_active, FIELD_INTEGER ), + DEFINE_FIELD( m_useDown, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hObject, FIELD_EHANDLE ), + DEFINE_FIELD( m_distance, FIELD_FLOAT ), + DEFINE_FIELD( m_movementLength, FIELD_FLOAT ), + DEFINE_FIELD( m_lastYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_soundState, FIELD_INTEGER ), + DEFINE_FIELD( m_viewModelIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_originalObjectPosition, FIELD_POSITION_VECTOR ), + DEFINE_EMBEDDED( m_gravCallback ), + // Physptrs can't be saved in embedded classes.. + DEFINE_PHYSPTR( m_gravCallback.m_controller ), + DEFINE_EMBEDDED_AUTO_ARRAY( m_activePellets ), + DEFINE_FIELD( m_pelletCount, FIELD_INTEGER ), + DEFINE_FIELD( m_objectPelletCount, FIELD_INTEGER ), + DEFINE_FIELD( m_pelletHeld, FIELD_INTEGER ), + DEFINE_FIELD( m_pelletAttract, FIELD_INTEGER ), + DEFINE_FIELD( m_glueTime, FIELD_TIME ), + DEFINE_FIELD( m_glueTouching, FIELD_BOOLEAN ), + +END_DATADESC() + + +enum physgun_soundstate { SS_SCANNING, SS_LOCKEDON }; +enum physgun_soundIndex { SI_LOCKEDON = 0, SI_SCANNING = 1, SI_LIGHTOBJECT = 2, SI_HEAVYOBJECT = 3, SI_ON, SI_OFF }; + + +//========================================================= +//========================================================= + +CWeaponGravityGun::CWeaponGravityGun() +{ + m_active = false; + m_bFiresUnderwater = true; + m_pelletAttract = -1; + m_pelletHeld = -1; +} + +//========================================================= +//========================================================= +void CWeaponGravityGun::Spawn( ) +{ + BaseClass::Spawn(); +// SetModel( GetWorldModel() ); + + FallInit(); +} + +void CWeaponGravityGun::OnRestore( void ) +{ + BaseClass::OnRestore(); + + if ( m_gravCallback.m_controller ) + { + m_gravCallback.m_controller->SetEventHandler( &m_gravCallback ); + } +} + + +//========================================================= +//========================================================= +void CWeaponGravityGun::Precache( void ) +{ + BaseClass::Precache(); + + g_physgunBeam = PrecacheModel(PHYSGUN_BEAM_SPRITE); + + PrecacheScriptSound( "Weapon_Physgun.Scanning" ); + PrecacheScriptSound( "Weapon_Physgun.LockedOn" ); + PrecacheScriptSound( "Weapon_Physgun.Scanning" ); + PrecacheScriptSound( "Weapon_Physgun.LightObject" ); + PrecacheScriptSound( "Weapon_Physgun.HeavyObject" ); +} + +void CWeaponGravityGun::EffectCreate( void ) +{ + EffectUpdate(); + m_active = true; +} + + +void CWeaponGravityGun::EffectUpdate( void ) +{ + Vector start, angles, forward, right; + trace_t tr; + + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + if ( !pOwner ) + return; + + m_viewModelIndex = pOwner->entindex(); + // Make sure I've got a view model + CBaseViewModel *vm = pOwner->GetViewModel(); + if ( vm ) + { + m_viewModelIndex = vm->entindex(); + } + + pOwner->EyeVectors( &forward, &right, NULL ); + + start = pOwner->Weapon_ShootPosition(); + Vector end = start + forward * 4096; + + UTIL_TraceLine( start, end, MASK_SHOT, pOwner, COLLISION_GROUP_NONE, &tr ); + end = tr.endpos; + float distance = tr.fraction * 4096; + if ( tr.fraction != 1 ) + { + // too close to the player, drop the object + if ( distance < 36 ) + { + DetachObject(); + return; + } + } + + if ( m_hObject == NULL && tr.DidHitNonWorldEntity() ) + { + CBaseEntity *pEntity = tr.m_pEnt; + // inform the object what was hit + ClearMultiDamage(); + pEntity->DispatchTraceAttack( CTakeDamageInfo( pOwner, pOwner, 0, DMG_PHYSGUN ), forward, &tr ); + ApplyMultiDamage(); + AttachObject( pEntity, start, tr.endpos, distance ); + m_lastYaw = pOwner->EyeAngles().y; + } + + // Add the incremental player yaw to the target transform + matrix3x4_t curMatrix, incMatrix, nextMatrix; + AngleMatrix( m_gravCallback.m_targetRotation, curMatrix ); + AngleMatrix( QAngle(0,pOwner->EyeAngles().y - m_lastYaw,0), incMatrix ); + ConcatTransforms( incMatrix, curMatrix, nextMatrix ); + MatrixAngles( nextMatrix, m_gravCallback.m_targetRotation ); + m_lastYaw = pOwner->EyeAngles().y; + + CBaseEntity *pObject = m_hObject; + if ( pObject ) + { + if ( m_useDown ) + { + if ( pOwner->m_afButtonPressed & IN_USE ) + { + m_useDown = false; + } + } + else + { + if ( pOwner->m_afButtonPressed & IN_USE ) + { + m_useDown = true; + } + } + + if ( m_useDown ) + { + pOwner->SetPhysicsFlag( PFLAG_DIROVERRIDE, true ); + if ( pOwner->m_nButtons & IN_FORWARD ) + { + m_distance = UTIL_Approach( 1024, m_distance, gpGlobals->frametime * 100 ); + } + if ( pOwner->m_nButtons & IN_BACK ) + { + m_distance = UTIL_Approach( 40, m_distance, gpGlobals->frametime * 100 ); + } + } + + if ( pOwner->m_nButtons & IN_WEAPON1 ) + { + m_distance = UTIL_Approach( 1024, m_distance, m_distance * 0.1 ); + } + if ( pOwner->m_nButtons & IN_WEAPON2 ) + { + m_distance = UTIL_Approach( 40, m_distance, m_distance * 0.1 ); + } + + // Send the object a physics damage message (0 damage). Some objects interpret this + // as something else being in control of their physics temporarily. + pObject->TakeDamage( CTakeDamageInfo( this, pOwner, 0, DMG_PHYSGUN ) ); + + Vector newPosition = start + forward * m_distance; + // 24 is a little larger than 16 * sqrt(2) (extent of player bbox) + // HACKHACK: We do this so we can "ignore" the player and the object we're manipulating + // If we had a filter for tracelines, we could simply filter both ents and start from "start" + Vector awayfromPlayer = start + forward * 24; + + UTIL_TraceLine( start, awayfromPlayer, MASK_SOLID, pOwner, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction == 1 ) + { + UTIL_TraceLine( awayfromPlayer, newPosition, MASK_SOLID, pObject, COLLISION_GROUP_NONE, &tr ); + Vector dir = tr.endpos - newPosition; + float distance = VectorNormalize(dir); + float maxDist = m_gravCallback.m_maxVel * gpGlobals->frametime; + if ( distance > maxDist ) + { + newPosition += dir * maxDist; + } + else + { + newPosition = tr.endpos; + } + } + else + { + newPosition = tr.endpos; + } + + CreatePelletAttraction( phys_gunglueradius.GetFloat(), pObject ); + + // If I'm looking more than 20 degrees away from the glue point, then give up + // This lets the player "gesture" for the glue to let go. + Vector pelletDir = m_gravCallback.m_worldPosition - start; + VectorNormalize(pelletDir); + if ( DotProduct( pelletDir, forward ) < 0.939 ) // 0.939 ~= cos(20deg) + { + // lose attach for 2 seconds if you're too far away + m_glueTime = gpGlobals->curtime + 1; + } + + if ( m_pelletHeld >= 0 && gpGlobals->curtime > m_glueTime ) + { + CGravityPellet *pPelletAttract = m_activePellets[m_pelletAttract].pellet; + + g_pEffects->Sparks( pPelletAttract->GetAbsOrigin() ); + } + + m_gravCallback.SetTargetPosition( newPosition ); + Vector dir = (newPosition - pObject->GetLocalOrigin()); + m_movementLength = dir.Length(); + } + else + { + m_gravCallback.SetTargetPosition( end ); + } + if ( m_pelletHeld >= 0 && gpGlobals->curtime > m_glueTime ) + { + Vector worldNormal, worldPos; + GetPelletWorldCoords( m_pelletAttract, &worldPos, &worldNormal ); + + m_gravCallback.SetAutoAlign( m_activePellets[m_pelletHeld].localNormal, m_activePellets[m_pelletHeld].pellet->GetLocalOrigin(), worldNormal, worldPos ); + } + else + { + m_gravCallback.ClearAutoAlign(); + } +} + +void CWeaponGravityGun::SoundCreate( void ) +{ + m_soundState = SS_SCANNING; + SoundStart(); +} + + +void CWeaponGravityGun::SoundDestroy( void ) +{ + SoundStop(); +} + + +void CWeaponGravityGun::SoundStop( void ) +{ + switch( m_soundState ) + { + case SS_SCANNING: + GetOwner()->StopSound( "Weapon_Physgun.Scanning" ); + break; + case SS_LOCKEDON: + GetOwner()->StopSound( "Weapon_Physgun.Scanning" ); + GetOwner()->StopSound( "Weapon_Physgun.LockedOn" ); + GetOwner()->StopSound( "Weapon_Physgun.LightObject" ); + GetOwner()->StopSound( "Weapon_Physgun.HeavyObject" ); + break; + } +} + + + +//----------------------------------------------------------------------------- +// Purpose: returns the linear fraction of value between low & high (0.0 - 1.0) * scale +// e.g. UTIL_LineFraction( 1.5, 1, 2, 1 ); will return 0.5 since 1.5 is +// halfway between 1 and 2 +// Input : value - a value between low & high (clamped) +// low - the value that maps to zero +// high - the value that maps to "scale" +// scale - the output scale +// Output : parametric fraction between low & high +//----------------------------------------------------------------------------- +static float UTIL_LineFraction( float value, float low, float high, float scale ) +{ + if ( value < low ) + value = low; + if ( value > high ) + value = high; + + float delta = high - low; + if ( delta == 0 ) + return 0; + + return scale * (value-low) / delta; +} + +void CWeaponGravityGun::SoundStart( void ) +{ + CPASAttenuationFilter filter( GetOwner() ); + filter.MakeReliable(); + + switch( m_soundState ) + { + case SS_SCANNING: + { + EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.Scanning" ); + } + break; + case SS_LOCKEDON: + { + // BUGBUG - If you start a sound with a pitch of 100, the pitch shift doesn't work! + + EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.LockedOn" ); + EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.Scanning" ); + EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.LightObject" ); + EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.HeavyObject" ); + } + break; + } + // volume, att, flags, pitch +} + +void CWeaponGravityGun::SoundUpdate( void ) +{ + int newState; + + if ( m_hObject ) + newState = SS_LOCKEDON; + else + newState = SS_SCANNING; + + if ( newState != m_soundState ) + { + SoundStop(); + m_soundState = newState; + SoundStart(); + } + + switch( m_soundState ) + { + case SS_SCANNING: + break; + case SS_LOCKEDON: + { + CPASAttenuationFilter filter( GetOwner() ); + filter.MakeReliable(); + + float height = m_hObject->GetAbsOrigin().z - m_originalObjectPosition.z; + + // go from pitch 90 to 150 over a height of 500 + int pitch = 90 + (int)UTIL_LineFraction( height, 0, 500, 60 ); + + CSoundParameters params; + if ( GetParametersForSound( "Weapon_Physgun.LockedOn", params, NULL ) ) + { + EmitSound_t ep( params ); + ep.m_nFlags = SND_CHANGE_VOL | SND_CHANGE_PITCH; + ep.m_nPitch = pitch; + + EmitSound( filter, GetOwner()->entindex(), ep ); + } + + // attenutate the movement sounds over 200 units of movement + float distance = UTIL_LineFraction( m_movementLength, 0, 200, 1.0 ); + + // blend the "mass" sounds between 50 and 500 kg + IPhysicsObject *pPhys = m_hObject->VPhysicsGetObject(); + + float fade = UTIL_LineFraction( pPhys->GetMass(), 50, 500, 1.0 ); + + if ( GetParametersForSound( "Weapon_Physgun.LightObject", params, NULL ) ) + { + EmitSound_t ep( params ); + ep.m_nFlags = SND_CHANGE_VOL; + ep.m_flVolume = fade * distance; + + EmitSound( filter, GetOwner()->entindex(), ep ); + } + + if ( GetParametersForSound( "Weapon_Physgun.HeavyObject", params, NULL ) ) + { + EmitSound_t ep( params ); + ep.m_nFlags = SND_CHANGE_VOL; + ep.m_flVolume = (1.0 - fade) * distance; + + EmitSound( filter, GetOwner()->entindex(), ep ); + } + } + break; + } +} + + +void CWeaponGravityGun::AddPellet( CGravityPellet *pPellet, CBaseEntity *pAttach, const Vector &surfaceNormal ) +{ + Assert(m_pelletCountIsInert() ) + { + if ( i != 0 ) + { + pelletlist_t tmp = m_activePellets[m_objectPelletCount]; + m_activePellets[m_objectPelletCount] = m_activePellets[i]; + m_activePellets[i] = tmp; + } + m_objectPelletCount++; + } + } + + SetObjectPelletsColor( 192, 255, 192 ); +} + +void CWeaponGravityGun::SetObjectPelletsColor( int r, int g, int b ) +{ + color32 color; + color.r = r; + color.g = g; + color.b = b; + color.a = 255; + + for ( int i = 0; i < m_objectPelletCount; i++ ) + { + CGravityPellet *pPellet = m_activePellets[i].pellet; + if ( !pPellet || pPellet->IsInert() ) + continue; + + pPellet->m_clrRender = color; + } +} + +CBaseEntity *CWeaponGravityGun::GetBeamEntity() +{ + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + if ( !pOwner ) + return NULL; + + // Make sure I've got a view model + CBaseViewModel *vm = pOwner->GetViewModel(); + if ( vm ) + return vm; + + return pOwner; +} + +void CWeaponGravityGun::DeleteActivePellets() +{ + CBaseEntity *pEnt = GetBeamEntity(); + + for ( int i = 0; i < m_pelletCount; i++ ) + { + CGravityPellet *pPellet = m_activePellets[i].pellet; + if ( !pPellet ) + continue; + + Vector forward; + AngleVectors( pPellet->GetAbsAngles(), &forward ); + g_pEffects->Dust( pPellet->GetAbsOrigin(), forward, 32, 30 ); + + // UNDONE: Probably should just do this client side + CBeam *pBeam = CBeam::BeamCreate( PHYSGUN_BEAM_SPRITE, 1.5 ); + pBeam->PointEntInit( pPellet->GetAbsOrigin(), pEnt ); + pBeam->SetEndAttachment( 1 ); + pBeam->SetBrightness( 255 ); + pBeam->SetColor( 255, 0, 0 ); + pBeam->RelinkBeam(); + pBeam->LiveForTime( 0.1 ); + + UTIL_Remove( pPellet ); + } + m_pelletCount = 0; +} + +void CWeaponGravityGun::CreatePelletAttraction( float radius, CBaseEntity *pObject ) +{ + int nearPellet = -1; + int objectPellet = -1; + float best = radius*radius; + // already have a pellet, check for in range + if ( m_pelletAttract >= 0 ) + { + Vector attract, held; + GetPelletWorldCoords( m_pelletAttract, &attract, NULL ); + GetPelletWorldCoords( m_pelletHeld, &held, NULL ); + float dist = (attract - held).Length(); + if ( dist < radius * 2 ) + { + nearPellet = m_pelletAttract; + objectPellet = m_pelletHeld; + best = dist * dist; + } + } + + if ( nearPellet < 0 ) + { + + for ( int i = 0; i < m_objectPelletCount; i++ ) + { + CGravityPellet *pPellet = m_activePellets[i].pellet; + if ( !pPellet ) + continue; + for ( int j = m_objectPelletCount; j < m_pelletCount; j++ ) + { + CGravityPellet *pTest = m_activePellets[j].pellet; + if ( !pTest ) + continue; + + if ( pTest->IsInert() ) + continue; + float distSqr = (pTest->GetAbsOrigin() - pPellet->GetAbsOrigin()).LengthSqr(); + if ( distSqr < best ) + { + Vector worldPos, worldNormal; + GetPelletWorldCoords( j, &worldPos, &worldNormal ); + // don't attract backside pellets (unless current pellet - prevent oscillation) + float dist = DotProduct( worldPos, worldNormal ); + if ( m_pelletAttract == j || DotProduct( pPellet->GetAbsOrigin(), worldNormal ) - dist >= 0 ) + { + best = distSqr; + nearPellet = j; + objectPellet = i; + } + } + } + } + } + + m_glueTouching = false; + if ( nearPellet < 0 || objectPellet < 0 ) + { + m_pelletAttract = -1; + m_pelletHeld = -1; + return; + } + + if ( nearPellet != m_pelletAttract || objectPellet != m_pelletHeld ) + { + m_glueTime = gpGlobals->curtime; + + m_pelletAttract = nearPellet; + m_pelletHeld = objectPellet; + } + + // check for bonding + if ( best < 3*3 ) + { + // This makes the pull towards the pellet stop getting stronger since some part of + // the object is touching + m_glueTouching = true; + } + } + + +IPhysicsObject *CWeaponGravityGun::GetPelletPhysObject( int pelletIndex ) +{ + if ( pelletIndex < 0 ) + return NULL; + + CBaseEntity *pEntity = m_activePellets[pelletIndex].parent; + if ( pEntity ) + return pEntity->VPhysicsGetObject(); + + return g_PhysWorldObject; +} + +void CWeaponGravityGun::EffectDestroy( void ) +{ + m_active = false; + SoundStop(); + + DetachObject(); +} + +void CWeaponGravityGun::DetachObject( void ) +{ + m_pelletHeld = -1; + m_pelletAttract = -1; + m_glueTouching = false; + SetObjectPelletsColor( 255, 0, 0 ); + m_objectPelletCount = 0; + + if ( m_hObject ) + { + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + Pickup_OnPhysGunDrop( m_hObject, pOwner, DROPPED_BY_CANNON ); + + m_gravCallback.DetachEntity(); + m_hObject = NULL; + } +} + +void CWeaponGravityGun::AttachObject( CBaseEntity *pObject, const Vector& start, const Vector &end, float distance ) +{ + m_hObject = pObject; + m_useDown = false; + IPhysicsObject *pPhysics = pObject ? (pObject->VPhysicsGetObject()) : NULL; + if ( pPhysics && pObject->GetMoveType() == MOVETYPE_VPHYSICS ) + { + m_distance = distance; + + m_gravCallback.AttachEntity( pObject, pPhysics, end ); + float mass = pPhysics->GetMass(); + Msg( "Object mass: %.2f lbs (%.2f kg)\n", kg2lbs(mass), mass ); + float vel = phys_gunvel.GetFloat(); + if ( mass > phys_gunmass.GetFloat() ) + { + vel = (vel*phys_gunmass.GetFloat())/mass; + } + m_gravCallback.SetMaxVelocity( vel ); +// Msg( "Object mass: %.2f lbs (%.2f kg) %f %f %f\n", kg2lbs(mass), mass, pObject->GetAbsOrigin().x, pObject->GetAbsOrigin().y, pObject->GetAbsOrigin().z ); +// Msg( "ANG: %f %f %f\n", pObject->GetAbsAngles().x, pObject->GetAbsAngles().y, pObject->GetAbsAngles().z ); + + m_originalObjectPosition = pObject->GetAbsOrigin(); + + m_pelletAttract = -1; + m_pelletHeld = -1; + + pPhysics->Wake(); + SortPelletsForObject( pObject ); + + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + if( pOwner ) + { + Pickup_OnPhysGunPickup( pObject, pOwner ); + } + } + else + { + m_hObject = NULL; + } +} + +//========================================================= +//========================================================= +void CWeaponGravityGun::PrimaryAttack( void ) +{ + if ( !m_active ) + { + SendWeaponAnim( ACT_VM_PRIMARYATTACK ); + EffectCreate(); + SoundCreate(); + } + else + { + EffectUpdate(); + SoundUpdate(); + } +} + +void CWeaponGravityGun::SecondaryAttack( void ) +{ + m_flNextSecondaryAttack = gpGlobals->curtime + 0.1; + if ( m_active ) + { + EffectDestroy(); + SoundDestroy(); + return; + } + + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + Assert( pOwner ); + + if ( pOwner->GetAmmoCount(m_iSecondaryAmmoType) <= 0 ) + return; + + m_viewModelIndex = pOwner->entindex(); + // Make sure I've got a view model + CBaseViewModel *vm = pOwner->GetViewModel(); + if ( vm ) + { + m_viewModelIndex = vm->entindex(); + } + + Vector forward; + pOwner->EyeVectors( &forward ); + + Vector start = pOwner->Weapon_ShootPosition(); + Vector end = start + forward * 4096; + + trace_t tr; + UTIL_TraceLine( start, end, MASK_SHOT, pOwner, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction == 1.0 || (tr.surface.flags & SURF_SKY) ) + return; + + CBaseEntity *pHit = tr.m_pEnt; + + if ( pHit->entindex() == 0 ) + { + pHit = NULL; + } + else + { + // if the object has no physics object, or isn't a physprop or brush entity, then don't glue + if ( !pHit->VPhysicsGetObject() || pHit->GetMoveType() != MOVETYPE_VPHYSICS ) + return; + } + + QAngle angles; + WeaponSound( SINGLE ); + pOwner->RemoveAmmo( 1, m_iSecondaryAmmoType ); + + VectorAngles( tr.plane.normal, angles ); + Vector endPoint = tr.endpos + tr.plane.normal; + CGravityPellet *pPellet = (CGravityPellet *)CBaseEntity::Create( "gravity_pellet", endPoint, angles, this ); + if ( pHit ) + { + pPellet->SetParent( pHit ); + } + AddPellet( pPellet, pHit, tr.plane.normal ); + + // UNDONE: Probably should just do this client side + CBaseEntity *pEnt = GetBeamEntity(); + CBeam *pBeam = CBeam::BeamCreate( PHYSGUN_BEAM_SPRITE, 1.5 ); + pBeam->PointEntInit( endPoint, pEnt ); + pBeam->SetEndAttachment( 1 ); + pBeam->SetBrightness( 255 ); + pBeam->SetColor( 255, 0, 0 ); + pBeam->RelinkBeam(); + pBeam->LiveForTime( 0.1 ); + +} + +void CWeaponGravityGun::WeaponIdle( void ) +{ + if ( HasWeaponIdleTimeElapsed() ) + { + SendWeaponAnim( ACT_VM_IDLE ); + if ( m_active ) + { + CBaseEntity *pObject = m_hObject; + // pellet is touching object, so glue it + if ( pObject && m_glueTouching ) + { + CGravityPellet *pPellet = m_activePellets[m_pelletAttract].pellet; + if ( pPellet->MakeConstraint( pObject ) ) + { + WeaponSound( SPECIAL1 ); + m_flNextPrimaryAttack = gpGlobals->curtime + 0.75; + m_activePellets[m_pelletHeld].pellet->MakeInert(); + } + } + + EffectDestroy(); + SoundDestroy(); + } + } +} + +void CWeaponGravityGun::ItemPostFrame( void ) +{ + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + if (!pOwner) + return; + + if ( pOwner->m_afButtonPressed & IN_ATTACK2 ) + { + SecondaryAttack(); + } + else if ( pOwner->m_nButtons & IN_ATTACK ) + { + PrimaryAttack(); + } + else if ( pOwner->m_afButtonPressed & IN_RELOAD ) + { + Reload(); + } + // ----------------------- + // No buttons down + // ----------------------- + else + { + WeaponIdle( ); + return; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWeaponGravityGun::HasAnyAmmo( void ) +{ + //Always report that we have ammo + return true; +} + +//========================================================= +//========================================================= +bool CWeaponGravityGun::Reload( void ) +{ + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + + if ( pOwner->GetAmmoCount(m_iSecondaryAmmoType) != MAX_PELLETS ) + { + pOwner->SetAmmoCount( MAX_PELLETS, m_iSecondaryAmmoType ); + DeleteActivePellets(); + WeaponSound( RELOAD ); + return true; + } + + return false; +} + +#define NUM_COLLISION_TESTS 2500 +void CC_CollisionTest( const CCommand &args ) +{ + if ( !physenv ) + return; + + Msg( "Testing collision system\n" ); + int i; + CBaseEntity *pSpot = gEntList.FindEntityByClassname( NULL, "info_player_start"); + Vector start = pSpot->GetAbsOrigin(); + static Vector *targets = NULL; + static bool first = true; + static float test[2] = {1,1}; + if ( first ) + { + targets = new Vector[NUM_COLLISION_TESTS]; + float radius = 0; + float theta = 0; + float phi = 0; + for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) + { + radius += NUM_COLLISION_TESTS * 123.123; + radius = fabs(fmod(radius, 128)); + theta += NUM_COLLISION_TESTS * 76.76; + theta = fabs(fmod(theta, DEG2RAD(360))); + phi += NUM_COLLISION_TESTS * 1997.99; + phi = fabs(fmod(phi, DEG2RAD(180))); + + float st, ct, sp, cp; + SinCos( theta, &st, &ct ); + SinCos( phi, &sp, &cp ); + + targets[i].x = radius * ct * sp; + targets[i].y = radius * st * sp; + targets[i].z = radius * cp; + + // make the trace 1024 units long + Vector dir = targets[i] - start; + VectorNormalize(dir); + targets[i] = start + dir * 1024; + } + first = false; + } + + //Vector results[NUM_COLLISION_TESTS]; + + int testType = 0; + if ( args.ArgC() >= 2 ) + { + testType = atoi( args[1] ); + } + float duration = 0; + Vector size[2]; + size[0].Init(0,0,0); + size[1].Init(16,16,16); + unsigned int dots = 0; + + for ( int j = 0; j < 2; j++ ) + { + float startTime = engine->Time(); + if ( testType == 1 ) + { + const CPhysCollide *pCollide = g_PhysWorldObject->GetCollide(); + trace_t tr; + + for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) + { + physcollision->TraceBox( start, targets[i], -size[j], size[j], pCollide, vec3_origin, vec3_angle, &tr ); + dots += physcollision->ReadStat(0); + //results[i] = tr.endpos; + } + } + else + { + testType = 0; + CBaseEntity *pWorld = GetContainingEntity( INDEXENT(0) ); + trace_t tr; + + for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) + { + UTIL_TraceModel( start, targets[i], -size[j], size[j], pWorld, COLLISION_GROUP_NONE, &tr ); + //results[i] = tr.endpos; + } + } + + duration += engine->Time() - startTime; + } + test[testType] = duration; + Msg("%d collisions in %.2f ms (%u dots)\n", NUM_COLLISION_TESTS, duration*1000, dots ); + Msg("Current speed ratio: %.2fX BSP:JGJK\n", test[1] / test[0] ); +#if 0 + int red = 255, green = 0, blue = 0; + for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) + { + NDebugOverlay::Line( start, results[i], red, green, blue, false, 2 ); + } +#endif +} +static ConCommand collision_test("collision_test", CC_CollisionTest, "Tests collision system", FCVAR_CHEAT ); diff --git a/sp/src/game/server/physics.cpp b/sp/src/game/server/physics.cpp new file mode 100644 index 00000000..3e2ab69c --- /dev/null +++ b/sp/src/game/server/physics.cpp @@ -0,0 +1,2938 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Interface layer for ipion IVP physics. +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//===========================================================================// + + +#include "cbase.h" +#include "coordsize.h" +#include "entitylist.h" +#include "vcollide_parse.h" +#include "soundenvelope.h" +#include "game.h" +#include "utlvector.h" +#include "init_factory.h" +#include "igamesystem.h" +#include "hierarchy.h" +#include "IEffects.h" +#include "engine/IEngineSound.h" +#include "world.h" +#include "decals.h" +#include "physics_fx.h" +#include "vphysics_sound.h" +#include "vphysics/vehicles.h" +#include "vehicle_sounds.h" +#include "movevars_shared.h" +#include "physics_saverestore.h" +#include "solidsetdefaults.h" +#include "tier0/vprof.h" +#include "engine/IStaticPropMgr.h" +#include "physics_prop_ragdoll.h" +#if HL2_EPISODIC +#include "particle_parse.h" +#endif +#include "vphysics/object_hash.h" +#include "vphysics/collision_set.h" +#include "vphysics/friction.h" +#include "fmtstr.h" +#include "physics_npc_solver.h" +#include "physics_collisionevent.h" +#include "vphysics/performance.h" +#include "positionwatcher.h" +#include "tier1/callqueue.h" +#include "vphysics/constraints.h" + +#ifdef PORTAL +#include "portal_physics_collisionevent.h" +#include "physicsshadowclone.h" +#include "PortalSimulation.h" +void PortalPhysFrame( float deltaTime ); //small wrapper for PhysFrame that simulates all 3 environments at once +#endif + +void PrecachePhysicsSounds( void ); + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar phys_speeds( "phys_speeds", "0" ); + +// defined in phys_constraint +extern IPhysicsConstraintEvent *g_pConstraintEvents; + + +CEntityList *g_pShadowEntities = NULL; +#ifdef PORTAL +CEntityList *g_pShadowEntities_Main = NULL; +#endif + +// local variables +static float g_PhysAverageSimTime; +CCallQueue g_PostSimulationQueue; + + +// local routines +static IPhysicsObject *PhysCreateWorld( CBaseEntity *pWorld ); +static void PhysFrame( float deltaTime ); +static bool IsDebris( int collisionGroup ); + +void TimescaleChanged( IConVar *var, const char *pOldString, float flOldValue ) +{ + if ( physenv ) + { + physenv->ResetSimulationClock(); + } +} + +ConVar phys_timescale( "phys_timescale", "1", 0, "Scale time for physics", TimescaleChanged ); + +#if _DEBUG +ConVar phys_dontprintint( "phys_dontprintint", "1", FCVAR_NONE, "Don't print inter-penetration warnings." ); +#endif + +#ifdef PORTAL + CPortal_CollisionEvent g_Collisions; +#else + CCollisionEvent g_Collisions; +#endif + + +IPhysicsCollisionSolver * const g_pCollisionSolver = &g_Collisions; +IPhysicsCollisionEvent * const g_pCollisionEventHandler = &g_Collisions; +IPhysicsObjectEvent * const g_pObjectEventHandler = &g_Collisions; + + +struct vehiclescript_t +{ + string_t scriptName; + vehicleparams_t params; + vehiclesounds_t sounds; +}; + +class CPhysicsHook : public CBaseGameSystemPerFrame +{ +public: + virtual const char *Name() { return "CPhysicsHook"; } + + virtual bool Init(); + virtual void LevelInitPreEntity(); + virtual void LevelInitPostEntity(); + virtual void LevelShutdownPreEntity(); + virtual void LevelShutdownPostEntity(); + virtual void FrameUpdatePostEntityThink(); + virtual void PreClientUpdate(); + + bool FindOrAddVehicleScript( const char *pScriptName, vehicleparams_t *pVehicle, vehiclesounds_t *pSounds ); + void FlushVehicleScripts() + { + m_vehicleScripts.RemoveAll(); + } + + bool ShouldSimulate() + { + return (physenv && !m_bPaused) ? true : false; + } + + physicssound::soundlist_t m_impactSounds; + CUtlVector m_breakSounds; + + CUtlVector m_massCenterOverrides; + CUtlVector m_vehicleScripts; + + float m_impactSoundTime; + bool m_bPaused; + bool m_isFinalTick; +}; + + +CPhysicsHook g_PhysicsHook; + +//----------------------------------------------------------------------------- +// Singleton access +//----------------------------------------------------------------------------- +IGameSystem* PhysicsGameSystem() +{ + return &g_PhysicsHook; +} + + +//----------------------------------------------------------------------------- +// Purpose: The physics hook callback implementations +//----------------------------------------------------------------------------- +bool CPhysicsHook::Init( void ) +{ + factorylist_t factories; + + // Get the list of interface factories to extract the physics DLL's factory + FactoryList_Retrieve( factories ); + + if ( !factories.physicsFactory ) + return false; + + if ((physics = (IPhysics *)factories.physicsFactory( VPHYSICS_INTERFACE_VERSION, NULL )) == NULL || + (physcollision = (IPhysicsCollision *)factories.physicsFactory( VPHYSICS_COLLISION_INTERFACE_VERSION, NULL )) == NULL || + (physprops = (IPhysicsSurfaceProps *)factories.physicsFactory( VPHYSICS_SURFACEPROPS_INTERFACE_VERSION, NULL )) == NULL + ) + return false; + + PhysParseSurfaceData( physprops, filesystem ); + + m_isFinalTick = true; + m_impactSoundTime = 0; + m_vehicleScripts.EnsureCapacity(4); + return true; +} + + +// a little debug wrapper to help fix bugs when entity pointers get trashed +#if 0 +struct physcheck_t +{ + IPhysicsObject *pPhys; + char string[512]; +}; + +CUtlVector< physcheck_t > physCheck; + +void PhysCheckAdd( IPhysicsObject *pPhys, const char *pString ) +{ + physcheck_t tmp; + tmp.pPhys = pPhys; + Q_strncpy( tmp.string, pString ,sizeof(tmp.string)); + physCheck.AddToTail( tmp ); +} + +const char *PhysCheck( IPhysicsObject *pPhys ) +{ + for ( int i = 0; i < physCheck.Size(); i++ ) + { + if ( physCheck[i].pPhys == pPhys ) + return physCheck[i].string; + } + + return "unknown"; +} +#endif + +void CPhysicsHook::LevelInitPreEntity() +{ + physenv = physics->CreateEnvironment(); + physics_performanceparams_t params; + params.Defaults(); + params.maxCollisionsPerObjectPerTimestep = 10; + physenv->SetPerformanceSettings( ¶ms ); + +#ifdef PORTAL + physenv_main = physenv; +#endif + { + g_EntityCollisionHash = physics->CreateObjectPairHash(); + } + factorylist_t factories; + FactoryList_Retrieve( factories ); + physenv->SetDebugOverlay( factories.engineFactory ); + physenv->EnableDeleteQueue( true ); + + physenv->SetCollisionSolver( &g_Collisions ); + physenv->SetCollisionEventHandler( &g_Collisions ); + physenv->SetConstraintEventHandler( g_pConstraintEvents ); + physenv->EnableConstraintNotify( true ); // callback when an object gets deleted that is attached to a constraint + + physenv->SetObjectEventHandler( &g_Collisions ); + + physenv->SetSimulationTimestep( DEFAULT_TICK_INTERVAL ); // 15 ms per tick + // HL Game gravity, not real-world gravity + physenv->SetGravity( Vector( 0, 0, -GetCurrentGravity() ) ); + g_PhysAverageSimTime = 0; + + g_PhysWorldObject = PhysCreateWorld( GetWorldEntity() ); + + g_pShadowEntities = new CEntityList; +#ifdef PORTAL + g_pShadowEntities_Main = g_pShadowEntities; +#endif + + PrecachePhysicsSounds(); + + m_bPaused = true; +} + + + +void CPhysicsHook::LevelInitPostEntity() +{ + m_bPaused = false; +} + +void CPhysicsHook::LevelShutdownPreEntity() +{ + if ( !physenv ) + return; + physenv->SetQuickDelete( true ); +} + +void CPhysicsHook::LevelShutdownPostEntity() +{ + if ( !physenv ) + return; + + g_pPhysSaveRestoreManager->ForgetAllModels(); + + g_Collisions.LevelShutdown(); + + physics->DestroyEnvironment( physenv ); + physenv = NULL; + + physics->DestroyObjectPairHash( g_EntityCollisionHash ); + g_EntityCollisionHash = NULL; + + physics->DestroyAllCollisionSets(); + + g_PhysWorldObject = NULL; + + delete g_pShadowEntities; + g_pShadowEntities = NULL; + m_impactSounds.RemoveAll(); + m_breakSounds.RemoveAll(); + m_massCenterOverrides.Purge(); + FlushVehicleScripts(); +} + + +bool CPhysicsHook::FindOrAddVehicleScript( const char *pScriptName, vehicleparams_t *pVehicle, vehiclesounds_t *pSounds ) +{ + bool bLoadedSounds = false; + int index = -1; + for ( int i = 0; i < m_vehicleScripts.Count(); i++ ) + { + if ( !Q_stricmp(m_vehicleScripts[i].scriptName.ToCStr(), pScriptName) ) + { + index = i; + bLoadedSounds = true; + break; + } + } + + if ( index < 0 ) + { + byte *pFile = UTIL_LoadFileForMe( pScriptName, NULL ); + if ( pFile ) + { + // new script, parse it and write to the table + index = m_vehicleScripts.AddToTail(); + m_vehicleScripts[index].scriptName = AllocPooledString(pScriptName); + m_vehicleScripts[index].sounds.Init(); + + IVPhysicsKeyParser *pParse = physcollision->VPhysicsKeyParserCreate( (char *)pFile ); + while ( !pParse->Finished() ) + { + const char *pBlock = pParse->GetCurrentBlockName(); + if ( !strcmpi( pBlock, "vehicle" ) ) + { + pParse->ParseVehicle( &m_vehicleScripts[index].params, NULL ); + } + else if ( !Q_stricmp( pBlock, "vehicle_sounds" ) ) + { + bLoadedSounds = true; + CVehicleSoundsParser soundParser; + pParse->ParseCustom( &m_vehicleScripts[index].sounds, &soundParser ); + } + else + { + pParse->SkipBlock(); + } + } + physcollision->VPhysicsKeyParserDestroy( pParse ); + UTIL_FreeFile( pFile ); + } + } + + if ( index >= 0 ) + { + if ( pVehicle ) + { + *pVehicle = m_vehicleScripts[index].params; + } + if ( pSounds ) + { + // We must pass back valid data here! + if ( bLoadedSounds == false ) + return false; + + *pSounds = m_vehicleScripts[index].sounds; + } + return true; + } + + return false; +} + +// called after entities think +void CPhysicsHook::FrameUpdatePostEntityThink( ) +{ + VPROF_BUDGET( "CPhysicsHook::FrameUpdatePostEntityThink", VPROF_BUDGETGROUP_PHYSICS ); + + // Tracker 24846: If game is paused, don't simulate vphysics + float interval = ( gpGlobals->frametime > 0.0f ) ? TICK_INTERVAL : 0.0f; + + // update the physics simulation, not we don't use gpGlobals->frametime, since that can be 30 msec or 15 msec + // depending on whether IsSimulatingOnAlternateTicks is true or not + if ( CBaseEntity::IsSimulatingOnAlternateTicks() ) + { + m_isFinalTick = false; + +#ifdef PORTAL //slight detour if we're the portal mod + PortalPhysFrame( interval ); +#else + PhysFrame( interval ); +#endif + + } + m_isFinalTick = true; + +#ifdef PORTAL //slight detour if we're the portal mod + PortalPhysFrame( interval ); +#else + PhysFrame( interval ); +#endif + +} + +void CPhysicsHook::PreClientUpdate() +{ + m_impactSoundTime += gpGlobals->frametime; + if ( m_impactSoundTime > 0.05f ) + { + physicssound::PlayImpactSounds( m_impactSounds ); + m_impactSoundTime = 0.0f; + physicssound::PlayBreakSounds( m_breakSounds ); + } +} + +bool PhysIsFinalTick() +{ + return g_PhysicsHook.m_isFinalTick; +} + +IPhysicsObject *PhysCreateWorld( CBaseEntity *pWorld ) +{ + staticpropmgr->CreateVPhysicsRepresentations( physenv, &g_SolidSetup, pWorld ); + return PhysCreateWorld_Shared( pWorld, modelinfo->GetVCollide(1), g_PhysDefaultObjectParams ); +} + + +// vehicle wheels can only collide with things that can't get stuck in them during game physics +// because they aren't in the game physics world at present +static bool WheelCollidesWith( IPhysicsObject *pObj, CBaseEntity *pEntity ) +{ +#if defined( INVASION_DLL ) + if ( pEntity->GetCollisionGroup() == TFCOLLISION_GROUP_OBJECT ) + return false; +#endif + + // Cull against interactive debris + if ( pEntity->GetCollisionGroup() == COLLISION_GROUP_INTERACTIVE_DEBRIS ) + return false; + + // Hit physics ents + if ( pEntity->GetMoveType() == MOVETYPE_PUSH || pEntity->GetMoveType() == MOVETYPE_VPHYSICS || pObj->IsStatic() ) + return true; + + return false; +} + +CCollisionEvent::CCollisionEvent() +{ + m_inCallback = 0; + m_bBufferTouchEvents = false; + m_lastTickFrictionError = 0; +} + +int CCollisionEvent::ShouldCollide( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1 ) +#if _DEBUG +{ + int x0 = ShouldCollide_2(pObj0, pObj1, pGameData0, pGameData1); + int x1 = ShouldCollide_2(pObj1, pObj0, pGameData1, pGameData0); + Assert(x0==x1); + return x0; +} +int CCollisionEvent::ShouldCollide_2( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1 ) +#endif +{ + CallbackContext check(this); + + CBaseEntity *pEntity0 = static_cast(pGameData0); + CBaseEntity *pEntity1 = static_cast(pGameData1); + + if ( !pEntity0 || !pEntity1 ) + return 1; + + unsigned short gameFlags0 = pObj0->GetGameFlags(); + unsigned short gameFlags1 = pObj1->GetGameFlags(); + + if ( pEntity0 == pEntity1 ) + { + // allow all-or-nothing per-entity disable + if ( (gameFlags0 | gameFlags1) & FVPHYSICS_NO_SELF_COLLISIONS ) + return 0; + + IPhysicsCollisionSet *pSet = physics->FindCollisionSet( pEntity0->GetModelIndex() ); + if ( pSet ) + return pSet->ShouldCollide( pObj0->GetGameIndex(), pObj1->GetGameIndex() ); + + return 1; + } + + // objects that are both constrained to the world don't collide with each other + if ( (gameFlags0 & gameFlags1) & FVPHYSICS_CONSTRAINT_STATIC ) + { + return 0; + } + + // Special collision rules for vehicle wheels + // Their entity collides with stuff using the normal rules, but they + // have different rules than the vehicle body for various reasons. + // sort of a hack because we don't have spheres to represent them in the game + // world for speculative collisions. + if ( pObj0->GetCallbackFlags() & CALLBACK_IS_VEHICLE_WHEEL ) + { + if ( !WheelCollidesWith( pObj1, pEntity1 ) ) + return false; + } + if ( pObj1->GetCallbackFlags() & CALLBACK_IS_VEHICLE_WHEEL ) + { + if ( !WheelCollidesWith( pObj0, pEntity0 ) ) + return false; + } + + if ( pEntity0->ForceVPhysicsCollide( pEntity1 ) || pEntity1->ForceVPhysicsCollide( pEntity0 ) ) + return 1; + + if ( pEntity0->edict() && pEntity1->edict() ) + { + // don't collide with your owner +#ifdef MAPBASE + if ( (pEntity0->GetOwnerEntity() == pEntity1 && !pEntity0->IsSolidFlagSet(FSOLID_COLLIDE_WITH_OWNER)) + || (pEntity1->GetOwnerEntity() == pEntity0 && !pEntity1->IsSolidFlagSet(FSOLID_COLLIDE_WITH_OWNER)) ) +#else + if ( pEntity0->GetOwnerEntity() == pEntity1 || pEntity1->GetOwnerEntity() == pEntity0 ) +#endif + return 0; + } + + if ( pEntity0->GetMoveParent() || pEntity1->GetMoveParent() ) + { + CBaseEntity *pParent0 = pEntity0->GetRootMoveParent(); + CBaseEntity *pParent1 = pEntity1->GetRootMoveParent(); + + // NOTE: Don't let siblings/parents collide. If you want this behavior, do it + // with constraints, not hierarchy! + if ( pParent0 == pParent1 ) + return 0; + + if ( g_EntityCollisionHash->IsObjectPairInHash( pParent0, pParent1 ) ) + return 0; + + IPhysicsObject *p0 = pParent0->VPhysicsGetObject(); + IPhysicsObject *p1 = pParent1->VPhysicsGetObject(); + if ( p0 && p1 ) + { + if ( g_EntityCollisionHash->IsObjectPairInHash( p0, p1 ) ) + return 0; + } + } + + int solid0 = pEntity0->GetSolid(); + int solid1 = pEntity1->GetSolid(); + int nSolidFlags0 = pEntity0->GetSolidFlags(); + int nSolidFlags1 = pEntity1->GetSolidFlags(); + + int movetype0 = pEntity0->GetMoveType(); + int movetype1 = pEntity1->GetMoveType(); + + // entities with non-physical move parents or entities with MOVETYPE_PUSH + // are considered as "AI movers". They are unchanged by collision; they exert + // physics forces on the rest of the system. + bool aiMove0 = (movetype0==MOVETYPE_PUSH) ? true : false; + bool aiMove1 = (movetype1==MOVETYPE_PUSH) ? true : false; + + if ( pEntity0->GetMoveParent() ) + { + // if the object & its parent are both MOVETYPE_VPHYSICS, then this must be a special case + // like a prop_ragdoll_attached + if ( !(movetype0 == MOVETYPE_VPHYSICS && pEntity0->GetRootMoveParent()->GetMoveType() == MOVETYPE_VPHYSICS) ) + { + aiMove0 = true; + } + } + if ( pEntity1->GetMoveParent() ) + { + // if the object & its parent are both MOVETYPE_VPHYSICS, then this must be a special case. + if ( !(movetype1 == MOVETYPE_VPHYSICS && pEntity1->GetRootMoveParent()->GetMoveType() == MOVETYPE_VPHYSICS) ) + { + aiMove1 = true; + } + } + + // AI movers don't collide with the world/static/pinned objects or other AI movers + if ( (aiMove0 && !pObj1->IsMoveable()) || + (aiMove1 && !pObj0->IsMoveable()) || + (aiMove0 && aiMove1) ) + return 0; + + // two objects under shadow control should not collide. The AI will figure it out + if ( pObj0->GetShadowController() && pObj1->GetShadowController() ) + return 0; + + // BRJ 1/24/03 + // You can remove the assert if it's problematic; I *believe* this condition + // should be met, but I'm not sure. + //Assert ( (solid0 != SOLID_NONE) && (solid1 != SOLID_NONE) ); + if ( (solid0 == SOLID_NONE) || (solid1 == SOLID_NONE) ) + return 0; + + // not solid doesn't collide with anything + if ( (nSolidFlags0|nSolidFlags1) & FSOLID_NOT_SOLID ) + { + // might be a vphysics trigger, collide with everything but "not solid" + if ( pObj0->IsTrigger() && !(nSolidFlags1 & FSOLID_NOT_SOLID) ) + return 1; + if ( pObj1->IsTrigger() && !(nSolidFlags0 & FSOLID_NOT_SOLID) ) + return 1; + + return 0; + } + + if ( (nSolidFlags0 & FSOLID_TRIGGER) && + !(solid1 == SOLID_VPHYSICS || solid1 == SOLID_BSP || movetype1 == MOVETYPE_VPHYSICS) ) + return 0; + + if ( (nSolidFlags1 & FSOLID_TRIGGER) && + !(solid0 == SOLID_VPHYSICS || solid0 == SOLID_BSP || movetype0 == MOVETYPE_VPHYSICS) ) + return 0; + + if ( !g_pGameRules->ShouldCollide( pEntity0->GetCollisionGroup(), pEntity1->GetCollisionGroup() ) ) + return 0; + + // check contents + if ( !(pObj0->GetContents() & pEntity1->PhysicsSolidMaskForEntity()) || !(pObj1->GetContents() & pEntity0->PhysicsSolidMaskForEntity()) ) + return 0; + + if ( g_EntityCollisionHash->IsObjectPairInHash( pGameData0, pGameData1 ) ) + return 0; + + if ( g_EntityCollisionHash->IsObjectPairInHash( pObj0, pObj1 ) ) + return 0; + + return 1; +} + +bool FindMaxContact( IPhysicsObject *pObject, float minForce, IPhysicsObject **pOtherObject, Vector *contactPos, Vector *pForce ) +{ + float mass = pObject->GetMass(); + float maxForce = minForce; + *pOtherObject = NULL; + IPhysicsFrictionSnapshot *pSnapshot = pObject->CreateFrictionSnapshot(); + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + if ( pOther->IsMoveable() && pOther->GetMass() > mass ) + { + float force = pSnapshot->GetNormalForce(); + if ( force > maxForce ) + { + *pOtherObject = pOther; + pSnapshot->GetContactPoint( *contactPos ); + pSnapshot->GetSurfaceNormal( *pForce ); + *pForce *= force; + } + } + pSnapshot->NextFrictionData(); + } + pObject->DestroyFrictionSnapshot( pSnapshot ); + if ( *pOtherObject ) + return true; + + return false; +} + +bool CCollisionEvent::ShouldFreezeObject( IPhysicsObject *pObject ) +{ + extern bool PropIsGib(CBaseEntity *pEntity); + // for now, don't apply a per-object limit to ai MOVETYPE_PUSH objects + // NOTE: If this becomes a problem (too many collision checks this tick) we should add a path + // to inform the logic in VPhysicsUpdatePusher() about the limit being applied so + // that it doesn't falsely block the object when it's simply been temporarily frozen + // for performance reasons + CBaseEntity *pEntity = static_cast(pObject->GetGameData()); + if ( pEntity ) + { + if (pEntity->GetMoveType() == MOVETYPE_PUSH ) + return false; + + // don't limit vehicle collisions either, limit can make breaking through a pile of breakable + // props very hitchy + if (pEntity->GetServerVehicle() && !(pObject->GetCallbackFlags() & CALLBACK_IS_VEHICLE_WHEEL)) + return false; + } + + // if we're freezing a debris object, then it's probably due to some kind of solver issue + // usually this is a large object resting on the debris object in question which is not + // very stable. + // After doing the experiment of constraining the dynamic range of mass while solving friction + // contacts, I like the results of this tradeoff better. So damage or remove the debris object + // wherever possible once we hit this case: + if ( IsDebris( pEntity->GetCollisionGroup()) && !pEntity->IsNPC() ) + { + IPhysicsObject *pOtherObject = NULL; + Vector contactPos; + Vector force; + // find the contact with the moveable object applying the most contact force + if ( FindMaxContact( pObject, pObject->GetMass() * 10, &pOtherObject, &contactPos, &force ) ) + { + CBaseEntity *pOther = static_cast(pOtherObject->GetGameData()); + // this object can take damage, crush it + if ( pEntity->m_takedamage > DAMAGE_EVENTS_ONLY ) + { + CTakeDamageInfo dmgInfo( pOther, pOther, force, contactPos, force.Length() * 0.1f, DMG_CRUSH ); + PhysCallbackDamage( pEntity, dmgInfo ); + } + else + { + // can't be damaged, so do something else: + if ( PropIsGib(pEntity) ) + { + // it's always safe to delete gibs, so kill this one to avoid simulation problems + PhysCallbackRemove( pEntity->NetworkProp() ); + } + else + { + // not a gib, create a solver: + // UNDONE: Add a property to override this in gameplay critical scenarios? + g_PostSimulationQueue.QueueCall( EntityPhysics_CreateSolver, pOther, pEntity, true, 1.0f ); + } + } + } + } + return true; +} + +bool CCollisionEvent::ShouldFreezeContacts( IPhysicsObject **pObjectList, int objectCount ) +{ + if ( m_lastTickFrictionError > gpGlobals->tickcount || m_lastTickFrictionError < (gpGlobals->tickcount-1) ) + { + CGWarning( 1, CON_GROUP_PHYSICS, "Performance Warning: large friction system (%d objects)!!!\n", objectCount ); +#if _DEBUG + for ( int i = 0; i < objectCount; i++ ) + { + CBaseEntity *pEntity = static_cast(pObjectList[i]->GetGameData()); + pEntity->m_debugOverlays |= OVERLAY_ABSBOX_BIT | OVERLAY_PIVOT_BIT; + } +#endif + } + m_lastTickFrictionError = gpGlobals->tickcount; + return false; +} + +// NOTE: these are fully edge triggered events +// called when an object wakes up (starts simulating) +void CCollisionEvent::ObjectWake( IPhysicsObject *pObject ) +{ + CBaseEntity *pEntity = static_cast(pObject->GetGameData()); + if ( pEntity && pEntity->HasDataObjectType( VPHYSICSWATCHER ) ) + { + ReportVPhysicsStateChanged( pObject, pEntity, true ); + } +} +// called when an object goes to sleep (no longer simulating) +void CCollisionEvent::ObjectSleep( IPhysicsObject *pObject ) +{ + CBaseEntity *pEntity = static_cast(pObject->GetGameData()); + if ( pEntity && pEntity->HasDataObjectType( VPHYSICSWATCHER ) ) + { + ReportVPhysicsStateChanged( pObject, pEntity, false ); + } +} + +bool PhysShouldCollide( IPhysicsObject *pObj0, IPhysicsObject *pObj1 ) +{ + void *pGameData0 = pObj0->GetGameData(); + void *pGameData1 = pObj1->GetGameData(); + if ( !pGameData0 || !pGameData1 ) + return false; + return g_Collisions.ShouldCollide( pObj0, pObj1, pGameData0, pGameData1 ) ? true : false; +} + +bool PhysIsInCallback() +{ + if ( (physenv && physenv->IsInSimulation()) || g_Collisions.IsInCallback() ) + return true; + + return false; +} + + +static void ReportPenetration( CBaseEntity *pEntity, float duration ) +{ + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + if ( g_pDeveloper->GetInt() > 1 ) + { + pEntity->m_debugOverlays |= OVERLAY_ABSBOX_BIT; + } + + pEntity->AddTimedOverlay( UTIL_VarArgs("VPhysics Penetration Error (%s)!", pEntity->GetDebugName()), duration ); + } +} + +static bool IsDebris( int collisionGroup ) +{ + switch ( collisionGroup ) + { + case COLLISION_GROUP_DEBRIS: + case COLLISION_GROUP_INTERACTIVE_DEBRIS: + case COLLISION_GROUP_DEBRIS_TRIGGER: + return true; + default: + break; + } + return false; +} + +static void UpdateEntityPenetrationFlag( CBaseEntity *pEntity, bool isPenetrating ) +{ + if ( !pEntity ) + return; + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + if ( !pList[i]->IsStatic() ) + { + if ( isPenetrating ) + { + PhysSetGameFlags( pList[i], FVPHYSICS_PENETRATING ); + } + else + { + PhysClearGameFlags( pList[i], FVPHYSICS_PENETRATING ); + } + } + } +} + +void CCollisionEvent::GetListOfPenetratingEntities( CBaseEntity *pSearch, CUtlVector &list ) +{ + for ( int i = m_penetrateEvents.Count()-1; i >= 0; --i ) + { + if ( m_penetrateEvents[i].hEntity0 == pSearch && m_penetrateEvents[i].hEntity1.Get() != NULL ) + { + list.AddToTail( m_penetrateEvents[i].hEntity1 ); + } + else if ( m_penetrateEvents[i].hEntity1 == pSearch && m_penetrateEvents[i].hEntity0.Get() != NULL ) + { + list.AddToTail( m_penetrateEvents[i].hEntity0 ); + } + } +} + +void CCollisionEvent::UpdatePenetrateEvents( void ) +{ + for ( int i = m_penetrateEvents.Count()-1; i >= 0; --i ) + { + CBaseEntity *pEntity0 = m_penetrateEvents[i].hEntity0; + CBaseEntity *pEntity1 = m_penetrateEvents[i].hEntity1; + + if ( m_penetrateEvents[i].collisionState == COLLSTATE_TRYDISABLE ) + { + if ( pEntity0 && pEntity1 ) + { + IPhysicsObject *pObj0 = pEntity0->VPhysicsGetObject(); + if ( pObj0 ) + { + PhysForceEntityToSleep( pEntity0, pObj0 ); + } + IPhysicsObject *pObj1 = pEntity1->VPhysicsGetObject(); + if ( pObj1 ) + { + PhysForceEntityToSleep( pEntity1, pObj1 ); + } + m_penetrateEvents[i].collisionState = COLLSTATE_DISABLED; + continue; + } + // missing entity or object, clear event + } + else if ( m_penetrateEvents[i].collisionState == COLLSTATE_TRYNPCSOLVER ) + { + if ( pEntity0 && pEntity1 ) + { + CAI_BaseNPC *pNPC = pEntity0->MyNPCPointer(); + CBaseEntity *pBlocker = pEntity1; + if ( !pNPC ) + { + pNPC = pEntity1->MyNPCPointer(); + Assert(pNPC); + pBlocker = pEntity0; + } + NPCPhysics_CreateSolver( pNPC, pBlocker, true, 1.0f ); + } + // transferred to solver, clear event + } + else if ( m_penetrateEvents[i].collisionState == COLLSTATE_TRYENTITYSOLVER ) + { + if ( pEntity0 && pEntity1 ) + { + if ( !IsDebris(pEntity1->GetCollisionGroup()) || pEntity1->GetMoveType() != MOVETYPE_VPHYSICS ) + { + CBaseEntity *pTmp = pEntity0; + pEntity0 = pEntity1; + pEntity1 = pTmp; + } + EntityPhysics_CreateSolver( pEntity0, pEntity1, true, 1.0f ); + } + // transferred to solver, clear event + } + else if ( gpGlobals->curtime - m_penetrateEvents[i].timeStamp > 1.0 ) + { + if ( m_penetrateEvents[i].collisionState == COLLSTATE_DISABLED ) + { + if ( pEntity0 && pEntity1 ) + { + IPhysicsObject *pObj0 = pEntity0->VPhysicsGetObject(); + IPhysicsObject *pObj1 = pEntity1->VPhysicsGetObject(); + if ( pObj0 && pObj1 ) + { + m_penetrateEvents[i].collisionState = COLLSTATE_ENABLED; + continue; + } + } + } + // haven't penetrated for 1 second, so remove + } + else + { + // recent timestamp, don't remove the event yet + continue; + } + // done, clear event + m_penetrateEvents.FastRemove(i); + UpdateEntityPenetrationFlag( pEntity0, false ); + UpdateEntityPenetrationFlag( pEntity1, false ); + } +} + +penetrateevent_t &CCollisionEvent::FindOrAddPenetrateEvent( CBaseEntity *pEntity0, CBaseEntity *pEntity1 ) +{ + int index = -1; + for ( int i = m_penetrateEvents.Count()-1; i >= 0; --i ) + { + if ( m_penetrateEvents[i].hEntity0.Get() == pEntity0 && m_penetrateEvents[i].hEntity1.Get() == pEntity1 ) + { + index = i; + break; + } + } + if ( index < 0 ) + { + index = m_penetrateEvents.AddToTail(); + penetrateevent_t &event = m_penetrateEvents[index]; + event.hEntity0 = pEntity0; + event.hEntity1 = pEntity1; + event.startTime = gpGlobals->curtime; + event.collisionState = COLLSTATE_ENABLED; + UpdateEntityPenetrationFlag( pEntity0, true ); + UpdateEntityPenetrationFlag( pEntity1, true ); + } + penetrateevent_t &event = m_penetrateEvents[index]; + event.timeStamp = gpGlobals->curtime; + return event; +} + + + +static ConVar phys_penetration_error_time( "phys_penetration_error_time", "10", 0, "Controls the duration of vphysics penetration error boxes." ); + +static bool CanResolvePenetrationWithNPC( CBaseEntity *pEntity, IPhysicsObject *pObject ) +{ + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + // hinged objects won't be able to be pushed out anyway, so don't try the npc solver + if ( !pObject->IsHinged() && !pObject->IsAttachedToConstraint(true) ) + { + if ( pObject->IsMoveable() || pEntity->GetServerVehicle() ) + return true; + } + } + return false; +} + +int CCollisionEvent::ShouldSolvePenetration( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1, float dt ) +{ + CallbackContext check(this); + + // Pointers to the entity for each physics object + CBaseEntity *pEntity0 = static_cast(pGameData0); + CBaseEntity *pEntity1 = static_cast(pGameData1); + + // this can get called as entities are being constructed on the other side of a game load or level transition + // Some entities may not be fully constructed, so don't call into their code until the level is running + if ( g_PhysicsHook.m_bPaused ) + return true; + + // solve it yourself here and return 0, or have the default implementation do it + if ( pEntity0 > pEntity1 ) + { + // swap sort + CBaseEntity *pTmp = pEntity0; + pEntity0 = pEntity1; + pEntity1 = pTmp; + IPhysicsObject *pTmpObj = pObj0; + pObj0 = pObj1; + pObj1 = pTmpObj; + } + if ( pEntity0 == pEntity1 ) + { + if ( pObj0->GetGameFlags() & FVPHYSICS_PART_OF_RAGDOLL ) + { + CGMsg( 2, CON_GROUP_PHYSICS, "Solving ragdoll self penetration! %s (%s) (%d v %d)\n", pObj0->GetName(), pEntity0->GetDebugName(), pObj0->GetGameIndex(), pObj1->GetGameIndex() ); + ragdoll_t *pRagdoll = Ragdoll_GetRagdoll( pEntity0 ); + pRagdoll->pGroup->SolvePenetration( pObj0, pObj1 ); + return false; + } + } + penetrateevent_t &event = FindOrAddPenetrateEvent( pEntity0, pEntity1 ); + float eventTime = gpGlobals->curtime - event.startTime; + + // NPC vs. physics object. Create a game DLL solver and remove this event + if ( (pEntity0->MyNPCPointer() && CanResolvePenetrationWithNPC(pEntity1, pObj1)) || + (pEntity1->MyNPCPointer() && CanResolvePenetrationWithNPC(pEntity0, pObj0)) ) + { + event.collisionState = COLLSTATE_TRYNPCSOLVER; + } + + if ( (IsDebris( pEntity0->GetCollisionGroup() ) && !pObj1->IsStatic()) || (IsDebris( pEntity1->GetCollisionGroup() ) && !pObj0->IsStatic()) ) + { + if ( eventTime > 0.5f ) + { + //Msg("Debris stuck in non-static!\n"); + event.collisionState = COLLSTATE_TRYENTITYSOLVER; + } + } +#if _DEBUG + if ( phys_dontprintint.GetBool() == false ) + { + const char *pName1 = STRING(pEntity0->GetModelName()); + const char *pName2 = STRING(pEntity1->GetModelName()); + if ( pEntity0 == pEntity1 ) + { + int index0 = physcollision->CollideIndex( pObj0->GetCollide() ); + int index1 = physcollision->CollideIndex( pObj1->GetCollide() ); + CGMsg( 1, CON_GROUP_PHYSICS, "***Inter-penetration on %s (%d & %d) (%.0f, %.0f)\n", pName1?pName1:"(null)", index0, index1, gpGlobals->curtime, eventTime ); + } + else + { + CGMsg( 1, CON_GROUP_PHYSICS, "***Inter-penetration between %s(%s) AND %s(%s) (%.0f, %.0f)\n", pName1?pName1:"(null)", pEntity0->GetDebugName(), pName2?pName2:"(null)", pEntity1->GetDebugName(), gpGlobals->curtime, eventTime ); + } + } +#endif + + if ( eventTime > 3 ) + { + // don't report penetrations on ragdolls with themselves, or outside of developer mode + if ( g_pDeveloper->GetInt() && pEntity0 != pEntity1 ) + { + ReportPenetration( pEntity0, phys_penetration_error_time.GetFloat() ); + ReportPenetration( pEntity1, phys_penetration_error_time.GetFloat() ); + } + event.startTime = gpGlobals->curtime; + // don't put players or game physics controlled objects to sleep + if ( !pEntity0->IsPlayer() && !pEntity1->IsPlayer() && !pObj0->GetShadowController() && !pObj1->GetShadowController() ) + { + // two objects have been stuck for more than 3 seconds, try disabling simulation + event.collisionState = COLLSTATE_TRYDISABLE; + return false; + } + } + + + return true; +} + + +void CCollisionEvent::FluidStartTouch( IPhysicsObject *pObject, IPhysicsFluidController *pFluid ) +{ + CallbackContext check(this); + if ( ( pObject == NULL ) || ( pFluid == NULL ) ) + return; + + CBaseEntity *pEntity = static_cast(pObject->GetGameData()); + if ( !pEntity ) + return; + + pEntity->AddEFlags( EFL_TOUCHING_FLUID ); + pEntity->OnEntityEvent( ENTITY_EVENT_WATER_TOUCH, (void*)pFluid->GetContents() ); + + float timeSinceLastCollision = DeltaTimeSinceLastFluid( pEntity ); + if ( timeSinceLastCollision < 0.5f ) + return; + + // UNDONE: Use this for splash logic instead? + // UNDONE: Use angular term too - push splashes in rotAxs cross normal direction? + Vector normal; + float dist; + pFluid->GetSurfacePlane( &normal, &dist ); + Vector vel; + AngularImpulse angVel; + pObject->GetVelocity( &vel, &angVel ); + Vector unitVel = vel; + VectorNormalize( unitVel ); + + // normal points out of the surface, we want the direction that points in + float dragScale = pFluid->GetDensity() * physenv->GetSimulationTimestep(); + normal = -normal; + float linearScale = 0.5f * DotProduct( unitVel, normal ) * pObject->CalculateLinearDrag( normal ) * dragScale; + linearScale = clamp( linearScale, 0.0f, 1.0f ); + vel *= -linearScale; + + // UNDONE: Figure out how much of the surface area has crossed the water surface and scale angScale by that + // For now assume 25% + Vector rotAxis = angVel; + VectorNormalize(rotAxis); + float angScale = 0.25f * pObject->CalculateAngularDrag( angVel ) * dragScale; + angScale = clamp( angScale, 0.0f, 1.0f ); + angVel *= -angScale; + + // compute the splash before we modify the velocity + PhysicsSplash( pFluid, pObject, pEntity ); + + // now damp out some motion toward the surface + pObject->AddVelocity( &vel, &angVel ); +} + +void CCollisionEvent::FluidEndTouch( IPhysicsObject *pObject, IPhysicsFluidController *pFluid ) +{ + CallbackContext check(this); + if ( ( pObject == NULL ) || ( pFluid == NULL ) ) + return; + + CBaseEntity *pEntity = static_cast(pObject->GetGameData()); + if ( !pEntity ) + return; + + float timeSinceLastCollision = DeltaTimeSinceLastFluid( pEntity ); + if ( timeSinceLastCollision >= 0.5f ) + { + PhysicsSplash( pFluid, pObject, pEntity ); + } + + pEntity->RemoveEFlags( EFL_TOUCHING_FLUID ); + pEntity->OnEntityEvent( ENTITY_EVENT_WATER_UNTOUCH, (void*)pFluid->GetContents() ); +} + +class CSkipKeys : public IVPhysicsKeyHandler +{ +public: + virtual void ParseKeyValue( void *pData, const char *pKey, const char *pValue ) {} + virtual void SetDefaults( void *pData ) {} +}; + +void PhysSolidOverride( solid_t &solid, string_t overrideScript ) +{ + if ( overrideScript != NULL_STRING) + { + // parser destroys this data + bool collisions = solid.params.enableCollisions; + + char pTmpString[4096]; + + // write a header for a solid_t + Q_strncpy( pTmpString, "solid { ", sizeof(pTmpString) ); + + // suck out the comma delimited tokens and turn them into quoted key/values + char szToken[256]; + const char *pStr = nexttoken(szToken, STRING(overrideScript), ',', sizeof(szToken)); + while ( szToken[0] != 0 ) + { + Q_strncat( pTmpString, "\"", sizeof(pTmpString), COPY_ALL_CHARACTERS ); + Q_strncat( pTmpString, szToken, sizeof(pTmpString), COPY_ALL_CHARACTERS ); + Q_strncat( pTmpString, "\" ", sizeof(pTmpString), COPY_ALL_CHARACTERS ); + pStr = nexttoken(szToken, pStr, ',', sizeof(szToken)); + } + // terminate the script + Q_strncat( pTmpString, "}", sizeof(pTmpString), COPY_ALL_CHARACTERS ); + + // parse that sucker + IVPhysicsKeyParser *pParse = physcollision->VPhysicsKeyParserCreate( pTmpString ); + CSkipKeys tmp; + pParse->ParseSolid( &solid, &tmp ); + physcollision->VPhysicsKeyParserDestroy( pParse ); + + // parser destroys this data + solid.params.enableCollisions = collisions; + } +} + +void PhysSetMassCenterOverride( masscenteroverride_t &override ) +{ + if ( override.entityName != NULL_STRING ) + { + g_PhysicsHook.m_massCenterOverrides.AddToTail( override ); + } +} + +// NOTE: This will remove the entry from the list as well +int PhysGetMassCenterOverrideIndex( string_t name ) +{ + if ( name != NULL_STRING && g_PhysicsHook.m_massCenterOverrides.Count() ) + { + for ( int i = 0; i < g_PhysicsHook.m_massCenterOverrides.Count(); i++ ) + { + if ( g_PhysicsHook.m_massCenterOverrides[i].entityName == name ) + { + return i; + } + } + } + return -1; +} + +void PhysGetMassCenterOverride( CBaseEntity *pEntity, vcollide_t *pCollide, solid_t &solidOut ) +{ + int index = PhysGetMassCenterOverrideIndex( pEntity->GetEntityName() ); + + if ( index >= 0 ) + { + masscenteroverride_t &override = g_PhysicsHook.m_massCenterOverrides[index]; + Vector massCenterWS = override.center; + switch ( override.alignType ) + { + case masscenteroverride_t::ALIGN_POINT: + VectorITransform( massCenterWS, pEntity->EntityToWorldTransform(), solidOut.massCenterOverride ); + break; + case masscenteroverride_t::ALIGN_AXIS: + { + Vector massCenterLocal, defaultMassCenterWS; + physcollision->CollideGetMassCenter( pCollide->solids[solidOut.index], &massCenterLocal ); + VectorTransform( massCenterLocal, pEntity->EntityToWorldTransform(), defaultMassCenterWS ); + massCenterWS += override.axis * + ( DotProduct(defaultMassCenterWS,override.axis) - DotProduct( override.axis, override.center ) ); + VectorITransform( massCenterWS, pEntity->EntityToWorldTransform(), solidOut.massCenterOverride ); + } + break; + } + g_PhysicsHook.m_massCenterOverrides.FastRemove( index ); + + if ( solidOut.massCenterOverride.Length() > DIST_EPSILON ) + { + solidOut.params.massCenterOverride = &solidOut.massCenterOverride; + } + } +} + +float PhysGetEntityMass( CBaseEntity *pEntity ) +{ + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + float otherMass = 0; + for ( int i = 0; i < physCount; i++ ) + { + otherMass += pList[i]->GetMass(); + } + + return otherMass; +} + + +typedef void (*EntityCallbackFunction) ( CBaseEntity *pEntity ); + +void IterateActivePhysicsEntities( EntityCallbackFunction func ) +{ + int activeCount = physenv->GetActiveObjectCount(); + IPhysicsObject **pActiveList = NULL; + if ( activeCount ) + { + pActiveList = (IPhysicsObject **)stackalloc( sizeof(IPhysicsObject *)*activeCount ); + physenv->GetActiveObjects( pActiveList ); + for ( int i = 0; i < activeCount; i++ ) + { + CBaseEntity *pEntity = reinterpret_cast(pActiveList[i]->GetGameData()); + if ( pEntity ) + { + func( pEntity ); + } + } + } +} + + +static void CallbackHighlight( CBaseEntity *pEntity ) +{ + pEntity->m_debugOverlays |= OVERLAY_ABSBOX_BIT | OVERLAY_PIVOT_BIT; +} + +static void CallbackReport( CBaseEntity *pEntity ) +{ + const char *pName = STRING(pEntity->GetEntityName()); + if ( !Q_strlen(pName) ) + { + pName = STRING(pEntity->GetModelName()); + } + Msg( "%s - %s\n", pEntity->GetClassname(), pName ); +} + +CON_COMMAND(physics_highlight_active, "Turns on the absbox for all active physics objects") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + IterateActivePhysicsEntities( CallbackHighlight ); +} + +CON_COMMAND(physics_report_active, "Lists all active physics objects") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + IterateActivePhysicsEntities( CallbackReport ); +} + +CON_COMMAND_F(surfaceprop, "Reports the surface properties at the cursor", FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + UTIL_TraceLine(pPlayer->EyePosition(), pPlayer->EyePosition() + forward * MAX_COORD_RANGE, + MASK_SHOT_HULL|CONTENTS_GRATE|CONTENTS_DEBRIS, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.DidHit() ) + { + const model_t *pModel = modelinfo->GetModel( tr.m_pEnt->GetModelIndex() ); + const char *pModelName = STRING(tr.m_pEnt->GetModelName()); + if ( tr.DidHitWorld() && tr.hitbox > 0 ) + { + ICollideable *pCollide = staticpropmgr->GetStaticPropByIndex( tr.hitbox-1 ); + pModel = pCollide->GetCollisionModel(); + pModelName = modelinfo->GetModelName( pModel ); + } + CFmtStr modelStuff; + if ( pModel ) + { + modelStuff.sprintf("%s.%s ", modelinfo->IsTranslucent( pModel ) ? "Translucent" : "Opaque", + modelinfo->IsTranslucentTwoPass( pModel ) ? " Two-pass." : "" ); + } + + // Calculate distance to surface that was hit + Vector vecVelocity = tr.startpos - tr.endpos; + int length = vecVelocity.Length(); + + CGMsg( 0, CON_GROUP_PHYSICS, "Hit surface \"%s\" (entity %s, model \"%s\" %s), texture \"%s\"\n", physprops->GetPropName( tr.surface.surfaceProps ), tr.m_pEnt->GetClassname(), pModelName, modelStuff.Access(), tr.surface.name ); + CGMsg( 0, CON_GROUP_PHYSICS, "Distance to surface: %d\n", length ); + } +} + +static void OutputVPhysicsDebugInfo( CBaseEntity *pEntity ) +{ + if ( pEntity ) + { + CGMsg( 0, CON_GROUP_PHYSICS, "Entity %s (%s) %s Collision Group %d\n", pEntity->GetClassname(), pEntity->GetDebugName(), pEntity->IsNavIgnored() ? "NAV IGNORE" : "", pEntity->GetCollisionGroup() ); + CUtlVector list; + g_Collisions.GetListOfPenetratingEntities( pEntity, list ); + for ( int i = 0; i < list.Count(); i++ ) + { + CGMsg( 0, CON_GROUP_PHYSICS, " penetration with entity %s (%s)\n", list[i]->GetDebugName(), STRING( list[i]->GetModelName() ) ); + } + + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + if ( physCount ) + { + if ( physCount > 1 ) + { + for ( int i = 0; i < physCount; i++ ) + { + CGMsg( 0, CON_GROUP_PHYSICS, "Object %d (of %d) =========================\n", i + 1, physCount ); + pList[i]->OutputDebugInfo(); + } + } + else + { + pList[0]->OutputDebugInfo(); + } + } + } +} + +class CConstraintFloodEntry +{ +public: + CConstraintFloodEntry() : isMarked(false), isConstraint(false) {} + + CUtlVector linkList; + bool isMarked; + bool isConstraint; +}; + +class CConstraintFloodList +{ +public: + CConstraintFloodList() + { + SetDefLessFunc( m_list ); + m_list.EnsureCapacity(64); + m_entryList.EnsureCapacity(64); + } + + bool IsWorldEntity( CBaseEntity *pEnt ) + { + if ( pEnt->edict() ) + return pEnt->IsWorld(); + return false; + } + + void AddLink( CBaseEntity *pEntity, CBaseEntity *pLink, bool bIsConstraint ) + { + if ( !pEntity || !pLink || IsWorldEntity(pEntity) || IsWorldEntity(pLink) ) + return; + int listIndex = m_list.Find(pEntity); + if ( listIndex == m_list.InvalidIndex() ) + { + int entryIndex = m_entryList.AddToTail(); + m_entryList[entryIndex].isConstraint = bIsConstraint; + listIndex = m_list.Insert( pEntity, entryIndex ); + } + int entryIndex = m_list.Element(listIndex); + CConstraintFloodEntry &entry = m_entryList.Element(entryIndex); + Assert( entry.isConstraint == bIsConstraint ); + if ( entry.linkList.Find(pLink) < 0 ) + { + entry.linkList.AddToTail( pLink ); + } + } + + void BuildGraphFromEntity( CBaseEntity *pEntity, CUtlVector &constraintList ) + { + int listIndex = m_list.Find(pEntity); + if ( listIndex != m_list.InvalidIndex() ) + { + int entryIndex = m_list.Element(listIndex); + CConstraintFloodEntry &entry = m_entryList.Element(entryIndex); + if ( !entry.isMarked ) + { + if ( entry.isConstraint ) + { + Assert( constraintList.Find(pEntity) < 0); + constraintList.AddToTail( pEntity ); + } + entry.isMarked = true; + for ( int i = 0; i < entry.linkList.Count(); i++ ) + { + // now recursively traverse the graph from here + BuildGraphFromEntity( entry.linkList[i], constraintList ); + } + } + } + } + CUtlMap m_list; + CUtlVector m_entryList; +}; + +// traverses the graph of attachments (currently supports springs & constraints) starting at an entity +// Then turns on debug info for each link in the graph (springs/constraints are links) +static void DebugConstraints( CBaseEntity *pEntity ) +{ + extern bool GetSpringAttachments( CBaseEntity *pEntity, CBaseEntity *pAttach[2], IPhysicsObject *pAttachVPhysics[2] ); + extern bool GetConstraintAttachments( CBaseEntity *pEntity, CBaseEntity *pAttach[2], IPhysicsObject *pAttachVPhysics[2] ); + extern void DebugConstraint(CBaseEntity *pEntity); + + if ( !pEntity ) + return; + + CBaseEntity *pAttach[2]; + IPhysicsObject *pAttachVPhysics[2]; + CConstraintFloodList list; + + for ( CBaseEntity *pList = gEntList.FirstEnt(); pList != NULL; pList = gEntList.NextEnt(pList) ) + { + if ( GetConstraintAttachments(pList, pAttach, pAttachVPhysics) || GetSpringAttachments(pList, pAttach, pAttachVPhysics) ) + { + list.AddLink( pList, pAttach[0], true ); + list.AddLink( pList, pAttach[1], true ); + list.AddLink( pAttach[0], pList, false ); + list.AddLink( pAttach[1], pList, false ); + } + } + + CUtlVector constraints; + list.BuildGraphFromEntity( pEntity, constraints ); + for ( int i = 0; i < constraints.Count(); i++ ) + { + if ( !GetConstraintAttachments(constraints[i], pAttach, pAttachVPhysics) ) + { + GetSpringAttachments(constraints[i], pAttach, pAttachVPhysics); + } + const char *pName0 = "world"; + const char *pName1 = "world"; + const char *pModel0 = ""; + const char *pModel1 = ""; + int index0 = 0; + int index1 = 0; + if ( pAttach[0] ) + { + pName0 = pAttach[0]->GetClassname(); + pModel0 = STRING(pAttach[0]->GetModelName()); + index0 = pAttachVPhysics[0]->GetGameIndex(); + } + if ( pAttach[1] ) + { + pName1 = pAttach[1]->GetClassname(); + pModel1 = STRING(pAttach[1]->GetModelName()); + index1 = pAttachVPhysics[1]->GetGameIndex(); + } + CGMsg( 0, CON_GROUP_PHYSICS, "**********************\n%s connects %s(%s:%d) to %s(%s:%d)\n", constraints[i]->GetClassname(), pName0, pModel0, index0, pName1, pModel1, index1 ); + DebugConstraint(constraints[i]); + constraints[i]->m_debugOverlays |= OVERLAY_BBOX_BIT | OVERLAY_TEXT_BIT; + } +} + +static void MarkVPhysicsDebug( CBaseEntity *pEntity ) +{ + if ( pEntity ) + { + IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); + if ( pPhysics ) + { + unsigned short callbacks = pPhysics->GetCallbackFlags(); + callbacks ^= CALLBACK_MARKED_FOR_TEST; + pPhysics->SetCallbackFlags( callbacks ); + } + } +} + +void PhysicsCommand( const CCommand &args, void (*func)( CBaseEntity *pEntity ) ) +{ + if ( args.ArgC() < 2 ) + { + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + UTIL_TraceLine(pPlayer->EyePosition(), pPlayer->EyePosition() + forward * MAX_COORD_RANGE, + MASK_SHOT_HULL|CONTENTS_GRATE|CONTENTS_DEBRIS, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.DidHit() ) + { + func( tr.m_pEnt ); + } + } + else + { + CBaseEntity *pEnt = NULL; + while ( ( pEnt = gEntList.FindEntityGeneric( pEnt, args[1] ) ) != NULL ) + { + func( pEnt ); + } + } +} + +CON_COMMAND(physics_constraints, "Highlights constraint system graph for an entity") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + PhysicsCommand( args, DebugConstraints ); +} + +CON_COMMAND(physics_debug_entity, "Dumps debug info for an entity") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + PhysicsCommand( args, OutputVPhysicsDebugInfo ); +} + +CON_COMMAND(physics_select, "Dumps debug info for an entity") +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + PhysicsCommand( args, MarkVPhysicsDebug ); +} + +CON_COMMAND( physics_budget, "Times the cost of each active object" ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + int activeCount = physenv->GetActiveObjectCount(); + + IPhysicsObject **pActiveList = NULL; + CUtlVector ents; + if ( activeCount ) + { + int i; + + pActiveList = (IPhysicsObject **)stackalloc( sizeof(IPhysicsObject *)*activeCount ); + physenv->GetActiveObjects( pActiveList ); + for ( i = 0; i < activeCount; i++ ) + { + CBaseEntity *pEntity = reinterpret_cast(pActiveList[i]->GetGameData()); + if ( pEntity ) + { + int index = -1; + for ( int j = 0; j < ents.Count(); j++ ) + { + if ( pEntity == ents[j] ) + { + index = j; + break; + } + } + if ( index >= 0 ) + continue; + + ents.AddToTail( pEntity ); + } + } + stackfree( pActiveList ); + + if ( !ents.Count() ) + return; + + CUtlVector times; + float totalTime = 0.f; + g_Collisions.BufferTouchEvents( true ); + float full = engine->Time(); + physenv->Simulate( DEFAULT_TICK_INTERVAL ); + full = engine->Time() - full; + float lastTime = full; + + times.SetSize( ents.Count() ); + + + // NOTE: This is just a heuristic. Attempt to estimate cost by putting each object to sleep in turn. + // note that simulation may wake the objects again and some costs scale with sets of objects/constraints/etc + // so these are only generally useful for broad questions, not real metrics! + for ( i = 0; i < ents.Count(); i++ ) + { + for ( int j = 0; j < i; j++ ) + { + PhysForceEntityToSleep( ents[j], ents[j]->VPhysicsGetObject() ); + } + float start = engine->Time(); + physenv->Simulate( DEFAULT_TICK_INTERVAL ); + float end = engine->Time(); + + float elapsed = end - start; + float avgTime = lastTime - elapsed; + times[i] = clamp( avgTime, 0.00001f, 1.0f ); + totalTime += times[i]; + lastTime = elapsed; + } + + totalTime = MAX( totalTime, 0.001 ); + for ( i = 0; i < ents.Count(); i++ ) + { + float fraction = times[i] / totalTime; + CGMsg( 0, CON_GROUP_PHYSICS, "%s (%s): %.3fms (%.3f%%) @ %s\n", ents[i]->GetClassname(), ents[i]->GetDebugName(), fraction * totalTime * 1000.0f, fraction * 100.0f, VecToString( ents[i]->GetAbsOrigin() ) ); + } + g_Collisions.BufferTouchEvents( false ); + } + +} + + +#ifdef PORTAL +ConVar sv_fullsyncclones("sv_fullsyncclones", "1", FCVAR_CHEAT ); +void PortalPhysFrame( float deltaTime ) //small wrapper for PhysFrame that simulates all environments at once +{ + CPortalSimulator::PrePhysFrame(); + + if( sv_fullsyncclones.GetBool() ) + CPhysicsShadowClone::FullSyncAllClones(); + + g_Collisions.BufferTouchEvents( true ); + + PhysFrame( deltaTime ); + + g_Collisions.PortalPostSimulationFrame(); + + g_Collisions.BufferTouchEvents( false ); + g_Collisions.FrameUpdate(); + + CPortalSimulator::PostPhysFrame(); +} +#endif + +// Advance physics by time (in seconds) +void PhysFrame( float deltaTime ) +{ + static int lastObjectCount = 0; + entitem_t *pItem; + + if ( !g_PhysicsHook.ShouldSimulate() ) + return; + + // Trap interrupts and clock changes + if ( deltaTime > 1.0f || deltaTime < 0.0f ) + { + deltaTime = 0; + CGMsg( 0, CON_GROUP_PHYSICS, "Reset physics clock\n" ); + } + else if ( deltaTime > 0.1f ) // limit incoming time to 100ms + { + deltaTime = 0.1f; + } + float simRealTime = 0; + + deltaTime *= phys_timescale.GetFloat(); + // !!!HACKHACK -- hard limit scaled time to avoid spending too much time in here + // Limit to 100 ms + if ( deltaTime > 0.100f ) + deltaTime = 0.100f; + + bool bProfile = phys_speeds.GetBool(); + + if ( bProfile ) + { + simRealTime = engine->Time(); + } + +#ifdef _DEBUG + physenv->DebugCheckContacts(); +#endif + +#ifndef PORTAL //instead of wrapping 1 simulation with this, portal needs to wrap 3 + g_Collisions.BufferTouchEvents( true ); +#endif + + physenv->Simulate( deltaTime ); + + int activeCount = physenv->GetActiveObjectCount(); + IPhysicsObject **pActiveList = NULL; + if ( activeCount ) + { + pActiveList = (IPhysicsObject **)stackalloc( sizeof(IPhysicsObject *)*activeCount ); + physenv->GetActiveObjects( pActiveList ); + + for ( int i = 0; i < activeCount; i++ ) + { + CBaseEntity *pEntity = reinterpret_cast(pActiveList[i]->GetGameData()); + if ( pEntity ) + { + if ( pEntity->CollisionProp()->DoesVPhysicsInvalidateSurroundingBox() ) + { + pEntity->CollisionProp()->MarkSurroundingBoundsDirty(); + } + pEntity->VPhysicsUpdate( pActiveList[i] ); + } + } + stackfree( pActiveList ); + } + + for ( pItem = g_pShadowEntities->m_pItemList; pItem; pItem = pItem->pNext ) + { + CBaseEntity *pEntity = pItem->hEnt.Get(); + if ( !pEntity ) + { + CGMsg( 0, CON_GROUP_PHYSICS, "Dangling pointer to physics entity!!!\n" ); + continue; + } + + IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); + // apply updates + if ( pPhysics && !pPhysics->IsAsleep() ) + { + pEntity->VPhysicsShadowUpdate( pPhysics ); + } + } + + if ( bProfile ) + { + simRealTime = engine->Time() - simRealTime; + + if ( simRealTime < 0 ) + simRealTime = 0; + g_PhysAverageSimTime *= 0.8; + g_PhysAverageSimTime += (simRealTime * 0.2); + if ( lastObjectCount != 0 || activeCount != 0 ) + { + CGMsg( 0, CON_GROUP_PHYSICS, "Physics: %3d objects, %4.1fms / AVG: %4.1fms\n", activeCount, simRealTime * 1000, g_PhysAverageSimTime * 1000 ); + } + + lastObjectCount = activeCount; + } + +#ifndef PORTAL //instead of wrapping 1 simulation with this, portal needs to wrap 3 + g_Collisions.BufferTouchEvents( false ); + g_Collisions.FrameUpdate(); +#endif +} + + +void PhysAddShadow( CBaseEntity *pEntity ) +{ + g_pShadowEntities->AddEntity( pEntity ); +} + +void PhysRemoveShadow( CBaseEntity *pEntity ) +{ + g_pShadowEntities->DeleteEntity( pEntity ); +} + +bool PhysHasShadow( CBaseEntity *pEntity ) +{ + EHANDLE hTestEnt = pEntity; + entitem_t *pCurrent = g_pShadowEntities->m_pItemList; + while( pCurrent ) + { + if( pCurrent->hEnt == hTestEnt ) + { + return true; + } + pCurrent = pCurrent->pNext; + } + return false; +} + +void PhysEnableFloating( IPhysicsObject *pObject, bool bEnable ) +{ + if ( pObject != NULL ) + { + unsigned short flags = pObject->GetCallbackFlags(); + if ( bEnable ) + { + flags |= CALLBACK_DO_FLUID_SIMULATION; + } + else + { + flags &= ~CALLBACK_DO_FLUID_SIMULATION; + } + pObject->SetCallbackFlags( flags ); + } +} + + +//----------------------------------------------------------------------------- +// CollisionEvent system +//----------------------------------------------------------------------------- +// NOTE: PreCollision/PostCollision ALWAYS come in matched pairs!!! +void CCollisionEvent::PreCollision( vcollisionevent_t *pEvent ) +{ + CallbackContext check(this); + m_gameEvent.Init( pEvent ); + + // gather the pre-collision data that the game needs to track + for ( int i = 0; i < 2; i++ ) + { + IPhysicsObject *pObject = pEvent->pObjects[i]; + if ( pObject ) + { + if ( pObject->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + CBaseEntity *pOtherEntity = reinterpret_cast(pEvent->pObjects[!i]->GetGameData()); + if ( pOtherEntity && !pOtherEntity->IsPlayer() ) + { + Vector velocity; + AngularImpulse angVel; + // HACKHACK: If we totally clear this out, then Havok will think the objects + // are penetrating and generate forces to separate them + // so make it fairly small and have a tiny collision instead. + pObject->GetVelocity( &velocity, &angVel ); + float len = VectorNormalize(velocity); + len = MAX( len, 10 ); + velocity *= len; + len = VectorNormalize(angVel); + len = MAX( len, 1 ); + angVel *= len; + pObject->SetVelocity( &velocity, &angVel ); + } + } + pObject->GetVelocity( &m_gameEvent.preVelocity[i], &m_gameEvent.preAngularVelocity[i] ); + } + } +} + +void CCollisionEvent::PostCollision( vcollisionevent_t *pEvent ) +{ + CallbackContext check(this); + bool isShadow[2] = {false,false}; + int i; + + for ( i = 0; i < 2; i++ ) + { + IPhysicsObject *pObject = pEvent->pObjects[i]; + if ( pObject ) + { + CBaseEntity *pEntity = reinterpret_cast(pObject->GetGameData()); + if ( !pEntity ) + return; + + // UNDONE: This is here to trap crashes due to NULLing out the game data on delete + m_gameEvent.pEntities[i] = pEntity; + unsigned int flags = pObject->GetCallbackFlags(); + pObject->GetVelocity( &m_gameEvent.postVelocity[i], NULL ); + if ( flags & CALLBACK_SHADOW_COLLISION ) + { + isShadow[i] = true; + } + + // Shouldn't get impacts with triggers + Assert( !pObject->IsTrigger() ); + } + } + + // copy off the post-collision variable data + m_gameEvent.collisionSpeed = pEvent->collisionSpeed; + m_gameEvent.pInternalData = pEvent->pInternalData; + + // special case for hitting self, only make one non-shadow call + if ( m_gameEvent.pEntities[0] == m_gameEvent.pEntities[1] ) + { + if ( pEvent->isCollision && m_gameEvent.pEntities[0] ) + { + m_gameEvent.pEntities[0]->VPhysicsCollision( 0, &m_gameEvent ); + } + return; + } + + if ( isShadow[0] && isShadow[1] ) + { + pEvent->isCollision = false; + } + + for ( i = 0; i < 2; i++ ) + { + if ( pEvent->isCollision ) + { + m_gameEvent.pEntities[i]->VPhysicsCollision( i, &m_gameEvent ); + } + if ( pEvent->isShadowCollision && isShadow[i] ) + { + m_gameEvent.pEntities[i]->VPhysicsShadowCollision( i, &m_gameEvent ); + } + } +} + +void PhysForceEntityToSleep( CBaseEntity *pEntity, IPhysicsObject *pObject ) +{ + // UNDONE: Check to see if the object is touching the player first? + // Might get the player stuck? + if ( !pObject || !pObject->IsMoveable() ) + return; + + CGMsg( 2, CON_GROUP_PHYSICS, "Putting entity to sleep: %s\n", pEntity->GetClassname() ); + MEM_ALLOC_CREDIT(); + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < physCount; i++ ) + { + PhysForceClearVelocity( pList[i] ); + pList[i]->Sleep(); + } +} + +void CCollisionEvent::Friction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit, IPhysicsCollisionData *pData ) +{ + CallbackContext check(this); + //Get our friction information + Vector vecPos, vecVel; + pData->GetContactPoint( vecPos ); + pObject->GetVelocityAtPoint( vecPos, &vecVel ); + + CBaseEntity *pEntity = reinterpret_cast(pObject->GetGameData()); + + if ( pEntity ) + { + friction_t *pFriction = g_Collisions.FindFriction( pEntity ); + + if ( pFriction && pFriction->pObject) + { + // in MP mode play sound and effects once every 500 msecs, + // no ongoing updates, takes too much bandwidth + if ( (pFriction->flLastEffectTime + 0.5f) > gpGlobals->curtime) + { + pFriction->flLastUpdateTime = gpGlobals->curtime; + return; + } + } + + pEntity->VPhysicsFriction( pObject, energy, surfaceProps, surfacePropsHit ); + } + + PhysFrictionEffect( vecPos, vecVel, energy, surfaceProps, surfacePropsHit ); +} + + +friction_t *CCollisionEvent::FindFriction( CBaseEntity *pObject ) +{ + friction_t *pFree = NULL; + + for ( int i = 0; i < ARRAYSIZE(m_current); i++ ) + { + if ( !m_current[i].pObject && !pFree ) + pFree = &m_current[i]; + + if ( m_current[i].pObject == pObject ) + return &m_current[i]; + } + + return pFree; +} + +void CCollisionEvent::ShutdownFriction( friction_t &friction ) +{ +// Msg( "Scrape Stop %s \n", STRING(friction.pObject->m_iClassname) ); + CSoundEnvelopeController::GetController().SoundDestroy( friction.patch ); + friction.patch = NULL; + friction.pObject = NULL; +} + +void CCollisionEvent::UpdateRemoveObjects() +{ + Assert(!PhysIsInCallback()); + for ( int i = 0 ; i < m_removeObjects.Count(); i++ ) + { + UTIL_Remove(m_removeObjects[i]); + } + m_removeObjects.RemoveAll(); +} + +void CCollisionEvent::PostSimulationFrame() +{ + UpdateDamageEvents(); + g_PostSimulationQueue.CallQueued(); + UpdateRemoveObjects(); +} + +void CCollisionEvent::FlushQueuedOperations() +{ + int loopCount = 0; + while ( loopCount < 20 ) + { + int count = m_triggerEvents.Count() + m_touchEvents.Count() + m_damageEvents.Count() + m_removeObjects.Count() + g_PostSimulationQueue.Count(); + if ( !count ) + break; + // testing, if this assert fires it proves we've fixed the crash + // after that the assert + warning can safely be removed + Assert(0); + CGWarning( 0, CON_GROUP_PHYSICS, "Physics queue not empty, error!\n" ); + loopCount++; + UpdateTouchEvents(); + UpdateDamageEvents(); + g_PostSimulationQueue.CallQueued(); + UpdateRemoveObjects(); + } +} + +void CCollisionEvent::FrameUpdate( void ) +{ + UpdateFrictionSounds(); + UpdateTouchEvents(); + UpdatePenetrateEvents(); + UpdateFluidEvents(); + UpdateDamageEvents(); // if there was no PSI in physics, we'll still need to do some of these because collisions are solved in between PSIs + g_PostSimulationQueue.CallQueued(); + UpdateRemoveObjects(); + + // There are some queued operations that must complete each frame, iterate until these are done + FlushQueuedOperations(); +} + +// the delete list is getting flushed, clean up ours +void PhysOnCleanupDeleteList() +{ + g_Collisions.FlushQueuedOperations(); + if ( physenv ) + { + physenv->CleanupDeleteList(); + } +} + +void CCollisionEvent::UpdateFluidEvents( void ) +{ + for ( int i = m_fluidEvents.Count()-1; i >= 0; --i ) + { + if ( (gpGlobals->curtime - m_fluidEvents[i].impactTime) > FLUID_TIME_MAX ) + { + m_fluidEvents.FastRemove(i); + } + } +} + + +float CCollisionEvent::DeltaTimeSinceLastFluid( CBaseEntity *pEntity ) +{ + for ( int i = m_fluidEvents.Count()-1; i >= 0; --i ) + { + if ( m_fluidEvents[i].hEntity.Get() == pEntity ) + { + return gpGlobals->curtime - m_fluidEvents[i].impactTime; + } + } + + int index = m_fluidEvents.AddToTail(); + m_fluidEvents[index].hEntity = pEntity; + m_fluidEvents[index].impactTime = gpGlobals->curtime; + return FLUID_TIME_MAX; +} + +void CCollisionEvent::UpdateFrictionSounds( void ) +{ + for ( int i = 0; i < ARRAYSIZE(m_current); i++ ) + { + if ( m_current[i].patch ) + { + if ( m_current[i].flLastUpdateTime < (gpGlobals->curtime-0.1f) ) + { + // friction wasn't updated the last 100msec, assume fiction finished + ShutdownFriction( m_current[i] ); + } + } + } +} + + +void CCollisionEvent::DispatchStartTouch( CBaseEntity *pEntity0, CBaseEntity *pEntity1, const Vector &point, const Vector &normal ) +{ + trace_t trace; + memset( &trace, 0, sizeof(trace) ); + trace.endpos = point; + trace.plane.dist = DotProduct( point, normal ); + trace.plane.normal = normal; + + // NOTE: This sets up the touch list for both entities, no call to pEntity1 is needed + pEntity0->PhysicsMarkEntitiesAsTouchingEventDriven( pEntity1, trace ); +} + +void CCollisionEvent::DispatchEndTouch( CBaseEntity *pEntity0, CBaseEntity *pEntity1 ) +{ + // frees the event-driven touchlinks + pEntity0->PhysicsNotifyOtherOfUntouch( pEntity0, pEntity1 ); + pEntity1->PhysicsNotifyOtherOfUntouch( pEntity1, pEntity0 ); +} + +void CCollisionEvent::UpdateTouchEvents( void ) +{ + int i; + // Turn on buffering in case new touch events occur during processing + bool bOldTouchEvents = m_bBufferTouchEvents; + m_bBufferTouchEvents = true; + for ( i = 0; i < m_touchEvents.Count(); i++ ) + { + const touchevent_t &event = m_touchEvents[i]; + if ( event.touchType == TOUCH_START ) + { + DispatchStartTouch( event.pEntity0, event.pEntity1, event.endPoint, event.normal ); + } + else + { + // TOUCH_END + DispatchEndTouch( event.pEntity0, event.pEntity1 ); + } + } + m_touchEvents.RemoveAll(); + + for ( i = 0; i < m_triggerEvents.Count(); i++ ) + { + m_currentTriggerEvent = m_triggerEvents[i]; + if ( m_currentTriggerEvent.bStart ) + { + m_currentTriggerEvent.pTriggerEntity->StartTouch( m_currentTriggerEvent.pEntity ); + } + else + { + m_currentTriggerEvent.pTriggerEntity->EndTouch( m_currentTriggerEvent.pEntity ); + } + } + m_triggerEvents.RemoveAll(); + m_currentTriggerEvent.Clear(); + m_bBufferTouchEvents = bOldTouchEvents; +} + +void CCollisionEvent::UpdateDamageEvents( void ) +{ + for ( int i = 0; i < m_damageEvents.Count(); i++ ) + { + damageevent_t &event = m_damageEvents[i]; + + // Track changes in the entity's life state + int iEntBits = event.pEntity->IsAlive() ? 0x0001 : 0; + iEntBits |= event.pEntity->IsMarkedForDeletion() ? 0x0002 : 0; + iEntBits |= (event.pEntity->GetSolidFlags() & FSOLID_NOT_SOLID) ? 0x0004 : 0; +#if 0 + // Go ahead and compute the current static stress when hit by a large object (with a force high enough to do damage). + // That way you die from the impact rather than the stress of the object resting on you whenever possible. + // This makes the damage effects cleaner. + if ( event.pInflictorPhysics && event.pInflictorPhysics->GetMass() > VPHYSICS_LARGE_OBJECT_MASS ) + { + CBaseCombatCharacter *pCombat = event.pEntity->MyCombatCharacterPointer(); + if ( pCombat ) + { + vphysics_objectstress_t stressOut; + event.info.AddDamage( pCombat->CalculatePhysicsStressDamage( &stressOut, pCombat->VPhysicsGetObject() ) ); + } + } +#endif + + event.pEntity->TakeDamage( event.info ); + int iEntBits2 = event.pEntity->IsAlive() ? 0x0001 : 0; + iEntBits2 |= event.pEntity->IsMarkedForDeletion() ? 0x0002 : 0; + iEntBits2 |= (event.pEntity->GetSolidFlags() & FSOLID_NOT_SOLID) ? 0x0004 : 0; + + if ( event.bRestoreVelocity && iEntBits != iEntBits2 ) + { + // UNDONE: Use ratio of masses to blend in a little of the collision response? + // UNDONE: Damage for future events is already computed - it would be nice to + // go back and recompute it now that the values have + // been adjusted + RestoreDamageInflictorState( event.pInflictorPhysics ); + } + } + m_damageEvents.RemoveAll(); + m_damageInflictors.RemoveAll(); +} + +void CCollisionEvent::RestoreDamageInflictorState( int inflictorStateIndex, float velocityBlend ) +{ + inflictorstate_t &state = m_damageInflictors[inflictorStateIndex]; + if ( state.restored ) + return; + + // so we only restore this guy once + state.restored = true; + + if ( velocityBlend > 0 ) + { + Vector velocity; + AngularImpulse angVel; + state.pInflictorPhysics->GetVelocity( &velocity, &angVel ); + state.savedVelocity = state.savedVelocity*velocityBlend + velocity*(1-velocityBlend); + state.savedAngularVelocity = state.savedAngularVelocity*velocityBlend + angVel*(1-velocityBlend); + state.pInflictorPhysics->SetVelocity( &state.savedVelocity, &state.savedAngularVelocity ); + } + + if ( state.nextIndex >= 0 ) + { + RestoreDamageInflictorState( state.nextIndex, velocityBlend ); + } +} + +void CCollisionEvent::RestoreDamageInflictorState( IPhysicsObject *pInflictor ) +{ + if ( !pInflictor ) + return; + + int index = FindDamageInflictor( pInflictor ); + if ( index >= 0 ) + { + inflictorstate_t &state = m_damageInflictors[index]; + if ( !state.restored ) + { + float velocityBlend = 1.0; + float inflictorMass = state.pInflictorPhysics->GetMass(); + if ( inflictorMass < VPHYSICS_LARGE_OBJECT_MASS && !(state.pInflictorPhysics->GetGameFlags() & FVPHYSICS_DMG_SLICE) ) + { + float otherMass = state.otherMassMax > 0 ? state.otherMassMax : 1; + float massRatio = inflictorMass / otherMass; + massRatio = clamp( massRatio, 0.1f, 10.0f ); + if ( massRatio < 1 ) + { + velocityBlend = RemapVal( massRatio, 0.1, 1, 0, 0.5 ); + } + else + { + velocityBlend = RemapVal( massRatio, 1.0, 10, 0.5, 1 ); + } + } + RestoreDamageInflictorState( index, velocityBlend ); + } + } +} + +bool CCollisionEvent::GetInflictorVelocity( IPhysicsObject *pInflictor, Vector &velocity, AngularImpulse &angVelocity ) +{ + int index = FindDamageInflictor( pInflictor ); + if ( index >= 0 ) + { + inflictorstate_t &state = m_damageInflictors[index]; + velocity = state.savedVelocity; + angVelocity = state.savedAngularVelocity; + return true; + } + + return false; +} + +bool PhysGetDamageInflictorVelocityStartOfFrame( IPhysicsObject *pInflictor, Vector &velocity, AngularImpulse &angVelocity ) +{ + return g_Collisions.GetInflictorVelocity( pInflictor, velocity, angVelocity ); +} + +void CCollisionEvent::AddTouchEvent( CBaseEntity *pEntity0, CBaseEntity *pEntity1, int touchType, const Vector &point, const Vector &normal ) +{ + if ( !pEntity0 || !pEntity1 ) + return; + + int index = m_touchEvents.AddToTail(); + touchevent_t &event = m_touchEvents[index]; + event.pEntity0 = pEntity0; + event.pEntity1 = pEntity1; + event.touchType = touchType; + event.endPoint = point; + event.normal = normal; +} + +void CCollisionEvent::AddDamageEvent( CBaseEntity *pEntity, const CTakeDamageInfo &info, IPhysicsObject *pInflictorPhysics, bool bRestoreVelocity, const Vector &savedVel, const AngularImpulse &savedAngVel ) +{ + if ( pEntity->IsMarkedForDeletion() ) + return; + + int iTimeBasedDamage = g_pGameRules->Damage_GetTimeBased(); + if ( !( info.GetDamageType() & (DMG_BURN | DMG_DROWN | iTimeBasedDamage | DMG_PREVENT_PHYSICS_FORCE) ) ) + { + Assert( info.GetDamageForce() != vec3_origin && info.GetDamagePosition() != vec3_origin ); + } + + int index = m_damageEvents.AddToTail(); + damageevent_t &event = m_damageEvents[index]; + event.pEntity = pEntity; + event.info = info; + event.pInflictorPhysics = pInflictorPhysics; + event.bRestoreVelocity = bRestoreVelocity; + if ( !pInflictorPhysics || !pInflictorPhysics->IsMoveable() ) + { + event.bRestoreVelocity = false; + } + + if ( event.bRestoreVelocity ) + { + float otherMass = pEntity->VPhysicsGetObject()->GetMass(); + int inflictorIndex = FindDamageInflictor(pInflictorPhysics); + if ( inflictorIndex >= 0 ) + { + // if this is a bigger mass, save that info + inflictorstate_t &state = m_damageInflictors[inflictorIndex]; + if ( otherMass > state.otherMassMax ) + { + state.otherMassMax = otherMass; + } + + } + else + { + AddDamageInflictor( pInflictorPhysics, otherMass, savedVel, savedAngVel, true ); + } + } + +} + +//----------------------------------------------------------------------------- +// Impulse events +//----------------------------------------------------------------------------- +static void PostSimulation_ImpulseEvent( IPhysicsObject *pObject, const Vector ¢erForce, const AngularImpulse ¢erTorque ) +{ + pObject->ApplyForceCenter( centerForce ); + pObject->ApplyTorqueCenter( centerTorque ); +} + +void PostSimulation_SetVelocityEvent( IPhysicsObject *pPhysicsObject, const Vector &vecVelocity ) +{ + pPhysicsObject->SetVelocity( &vecVelocity, NULL ); +} + +void CCollisionEvent::AddRemoveObject(IServerNetworkable *pRemove) +{ + if ( pRemove && m_removeObjects.Find(pRemove) == -1 ) + { + m_removeObjects.AddToTail(pRemove); + } +} +int CCollisionEvent::FindDamageInflictor( IPhysicsObject *pInflictorPhysics ) +{ + // UNDONE: Linear search? Probably ok with a low count here + for ( int i = m_damageInflictors.Count()-1; i >= 0; --i ) + { + const inflictorstate_t &state = m_damageInflictors[i]; + if ( state.pInflictorPhysics == pInflictorPhysics ) + return i; + } + + return -1; +} + + +int CCollisionEvent::AddDamageInflictor( IPhysicsObject *pInflictorPhysics, float otherMass, const Vector &savedVel, const AngularImpulse &savedAngVel, bool addList ) +{ + // NOTE: Save off the state of the object before collision + // restore if the impact is a kill + // UNDONE: Should we absorb some energy here? + // NOTE: we can't save a delta because there could be subsequent post-fatal collisions + + int addIndex = m_damageInflictors.AddToTail(); + { + inflictorstate_t &state = m_damageInflictors[addIndex]; + state.pInflictorPhysics = pInflictorPhysics; + state.savedVelocity = savedVel; + state.savedAngularVelocity = savedAngVel; + state.otherMassMax = otherMass; + state.restored = false; + state.nextIndex = -1; + } + + if ( addList ) + { + CBaseEntity *pEntity = static_cast(pInflictorPhysics->GetGameData()); + if ( pEntity ) + { + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int physCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + if ( physCount > 1 ) + { + int currentIndex = addIndex; + for ( int i = 0; i < physCount; i++ ) + { + if ( pList[i] != pInflictorPhysics ) + { + Vector vel; + AngularImpulse angVel; + pList[i]->GetVelocity( &vel, &angVel ); + int next = AddDamageInflictor( pList[i], otherMass, vel, angVel, false ); + m_damageInflictors[currentIndex].nextIndex = next; + currentIndex = next; + } + } + } + } + } + return addIndex; +} + + +void CCollisionEvent::LevelShutdown( void ) +{ + for ( int i = 0; i < ARRAYSIZE(m_current); i++ ) + { + if ( m_current[i].patch ) + { + ShutdownFriction( m_current[i] ); + } + } +} + + +void CCollisionEvent::StartTouch( IPhysicsObject *pObject1, IPhysicsObject *pObject2, IPhysicsCollisionData *pTouchData ) +{ + CallbackContext check(this); + CBaseEntity *pEntity1 = static_cast(pObject1->GetGameData()); + CBaseEntity *pEntity2 = static_cast(pObject2->GetGameData()); + + if ( !pEntity1 || !pEntity2 ) + return; + + Vector endPoint, normal; + pTouchData->GetContactPoint( endPoint ); + pTouchData->GetSurfaceNormal( normal ); + if ( !m_bBufferTouchEvents ) + { + DispatchStartTouch( pEntity1, pEntity2, endPoint, normal ); + } + else + { + AddTouchEvent( pEntity1, pEntity2, TOUCH_START, endPoint, normal ); + } +} + +static int CountPhysicsObjectEntityContacts( IPhysicsObject *pObject, CBaseEntity *pEntity ) +{ + IPhysicsFrictionSnapshot *pSnapshot = pObject->CreateFrictionSnapshot(); + int count = 0; + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + CBaseEntity *pOtherEntity = static_cast(pOther->GetGameData()); + if ( pOtherEntity == pEntity ) + count++; + pSnapshot->NextFrictionData(); + } + pObject->DestroyFrictionSnapshot( pSnapshot ); + return count; +} + +void CCollisionEvent::EndTouch( IPhysicsObject *pObject1, IPhysicsObject *pObject2, IPhysicsCollisionData *pTouchData ) +{ + CallbackContext check(this); + CBaseEntity *pEntity1 = static_cast(pObject1->GetGameData()); + CBaseEntity *pEntity2 = static_cast(pObject2->GetGameData()); + + if ( !pEntity1 || !pEntity2 ) + return; + + // contact point deleted, but entities are still touching? + IPhysicsObject *list[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pEntity1->VPhysicsGetObjectList( list, ARRAYSIZE(list) ); + + int contactCount = 0; + for ( int i = 0; i < count; i++ ) + { + contactCount += CountPhysicsObjectEntityContacts( list[i], pEntity2 ); + + // still touching + if ( contactCount > 1 ) + return; + } + + // should have exactly one contact point (the one getting deleted here) + //Assert( contactCount == 1 ); + + Vector endPoint, normal; + pTouchData->GetContactPoint( endPoint ); + pTouchData->GetSurfaceNormal( normal ); + + if ( !m_bBufferTouchEvents ) + { + DispatchEndTouch( pEntity1, pEntity2 ); + } + else + { + AddTouchEvent( pEntity1, pEntity2, TOUCH_END, vec3_origin, vec3_origin ); + } +} + +// UNDONE: This is functional, but minimally. +void CCollisionEvent::ObjectEnterTrigger( IPhysicsObject *pTrigger, IPhysicsObject *pObject ) +{ + CBaseEntity *pTriggerEntity = static_cast(pTrigger->GetGameData()); + CBaseEntity *pEntity = static_cast(pObject->GetGameData()); + if ( pTriggerEntity && pEntity ) + { + // UNDONE: Don't buffer these until we can solve generating touches at object creation time + if ( 0 && m_bBufferTouchEvents ) + { + int index = m_triggerEvents.AddToTail(); + m_triggerEvents[index].Init( pTriggerEntity, pTrigger, pEntity, pObject, true ); + } + else + { + CallbackContext check(this); + m_currentTriggerEvent.Init( pTriggerEntity, pTrigger, pEntity, pObject, true ); + pTriggerEntity->StartTouch( pEntity ); + m_currentTriggerEvent.Clear(); + } + } +} + +void CCollisionEvent::ObjectLeaveTrigger( IPhysicsObject *pTrigger, IPhysicsObject *pObject ) +{ + CBaseEntity *pTriggerEntity = static_cast(pTrigger->GetGameData()); + CBaseEntity *pEntity = static_cast(pObject->GetGameData()); + if ( pTriggerEntity && pEntity ) + { + // UNDONE: Don't buffer these until we can solve generating touches at object creation time + if ( 0 && m_bBufferTouchEvents ) + { + int index = m_triggerEvents.AddToTail(); + m_triggerEvents[index].Init( pTriggerEntity, pTrigger, pEntity, pObject, false ); + } + else + { + CallbackContext check(this); + m_currentTriggerEvent.Init( pTriggerEntity, pTrigger, pEntity, pObject, false ); + pTriggerEntity->EndTouch( pEntity ); + m_currentTriggerEvent.Clear(); + } + } +} + +bool CCollisionEvent::GetTriggerEvent( triggerevent_t *pEvent, CBaseEntity *pTriggerEntity ) +{ + if ( pEvent && pTriggerEntity == m_currentTriggerEvent.pTriggerEntity ) + { + *pEvent = m_currentTriggerEvent; + return true; + } + + return false; +} + +void PhysGetListOfPenetratingEntities( CBaseEntity *pSearch, CUtlVector &list ) +{ + g_Collisions.GetListOfPenetratingEntities( pSearch, list ); +} + +bool PhysGetTriggerEvent( triggerevent_t *pEvent, CBaseEntity *pTriggerEntity ) +{ + return g_Collisions.GetTriggerEvent( pEvent, pTriggerEntity ); +} +//----------------------------------------------------------------------------- + + +//----------------------------------------------------------------------------- +// External interface to collision sounds +//----------------------------------------------------------------------------- + +void PhysicsImpactSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, int channel, int surfaceProps, int surfacePropsHit, float volume, float impactSpeed ) +{ + physicssound::AddImpactSound( g_PhysicsHook.m_impactSounds, pEntity, pEntity->entindex(), channel, pPhysObject, surfaceProps, surfacePropsHit, volume, impactSpeed ); +} + +void PhysCollisionSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, int channel, int surfaceProps, int surfacePropsHit, float deltaTime, float speed ) +{ + if ( deltaTime < 0.05f || speed < 70.0f ) + return; + + float volume = speed * speed * (1.0f/(320.0f*320.0f)); // max volume at 320 in/s + if ( volume > 1.0f ) + volume = 1.0f; + + PhysicsImpactSound( pEntity, pPhysObject, channel, surfaceProps, surfacePropsHit, volume, speed ); +} + +void PhysBreakSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, Vector vecOrigin ) +{ + if ( !pPhysObject) + return; + + physicssound::AddBreakSound( g_PhysicsHook.m_breakSounds, vecOrigin, pPhysObject->GetMaterialIndex() ); +} + +ConVar collision_shake_amp("collision_shake_amp", "0.2"); +ConVar collision_shake_freq("collision_shake_freq", "0.5"); +ConVar collision_shake_time("collision_shake_time", "0.5"); + +void PhysCollisionScreenShake( gamevcollisionevent_t *pEvent, int index ) +{ + int otherIndex = !index; + float mass = pEvent->pObjects[index]->GetMass(); + if ( mass >= VPHYSICS_LARGE_OBJECT_MASS && pEvent->pObjects[otherIndex]->IsStatic() && + !(pEvent->pObjects[index]->GetGameFlags() & FVPHYSICS_PENETRATING) ) + { + mass = clamp(mass, VPHYSICS_LARGE_OBJECT_MASS, 2000.f); + if ( pEvent->collisionSpeed > 30 && pEvent->deltaCollisionTime > 0.25f ) + { + Vector vecPos; + pEvent->pInternalData->GetContactPoint( vecPos ); + float impulse = pEvent->collisionSpeed * mass; + float amplitude = impulse * (collision_shake_amp.GetFloat() / (30.0f * VPHYSICS_LARGE_OBJECT_MASS)); + UTIL_ScreenShake( vecPos, amplitude, collision_shake_freq.GetFloat(), collision_shake_time.GetFloat(), amplitude * 60, SHAKE_START ); + } + } +} + +#if HL2_EPISODIC +// Uses DispatchParticleEffect because, so far as I know, that is the new means of kicking +// off flinders for this kind of collision. Should this be in g_pEffects instead? +void PhysCollisionWarpEffect( gamevcollisionevent_t *pEvent, surfacedata_t *phit ) +{ + Vector vecPos; + QAngle vecAngles; + + pEvent->pInternalData->GetContactPoint( vecPos ); + { + Vector vecNormal; + pEvent->pInternalData->GetSurfaceNormal(vecNormal); + VectorAngles( vecNormal, vecAngles ); + } + + DispatchParticleEffect( "warp_shield_impact", vecPos, vecAngles ); +} +#endif + +void PhysCollisionDust( gamevcollisionevent_t *pEvent, surfacedata_t *phit ) +{ + + switch ( phit->game.material ) + { + case CHAR_TEX_SAND: + case CHAR_TEX_DIRT: + + if ( pEvent->collisionSpeed < 200.0f ) + return; + + break; + + case CHAR_TEX_CONCRETE: + + if ( pEvent->collisionSpeed < 340.0f ) + return; + + break; + +#if HL2_EPISODIC + // this is probably redundant because BaseEntity::VHandleCollision should have already dispatched us elsewhere + case CHAR_TEX_WARPSHIELD: + PhysCollisionWarpEffect(pEvent,phit); + return; + + break; +#endif + + default: + return; + } + + //Kick up dust + Vector vecPos, vecVel; + + pEvent->pInternalData->GetContactPoint( vecPos ); + + vecVel.Random( -1.0f, 1.0f ); + vecVel.z = random->RandomFloat( 0.3f, 1.0f ); + VectorNormalize( vecVel ); + g_pEffects->Dust( vecPos, vecVel, 8.0f, pEvent->collisionSpeed ); +} + +void PhysFrictionSound( CBaseEntity *pEntity, IPhysicsObject *pObject, const char *pSoundName, HSOUNDSCRIPTHANDLE& handle, float flVolume ) +{ + if ( !pEntity ) + return; + + // cut out the quiet sounds + // UNDONE: Separate threshold for starting a sound vs. continuing? + flVolume = clamp( flVolume, 0.0f, 1.0f ); + if ( flVolume > (1.0f/128.0f) ) + { + friction_t *pFriction = g_Collisions.FindFriction( pEntity ); + if ( !pFriction ) + return; + + CSoundParameters params; + if ( !CBaseEntity::GetParametersForSound( pSoundName, handle, params, NULL ) ) + return; + + if ( !pFriction->pObject ) + { + // don't create really quiet scrapes + if ( params.volume * flVolume <= 0.1f ) + return; + + pFriction->pObject = pEntity; + CPASAttenuationFilter filter( pEntity, params.soundlevel ); + pFriction->patch = CSoundEnvelopeController::GetController().SoundCreate( + filter, pEntity->entindex(), CHAN_BODY, pSoundName, params.soundlevel ); + CSoundEnvelopeController::GetController().Play( pFriction->patch, params.volume * flVolume, params.pitch ); + } + else + { + float pitch = (flVolume * (params.pitchhigh - params.pitchlow)) + params.pitchlow; + CSoundEnvelopeController::GetController().SoundChangeVolume( pFriction->patch, params.volume * flVolume, 0.1f ); + CSoundEnvelopeController::GetController().SoundChangePitch( pFriction->patch, pitch, 0.1f ); + } + + pFriction->flLastUpdateTime = gpGlobals->curtime; + pFriction->flLastEffectTime = gpGlobals->curtime; + } +} + +void PhysCleanupFrictionSounds( CBaseEntity *pEntity ) +{ + friction_t *pFriction = g_Collisions.FindFriction( pEntity ); + if ( pFriction && pFriction->patch ) + { + g_Collisions.ShutdownFriction( *pFriction ); + } +} + + +//----------------------------------------------------------------------------- +// Applies force impulses at a later time +//----------------------------------------------------------------------------- +void PhysCallbackImpulse( IPhysicsObject *pPhysicsObject, const Vector &vecCenterForce, const AngularImpulse &vecCenterTorque ) +{ + Assert( physenv->IsInSimulation() ); + g_PostSimulationQueue.QueueCall( PostSimulation_ImpulseEvent, pPhysicsObject, RefToVal(vecCenterForce), RefToVal(vecCenterTorque) ); +} + +void PhysCallbackSetVelocity( IPhysicsObject *pPhysicsObject, const Vector &vecVelocity ) +{ + Assert( physenv->IsInSimulation() ); + g_PostSimulationQueue.QueueCall( PostSimulation_SetVelocityEvent, pPhysicsObject, RefToVal(vecVelocity) ); +} + +void PhysCallbackDamage( CBaseEntity *pEntity, const CTakeDamageInfo &info, gamevcollisionevent_t &event, int hurtIndex ) +{ + Assert( physenv->IsInSimulation() ); + int otherIndex = !hurtIndex; + g_Collisions.AddDamageEvent( pEntity, info, event.pObjects[otherIndex], true, event.preVelocity[otherIndex], event.preAngularVelocity[otherIndex] ); +} + +void PhysCallbackDamage( CBaseEntity *pEntity, const CTakeDamageInfo &info ) +{ + if ( PhysIsInCallback() ) + { + CBaseEntity *pInflictor = info.GetInflictor(); + IPhysicsObject *pInflictorPhysics = (pInflictor) ? pInflictor->VPhysicsGetObject() : NULL; + g_Collisions.AddDamageEvent( pEntity, info, pInflictorPhysics, false, vec3_origin, vec3_origin ); + if ( pEntity && info.GetInflictor() ) + { + CGMsg( 2, CON_GROUP_PHYSICS, "Warning: Physics damage event with no recovery info!\nObjects: %s, %s\n", pEntity->GetClassname(), info.GetInflictor()->GetClassname() ); + } + } + else + { + pEntity->TakeDamage( info ); + } +} + +void PhysCallbackRemove(IServerNetworkable *pRemove) +{ + if ( PhysIsInCallback() ) + { + g_Collisions.AddRemoveObject(pRemove); + } + else + { + UTIL_Remove(pRemove); + } +} + +void PhysSetEntityGameFlags( CBaseEntity *pEntity, unsigned short flags ) +{ + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + PhysSetGameFlags( pList[i], flags ); + } +} + +bool PhysFindOrAddVehicleScript( const char *pScriptName, vehicleparams_t *pParams, vehiclesounds_t *pSounds ) +{ + return g_PhysicsHook.FindOrAddVehicleScript(pScriptName, pParams, pSounds); +} + +void PhysFlushVehicleScripts() +{ + g_PhysicsHook.FlushVehicleScripts(); +} + +IPhysicsObject *FindPhysicsObjectByName( const char *pName, CBaseEntity *pErrorEntity ) +{ + if ( !pName || !strlen(pName) ) + return NULL; + + CBaseEntity *pEntity = NULL; + IPhysicsObject *pBestObject = NULL; + while (1) + { + pEntity = gEntList.FindEntityByName( pEntity, pName ); + if ( !pEntity ) + break; + if ( pEntity->VPhysicsGetObject() ) + { + if ( pBestObject ) + { + const char *pErrorName = pErrorEntity ? pErrorEntity->GetClassname() : "Unknown"; + Vector origin = pErrorEntity ? pErrorEntity->GetAbsOrigin() : vec3_origin; + CGWarning( 1, CON_GROUP_PHYSICS, "entity %s at %s has physics attachment to more than one entity with the name %s!!!\n", pErrorName, VecToString( origin ), pName ); + while ( ( pEntity = gEntList.FindEntityByName( pEntity, pName ) ) != NULL ) + { + CGWarning( 1, CON_GROUP_PHYSICS, "Found %s\n", pEntity->GetClassname() ); + } + break; + + } + pBestObject = pEntity->VPhysicsGetObject(); + } + } + return pBestObject; +} + +void CC_AirDensity( const CCommand &args ) +{ + if ( !physenv ) + return; + + if ( args.ArgC() < 2 ) + { + CGMsg( 0, CON_GROUP_PHYSICS, "air_density \nCurrent air density is %.2f\n", physenv->GetAirDensity() ); + } + else + { + float density = atof( args[1] ); + physenv->SetAirDensity( density ); + } +} +static ConCommand air_density("air_density", CC_AirDensity, "Changes the density of air for drag computations.", FCVAR_CHEAT); + +void DebugDrawContactPoints(IPhysicsObject *pPhysics) +{ + IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); + + while ( pSnapshot->IsValid() ) + { + Vector pt, normal; + pSnapshot->GetContactPoint( pt ); + pSnapshot->GetSurfaceNormal( normal ); + NDebugOverlay::Box( pt, -Vector(1,1,1), Vector(1,1,1), 0, 255, 0, 32, 0 ); + NDebugOverlay::Line( pt, pt - normal * 20, 0, 255, 0, false, 0 ); + IPhysicsObject *pOther = pSnapshot->GetObject(1); + CBaseEntity *pEntity0 = static_cast(pOther->GetGameData()); + CFmtStr str("%s (%s): %s [%0.2f]", pEntity0->GetClassname(), STRING(pEntity0->GetModelName()), pEntity0->GetDebugName(), pSnapshot->GetFrictionCoefficient() ); + NDebugOverlay::Text( pt, str.Access(), false, 0 ); + pSnapshot->NextFrictionData(); + } + pSnapshot->DeleteAllMarkedContacts( true ); + pPhysics->DestroyFrictionSnapshot( pSnapshot ); +} + + + +#if 0 + +#include "filesystem.h" +//----------------------------------------------------------------------------- +// Purpose: This will append a collide to a glview file. Then you can view the +// collisionmodels with glview. +// Input : *pCollide - collision model +// &origin - position of the instance of this model +// &angles - orientation of instance +// *pFilename - output text file +//----------------------------------------------------------------------------- +// examples: +// world: +// DumpCollideToGlView( pWorldCollide->solids[0], vec3_origin, vec3_origin, "jaycollide.txt" ); +// static_prop: +// DumpCollideToGlView( info.m_pCollide->solids[0], info.m_Origin, info.m_Angles, "jaycollide.txt" ); +// +//----------------------------------------------------------------------------- +void DumpCollideToGlView( CPhysCollide *pCollide, const Vector &origin, const QAngle &angles, const char *pFilename ) +{ + if ( !pCollide ) + return; + + printf("Writing %s...\n", pFilename ); + Vector *outVerts; + int vertCount = physcollision->CreateDebugMesh( pCollide, &outVerts ); + FileHandle_t fp = filesystem->Open( pFilename, "ab" ); + int triCount = vertCount / 3; + int vert = 0; + VMatrix tmp = SetupMatrixOrgAngles( origin, angles ); + int i; + for ( i = 0; i < vertCount; i++ ) + { + outVerts[i] = tmp.VMul4x3( outVerts[i] ); + } + for ( i = 0; i < triCount; i++ ) + { + filesystem->FPrintf( fp, "3\n" ); + filesystem->FPrintf( fp, "%6.3f %6.3f %6.3f 1 0 0\n", outVerts[vert].x, outVerts[vert].y, outVerts[vert].z ); + vert++; + filesystem->FPrintf( fp, "%6.3f %6.3f %6.3f 0 1 0\n", outVerts[vert].x, outVerts[vert].y, outVerts[vert].z ); + vert++; + filesystem->FPrintf( fp, "%6.3f %6.3f %6.3f 0 0 1\n", outVerts[vert].x, outVerts[vert].y, outVerts[vert].z ); + vert++; + } + filesystem->Close( fp ); + physcollision->DestroyDebugMesh( vertCount, outVerts ); +} +#endif + diff --git a/sp/src/game/server/physics.h b/sp/src/game/server/physics.h new file mode 100644 index 00000000..541a1654 --- /dev/null +++ b/sp/src/game/server/physics.h @@ -0,0 +1,189 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: This is the abstraction layer for the physics simulation system +// Any calls to the external physics library (ipion) should be made through this +// layer. Eventually, the physics system will probably become a DLL and made +// accessible to the client & server side code. +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef PHYSICS_H +#define PHYSICS_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "physics_shared.h" + +class CBaseEntity; +class IPhysicsMaterial; +class IPhysicsConstraint; +class IPhysicsSpring; +class IPhysicsSurfaceProps; +class CTakeDamageInfo; +class ConVar; + +extern IPhysicsMaterial *g_Material; +extern ConVar phys_pushscale; +extern ConVar phys_timescale; + +struct objectparams_t; +extern IPhysicsGameTrace *physgametrace; + +class IPhysicsCollisionSolver; +class IPhysicsCollisionEvent; +class IPhysicsObjectEvent; +extern IPhysicsCollisionSolver * const g_pCollisionSolver; +extern IPhysicsCollisionEvent * const g_pCollisionEventHandler; +extern IPhysicsObjectEvent * const g_pObjectEventHandler; + +// HACKHACK: We treat anything >= 500kg as a special "large mass" that does more impact damage +// and has special recovery on crushing/killing other objects +// also causes screen shakes on impact with static/world objects +const float VPHYSICS_LARGE_OBJECT_MASS = 500.0f; + +struct gamevcollisionevent_t : public vcollisionevent_t +{ + Vector preVelocity[2]; + Vector postVelocity[2]; + AngularImpulse preAngularVelocity[2]; + CBaseEntity *pEntities[2]; + + void Init( vcollisionevent_t *pEvent ) + { + *((vcollisionevent_t *)this) = *pEvent; + pEntities[0] = NULL; + pEntities[1] = NULL; + } +}; + +struct triggerevent_t +{ + CBaseEntity *pTriggerEntity; + IPhysicsObject *pTriggerPhysics; + CBaseEntity *pEntity; + IPhysicsObject *pObject; + bool bStart; + + inline void Init( CBaseEntity *triggerEntity, IPhysicsObject *triggerPhysics, CBaseEntity *entity, IPhysicsObject *object, bool startTouch ) + { + pTriggerEntity = triggerEntity; + pTriggerPhysics= triggerPhysics; + pEntity = entity; + pObject = object; + bStart = startTouch; + } + inline void Clear() + { + memset( this, 0, sizeof(*this) ); + } +}; + +// parse solid parameter overrides out of a string +void PhysSolidOverride( solid_t &solid, string_t overrideScript ); + +extern CEntityList *g_pShadowEntities; +#ifdef PORTAL +extern CEntityList *g_pShadowEntities_Main; +#endif +void PhysAddShadow( CBaseEntity *pEntity ); +void PhysRemoveShadow( CBaseEntity *pEntity ); +bool PhysHasShadow( CBaseEntity *pEntity ); + +void PhysEnableFloating( IPhysicsObject *pObject, bool bEnable ); + +void PhysCollisionSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, int channel, int surfaceProps, int surfacePropsHit, float deltaTime, float speed ); +void PhysCollisionScreenShake( gamevcollisionevent_t *pEvent, int index ); +void PhysCollisionDust( gamevcollisionevent_t *pEvent, surfacedata_t *phit ); +#if HL2_EPISODIC +void PhysCollisionWarpEffect( gamevcollisionevent_t *pEvent, surfacedata_t *phit ); +#endif +void PhysBreakSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, Vector vecOrigin ); + +// plays the impact sound for a particular material +void PhysicsImpactSound( CBaseEntity *pEntity, IPhysicsObject *pPhysObject, int channel, int surfaceProps, int surfacePropsHit, float volume, float impactSpeed ); + +void PhysCallbackDamage( CBaseEntity *pEntity, const CTakeDamageInfo &info ); +void PhysCallbackDamage( CBaseEntity *pEntity, const CTakeDamageInfo &info, gamevcollisionevent_t &event, int hurtIndex ); + +// Applies force impulses at a later time +void PhysCallbackImpulse( IPhysicsObject *pPhysicsObject, const Vector &vecCenterForce, const AngularImpulse &vecCenterTorque ); + +// Sets the velocity at a later time +void PhysCallbackSetVelocity( IPhysicsObject *pPhysicsObject, const Vector &vecVelocity ); + +// queue up a delete on this object +void PhysCallbackRemove(IServerNetworkable *pRemove); + +bool PhysGetDamageInflictorVelocityStartOfFrame( IPhysicsObject *pInflictor, Vector &velocity, AngularImpulse &angVelocity ); + +// force a physics entity to sleep immediately +void PhysForceEntityToSleep( CBaseEntity *pEntity, IPhysicsObject *pObject ); + +// teleport an entity to it's position relative to an object it's constrained to +void PhysTeleportConstrainedEntity( CBaseEntity *pTeleportSource, IPhysicsObject *pObject0, IPhysicsObject *pObject1, const Vector &prevPosition, const QAngle &prevAngles, bool physicsRotate ); + +void PhysGetListOfPenetratingEntities( CBaseEntity *pSearch, CUtlVector &list ); +bool PhysShouldCollide( IPhysicsObject *pObj0, IPhysicsObject *pObj1 ); + +// returns true when processing a callback - so we can defer things that can't be done inside a callback +bool PhysIsInCallback(); +bool PhysIsFinalTick(); + +bool PhysGetTriggerEvent( triggerevent_t *pEvent, CBaseEntity *pTrigger ); +// note: pErrorEntity is used to report errors (object not found, more than one found). It can be NULL +IPhysicsObject *FindPhysicsObjectByName( const char *pName, CBaseEntity *pErrorEntity ); +bool PhysFindOrAddVehicleScript( const char *pScriptName, struct vehicleparams_t *pParams, struct vehiclesounds_t *pSounds ); +void PhysFlushVehicleScripts(); + +// this is called to flush all queues when the delete list is cleared +void PhysOnCleanupDeleteList(); + +struct masscenteroverride_t +{ + enum align_type + { + ALIGN_POINT = 0, + ALIGN_AXIS = 1, + }; + + void Defaults() + { + entityName = NULL_STRING; + } + + void SnapToPoint( string_t name, const Vector &pointWS ) + { + entityName = name; + center = pointWS; + axis.Init(); + alignType = ALIGN_POINT; + } + + void SnapToAxis( string_t name, const Vector &axisStartWS, const Vector &unitAxisDirWS ) + { + entityName = name; + center = axisStartWS; + axis = unitAxisDirWS; + alignType = ALIGN_AXIS; + } + + Vector center; + Vector axis; + int alignType; + string_t entityName; +}; + +void PhysSetMassCenterOverride( masscenteroverride_t &override ); +// NOTE: this removes the entry from the table as well as retrieving it +void PhysGetMassCenterOverride( CBaseEntity *pEntity, vcollide_t *pCollide, solid_t &solidOut ); +float PhysGetEntityMass( CBaseEntity *pEntity ); +void PhysSetEntityGameFlags( CBaseEntity *pEntity, unsigned short flags ); + +void DebugDrawContactPoints(IPhysicsObject *pPhysics); + +#endif // PHYSICS_H diff --git a/sp/src/game/server/physics_bone_follower.cpp b/sp/src/game/server/physics_bone_follower.cpp new file mode 100644 index 00000000..fc980cc3 --- /dev/null +++ b/sp/src/game/server/physics_bone_follower.cpp @@ -0,0 +1,485 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "bone_setup.h" +#include "physics_bone_follower.h" +#include "vcollide_parse.h" +#include "saverestore_utlvector.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +BEGIN_SIMPLE_DATADESC( physfollower_t ) +DEFINE_FIELD( boneIndex, FIELD_INTEGER ), +DEFINE_FIELD( hFollower, FIELD_EHANDLE ), +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( CBoneFollowerManager ) +DEFINE_GLOBAL_FIELD( m_iNumBones, FIELD_INTEGER ), +DEFINE_GLOBAL_UTLVECTOR( m_physBones, FIELD_EMBEDDED ), +END_DATADESC() + +//================================================================================================================ +// BONE FOLLOWER MANAGER +//================================================================================================================ +CBoneFollowerManager::CBoneFollowerManager() +{ + m_iNumBones = 0; +} + +CBoneFollowerManager::~CBoneFollowerManager() +{ + // if this fires then someone isn't destroying their bonefollowers in UpdateOnRemove + Assert(m_iNumBones==0); + DestroyBoneFollowers(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEntity - +// iNumBones - +// **pFollowerBoneNames - +//----------------------------------------------------------------------------- +void CBoneFollowerManager::InitBoneFollowers( CBaseAnimating *pParentEntity, int iNumBones, const char **pFollowerBoneNames ) +{ + m_iNumBones = iNumBones; + m_physBones.EnsureCount( iNumBones ); + + // Now init all the bones + for ( int i = 0; i < iNumBones; i++ ) + { + CreatePhysicsFollower( pParentEntity, m_physBones[i], pFollowerBoneNames[i], NULL ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBoneFollowerManager::AddBoneFollower( CBaseAnimating *pParentEntity, const char *pFollowerBoneName, solid_t *pSolid ) +{ + m_iNumBones++; + + int iIndex = m_physBones.AddToTail(); + CreatePhysicsFollower( pParentEntity, m_physBones[iIndex], pFollowerBoneName, pSolid ); +} + +// walk the hitboxes and find the first one that is attached to the physics bone in question +// return the hitgroup of that box +static int HitGroupFromPhysicsBone( CBaseAnimating *pAnim, int physicsBone ) +{ + CStudioHdr *pStudioHdr = pAnim->GetModelPtr( ); + mstudiohitboxset_t *set = pStudioHdr->pHitboxSet( pAnim->m_nHitboxSet ); + for ( int i = 0; i < set->numhitboxes; i++ ) + { + if ( pStudioHdr->pBone( set->pHitbox(i)->bone )->physicsbone == physicsBone ) + { + return set->pHitbox(i)->group; + } + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &follow - +// *pBoneName - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBoneFollowerManager::CreatePhysicsFollower( CBaseAnimating *pParentEntity, physfollower_t &follow, const char *pBoneName, solid_t *pSolid ) +{ + CStudioHdr *pStudioHdr = pParentEntity->GetModelPtr(); + matrix3x4_t boneToWorld; + solid_t solidTmp; + + Vector bonePosition; + QAngle boneAngles; + + int boneIndex = Studio_BoneIndexByName( pStudioHdr, pBoneName ); + + if ( boneIndex >= 0 ) + { + mstudiobone_t *pBone = pStudioHdr->pBone( boneIndex ); + + int physicsBone = pBone->physicsbone; + if ( !pSolid ) + { + if ( !PhysModelParseSolidByIndex( solidTmp, pParentEntity, pParentEntity->GetModelIndex(), physicsBone ) ) + return false; + pSolid = &solidTmp; + } + + // fixup in case ragdoll is assigned to a parent of the requested follower bone + follow.boneIndex = Studio_BoneIndexByName( pStudioHdr, pSolid->name ); + if ( follow.boneIndex < 0 ) + { + follow.boneIndex = boneIndex; + } + + pParentEntity->GetBoneTransform( follow.boneIndex, boneToWorld ); + MatrixAngles( boneToWorld, boneAngles, bonePosition ); + + follow.hFollower = CBoneFollower::Create( pParentEntity, STRING(pParentEntity->GetModelName()), *pSolid, bonePosition, boneAngles ); + follow.hFollower->SetTraceData( physicsBone, HitGroupFromPhysicsBone( pParentEntity, physicsBone ) ); + follow.hFollower->SetBlocksLOS( pParentEntity->BlocksLOS() ); + return true; + } + else + { + Warning( "ERROR: Tried to create bone follower on invalid bone %s\n", pBoneName ); + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBoneFollowerManager::UpdateBoneFollowers( CBaseAnimating *pParentEntity ) +{ + if ( m_iNumBones ) + { + matrix3x4_t boneToWorld; + Vector bonePosition; + QAngle boneAngles; + for ( int i = 0; i < m_iNumBones; i++ ) + { + if ( !m_physBones[i].hFollower ) + continue; + + pParentEntity->GetBoneTransform( m_physBones[i].boneIndex, boneToWorld ); + MatrixAngles( boneToWorld, boneAngles, bonePosition ); + m_physBones[i].hFollower->UpdateFollower( bonePosition, boneAngles, 0.1 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBoneFollowerManager::DestroyBoneFollowers( void ) +{ + for ( int i = 0; i < m_iNumBones; i++ ) + { + if ( !m_physBones[i].hFollower ) + continue; + + UTIL_Remove( m_physBones[i].hFollower ); + m_physBones[i].hFollower = NULL; + } + + m_physBones.Purge(); + m_iNumBones = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +physfollower_t *CBoneFollowerManager::GetBoneFollower( int iFollowerIndex ) +{ + Assert( iFollowerIndex >= 0 && iFollowerIndex < m_iNumBones ); + if ( iFollowerIndex >= 0 && iFollowerIndex < m_iNumBones ) + return &m_physBones[iFollowerIndex]; + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Retrieve the index for a supplied bone follower +// Input : *pFollower - Bone follower to look up +// Output : -1 if not found, otherwise the index of the bone follower +//----------------------------------------------------------------------------- +int CBoneFollowerManager::GetBoneFollowerIndex( CBoneFollower *pFollower ) +{ + if ( pFollower == NULL ) + return -1; + + for ( int i = 0; i < m_iNumBones; i++ ) + { + if ( !m_physBones[i].hFollower ) + continue; + + if ( m_physBones[i].hFollower == pFollower ) + return i; + } + + return -1; +} + +//================================================================================================================ +// BONE FOLLOWER +//================================================================================================================ + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CBoneFollower ) + + DEFINE_FIELD( m_modelIndex, FIELD_MODELINDEX ), + DEFINE_FIELD( m_solidIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_physicsBone, FIELD_INTEGER ), + DEFINE_FIELD( m_hitGroup, FIELD_INTEGER ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CBoneFollower, DT_BoneFollower ) + SendPropModelIndex(SENDINFO(m_modelIndex)), + SendPropInt(SENDINFO(m_solidIndex), 6, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +bool CBoneFollower::Init( CBaseEntity *pOwner, const char *pModelName, solid_t &solid, const Vector &position, const QAngle &orientation ) +{ + SetOwnerEntity( pOwner ); + UTIL_SetModel( this, pModelName ); + + AddEffects( EF_NODRAW ); // invisible + + m_modelIndex = modelinfo->GetModelIndex( pModelName ); + m_solidIndex = solid.index; + SetAbsOrigin( position ); + SetAbsAngles( orientation ); + SetMoveType( MOVETYPE_PUSH ); + SetSolid( SOLID_VPHYSICS ); + SetCollisionGroup( pOwner->GetCollisionGroup() ); + AddSolidFlags( FSOLID_CUSTOMRAYTEST | FSOLID_CUSTOMBOXTEST ); + solid.params.pGameData = (void *)this; + IPhysicsObject *pPhysics = VPhysicsInitShadow( false, false, &solid ); + if ( !pPhysics ) + return false; + + // we can't use the default model bounds because each entity is only one bone of the model + // so compute the OBB of the physics model and use that. + Vector mins, maxs; + physcollision->CollideGetAABB( &mins, &maxs, pPhysics->GetCollide(), vec3_origin, vec3_angle ); + SetCollisionBounds( mins, maxs ); + + pPhysics->SetCallbackFlags( pPhysics->GetCallbackFlags() | CALLBACK_GLOBAL_TOUCH ); + pPhysics->EnableGravity( false ); + // This is not a normal shadow controller that is trying to go to a space occupied by an entity in the game physics + // This entity is not running PhysicsPusher(), so Vphysics is supposed to move it + // This line of code informs vphysics of that fact + if ( pOwner->IsNPC() ) + { + pPhysics->GetShadowController()->SetPhysicallyControlled( true ); + } + + return true; +} + +int CBoneFollower::UpdateTransmitState() +{ + // Send to the client for client-side collisions and visualization + return SetTransmitState( FL_EDICT_PVSCHECK ); +} + +void CBoneFollower::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + Vector origin; + QAngle angles; + + pPhysics->GetPosition( &origin, &angles ); + + SetAbsOrigin( origin ); + SetAbsAngles( angles ); +} + +// a little helper class to temporarily change the physics object +// for an entity - and change it back when it goes out of scope. +class CPhysicsSwapTemp +{ +public: + CPhysicsSwapTemp( CBaseEntity *pEntity, IPhysicsObject *pTmpPhysics ) + { + Assert(pEntity); + Assert(pTmpPhysics); + m_pEntity = pEntity; + m_pPhysics = m_pEntity->VPhysicsGetObject(); + if ( m_pPhysics ) + { + m_pEntity->VPhysicsSwapObject( pTmpPhysics ); + } + else + { + m_pEntity->VPhysicsSetObject( pTmpPhysics ); + } + } + ~CPhysicsSwapTemp() + { + m_pEntity->VPhysicsSwapObject( m_pPhysics ); + } + +private: + CBaseEntity *m_pEntity; + IPhysicsObject *m_pPhysics; +}; + + +void CBoneFollower::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + CPhysicsSwapTemp tmp(pOwner, pEvent->pObjects[index] ); + pOwner->VPhysicsCollision( index, pEvent ); + } +} + +void CBoneFollower::VPhysicsShadowCollision( int index, gamevcollisionevent_t *pEvent ) +{ + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + CPhysicsSwapTemp tmp(pOwner, pEvent->pObjects[index] ); + pOwner->VPhysicsShadowCollision( index, pEvent ); + } +} + +void CBoneFollower::VPhysicsFriction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit ) +{ + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + CPhysicsSwapTemp tmp(pOwner, pObject ); + pOwner->VPhysicsFriction( pObject, energy, surfaceProps, surfacePropsHit ); + } +} + +bool CBoneFollower::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ + vcollide_t *pCollide = modelinfo->GetVCollide( GetModelIndex() ); + Assert( pCollide && pCollide->solidCount > m_solidIndex ); + + UTIL_ClearTrace( trace ); + + physcollision->TraceBox( ray, pCollide->solids[m_solidIndex], GetAbsOrigin(), GetAbsAngles(), &trace ); + + if ( trace.fraction >= 1 ) + return false; + + // return owner as trace hit + trace.m_pEnt = GetOwnerEntity(); + trace.hitgroup = m_hitGroup; + trace.physicsbone = m_physicsBone; + return true; +} + +void CBoneFollower::UpdateFollower( const Vector &position, const QAngle &orientation, float flInterval ) +{ + // UNDONE: Shadow update needs timing info? + VPhysicsGetObject()->UpdateShadow( position, orientation, false, flInterval ); +} + +void CBoneFollower::SetTraceData( int physicsBone, int hitGroup ) +{ + m_hitGroup = hitGroup; + m_physicsBone = physicsBone; +} + +CBoneFollower *CBoneFollower::Create( CBaseEntity *pOwner, const char *pModelName, solid_t &solid, const Vector &position, const QAngle &orientation ) +{ + CBoneFollower *pFollower = (CBoneFollower *)CreateEntityByName( "phys_bone_follower" ); + if ( pFollower ) + { + pFollower->Init( pOwner, pModelName, solid, position, orientation ); + } + return pFollower; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBoneFollower::ObjectCaps() +{ + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + if( pOwner->m_iGlobalname != NULL_STRING ) + { + int caps = BaseClass::ObjectCaps() | pOwner->ObjectCaps(); + caps &= ~FCAP_ACROSS_TRANSITION; + return caps; + } + } + + return BaseClass::ObjectCaps(); +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBoneFollower::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + pOwner->Use( pActivator, pCaller, useType, value ); + return; + } + + BaseClass::Use( pActivator, pCaller, useType, value ); +} + +//----------------------------------------------------------------------------- +// Purpose: Pass on Touch calls to the entity we're following +//----------------------------------------------------------------------------- +void CBoneFollower::Touch( CBaseEntity *pOther ) +{ + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + //TODO: fill in the touch trace with the hitbox number associated with this bone + pOwner->Touch( pOther ); + return; + } + + BaseClass::Touch( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: Pass on trace attack calls to the entity we're following +//----------------------------------------------------------------------------- +void CBoneFollower::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner ) + { + pOwner->DispatchTraceAttack( info, vecDir, ptr, pAccumulator ); + return; + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + +LINK_ENTITY_TO_CLASS( phys_bone_follower, CBoneFollower ); + + + +// create a manager and a list of followers directly from a ragdoll +void CreateBoneFollowersFromRagdoll( CBaseAnimating *pEntity, CBoneFollowerManager *pManager, vcollide_t *pCollide ) +{ + IVPhysicsKeyParser *pParse = physcollision->VPhysicsKeyParserCreate( pCollide->pKeyValues ); + while ( !pParse->Finished() ) + { + const char *pBlock = pParse->GetCurrentBlockName(); + if ( !strcmpi( pBlock, "solid" ) ) + { + solid_t solid; + + pParse->ParseSolid( &solid, NULL ); + // collisions are off by default, turn them on + solid.params.enableCollisions = true; + solid.params.pName = STRING(pEntity->GetModelName()); + + pManager->AddBoneFollower( pEntity, solid.name, &solid ); + } + else + { + pParse->SkipBlock(); + } + } +} diff --git a/sp/src/game/server/physics_bone_follower.h b/sp/src/game/server/physics_bone_follower.h new file mode 100644 index 00000000..10059afa --- /dev/null +++ b/sp/src/game/server/physics_bone_follower.h @@ -0,0 +1,104 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PHYSICS_BONE_FOLLOWER_H +#define PHYSICS_BONE_FOLLOWER_H +#ifdef _WIN32 +#pragma once +#endif + +class CBoneFollower; + +// +// To use bone followers in an entity, contain a CBoneFollowerManager in it. Then: +// - Call InitBoneFollowers() in the entity's CreateVPhysics(). +// - Call UpdateBoneFollowers() after you move your bones. +// - Call DestroyBoneFollowers() when your entity's removed + +struct physfollower_t +{ + DECLARE_SIMPLE_DATADESC(); + int boneIndex; + CHandle hFollower; +}; + +struct vcollide_t; + +// create a manager and a list of followers directly from a ragdoll +void CreateBoneFollowersFromRagdoll( CBaseAnimating *pEntity, class CBoneFollowerManager *pManager, vcollide_t *pCollide ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CBoneFollowerManager +{ + DECLARE_SIMPLE_DATADESC(); +public: + CBoneFollowerManager(); + ~CBoneFollowerManager(); + + // Use either of these to create the bone followers in your entity's CreateVPhysics() + void InitBoneFollowers( CBaseAnimating *pParentEntity, int iNumBones, const char **pFollowerBoneNames ); + void AddBoneFollower( CBaseAnimating *pParentEntity, const char *pFollowerBoneName, solid_t *pSolid = NULL ); // Adds a single bone follower + + // Call this after you move your bones + void UpdateBoneFollowers( CBaseAnimating *pParentEntity ); + + // Call this when your entity's removed + void DestroyBoneFollowers( void ); + + physfollower_t *GetBoneFollower( int iFollowerIndex ); + int GetBoneFollowerIndex( CBoneFollower *pFollower ); + int GetNumBoneFollowers( void ) const { return m_iNumBones; } + +private: + bool CreatePhysicsFollower( CBaseAnimating *pParentEntity, physfollower_t &follow, const char *pBoneName, solid_t *pSolid ); + +private: + int m_iNumBones; + CUtlVector m_physBones; +}; + + +class CBoneFollower : public CBaseEntity +{ + DECLARE_CLASS( CBoneFollower, CBaseEntity ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); +public: + // CBaseEntity + void VPhysicsUpdate( IPhysicsObject *pPhysics ); + int UpdateTransmitState(void); + + // NOTE: These are forwarded to the parent object! + void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + void VPhysicsFriction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit ); + void VPhysicsShadowCollision( int index, gamevcollisionevent_t *pEvent ); + + bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); + int ObjectCaps( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void Touch( CBaseEntity *pOther ); + + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + // locals + bool Init( CBaseEntity *pOwner, const char *pModelName, solid_t &solid, const Vector &position, const QAngle &orientation ); + void UpdateFollower( const Vector &position, const QAngle &orientation, float flInterval ); + void SetTraceData( int physicsBone, int hitGroup ); + + // factory + static CBoneFollower *Create( CBaseEntity *pOwner, const char *pModelName, solid_t &solid, const Vector &position, const QAngle &orientation ); + +private: + CNetworkVar( int, m_modelIndex ); + CNetworkVar( int, m_solidIndex ); + int m_physicsBone; + int m_hitGroup; +}; + +#endif // PHYSICS_BONE_FOLLOWER_H diff --git a/sp/src/game/server/physics_cannister.cpp b/sp/src/game/server/physics_cannister.cpp new file mode 100644 index 00000000..848463bf --- /dev/null +++ b/sp/src/game/server/physics_cannister.cpp @@ -0,0 +1,483 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "basecombatcharacter.h" +#include "entityoutput.h" +#include "physics.h" +#include "explode.h" +#include "vphysics_interface.h" +#include "collisionutils.h" +#include "steamjet.h" +#include "eventqueue.h" +#include "soundflags.h" +#include "engine/IEngineSound.h" +#include "props.h" +#include "physics_cannister.h" +#include "globals.h" +#include "physics_saverestore.h" +#include "shareddefs.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_CANNISTER_ASLEEP 0x0001 +#define SF_CANNISTER_EXPLODE 0x0002 + +BEGIN_SIMPLE_DATADESC( CThrustController ) + + DEFINE_FIELD( m_thrustVector, FIELD_VECTOR ), + DEFINE_FIELD( m_torqueVector, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_thrust, FIELD_FLOAT, "thrust" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( physics_cannister, CPhysicsCannister ); + +BEGIN_DATADESC( CPhysicsCannister ) + + DEFINE_OUTPUT( m_onActivate, "OnActivate" ), + DEFINE_OUTPUT( m_OnAwakened, "OnAwakened" ), + DEFINE_FIELD( m_thrustOrigin, FIELD_VECTOR ), // this is a position, but in local space + DEFINE_EMBEDDED( m_thruster ), + DEFINE_PHYSPTR( m_pController ), + DEFINE_FIELD( m_pJet, FIELD_CLASSPTR ), + DEFINE_FIELD( m_active, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_thrustTime, FIELD_FLOAT, "fuel" ), + DEFINE_KEYFIELD( m_damage, FIELD_FLOAT, "expdamage" ), + DEFINE_KEYFIELD( m_damageRadius, FIELD_FLOAT, "expradius" ), + DEFINE_FIELD( m_activateTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_gasSound, FIELD_SOUNDNAME, "gassound" ), + DEFINE_FIELD( m_bFired, FIELD_BOOLEAN ), + + // Physics Influence + DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), + DEFINE_FIELD( m_hLauncher, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), + DEFINE_INPUTFUNC( FIELD_VOID, "Explode", InputExplode ), + DEFINE_INPUTFUNC( FIELD_VOID, "Wake", InputWake ), + + DEFINE_THINKFUNC( BeginShutdownThink ), + DEFINE_ENTITYFUNC( ExplodeTouch ), + +END_DATADESC() + +void CPhysicsCannister::Spawn( void ) +{ + Precache(); + SetModel( STRING(GetModelName()) ); + SetBloodColor( DONT_BLEED ); + + AddSolidFlags( FSOLID_CUSTOMRAYTEST ); + m_takedamage = DAMAGE_YES; + SetNextThink( TICK_NEVER_THINK ); + + if ( m_iHealth <= 0 ) + m_iHealth = 25; + + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0; + SetCycle( 0 ); + m_bFired = false; + + // not thrusting + m_active = false; + + CreateVPhysics(); + if ( !VPhysicsGetObject() ) + { + // must have a physics object or code will crash later + UTIL_Remove(this); + } +} + +void CPhysicsCannister::OnRestore() +{ + BaseClass::OnRestore(); + if ( m_pController ) + { + m_pController->SetEventHandler( &m_thruster ); + } +} + +bool CPhysicsCannister::CreateVPhysics() +{ + bool asleep = HasSpawnFlags(SF_CANNISTER_ASLEEP); + + VPhysicsInitNormal( SOLID_VPHYSICS, 0, asleep ); + return true; +} + +bool CPhysicsCannister::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ + Vector vecAbsMins, vecAbsMaxs; + CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); + + if ( !IsBoxIntersectingRay( vecAbsMins, vecAbsMaxs, ray.m_Start, ray.m_Delta ) ) + return false; + + return BaseClass::TestCollision( ray, mask, trace ); +} + +Vector CPhysicsCannister::CalcLocalThrust( const Vector &offset ) +{ + matrix3x4_t nozzleMatrix; + Vector thrustDirection; + + GetAttachment( LookupAttachment("nozzle"), nozzleMatrix ); + MatrixGetColumn( nozzleMatrix, 2, thrustDirection ); + MatrixGetColumn( nozzleMatrix, 3, m_thrustOrigin ); + thrustDirection = -5*thrustDirection + offset; + VectorNormalize( thrustDirection ); + return thrustDirection; +} + + +CPhysicsCannister::~CPhysicsCannister( void ) +{ +} + +void CPhysicsCannister::Precache( void ) +{ + PropBreakablePrecacheAll( GetModelName() ); + if ( m_gasSound != NULL_STRING ) + { + PrecacheScriptSound( STRING(m_gasSound) ); + } + BaseClass::Precache(); +} + +int CPhysicsCannister::OnTakeDamage( const CTakeDamageInfo &info ) +{ + // HACKHACK: Shouldn't g_vecAttackDir be a parameter to this function? + if ( !m_takedamage ) + return 0; + + if ( !m_active ) + { + m_iHealth -= info.GetDamage(); + if ( m_iHealth < 0 ) + { + Explode( info.GetAttacker() ); + } + else + { + // explosions that don't destroy will activate + // 50% of the time blunt damage will activate as well + if ( (info.GetDamageType() & DMG_BLAST) || + ( (info.GetDamageType() & (DMG_CLUB|DMG_SLASH|DMG_CRUSH) ) && random->RandomInt(1,100) < 50 ) ) + { + CannisterActivate( info.GetAttacker(), g_vecAttackDir ); + } + } + return 1; + } + + if ( (gpGlobals->curtime - m_activateTime) <= 0.1 ) + return 0; + + if ( info.GetDamageType() & (DMG_BULLET|DMG_BUCKSHOT|DMG_BURN|DMG_BLAST) ) + { + Explode( info.GetAttacker() ); + } + + return 0; +} + + +void CPhysicsCannister::TraceAttack( const CTakeDamageInfo &info, const Vector &dir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if ( !m_active && ptr->hitgroup != 0 ) + { + Vector direction = -dir; + direction.z -= 5; + VectorNormalize( direction ); + CannisterActivate( info.GetAttacker(), direction ); + } + BaseClass::TraceAttack( info, dir, ptr, pAccumulator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsCannister::CannisterActivate( CBaseEntity *pActivator, const Vector &thrustOffset ) +{ + // already active or spent + if ( m_active || !m_thrustTime ) + { + return; + } + + m_hLauncher = pActivator; + + Vector thrustDirection = CalcLocalThrust( thrustOffset ); + m_onActivate.FireOutput( pActivator, this, 0 ); + m_thruster.CalcThrust( m_thrustOrigin, thrustDirection, VPhysicsGetObject() ); + m_pController = physenv->CreateMotionController( &m_thruster ); + IPhysicsObject *pPhys = VPhysicsGetObject(); + m_pController->AttachObject( pPhys, true ); + // Make sure the object is simulated + pPhys->Wake(); + + m_active = true; + m_activateTime = gpGlobals->curtime; + SetNextThink( gpGlobals->curtime + m_thrustTime ); + SetThink( &CPhysicsCannister::BeginShutdownThink ); + + QAngle angles; + VectorAngles( -thrustDirection, angles ); + m_pJet = dynamic_cast( CBaseEntity::Create( "env_steam", m_thrustOrigin, angles, this ) ); + m_pJet->SetParent( this ); + + float extra = m_thruster.m_thrust * (1/5000.f); + extra = clamp( extra, 0.f, 1.f ); + + m_pJet->m_SpreadSpeed = 15 * m_thruster.m_thrust * 0.001; + m_pJet->m_Speed = 128 + 100 * extra; + m_pJet->m_StartSize = 10; + m_pJet->m_EndSize = 25; + + m_pJet->m_Rate = 52 + (int)extra*20; + m_pJet->m_JetLength = 64; + m_pJet->m_clrRender = m_clrRender; + + m_pJet->Use( this, this, USE_ON, 1 ); + if ( m_gasSound != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = STRING(m_gasSound); + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: The cannister's been fired by a weapon, so it should stay pretty accurate +//----------------------------------------------------------------------------- +void CPhysicsCannister::CannisterFire( CBaseEntity *pActivator ) +{ + m_bFired = true; + + // Increase thrust + m_thruster.m_thrust *= 4; + + // Only last a short time + m_thrustTime = 10.0; + + // Explode on contact + SetTouch( &CPhysicsCannister::ExplodeTouch ); + + CannisterActivate( pActivator, vec3_origin ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for activating the cannister. +//----------------------------------------------------------------------------- +void CPhysicsCannister::InputActivate( inputdata_t &data ) +{ + CannisterActivate( data.pActivator, Vector(0,0.1,-0.25) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for deactivating the cannister. +//----------------------------------------------------------------------------- +void CPhysicsCannister::InputDeactivate(inputdata_t &data) +{ + Deactivate(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for making the cannister go boom. +//----------------------------------------------------------------------------- +void CPhysicsCannister::InputExplode(inputdata_t &data) +{ + Explode( data.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for waking up the cannister if it is sleeping. +//----------------------------------------------------------------------------- +void CPhysicsCannister::InputWake( inputdata_t &data ) +{ + IPhysicsObject *pPhys = VPhysicsGetObject(); + if ( pPhys != NULL ) + { + pPhys->Wake(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsCannister::Deactivate(void) +{ + if ( !m_pController ) + return; + + m_pController->DetachObject( VPhysicsGetObject() ); + physenv->DestroyMotionController( m_pController ); + m_pController = NULL; + SetNextThink( TICK_NEVER_THINK ); + m_thrustTime = 0; + m_active = false; + if ( m_pJet ) + { + ShutdownJet(); + } + if ( m_gasSound != NULL_STRING ) + { + StopSound( entindex(), CHAN_ITEM, STRING(m_gasSound) ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsCannister::Explode( CBaseEntity *pAttacker ) +{ + // don't recurse + m_takedamage = 0; + Deactivate(); + + Vector velocity; + AngularImpulse angVelocity; + IPhysicsObject *pPhysics = VPhysicsGetObject(); + + pPhysics->GetVelocity( &velocity, &angVelocity ); + PropBreakableCreateAll( GetModelIndex(), pPhysics, GetAbsOrigin(), GetAbsAngles(), velocity, angVelocity, 1.0, 20, COLLISION_GROUP_DEBRIS ); + ExplosionCreate( GetAbsOrigin(), GetAbsAngles(), pAttacker, m_damage, 0, true ); + UTIL_Remove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Explode when I next hit a damageable entity +//----------------------------------------------------------------------------- +void CPhysicsCannister::ExplodeTouch( CBaseEntity *pOther ) +{ + if ( !pOther->m_takedamage ) + return; + + Explode( m_hLauncher ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsCannister::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + if ( m_bFired && m_active ) + { + int otherIndex = !index; + CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex]; + if ( pEvent->deltaCollisionTime < 0.5 && (pHitEntity == this) ) + return; + + // If we hit hard enough. explode + if ( pEvent->collisionSpeed > 1000 ) + { + Explode( m_hLauncher ); + return; + } + } + + BaseClass::VPhysicsCollision( index, pEvent ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsCannister::ShutdownJet( void ) +{ + g_EventQueue.AddEvent( m_pJet, "kill", 5, NULL, NULL ); + + m_pJet->m_bEmit = false; + m_pJet->m_Rate = 0; + m_pJet = NULL; + SetNextThink( TICK_NEVER_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: The think just shuts the cannister down +//----------------------------------------------------------------------------- +void CPhysicsCannister::BeginShutdownThink( void ) +{ + Deactivate(); +} + +//----------------------------------------------------------------------------- +// Physics Attacker +//----------------------------------------------------------------------------- +void CPhysicsCannister::SetPhysicsAttacker( CBasePlayer *pEntity, float flTime ) +{ + m_hPhysicsAttacker = pEntity; + m_flLastPhysicsInfluenceTime = flTime; +} + + +//----------------------------------------------------------------------------- +// Purpose: Keep track of physgun influence +//----------------------------------------------------------------------------- +void CPhysicsCannister::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + SetPhysicsAttacker( pPhysGunUser, gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsCannister::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + SetPhysicsAttacker( pPhysGunUser, gpGlobals->curtime ); + if ( Reason == LAUNCHED_BY_CANNON ) + { + CannisterActivate( pPhysGunUser, vec3_origin ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBasePlayer *CPhysicsCannister::HasPhysicsAttacker( float dt ) +{ + if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) + { + return m_hPhysicsAttacker; + } + return NULL; +} +//----------------------------------------------------------------------------- +// Purpose: Update the visible representation of the physic system's representation of this object +//----------------------------------------------------------------------------- +void CPhysicsCannister::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + BaseClass::VPhysicsUpdate( pPhysics ); + + // if this is the first time we have moved, fire our target + if ( HasSpawnFlags( SF_CANNISTER_ASLEEP ) ) + { + if ( !pPhysics->IsAsleep() ) + { + m_OnAwakened.FireOutput(this, this); + RemoveSpawnFlags( SF_CANNISTER_ASLEEP ); + } + } +} \ No newline at end of file diff --git a/sp/src/game/server/physics_cannister.h b/sp/src/game/server/physics_cannister.h new file mode 100644 index 00000000..72a1b68c --- /dev/null +++ b/sp/src/game/server/physics_cannister.h @@ -0,0 +1,145 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PHYSICS_CANNISTER_H +#define PHYSICS_CANNISTER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "player_pickup.h" + +class CSteamJet; + +class CThrustController : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); + +public: + IMotionEvent::simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) + { + angular = m_torqueVector; + linear = m_thrustVector; + return SIM_LOCAL_ACCELERATION; + } + + void CalcThrust( const Vector &position, const Vector &direction, IPhysicsObject *pPhys ) + { + Vector force = direction * m_thrust * pPhys->GetMass(); + + // Adjust for the position of the thruster -- apply proper torque) + pPhys->CalculateVelocityOffset( force, position, &m_thrustVector, &m_torqueVector ); + pPhys->WorldToLocalVector( &m_thrustVector, m_thrustVector ); + } + + Vector m_thrustVector; + AngularImpulse m_torqueVector; + float m_thrust; +}; + +class CPhysicsCannister : public CBaseCombatCharacter, public CDefaultPlayerPickupVPhysics +{ + DECLARE_CLASS( CPhysicsCannister, CBaseCombatCharacter ); +public: + ~CPhysicsCannister( void ); + + void Spawn( void ); + void Precache( void ); + virtual void OnRestore(); + bool CreateVPhysics(); + + DECLARE_DATADESC(); + virtual void VPhysicsUpdate( IPhysicsObject *pPhysics ); + + virtual QAngle PreferredCarryAngles( void ) { return QAngle( -90, 0, 0 ); } + virtual bool HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ) { return true; } + + // + // Input handlers. + // + void InputActivate(inputdata_t &data); + void InputDeactivate(inputdata_t &data); + void InputExplode(inputdata_t &data); + void InputWake( inputdata_t &data ); + + bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + int ObjectCaps() + { + return (BaseClass::ObjectCaps() | FCAP_IMPULSE_USE); + } + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) + { + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( pPlayer ) + { + pPlayer->PickupObject( this ); + } + } + + void CannisterActivate( CBaseEntity *pActivator, const Vector &thrustOffset ); + void CannisterFire( CBaseEntity *pActivator ); + void Deactivate( void ); + void Explode( CBaseEntity *pAttacker ); + void ExplodeTouch( CBaseEntity *pOther ); + void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + + // Don't treat as a live target + virtual bool IsAlive( void ) { return false; } + + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &dir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + void ShutdownJet( void ); + void BeginShutdownThink( void ); + +public: + virtual bool OnAttemptPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) { return true; } + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ); + virtual CBasePlayer *HasPhysicsAttacker( float dt ); + virtual bool ShouldPuntUseLaunchForces( PhysGunForce_t reason ) + { + if ( reason == PHYSGUN_FORCE_LAUNCHED ) + return (m_thrustTime!=0); + + return false; + } + virtual AngularImpulse PhysGunLaunchAngularImpulse( void ) { return vec3_origin; } + virtual Vector PhysGunLaunchVelocity( const Vector &forward, float flMass ) { return vec3_origin; } + +protected: + void SetPhysicsAttacker( CBasePlayer *pEntity, float flTime ); + + +public: + Vector m_thrustOrigin; + CThrustController m_thruster; + IPhysicsMotionController *m_pController; + CSteamJet *m_pJet; + bool m_active; + float m_thrustTime; + float m_damage; + float m_damageRadius; + + float m_activateTime; + string_t m_gasSound; + + bool m_bFired; // True if this cannister was fire by a weapon + + COutputEvent m_onActivate; + COutputEvent m_OnAwakened; + + CHandle m_hPhysicsAttacker; + float m_flLastPhysicsInfluenceTime; + EHANDLE m_hLauncher; // Entity that caused this cannister to launch + +private: + Vector CalcLocalThrust( const Vector &offset ); +}; + +#endif // PHYSICS_CANNISTER_H diff --git a/sp/src/game/server/physics_collisionevent.h b/sp/src/game/server/physics_collisionevent.h new file mode 100644 index 00000000..67f91d3f --- /dev/null +++ b/sp/src/game/server/physics_collisionevent.h @@ -0,0 +1,176 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Pulling CCollisionEvent's definition out of physics.cpp so it can be abstracted upon (for the portal mod) +// +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef PHYSICS_COLLISIONEVENT_H +#define PHYSICS_COLLISIONEVENT_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "physics.h" +#include "tier1/callqueue.h" + +extern CCallQueue g_PostSimulationQueue; + +struct damageevent_t +{ + CBaseEntity *pEntity; + IPhysicsObject *pInflictorPhysics; + CTakeDamageInfo info; + bool bRestoreVelocity; +}; + +struct inflictorstate_t +{ + Vector savedVelocity; + AngularImpulse savedAngularVelocity; + IPhysicsObject *pInflictorPhysics; + float otherMassMax; + short nextIndex; + short restored; +}; + +enum +{ + COLLSTATE_ENABLED = 0, + COLLSTATE_TRYDISABLE = 1, + COLLSTATE_TRYNPCSOLVER = 2, + COLLSTATE_TRYENTITYSOLVER = 3, + COLLSTATE_DISABLED = 4 +}; + +struct penetrateevent_t +{ + EHANDLE hEntity0; + EHANDLE hEntity1; + float startTime; + float timeStamp; + int collisionState; +}; + +class CCollisionEvent : public IPhysicsCollisionEvent, public IPhysicsCollisionSolver, public IPhysicsObjectEvent +{ +public: + CCollisionEvent(); + friction_t *FindFriction( CBaseEntity *pObject ); + void ShutdownFriction( friction_t &friction ); + void FrameUpdate(); + void LevelShutdown( void ); + + // IPhysicsCollisionEvent + void PreCollision( vcollisionevent_t *pEvent ); + void PostCollision( vcollisionevent_t *pEvent ); + void Friction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit, IPhysicsCollisionData *pData ); + void StartTouch( IPhysicsObject *pObject1, IPhysicsObject *pObject2, IPhysicsCollisionData *pTouchData ); + void EndTouch( IPhysicsObject *pObject1, IPhysicsObject *pObject2, IPhysicsCollisionData *pTouchData ); + void FluidStartTouch( IPhysicsObject *pObject, IPhysicsFluidController *pFluid ); + void FluidEndTouch( IPhysicsObject *pObject, IPhysicsFluidController *pFluid ); + void PostSimulationFrame(); + void ObjectEnterTrigger( IPhysicsObject *pTrigger, IPhysicsObject *pObject ); + void ObjectLeaveTrigger( IPhysicsObject *pTrigger, IPhysicsObject *pObject ); + + bool GetTriggerEvent( triggerevent_t *pEvent, CBaseEntity *pTriggerEntity ); + void BufferTouchEvents( bool enable ) { m_bBufferTouchEvents = enable; } + virtual void AddDamageEvent( CBaseEntity *pEntity, const CTakeDamageInfo &info, IPhysicsObject *pInflictorPhysics, bool bRestoreVelocity, const Vector &savedVel, const AngularImpulse &savedAngVel ); + void AddImpulseEvent( IPhysicsObject *pPhysicsObject, const Vector &vecCenterForce, const AngularImpulse &vecCenterTorque ); + void AddSetVelocityEvent( IPhysicsObject *pPhysicsObject, const Vector &vecVelocity ); + void AddRemoveObject(IServerNetworkable *pRemove); + void FlushQueuedOperations(); + + // IPhysicsCollisionSolver + int ShouldCollide( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1 ); + int ShouldSolvePenetration( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1, float dt ); + bool ShouldFreezeObject( IPhysicsObject *pObject ); + static const char *ModuleName() { return CBaseEntity::IsServer() ? "SERVER" : "CLIENT"; } + int AdditionalCollisionChecksThisTick( int currentChecksDone ) + { + //CallbackContext check(this); + if ( currentChecksDone < 1200 ) + { + DevMsg(1,"%s: VPhysics Collision detection getting expensive, check for too many convex pieces!\n", ModuleName()); + return 1200 - currentChecksDone; + } + DevMsg(1,"%s: VPhysics exceeded collision check limit (%d)!!!\nInterpenetration may result!\n", ModuleName(), currentChecksDone ); + return 0; + } + bool ShouldFreezeContacts( IPhysicsObject **pObjectList, int objectCount ); + + // IPhysicsObjectEvent + // these can be used to optimize out queries on sleeping objects + // Called when an object is woken after sleeping + virtual void ObjectWake( IPhysicsObject *pObject ); + // called when an object goes to sleep (no longer simulating) + virtual void ObjectSleep( IPhysicsObject *pObject ); + + + // locals + bool GetInflictorVelocity( IPhysicsObject *pInflictor, Vector &velocity, AngularImpulse &angVelocity ); + + void GetListOfPenetratingEntities( CBaseEntity *pSearch, CUtlVector &list ); + bool IsInCallback() { return m_inCallback > 0 ? true : false; } + +private: +#if _DEBUG + int ShouldCollide_2( IPhysicsObject *pObj0, IPhysicsObject *pObj1, void *pGameData0, void *pGameData1 ); +#endif + + void UpdateFrictionSounds(); + void UpdateTouchEvents(); + void UpdateDamageEvents(); + void UpdatePenetrateEvents( void ); + void UpdateFluidEvents(); + void UpdateRemoveObjects(); + void AddTouchEvent( CBaseEntity *pEntity0, CBaseEntity *pEntity1, int touchType, const Vector &point, const Vector &normal ); + penetrateevent_t &FindOrAddPenetrateEvent( CBaseEntity *pEntity0, CBaseEntity *pEntity1 ); + float DeltaTimeSinceLastFluid( CBaseEntity *pEntity ); + + void RestoreDamageInflictorState( IPhysicsObject *pInflictor ); + void RestoreDamageInflictorState( int inflictorStateIndex, float velocityBlend ); + int AddDamageInflictor( IPhysicsObject *pInflictorPhysics, float otherMass, const Vector &savedVel, const AngularImpulse &savedAngVel, bool addList ); + int FindDamageInflictor( IPhysicsObject *pInflictorPhysics ); + + // make the call into the entity system + void DispatchStartTouch( CBaseEntity *pEntity0, CBaseEntity *pEntity1, const Vector &point, const Vector &normal ); + void DispatchEndTouch( CBaseEntity *pEntity0, CBaseEntity *pEntity1 ); + + class CallbackContext + { + public: + CallbackContext(CCollisionEvent *pOuter) + { + m_pOuter = pOuter; + m_pOuter->m_inCallback++; + } + ~CallbackContext() + { + m_pOuter->m_inCallback--; + } + private: + CCollisionEvent *m_pOuter; + }; + friend class CallbackContext; + + friction_t m_current[4]; + gamevcollisionevent_t m_gameEvent; + CUtlVector m_triggerEvents; + triggerevent_t m_currentTriggerEvent; + CUtlVector m_touchEvents; + CUtlVector m_damageEvents; + CUtlVector m_damageInflictors; + CUtlVector m_penetrateEvents; + CUtlVector m_fluidEvents; + CUtlVector m_removeObjects; + int m_inCallback; + int m_lastTickFrictionError; // counter to control printing of the dev warning for large contact systems + bool m_bBufferTouchEvents; +}; + +#endif //#ifndef PHYSICS_COLLISIONEVENT_H diff --git a/sp/src/game/server/physics_fx.cpp b/sp/src/game/server/physics_fx.cpp new file mode 100644 index 00000000..26043720 --- /dev/null +++ b/sp/src/game/server/physics_fx.cpp @@ -0,0 +1,238 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "physics.h" +#include "te_effect_dispatch.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static int BestAxisMatchingNormal( matrix3x4_t &matrix, const Vector &normal ) +{ + float bestDot = -1; + int best = 0; + for ( int i = 0; i < 3; i++ ) + { + Vector tmp; + MatrixGetColumn( matrix, i, tmp ); + float dot = fabs(DotProduct( tmp, normal )); + if ( dot > bestDot ) + { + bestDot = dot; + best = i; + } + } + + return best; +} + +void PhysicsSplash( IPhysicsFluidController *pFluid, IPhysicsObject *pObject, CBaseEntity *pEntity ) +{ + Vector normal; + float dist; + pFluid->GetSurfacePlane( &normal, &dist ); + + matrix3x4_t &matrix = pEntity->EntityToWorldTransform(); + + // Find the local axis that best matches the water surface normal + int bestAxis = BestAxisMatchingNormal( matrix, normal ); + + Vector tangent, binormal; + MatrixGetColumn( matrix, (bestAxis+1)%3, tangent ); + binormal = CrossProduct( normal, tangent ); + VectorNormalize( binormal ); + tangent = CrossProduct( binormal, normal ); + VectorNormalize( tangent ); + + // Now we have a basis tangent to the surface that matches the object's local orientation as well as possible + // compute an OBB using this basis + + // Get object extents in basis + Vector tanPts[2], binPts[2]; + tanPts[0] = physcollision->CollideGetExtent( pObject->GetCollide(), pEntity->GetAbsOrigin(), pEntity->GetAbsAngles(), -tangent ); + tanPts[1] = physcollision->CollideGetExtent( pObject->GetCollide(), pEntity->GetAbsOrigin(), pEntity->GetAbsAngles(), tangent ); + binPts[0] = physcollision->CollideGetExtent( pObject->GetCollide(), pEntity->GetAbsOrigin(), pEntity->GetAbsAngles(), -binormal ); + binPts[1] = physcollision->CollideGetExtent( pObject->GetCollide(), pEntity->GetAbsOrigin(), pEntity->GetAbsAngles(), binormal ); + + // now compute the centered bbox + float mins[2], maxs[2], center[2], extents[2]; + mins[0] = DotProduct( tanPts[0], tangent ); + maxs[0] = DotProduct( tanPts[1], tangent ); + + mins[1] = DotProduct( binPts[0], binormal ); + maxs[1] = DotProduct( binPts[1], binormal ); + + center[0] = 0.5 * (mins[0] + maxs[0]); + center[1] = 0.5 * (mins[1] + maxs[1]); + + extents[0] = maxs[0] - center[0]; + extents[1] = maxs[1] - center[1]; + + Vector centerPoint = center[0] * tangent + center[1] * binormal + dist * normal; + + Vector axes[2]; + axes[0] = (maxs[0] - center[0]) * tangent; + axes[1] = (maxs[1] - center[1]) * binormal; + + // visualize OBB hit + /* + Vector corner1 = centerPoint - axes[0] - axes[1]; + Vector corner2 = centerPoint + axes[0] - axes[1]; + Vector corner3 = centerPoint + axes[0] + axes[1]; + Vector corner4 = centerPoint - axes[0] + axes[1]; + NDebugOverlay::Line( corner1, corner2, 0, 0, 255, false, 10 ); + NDebugOverlay::Line( corner2, corner3, 0, 0, 255, false, 10 ); + NDebugOverlay::Line( corner3, corner4, 0, 0, 255, false, 10 ); + NDebugOverlay::Line( corner4, corner1, 0, 0, 255, false, 10 ); + */ + + Vector corner[4]; + + corner[0] = centerPoint - axes[0] - axes[1]; + corner[1] = centerPoint + axes[0] - axes[1]; + corner[2] = centerPoint + axes[0] + axes[1]; + corner[3] = centerPoint - axes[0] + axes[1]; + + CEffectData data; + + if ( pObject->GetGameFlags() & FVPHYSICS_PART_OF_RAGDOLL ) + { + /* + data.m_vOrigin = centerPoint; + data.m_vNormal = normal; + VectorAngles( normal, data.m_vAngles ); + data.m_flScale = random->RandomFloat( 8, 10 ); + + DispatchEffect( "watersplash", data ); + + int splashes = 4; + Vector point; + + for ( int i = 0; i < splashes; i++ ) + { + point = RandomVector( -32.0f, 32.0f ); + point[2] = 0.0f; + + point += corner[i]; + + data.m_vOrigin = point; + data.m_vNormal = normal; + VectorAngles( normal, data.m_vAngles ); + data.m_flScale = random->RandomFloat( 4, 6 ); + + DispatchEffect( "watersplash", data ); + } + */ + + //FIXME: This code will not work correctly given how the ragdoll/fluid collision is acting currently + return; + } + + Vector vel; + pObject->GetVelocity( &vel, NULL ); + float rawSpeed = -DotProduct( normal, vel ); + + // proportional to cross-sectional area times velocity squared (fluid pressure) + float speed = rawSpeed * rawSpeed * extents[0] * extents[1] * (1.0f / 2500000.0f) * pObject->GetMass() * (0.01f); + + speed = clamp( speed, 0.f, 50.f ); + + bool bRippleOnly = false; + + // allow the entity to perform a custom splash effect + if ( pEntity->PhysicsSplash( centerPoint, normal, rawSpeed, speed ) ) + return; + + //Deny really weak hits + //FIXME: We still need to ripple the surface in this case + if ( speed <= 0.35f ) + { + if ( speed <= 0.1f ) + return; + + bRippleOnly = true; + } + + float size = RemapVal( speed, 0.35, 50, 8, 18 ); + + //Find the surface area + float radius = extents[0] * extents[1]; + //int splashes = clamp ( radius / 128.0f, 1, 2 ); //One splash for every three square feet of area + + //Msg( "Speed: %.2f, Size: %.2f\n, Radius: %.2f, Splashes: %d", speed, size, radius, splashes ); + + Vector point; + + data.m_fFlags = 0; + data.m_vOrigin = centerPoint; + data.m_vNormal = normal; + VectorAngles( normal, data.m_vAngles ); + data.m_flScale = size + random->RandomFloat( 0, 2 ); + if ( pEntity->GetWaterType() & CONTENTS_SLIME ) + { + data.m_fFlags |= FX_WATER_IN_SLIME; + } + + if ( bRippleOnly ) + { + DispatchEffect( "waterripple", data ); + } + else + { + DispatchEffect( "watersplash", data ); + } + + if ( radius > 500.0f ) + { + int splashes = random->RandomInt( 1, 4 ); + + for ( int i = 0; i < splashes; i++ ) + { + point = RandomVector( -4.0f, 4.0f ); + point[2] = 0.0f; + + point += corner[i]; + + data.m_fFlags = 0; + data.m_vOrigin = point; + data.m_vNormal = normal; + VectorAngles( normal, data.m_vAngles ); + data.m_flScale = size + random->RandomFloat( -3, 1 ); + if ( pEntity->GetWaterType() & CONTENTS_SLIME ) + { + data.m_fFlags |= FX_WATER_IN_SLIME; + } + + if ( bRippleOnly ) + { + DispatchEffect( "waterripple", data ); + } + else + { + DispatchEffect( "watersplash", data ); + } + } + } + + /* + for ( i = 0; i < splashes; i++ ) + { + point = RandomVector( -8.0f, 8.0f ); + point[2] = 0.0f; + + point += centerPoint + axes[0] * random->RandomFloat( -1, 1 ) + axes[1] * random->RandomFloat( -1, 1 ); + + data.m_vOrigin = point; + data.m_vNormal = normal; + VectorAngles( normal, data.m_vAngles ); + data.m_flScale = size + random->RandomFloat( -2, 4 ); + + DispatchEffect( "watersplash", data ); + } + */ +} diff --git a/sp/src/game/server/physics_fx.h b/sp/src/game/server/physics_fx.h new file mode 100644 index 00000000..2d267104 --- /dev/null +++ b/sp/src/game/server/physics_fx.h @@ -0,0 +1,19 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PHYSICS_FX_H +#define PHYSICS_FX_H +#ifdef _WIN32 +#pragma once +#endif + + +class CBaseEntity; +class IPhysicsFluidController; + +void PhysicsSplash( IPhysicsFluidController *pFluid, IPhysicsObject *pObject, CBaseEntity *pEntity ); + +#endif // PHYSICS_FX_H diff --git a/sp/src/game/server/physics_impact_damage.cpp b/sp/src/game/server/physics_impact_damage.cpp new file mode 100644 index 00000000..312b8a1e --- /dev/null +++ b/sp/src/game/server/physics_impact_damage.cpp @@ -0,0 +1,766 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "physics_impact_damage.h" +#include "shareddefs.h" +#include "vphysics/friction.h" +#include "vphysics/player_controller.h" +#include "world.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//============================================================================================== +// PLAYER PHYSICS DAMAGE TABLE +//============================================================================================== +static impactentry_t playerLinearTable[] = +{ + { 150*150, 5 }, + { 250*250, 10 }, + { 450*450, 20 }, + { 550*550, 50 }, + { 700*700, 100 }, + { 1000*1000, 500 }, +}; + +static impactentry_t playerAngularTable[] = +{ + { 100*100, 10 }, + { 150*150, 20 }, + { 200*200, 50 }, + { 300*300, 500 }, +}; + +impactdamagetable_t gDefaultPlayerImpactDamageTable = +{ + playerLinearTable, + playerAngularTable, + + ARRAYSIZE(playerLinearTable), + ARRAYSIZE(playerAngularTable), + + 24*24.0f, // minimum linear speed + 360*360.0f, // minimum angular speed + 2.0f, // can't take damage from anything under 2kg + + 5.0f, // anything less than 5kg is "small" + 5.0f, // never take more than 5 pts of damage from anything under 5kg + 36*36.0f, // <5kg objects must go faster than 36 in/s to do damage + + 0.0f, // large mass in kg (no large mass effects) + 1.0f, // large mass scale + 2.0f, // large mass falling scale + 320.0f, // min velocity for player speed to cause damage + +}; + +//============================================================================================== +// PLAYER-IN-VEHICLE PHYSICS DAMAGE TABLE +//============================================================================================== +static impactentry_t playerVehicleLinearTable[] = +{ + { 450*450, 5 }, + { 600*600, 10 }, + { 700*700, 25 }, + { 1000*1000, 50 }, + { 1500*1500, 100 }, + { 2000*2000, 500 }, +}; + +static impactentry_t playerVehicleAngularTable[] = +{ + { 100*100, 10 }, + { 150*150, 20 }, + { 200*200, 50 }, + { 300*300, 500 }, +}; + +impactdamagetable_t gDefaultPlayerVehicleImpactDamageTable = +{ + playerVehicleLinearTable, + playerVehicleAngularTable, + + ARRAYSIZE(playerVehicleLinearTable), + ARRAYSIZE(playerVehicleAngularTable), + + 24*24, // minimum linear speed + 360*360, // minimum angular speed + 80, // can't take damage from anything under 80 kg + + 150, // anything less than 150kg is "small" + 5, // never take more than 5 pts of damage from anything under 150kg + 36*36, // <150kg objects must go faster than 36 in/s to do damage + + 0, // large mass in kg (no large mass effects) + 1.0f, // large mass scale + 1.0f, // large mass falling scale + 0.0f, // min vel +}; + + +//============================================================================================== +// NPC PHYSICS DAMAGE TABLE +//============================================================================================== +static impactentry_t npcLinearTable[] = +{ + { 150*150, 5 }, + { 250*250, 10 }, + { 350*350, 50 }, + { 500*500, 100 }, + { 1000*1000, 500 }, +}; + +static impactentry_t npcAngularTable[] = +{ + { 100*100, 10 }, + { 150*150, 25 }, + { 200*200, 50 }, + { 250*250, 500 }, +}; + +impactdamagetable_t gDefaultNPCImpactDamageTable = +{ + npcLinearTable, + npcAngularTable, + + ARRAYSIZE(npcLinearTable), + ARRAYSIZE(npcAngularTable), + + 24*24, // minimum linear speed squared + 360*360, // minimum angular speed squared (360 deg/s to cause spin/slice damage) + 2, // can't take damage from anything under 2kg + + 5, // anything less than 5kg is "small" + 5, // never take more than 5 pts of damage from anything under 5kg + 36*36, // <5kg objects must go faster than 36 in/s to do damage + + VPHYSICS_LARGE_OBJECT_MASS, // large mass in kg + 4, // large mass scale (anything over 500kg does 4X as much energy to read from damage table) + 5, // large mass falling scale (emphasize falling/crushing damage over sideways impacts since the stress will kill you anyway) + 0.0f, // min vel +}; + +//============================================================================================== +// GLASS DAMAGE TABLE +//============================================================================================== +static impactentry_t glassLinearTable[] = +{ + { 25*25, 10 }, + { 50*50, 20 }, + { 100*100, 50 }, + { 200*200, 75 }, + { 500*500, 100 }, + { 250*250, 500 }, +}; + +static impactentry_t glassAngularTable[] = +{ + { 50*50, 25 }, + { 100*100, 50 }, + { 200*200, 100 }, + { 250*250, 500 }, +}; + +impactdamagetable_t gGlassImpactDamageTable = +{ + glassLinearTable, + glassAngularTable, + + ARRAYSIZE(glassLinearTable), + ARRAYSIZE(glassAngularTable), + + 8*8, // minimum linear speed squared + 360*360, // minimum angular speed squared (360 deg/s to cause spin/slice damage) + 2, // can't take damage from anything under 2kg + + 1, // anything less than 1kg is "small" + 10, // never take more than 10 pts of damage from anything under 1kg + 8*8, // <1kg objects must go faster than 8 in/s to do damage + + 50, // large mass in kg + 4, // large mass scale (anything over 50kg does 4X as much energy to read from damage table) + 0.0f, // min vel +}; + +//============================================================================================== +// PHYSICS TABLE NAMES +//============================================================================================== +struct damagetable_t +{ + const char *pszTableName; + impactdamagetable_t *pTable; +}; + +static damagetable_t gDamageTableRegistry[] = +{ + { + "player", + &gDefaultPlayerImpactDamageTable, + }, + { + "player_vehicle", + &gDefaultPlayerVehicleImpactDamageTable, + }, + { + "npc", + &gDefaultNPCImpactDamageTable, + }, + { + "glass", + &gGlassImpactDamageTable, + }, +}; + +//============================================================================================== +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float ReadDamageTable( impactentry_t *pTable, int tableCount, float impulse, bool bDebug ) +{ + if ( pTable ) + { + int i; + for ( i = 0; i < tableCount; i++ ) + { + if ( impulse < pTable[i].impulse ) + break; + } + if ( i > 0 ) + { + i--; + if ( bDebug ) + { + Msg("Damage %.0f, energy %.0f\n", pTable[i].damage, FastSqrt(impulse) ); + } + return pTable[i].damage; + } + } + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CalculatePhysicsImpactDamage( int index, gamevcollisionevent_t *pEvent, const impactdamagetable_t &table, float energyScale, bool allowStaticDamage, int &damageType, bool bDamageFromHeldObjects ) +{ + damageType = DMG_CRUSH; + int otherIndex = !index; + + // UNDONE: Expose a flag for self-inflicted damage? Can't think of a valid case so far. + if ( pEvent->pEntities[0] == pEvent->pEntities[1] ) + return 0; + + if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_NO_NPC_IMPACT_DMG ) + { + if( pEvent->pEntities[index]->IsNPC() || pEvent->pEntities[index]->IsPlayer() ) + { + return 0; + } + } + + // use implicit velocities on ragdolls since they may have high constraint velocities that aren't actually executed, just pushed through contacts + if (( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PART_OF_RAGDOLL) && pEvent->pEntities[index]->IsPlayer() ) + { + pEvent->pObjects[otherIndex]->GetImplicitVelocity( &pEvent->preVelocity[otherIndex], &pEvent->preAngularVelocity[otherIndex] ); + } + + // Dissolving impact damage results in death always. + if ( ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_DMG_DISSOLVE ) && + !pEvent->pEntities[index]->IsEFlagSet(EFL_NO_DISSOLVE) ) + { + damageType |= DMG_DISSOLVE; + return 1000; + } + + if ( energyScale <= 0.0f ) + return 0; + + const int gameFlagsNoDamage = FVPHYSICS_CONSTRAINT_STATIC | FVPHYSICS_NO_IMPACT_DMG; + + // NOTE: Crushing damage is handled by stress calcs in vphysics update functions, this is ONLY impact damage + // this is a non-moving object due to a constraint - no damage + if ( pEvent->pObjects[otherIndex]->GetGameFlags() & gameFlagsNoDamage ) + return 0; + + // If it doesn't take damage from held objects and the object is being held - no damage + if ( !bDamageFromHeldObjects && ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) ) + { + // If it doesn't take damage from held objects - no damage + if ( !bDamageFromHeldObjects ) + return 0; + } + + if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_MULTIOBJECT_ENTITY ) + { + // UNDONE: Add up mass here for car wheels and prop_ragdoll pieces? + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pEvent->pEntities[otherIndex]->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + if ( pList[i]->GetGameFlags() & gameFlagsNoDamage ) + return 0; + } + } + + if ( pEvent->pObjects[index]->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + // players can't damage held objects + if ( pEvent->pEntities[otherIndex]->IsPlayer() ) + return 0; + + allowStaticDamage = false; + } + +#if 0 + { + PhysGetDamageInflictorVelocityStartOfFrame( pEvent->pObjects[otherIndex], pEvent->preVelocity[otherIndex], pEvent->preAngularVelocity[otherIndex] ); + } +#endif + + float otherSpeedSqr = pEvent->preVelocity[otherIndex].LengthSqr(); + float otherAngSqr = 0; + + // factor in angular for sharp objects + if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_DMG_SLICE ) + { + otherAngSqr = pEvent->preAngularVelocity[otherIndex].LengthSqr(); + } + + float otherMass = pEvent->pObjects[otherIndex]->GetMass(); + + if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + // if the player is holding the object, use its real mass (player holding reduced the mass) + CBasePlayer *pPlayer = NULL; + + if ( 1 == gpGlobals->maxClients ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + else + { + // See which MP player is holding the physics object and then use that player to get the real mass of the object. + // This is ugly but better than having linkage between an object and its "holding" player. + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *tempPlayer = UTIL_PlayerByIndex( i ); + if ( tempPlayer && pEvent->pEntities[index] == tempPlayer->GetHeldObject() ) + { + pPlayer = tempPlayer; + break; + } + } + } + + if ( pPlayer ) + { + otherMass = pPlayer->GetHeldObjectMass( pEvent->pObjects[otherIndex] ); + } + } + + // NOTE: sum the mass of each object in this system for the purpose of damage + if ( pEvent->pEntities[otherIndex] && (pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_MULTIOBJECT_ENTITY) ) + { + otherMass = PhysGetEntityMass( pEvent->pEntities[otherIndex] ); + } + + if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_HEAVY_OBJECT ) + { + otherMass = table.largeMassMin; + if ( energyScale < 2.0f ) + { + energyScale = 2.0f; + } + } + + // UNDONE: allowStaticDamage is a hack - work out some method for + // breakable props to impact the world and break!! + if ( !allowStaticDamage ) + { + if ( otherMass < table.minMass ) + return 0; + // check to see if the object is small + if ( otherMass < table.smallMassMax && otherSpeedSqr < table.smallMassMinSpeedSqr ) + return 0; + + if ( otherSpeedSqr < table.minSpeedSqr && otherAngSqr < table.minRotSpeedSqr ) + return 0; + } + + // Add extra oomph for floating objects + if ( pEvent->pEntities[index]->IsFloating() && !pEvent->pEntities[otherIndex]->IsWorld() ) + { + if ( energyScale < 3.0f ) + { + energyScale = 3.0f; + } + } + + float damage = 0; + bool bDebug = false;//(&table == &gDefaultPlayerImpactDamageTable); + + // don't ever take spin damage from slowly spinning objects + if ( otherAngSqr > table.minRotSpeedSqr ) + { + Vector otherInertia = pEvent->pObjects[otherIndex]->GetInertia(); + float angularMom = DotProductAbs( otherInertia, pEvent->preAngularVelocity[otherIndex] ); + damage = ReadDamageTable( table.angularTable, table.angularCount, angularMom * energyScale, bDebug ); + if ( damage > 0 ) + { +// Msg("Spin : %.1f, Damage %.0f\n", FastSqrt(angularMom), damage ); + damageType |= DMG_SLASH; + } + } + + float deltaV = pEvent->preVelocity[index].Length() - pEvent->postVelocity[index].Length(); + float mass = pEvent->pObjects[index]->GetMass(); + + // If I lost speed, and I lost less than min velocity, then filter out this energy + if ( deltaV > 0 && deltaV < table.myMinVelocity ) + { + deltaV = 0; + } + float eliminatedEnergy = deltaV * deltaV * mass; + + deltaV = pEvent->preVelocity[otherIndex].Length() - pEvent->postVelocity[otherIndex].Length(); + float otherEliminatedEnergy = deltaV * deltaV * otherMass; + + // exaggerate the effects of really large objects + if ( otherMass >= table.largeMassMin ) + { + otherEliminatedEnergy *= table.largeMassScale; + float dz = pEvent->preVelocity[otherIndex].z - pEvent->postVelocity[otherIndex].z; + + if ( deltaV > 0 && dz < 0 && pEvent->preVelocity[otherIndex].z < 0 ) + { + float factor = fabs(dz / deltaV); + otherEliminatedEnergy *= (1 + factor * (table.largeMassFallingScale - 1.0f)); + } + } + + eliminatedEnergy += otherEliminatedEnergy; + + // now in units of this character's speed squared + float invMass = pEvent->pObjects[index]->GetInvMass(); + if ( !pEvent->pObjects[index]->IsMoveable() ) + { + // inv mass is zero, but impact damage is enabled on this + // prop, so recompute: + invMass = 1.0f / pEvent->pObjects[index]->GetMass(); + } + else if ( pEvent->pObjects[index]->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + // if the player is holding the object, use it's real mass (player holding reduced the mass) + CBasePlayer *pPlayer = NULL; + if ( 1 == gpGlobals->maxClients ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + else + { + // See which MP player is holding the physics object and then use that player to get the real mass of the object. + // This is ugly but better than having linkage between an object and its "holding" player. + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *tempPlayer = UTIL_PlayerByIndex( i ); + if ( tempPlayer && pEvent->pEntities[index] == tempPlayer->GetHeldObject() ) + { + pPlayer = tempPlayer; + break; + } + } + } + } + + eliminatedEnergy *= invMass * energyScale; + + damage += ReadDamageTable( table.linearTable, table.linearCount, eliminatedEnergy, bDebug ); + + if ( !pEvent->pObjects[otherIndex]->IsStatic() && otherMass < table.smallMassMax && table.smallMassCap > 0 ) + { + damage = clamp( damage, 0.f, table.smallMassCap ); + } + + return damage; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CalculateDefaultPhysicsDamage( int index, gamevcollisionevent_t *pEvent, float energyScale, bool allowStaticDamage, int &damageType, string_t iszDamageTableName, bool bDamageFromHeldObjects ) +{ + // If we have a specified damage table, find it and use it instead + if ( iszDamageTableName != NULL_STRING ) + { + for ( int i = 0; i < ARRAYSIZE(gDamageTableRegistry); i++ ) + { + if ( !Q_strcmp( gDamageTableRegistry[i].pszTableName, STRING(iszDamageTableName) ) ) + return CalculatePhysicsImpactDamage( index, pEvent, *(gDamageTableRegistry[i].pTable), energyScale, allowStaticDamage, damageType, bDamageFromHeldObjects ); + } + + Warning("Failed to find custom physics damage table name: %s\n", STRING(iszDamageTableName) ); + } + + return CalculatePhysicsImpactDamage( index, pEvent, gDefaultNPCImpactDamageTable, energyScale, allowStaticDamage, damageType, bDamageFromHeldObjects ); +} + +static bool IsPhysicallyControlled( CBaseEntity *pEntity, IPhysicsObject *pPhysics ) +{ + bool isPhysical = false; + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + isPhysical = true; + } + else + { + if ( pPhysics->GetShadowController() ) + { + isPhysical = pPhysics->GetShadowController()->IsPhysicallyControlled(); + } + } + return isPhysical; +} +float CalculateObjectStress( IPhysicsObject *pObject, CBaseEntity *pInputOwnerEntity, vphysics_objectstress_t *pOutput ) +{ + CUtlVector< CBaseEntity * > pObjectList; + CUtlVector< Vector > objectForce; + bool hasLargeObject = false; + + // add a slot for static objects + pObjectList.AddToTail( NULL ); + objectForce.AddToTail( vec3_origin ); + // add a slot for friendly objects + pObjectList.AddToTail( NULL ); + objectForce.AddToTail( vec3_origin ); + + CBaseCombatCharacter *pBCC = pInputOwnerEntity->MyCombatCharacterPointer(); + + IPhysicsFrictionSnapshot *pSnapshot = pObject->CreateFrictionSnapshot(); + float objMass = pObject->GetMass(); + while ( pSnapshot->IsValid() ) + { + float force = pSnapshot->GetNormalForce(); + if ( force > 0.0f ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + CBaseEntity *pOtherEntity = static_cast(pOther->GetGameData()); + if ( !pOtherEntity ) + { + // object was just deleted, but we still have a contact point this frame... + // just assume it came from the world. + pOtherEntity = GetWorldEntity(); + } + CBaseEntity *pOtherOwner = pOtherEntity; + if ( pOtherEntity->GetOwnerEntity() ) + { + pOtherOwner = pOtherEntity->GetOwnerEntity(); + } + + int outIndex = 0; + if ( !pOther->IsMoveable() ) + { + outIndex = 0; + } + // NavIgnored objects are often being pushed by a friendly + else if ( pBCC && (pBCC->IRelationType( pOtherOwner ) == D_LI || pOtherEntity->IsNavIgnored()) ) + { + outIndex = 1; + } + // player held objects do no stress + else if ( pOther->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + outIndex = 1; + } + else + { + if ( pOther->GetMass() >= VPHYSICS_LARGE_OBJECT_MASS ) + { + if ( pInputOwnerEntity->GetGroundEntity() != pOtherEntity) + { + hasLargeObject = true; + } + } + // moveable, non-friendly + + // aggregate contacts over each object to avoid greater stress in multiple contact cases + // NOTE: Contacts should be in order, so this shouldn't ever search, but just in case + outIndex = pObjectList.Count(); + for ( int i = pObjectList.Count()-1; i >= 2; --i ) + { + if ( pObjectList[i] == pOtherOwner ) + { + outIndex = i; + break; + } + } + if ( outIndex == pObjectList.Count() ) + { + pObjectList.AddToTail( pOtherOwner ); + objectForce.AddToTail( vec3_origin ); + } + } + + if ( outIndex != 0 && pInputOwnerEntity->GetMoveType() != MOVETYPE_VPHYSICS && !IsPhysicallyControlled(pOtherEntity, pOther) ) + { + // UNDONE: Test this! This is to remove any shadow/shadow stress. The game should handle this with blocked/damage + force = 0.0f; + } + + Vector normal; + pSnapshot->GetSurfaceNormal( normal ); + objectForce[outIndex] += normal * force; + } + pSnapshot->NextFrictionData(); + } + pObject->DestroyFrictionSnapshot( pSnapshot ); + pSnapshot = NULL; + + // clear out all friendly force + objectForce[1].Init(); + + float sum = 0; + Vector negativeForce = vec3_origin; + Vector positiveForce = vec3_origin; + + Assert( pObjectList.Count() == objectForce.Count() ); + for ( int objectIndex = pObjectList.Count()-1; objectIndex >= 0; --objectIndex ) + { + sum += objectForce[objectIndex].Length(); + for ( int i = 0; i < 3; i++ ) + { + if ( objectForce[objectIndex][i] < 0 ) + { + negativeForce[i] -= objectForce[objectIndex][i]; + } + else + { + positiveForce[i] += objectForce[objectIndex][i]; + } + } + } + + // "external" stress is two way (something pushes on the object and something else pushes back) + // so the set of minimum values per component are the projections of the two-way force + // "internal" stress is one way (the object is pushing against something OR something pushing back) + // the momentum must have come from inside the object (gravity, controller, etc) + Vector internalForce = vec3_origin; + Vector externalForce = vec3_origin; + + for ( int i = 0; i < 3; i++ ) + { + if ( negativeForce[i] < positiveForce[i] ) + { + internalForce[i] = positiveForce[i] - negativeForce[i]; + externalForce[i] = negativeForce[i]; + } + else + { + internalForce[i] = negativeForce[i] - positiveForce[i]; + externalForce[i] = positiveForce[i]; + } + } + + // sum is kg in / s + Vector gravVector; + physenv->GetGravity( &gravVector ); + float gravity = gravVector.Length(); + if ( pInputOwnerEntity->GetMoveType() != MOVETYPE_VPHYSICS && pObject->IsMoveable() ) + { + Vector lastVel; + lastVel.Init(); + if ( pObject->GetShadowController() ) + { + pObject->GetShadowController()->GetLastImpulse( &lastVel ); + } + else + { + if ( ( pObject->GetCallbackFlags() & CALLBACK_IS_PLAYER_CONTROLLER ) ) + { + CBasePlayer *pPlayer = ToBasePlayer( pInputOwnerEntity ); + IPhysicsPlayerController *pController = pPlayer ? pPlayer->GetPhysicsController() : NULL; + if ( pController ) + { + pController->GetLastImpulse( &lastVel ); + } + } + } + + // Work in progress... + + // Peek into the controller for this object. Look at the input velocity and make sure it's all + // accounted for in the computed stress. If not, redistribute external to internal as it's + // probably being reflected in a way we can't measure here. + float inputLen = lastVel.Length() * (1.0f / physenv->GetSimulationTimestep()) * objMass; + if ( inputLen > 0.0f ) + { + float internalLen = internalForce.Length(); + if ( internalLen < inputLen ) + { + float ratio = internalLen / inputLen; + Vector delta = internalForce * (1.0f - ratio); + internalForce += delta; + float deltaLen = delta.Length(); + sum -= deltaLen; + float extLen = VectorNormalize(externalForce) - deltaLen; + if ( extLen < 0 ) + { + extLen = 0; + } + externalForce *= extLen; + } + } + } + + float invGravity = gravity; + if ( invGravity <= 0 ) + { + invGravity = 1.0f; + } + else + { + invGravity = 1.0f / invGravity; + } + sum *= invGravity; + internalForce *= invGravity; + externalForce *= invGravity; + if ( !pObject->IsMoveable() ) + { + // the above algorithm will see almost all force as internal if the object is not moveable + // (it doesn't push on anything else, so nothing is reciprocated) + // exceptions for friction of a single other object with multiple contact points on this object + + // But the game wants to see it all as external because obviously the object can't move, so it can't have + // internal stress + externalForce = internalForce; + internalForce.Init(); + + if ( !pObject->IsStatic() ) + { + sum += objMass; + } + } + else + { + // assume object is at rest + if ( sum > objMass ) + { + sum = objMass + (sum-objMass) * 0.5; + } + } + + if ( pOutput ) + { + pOutput->exertedStress = internalForce.Length(); + pOutput->receivedStress = externalForce.Length(); + pOutput->hasNonStaticStress = pObjectList.Count() > 2 ? true : false; + pOutput->hasLargeObjectContact = hasLargeObject; + } + + // sum is now kg + return sum; +} diff --git a/sp/src/game/server/physics_impact_damage.h b/sp/src/game/server/physics_impact_damage.h new file mode 100644 index 00000000..2bd46a1e --- /dev/null +++ b/sp/src/game/server/physics_impact_damage.h @@ -0,0 +1,66 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PHYSICS_IMPACT_DAMAGE_H +#define PHYSICS_IMPACT_DAMAGE_H +#ifdef _WIN32 +#pragma once +#endif + + +struct impactentry_t +{ + float impulse; + float damage; +}; + +// UNDONE: Add a flag to turn off aggregation of mass in object systems (e.g. ragdolls, vehicles)? +struct impactdamagetable_t +{ + impactentry_t *linearTable; + impactentry_t *angularTable; + int linearCount; // array size of linearTable + int angularCount; // array size of angularTable + + float minSpeedSqr; // minimum squared impact speed for damage + float minRotSpeedSqr; + float minMass; // minimum mass to do damage + + // filter out reall small objects, set all to zero to disable + float smallMassMax; + float smallMassCap; + float smallMassMinSpeedSqr; + + // exaggerate the effects of really large objects, set all to 1 to disable + float largeMassMin; + float largeMassScale; + float largeMassFallingScale; // emphasize downward impacts so that this will kill instead of stress (we have more information here than there) + float myMinVelocity; // filter out any energy lost by me unless my velocity is greater than this +}; + + + +extern impactdamagetable_t gDefaultNPCImpactDamageTable; +extern impactdamagetable_t gDefaultPlayerImpactDamageTable; +extern impactdamagetable_t gDefaultPlayerVehicleImpactDamageTable; + +// NOTE Default uses default NPC table +float CalculateDefaultPhysicsDamage( int index, gamevcollisionevent_t *pEvent, float energyScale, bool allowStaticDamage, int &damageTypeOut, string_t iszDamageTableName = NULL_STRING, bool bDamageFromHeldObjects = false ); + +// use passes in the table +float CalculatePhysicsImpactDamage( int index, gamevcollisionevent_t *pEvent, const impactdamagetable_t &table, float energyScale, bool allowStaticDamage, int &damageTypeOut, bool bDamageFromHeldObjects = false ); + +struct vphysics_objectstress_t +{ + float exertedStress; + float receivedStress; + bool hasNonStaticStress; + bool hasLargeObjectContact; +}; + +float CalculateObjectStress( IPhysicsObject *pObject, CBaseEntity *pOwnerEntity, vphysics_objectstress_t *pOutput ); + +#endif // PHYSICS_IMPACT_DAMAGE_H diff --git a/sp/src/game/server/physics_main.cpp b/sp/src/game/server/physics_main.cpp new file mode 100644 index 00000000..b0b2d867 --- /dev/null +++ b/sp/src/game/server/physics_main.cpp @@ -0,0 +1,2073 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Physics simulation for non-havok/ipion objects +// +// $NoKeywords: $ +//=============================================================================// + + +#include "cbase.h" +#ifdef _WIN32 +#include "typeinfo.h" +// BUGBUG: typeinfo stomps some of the warning settings (in yvals.h) +#pragma warning(disable:4244) +#elif POSIX +#include +#else +#error "need typeinfo defined" +#endif + +#include "player.h" +#include "ai_basenpc.h" +#include "gamerules.h" +#include "vphysics_interface.h" +#include "mempool.h" +#include "entitylist.h" +#include "engine/IEngineSound.h" +#include "datacache/imdlcache.h" +#include "ispatialpartition.h" +#include "tier0/vprof.h" +#include "movevars_shared.h" +#include "hierarchy.h" +#include "trains.h" +#include "vphysicsupdateai.h" +#include "tier0/vcrmode.h" +#include "pushentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ConVar think_limit; +#ifdef _XBOX +ConVar vprof_think_limit( "vprof_think_limit", "0" ); +#endif + +ConVar vprof_scope_entity_thinks( "vprof_scope_entity_thinks", "0" ); +ConVar vprof_scope_entity_gamephys( "vprof_scope_entity_gamephys", "0" ); + +ConVar npc_vphysics ( "npc_vphysics","0"); +//----------------------------------------------------------------------------- +// helper method for trace hull as used by physics... +//----------------------------------------------------------------------------- +static void Physics_TraceEntity( CBaseEntity* pBaseEntity, const Vector &vecAbsStart, + const Vector &vecAbsEnd, unsigned int mask, trace_t *ptr ) +{ + // FIXME: I really am not sure the best way of doing this + // The TraceHull code below for shots will make sure the object passes + // through shields which do not block that damage type. It will also + // send messages to the shields that they've been hit. + if (pBaseEntity->GetDamageType() != DMG_GENERIC) + { + GameRules()->WeaponTraceEntity( pBaseEntity, vecAbsStart, vecAbsEnd, mask, ptr ); + } + else + { + UTIL_TraceEntity( pBaseEntity, vecAbsStart, vecAbsEnd, mask, ptr ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Does not change the entities velocity at all +// Input : push - +// Output : trace_t +//----------------------------------------------------------------------------- +static void PhysicsCheckSweep( CBaseEntity *pEntity, const Vector& vecAbsStart, const Vector &vecAbsDelta, trace_t *pTrace ) +{ + unsigned int mask = pEntity->PhysicsSolidMaskForEntity(); + + Vector vecAbsEnd; + VectorAdd( vecAbsStart, vecAbsDelta, vecAbsEnd ); + + // Set collision type + if ( !pEntity->IsSolid() || pEntity->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS) ) + { + if ( pEntity->GetMoveParent() ) + { + UTIL_ClearTrace( *pTrace ); + return; + } + + // don't collide with monsters + mask &= ~CONTENTS_MONSTER; + } + + Physics_TraceEntity( pEntity, vecAbsStart, vecAbsEnd, mask, pTrace ); +} + +CPhysicsPushedEntities s_PushedEntities; +#ifndef TF_DLL +CPhysicsPushedEntities *g_pPushedEntities = &s_PushedEntities; +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPhysicsPushedEntities::CPhysicsPushedEntities( void ) : m_rgPusher(8, 8), m_rgMoved(32, 32) +{ + m_flMoveTime = -1.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: Store off entity and copy original origin to temporary array +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::AddEntity( CBaseEntity *ent ) +{ + int i = m_rgMoved.AddToTail(); + m_rgMoved[i].m_pEntity = ent; + m_rgMoved[i].m_vecStartAbsOrigin = ent->GetAbsOrigin(); +} + + +//----------------------------------------------------------------------------- +// Unlink + relink the pusher list so we can actually do the push +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::UnlinkPusherList( int *pPusherHandles ) +{ + for ( int i = m_rgPusher.Count(); --i >= 0; ) + { + pPusherHandles[i] = partition->HideElement( m_rgPusher[i].m_pEntity->CollisionProp()->GetPartitionHandle() ); + } +} + +void CPhysicsPushedEntities::RelinkPusherList( int *pPusherHandles ) +{ + for ( int i = m_rgPusher.Count(); --i >= 0; ) + { + partition->UnhideElement( m_rgPusher[i].m_pEntity->CollisionProp()->GetPartitionHandle(), pPusherHandles[i] ); + } +} + + +//----------------------------------------------------------------------------- +// Compute the direction to move the rotation blocker +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::ComputeRotationalPushDirection( CBaseEntity *pBlocker, const RotatingPushMove_t &rotPushMove, Vector *pMove, CBaseEntity *pRoot ) +{ + // calculate destination position + // "start" is relative to the *root* pusher, world orientation + Vector start = pBlocker->CollisionProp()->GetCollisionOrigin(); + if ( pRoot->GetSolid() == SOLID_VPHYSICS ) + { + // HACKHACK: Use move dir to guess which corner of the box determines contact and rotate the box so + // that corner remains in the same local position. + // BUGBUG: This will break, but not as badly as the previous solution!!! + Vector vecAbsMins, vecAbsMaxs; + pBlocker->CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); + start.x = (pMove->x < 0) ? vecAbsMaxs.x : vecAbsMins.x; + start.y = (pMove->y < 0) ? vecAbsMaxs.y : vecAbsMins.y; + start.z = (pMove->z < 0) ? vecAbsMaxs.z : vecAbsMins.z; + + CBasePlayer *pPlayer = ToBasePlayer(pBlocker); + if ( pPlayer ) + { + // notify the player physics code so it can use vphysics to keep players from getting stuck + pPlayer->SetPhysicsFlag( PFLAG_GAMEPHYSICS_ROTPUSH, true ); + } + } + + // org is pusher local coordinate of start + Vector local; + // transform starting point into local space + VectorITransform( start, rotPushMove.startLocalToWorld, local ); + // rotate local org into world space at end of rotation + Vector end; + VectorTransform( local, rotPushMove.endLocalToWorld, end ); + + // move is the difference (in world space) that the move will push this object + VectorSubtract( end, start, *pMove ); +} + +class CTraceFilterPushFinal : public CTraceFilterSimple +{ + DECLARE_CLASS( CTraceFilterPushFinal, CTraceFilterSimple ); + +public: + CTraceFilterPushFinal( CBaseEntity *pEntity, int nCollisionGroup ) + : CTraceFilterSimple( pEntity, nCollisionGroup ) + { + + } + + bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + Assert( dynamic_cast(pHandleEntity) ); + CBaseEntity *pTestEntity = static_cast(pHandleEntity); + + // UNDONE: This should really filter to just the pushing entities + if ( pTestEntity->GetMoveType() == MOVETYPE_VPHYSICS && + pTestEntity->VPhysicsGetObject() && pTestEntity->VPhysicsGetObject()->IsMoveable() ) + return false; + + return BaseClass::ShouldHitEntity( pHandleEntity, contentsMask ); + } + +}; + +bool CPhysicsPushedEntities::IsPushedPositionValid( CBaseEntity *pBlocker ) +{ + CTraceFilterPushFinal pushFilter(pBlocker, pBlocker->GetCollisionGroup() ); + + trace_t trace; + UTIL_TraceEntity( pBlocker, pBlocker->GetAbsOrigin(), pBlocker->GetAbsOrigin(), pBlocker->PhysicsSolidMaskForEntity(), &pushFilter, &trace ); + + return !trace.startsolid; +} + +//----------------------------------------------------------------------------- +// Speculatively checks to see if all entities in this list can be pushed +//----------------------------------------------------------------------------- +bool CPhysicsPushedEntities::SpeculativelyCheckPush( PhysicsPushedInfo_t &info, const Vector &vecAbsPush, bool bRotationalPush ) +{ + CBaseEntity *pBlocker = info.m_pEntity; + + // See if it's possible to move the entity, but disable all pushers in the hierarchy first + int *pPusherHandles = (int*)stackalloc( m_rgPusher.Count() * sizeof(int) ); + UnlinkPusherList( pPusherHandles ); + CTraceFilterPushMove pushFilter(pBlocker, pBlocker->GetCollisionGroup() ); + + Vector pushDestPosition = pBlocker->GetAbsOrigin() + vecAbsPush; + UTIL_TraceEntity( pBlocker, pBlocker->GetAbsOrigin(), pushDestPosition, + pBlocker->PhysicsSolidMaskForEntity(), &pushFilter, &info.m_Trace ); + + RelinkPusherList(pPusherHandles); + info.m_bPusherIsGround = false; + if ( pBlocker->GetGroundEntity() && pBlocker->GetGroundEntity()->GetRootMoveParent() == m_rgPusher[0].m_pEntity ) + { + info.m_bPusherIsGround = true; + } + + bool bIsUnblockable = (m_bIsUnblockableByPlayer && (pBlocker->IsPlayer() || pBlocker->MyNPCPointer())) ? true : false; + if ( bIsUnblockable ) + { + pBlocker->SetAbsOrigin( pushDestPosition ); + } + else + { + // Move the blocker into its new position + if ( info.m_Trace.fraction ) + { + pBlocker->SetAbsOrigin( info.m_Trace.endpos ); + } + + // We're not blocked if the blocker is point-sized or non-solid + if ( pBlocker->IsPointSized() || !pBlocker->IsSolid() || + pBlocker->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS ) ) + { + return true; + } + + if ( (!bRotationalPush) && (info.m_Trace.fraction == 1.0) ) + { + //Assert( pBlocker->PhysicsTestEntityPosition() == false ); + if ( !IsPushedPositionValid(pBlocker) ) + { + Warning("Interpenetrating entities! (%s and %s)\n", + pBlocker->GetClassname(), m_rgPusher[0].m_pEntity->GetClassname() ); + } + + return true; + } + } + + // Check to see if we're still blocked by the pushers + // FIXME: If the trace fraction == 0 can we early out also? + info.m_bBlocked = !IsPushedPositionValid(pBlocker); + + if ( !info.m_bBlocked ) + return true; + + // if the player is blocking the train try nudging him around to fix accumulated error + if ( bIsUnblockable ) + { + Vector org = pBlocker->GetAbsOrigin(); + for ( int checkCount = 0; checkCount < 4; checkCount++ ) + { + Vector move; + MatrixGetColumn( m_rgPusher[0].m_pEntity->EntityToWorldTransform(), checkCount>>1, move ); + + // alternate movements 1/2" in each direction + float factor = ( checkCount & 1 ) ? -0.5f : 0.5f; + pBlocker->SetAbsOrigin( org + move * factor ); + info.m_bBlocked = !IsPushedPositionValid(pBlocker); + if ( !info.m_bBlocked ) + return true; + } + pBlocker->SetAbsOrigin( pushDestPosition ); + +#ifndef TF_DLL + DevMsg(1, "Ignoring player blocking train!\n"); +#endif + return true; + } + return false; +} + + +//----------------------------------------------------------------------------- +// Speculatively checks to see if all entities in this list can be pushed +//----------------------------------------------------------------------------- +bool CPhysicsPushedEntities::SpeculativelyCheckRotPush( const RotatingPushMove_t &rotPushMove, CBaseEntity *pRoot ) +{ + Vector vecAbsPush; + m_nBlocker = -1; + for (int i = m_rgMoved.Count(); --i >= 0; ) + { + ComputeRotationalPushDirection( m_rgMoved[i].m_pEntity, rotPushMove, &vecAbsPush, pRoot ); + if (!SpeculativelyCheckPush( m_rgMoved[i], vecAbsPush, true )) + { + m_nBlocker = i; + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Speculatively checks to see if all entities in this list can be pushed +//----------------------------------------------------------------------------- +bool CPhysicsPushedEntities::SpeculativelyCheckLinearPush( const Vector &vecAbsPush ) +{ + m_nBlocker = -1; + for (int i = m_rgMoved.Count(); --i >= 0; ) + { + if (!SpeculativelyCheckPush( m_rgMoved[i], vecAbsPush, false )) + { + m_nBlocker = i; + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Causes all entities in the list to touch triggers from their prev position +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::FinishPushers() +{ + // We succeeded! Now that we know the final location of all entities, + // touch triggers + update physics objects + do other fixup + for ( int i = m_rgPusher.Count(); --i >= 0; ) + { + PhysicsPusherInfo_t &info = m_rgPusher[i]; + + // Cause touch functions to be called + // FIXME: Need to make moved entities not touch triggers until we know we're ok + // FIXME: it'd be better for the engine to just have a touch method + info.m_pEntity->PhysicsTouchTriggers( &info.m_vecStartAbsOrigin ); + + info.m_pEntity->UpdatePhysicsShadowToCurrentPosition( gpGlobals->frametime ); + } +} + + +//----------------------------------------------------------------------------- +// Causes all entities in the list to touch triggers from their prev position +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::FinishRotPushedEntity( CBaseEntity *pPushedEntity, const RotatingPushMove_t &rotPushMove ) +{ + // Impart angular velocity of push onto pushed objects + if ( pPushedEntity->IsPlayer() ) + { + QAngle angVel = pPushedEntity->GetLocalAngularVelocity(); + angVel[1] = rotPushMove.amove[1]; + pPushedEntity->SetLocalAngularVelocity(angVel); + + // Look up associated client + CBasePlayer *player = ( CBasePlayer * )pPushedEntity; + player->pl.fixangle = FIXANGLE_RELATIVE; + // Because we can run multiple ticks per server frame, accumulate a total offset here instead of straight + // setting it. The engine will reset anglechange to 0 when the message is actually sent to the client + player->pl.anglechange += rotPushMove.amove; + } + else + { + QAngle angles = pPushedEntity->GetAbsAngles(); + + // only rotate YAW with pushing. Freely rotateable entities should either use VPHYSICS + // or be set up as children + angles.y += rotPushMove.amove.y; + pPushedEntity->SetAbsAngles( angles ); + } +} + + +//----------------------------------------------------------------------------- +// Causes all entities in the list to touch triggers from their prev position +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::FinishPush( bool bIsRotPush, const RotatingPushMove_t *pRotPushMove ) +{ + FinishPushers(); + + for ( int i = m_rgMoved.Count(); --i >= 0; ) + { + PhysicsPushedInfo_t &info = m_rgMoved[i]; + CBaseEntity *pPushedEntity = info.m_pEntity; + + // Cause touch functions to be called + // FIXME: it'd be better for the engine to just have a touch method + info.m_pEntity->PhysicsTouchTriggers( &info.m_vecStartAbsOrigin ); + info.m_pEntity->UpdatePhysicsShadowToCurrentPosition( gpGlobals->frametime ); + CAI_BaseNPC *pNPC = info.m_pEntity->MyNPCPointer(); + if ( info.m_bPusherIsGround && pNPC ) + { + pNPC->NotifyPushMove(); + } + + + // Register physics impacts... + if (info.m_Trace.m_pEnt) + { + pPushedEntity->PhysicsImpact( info.m_Trace.m_pEnt, info.m_Trace ); + } + + if (bIsRotPush) + { + FinishRotPushedEntity( pPushedEntity, *pRotPushMove ); + } + } +} + +// save initial state when beginning a push sequence +void CPhysicsPushedEntities::BeginPush( CBaseEntity *pRoot ) +{ + m_rgMoved.RemoveAll(); + m_rgPusher.RemoveAll(); + + m_rootPusherStartLocalOrigin = pRoot->GetLocalOrigin(); + m_rootPusherStartLocalAngles = pRoot->GetLocalAngles(); + m_rootPusherStartLocaltime = pRoot->GetLocalTime(); +} + +// store off a list of what has changed - so vphysicsUpdate can undo this if the object gets blocked +void CPhysicsPushedEntities::StoreMovedEntities( physicspushlist_t &list ) +{ + list.localMoveTime = m_rootPusherStartLocaltime; + list.localOrigin = m_rootPusherStartLocalOrigin; + list.localAngles = m_rootPusherStartLocalAngles; + list.pushedCount = CountMovedEntities(); + Assert(list.pushedCount < ARRAYSIZE(list.pushedEnts)); + if ( list.pushedCount > ARRAYSIZE(list.pushedEnts) ) + { + list.pushedCount = ARRAYSIZE(list.pushedEnts); + } + for ( int i = 0; i < list.pushedCount; i++ ) + { + list.pushedEnts[i] = m_rgMoved[i].m_pEntity; + list.pushVec[i] = m_rgMoved[i].m_pEntity->GetAbsOrigin() - m_rgMoved[i].m_vecStartAbsOrigin; + } +} + +//----------------------------------------------------------------------------- +// Registers a blockage +//----------------------------------------------------------------------------- +CBaseEntity *CPhysicsPushedEntities::RegisterBlockage() +{ + Assert( m_nBlocker >= 0 ); + + // Generate a PhysicsImpact against the blocker... + PhysicsPushedInfo_t &info = m_rgMoved[m_nBlocker]; + if ( info.m_Trace.m_pEnt ) + { + info.m_pEntity->PhysicsImpact( info.m_Trace.m_pEnt, info.m_Trace ); + } + + // This is the dude + return info.m_pEntity; +} + + +//----------------------------------------------------------------------------- +// Purpose: Restore entities that might have been moved +// Input : fromrotation - if the move is from a rotation, then angular move must also be reverted +// *amove - +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::RestoreEntities( ) +{ + // Reset all of the pushed entities to get them back into place also + for ( int i = m_rgMoved.Count(); --i >= 0; ) + { + m_rgMoved[ i ].m_pEntity->SetAbsOrigin( m_rgMoved[ i ].m_vecStartAbsOrigin ); + } +} + + + + +//----------------------------------------------------------------------------- +// Purpose: This is a trace filter that only hits an exclusive list of entities +//----------------------------------------------------------------------------- +class CTraceFilterAgainstEntityList : public ITraceFilter +{ +public: + virtual bool ShouldHitEntity( IHandleEntity *pEntity, int contentsMask ) + { + for ( int i = m_entityList.Count()-1; i >= 0; --i ) + { + if ( m_entityList[i] == pEntity ) + return true; + } + + return false; + } + + virtual TraceType_t GetTraceType() const + { + return TRACE_ENTITIES_ONLY; + } + + void AddEntityToHit( IHandleEntity *pEntity ) + { + m_entityList.AddToTail(pEntity); + } + + CUtlVector m_entityList; +}; + +//----------------------------------------------------------------------------- +// Generates a list of potential blocking entities +//----------------------------------------------------------------------------- +class CPushBlockerEnum : public IPartitionEnumerator +{ +public: + CPushBlockerEnum( CPhysicsPushedEntities *pPushedEntities ) : m_pPushedEntities(pPushedEntities) + { + // All elements are part of the same hierarchy, so they all have + // the same root, so it doesn't matter which one we grab + m_pRootHighestParent = m_pPushedEntities->m_rgPusher[0].m_pEntity->GetRootMoveParent(); + ++s_nEnumCount; + + m_collisionGroupCount = 0; + for ( int i = m_pPushedEntities->m_rgPusher.Count(); --i >= 0; ) + { + if ( !m_pPushedEntities->m_rgPusher[i].m_pEntity->IsSolid() ) + continue; + + m_pushersOnly.AddEntityToHit( m_pPushedEntities->m_rgPusher[i].m_pEntity ); + int collisionGroup = m_pPushedEntities->m_rgPusher[i].m_pEntity->GetCollisionGroup(); + AddCollisionGroup(collisionGroup); + } + + } + + virtual IterationRetval_t EnumElement( IHandleEntity *pHandleEntity ) + { + CBaseEntity *pCheck = GetPushableEntity( pHandleEntity ); + if ( !pCheck ) + return ITERATION_CONTINUE; + + // Mark it as seen + pCheck->m_nPushEnumCount = s_nEnumCount; + m_pPushedEntities->AddEntity( pCheck ); + + return ITERATION_CONTINUE; + } + +private: + + inline void AddCollisionGroup(int collisionGroup) + { + for ( int i = 0; i < m_collisionGroupCount; i++ ) + { + if ( m_collisionGroups[i] == collisionGroup ) + return; + } + if ( m_collisionGroupCount < ARRAYSIZE(m_collisionGroups) ) + { + m_collisionGroups[m_collisionGroupCount] = collisionGroup; + m_collisionGroupCount++; + } + } + + bool IsStandingOnPusher( CBaseEntity *pCheck ) + { + CBaseEntity *pGroundEnt = pCheck->GetGroundEntity(); + if ( pCheck->GetFlags() & FL_ONGROUND || pGroundEnt ) + { + for ( int i = m_pPushedEntities->m_rgPusher.Count(); --i >= 0; ) + { + if (m_pPushedEntities->m_rgPusher[i].m_pEntity == pGroundEnt) + { + return true; + } + } + } + return false; + } + + bool IntersectsPushers( CBaseEntity *pTest ) + { + trace_t tr; + + ICollideable *pCollision = pTest->GetCollideable(); + enginetrace->SweepCollideable( pCollision, pTest->GetAbsOrigin(), pTest->GetAbsOrigin(), pCollision->GetCollisionAngles(), + pTest->PhysicsSolidMaskForEntity(), &m_pushersOnly, &tr ); + + return tr.startsolid; + } + + CBaseEntity *GetPushableEntity( IHandleEntity *pHandleEntity ) + { + CBaseEntity *pCheck = gEntList.GetBaseEntity( pHandleEntity->GetRefEHandle() ); + if ( !pCheck ) + return NULL; + + // Don't bother if we've already seen this one... + if (pCheck->m_nPushEnumCount == s_nEnumCount) + return NULL; + + if ( !pCheck->IsSolid() ) + return NULL; + + if ( pCheck->GetMoveType() == MOVETYPE_PUSH || + pCheck->GetMoveType() == MOVETYPE_NONE || + pCheck->GetMoveType() == MOVETYPE_VPHYSICS || + pCheck->GetMoveType() == MOVETYPE_NOCLIP ) + { + return NULL; + } + + bool bCollide = false; + for ( int i = 0; i < m_collisionGroupCount; i++ ) + { + if ( g_pGameRules->ShouldCollide( pCheck->GetCollisionGroup(), m_collisionGroups[i] ) ) + { + bCollide = true; + break; + } + } + if ( !bCollide ) + return NULL; + // We're not pushing stuff we're hierarchically attached to + CBaseEntity *pCheckHighestParent = pCheck->GetRootMoveParent(); + if (pCheckHighestParent == m_pRootHighestParent) + return NULL; + + // If we're standing on the pusher or any rigidly attached child + // of the pusher, we don't need to bother checking for interpenetration + if ( !IsStandingOnPusher(pCheck) ) + { + // Our surrounding boxes are touching. But we may well not be colliding.... + // see if the ent's bbox is inside the pusher's final position + if ( !IntersectsPushers( pCheck ) ) + return NULL; + } + + // NOTE: This is pretty tricky here. If a rigidly attached child comes into + // contact with a pusher, we *cannot* push the child. Instead, we must push + // the highest parent of that child. + return pCheckHighestParent; + } + +private: + static int s_nEnumCount; + CPhysicsPushedEntities *m_pPushedEntities; + CBaseEntity *m_pRootHighestParent; + CTraceFilterAgainstEntityList m_pushersOnly; + int m_collisionGroups[8]; + int m_collisionGroupCount; +}; + +int CPushBlockerEnum::s_nEnumCount = 0; + +//----------------------------------------------------------------------------- +// Generates a list of potential blocking entities +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::GenerateBlockingEntityList() +{ + VPROF("CPhysicsPushedEntities::GenerateBlockingEntityList"); + + m_rgMoved.RemoveAll(); + CPushBlockerEnum blockerEnum( this ); + + for ( int i = m_rgPusher.Count(); --i >= 0; ) + { + CBaseEntity *pPusher = m_rgPusher[i].m_pEntity; + + // Don't bother if the pusher isn't solid + if ( !pPusher->IsSolid() || pPusher->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS ) ) + { + continue; + } + + Vector vecAbsMins, vecAbsMaxs; + pPusher->CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); + partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, vecAbsMins, vecAbsMaxs, false, &blockerEnum ); + + //Go back throught the generated list. + } +} + +//----------------------------------------------------------------------------- +// Generates a list of potential blocking entities +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::GenerateBlockingEntityListAddBox( const Vector &vecMoved ) +{ + VPROF("CPhysicsPushedEntities::GenerateBlockingEntityListAddBox"); + + m_rgMoved.RemoveAll(); + CPushBlockerEnum blockerEnum( this ); + + for ( int i = m_rgPusher.Count(); --i >= 0; ) + { + CBaseEntity *pPusher = m_rgPusher[i].m_pEntity; + + // Don't bother if the pusher isn't solid + if ( !pPusher->IsSolid() || pPusher->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS ) ) + { + continue; + } + + Vector vecAbsMins, vecAbsMaxs; + pPusher->CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); + for ( int iAxis = 0; iAxis < 3; ++iAxis ) + { + if ( vecMoved[iAxis] >= 0.0f ) + { + vecAbsMins[iAxis] -= vecMoved[iAxis]; + } + else + { + vecAbsMaxs[iAxis] -= vecMoved[iAxis]; + } + } + + partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, vecAbsMins, vecAbsMaxs, false, &blockerEnum ); + + //Go back throught the generated list. + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets a list of all entities hierarchically attached to the root +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::SetupAllInHierarchy( CBaseEntity *pParent ) +{ + if (!pParent) + return; + + VPROF("CPhysicsPushedEntities::SetupAllInHierarchy"); + + // Make sure to snack the position +before+ relink because applying the + // rotation (which occurs in relink) will put it at the final location + // NOTE: The root object at this point is actually at its final position. + // We'll fix that up later + int i = m_rgPusher.AddToTail(); + m_rgPusher[i].m_pEntity = pParent; + m_rgPusher[i].m_vecStartAbsOrigin = pParent->GetAbsOrigin(); + + CBaseEntity *pChild; + for ( pChild = pParent->FirstMoveChild(); pChild != NULL; pChild = pChild->NextMovePeer() ) + { + SetupAllInHierarchy( pChild ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Rotates the root entity, fills in the pushmove structure +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::RotateRootEntity( CBaseEntity *pRoot, float movetime, RotatingPushMove_t &rotation ) +{ + VPROF("CPhysicsPushedEntities::RotateRootEntity"); + + rotation.amove = pRoot->GetLocalAngularVelocity() * movetime; + rotation.origin = pRoot->GetAbsOrigin(); + + // Knowing the initial + ending basis is needed for determining + // which corner we're pushing + MatrixCopy( pRoot->EntityToWorldTransform(), rotation.startLocalToWorld ); + + // rotate the pusher to it's final position + QAngle angles = pRoot->GetLocalAngles(); + angles += pRoot->GetLocalAngularVelocity() * movetime; + + pRoot->SetLocalAngles( angles ); + + // Compute the change in absangles + MatrixCopy( pRoot->EntityToWorldTransform(), rotation.endLocalToWorld ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Tries to rotate an entity hierarchy, returns the blocker if any +//----------------------------------------------------------------------------- +CBaseEntity *CPhysicsPushedEntities::PerformRotatePush( CBaseEntity *pRoot, float movetime ) +{ + VPROF("CPhysicsPushedEntities::PerformRotatePush"); + + m_bIsUnblockableByPlayer = (pRoot->GetFlags() & FL_UNBLOCKABLE_BY_PLAYER) ? true : false; + // Build a list of this entity + all its children because we're going to try to move them all + // This will also make sure each entity is linked in the appropriate place + // with correct absboxes + m_rgPusher.RemoveAll(); + SetupAllInHierarchy( pRoot ); + + // save where we rotated from, in case we're blocked + QAngle angPrevAngles = pRoot->GetLocalAngles(); + + // Apply the rotation + RotatingPushMove_t rotPushMove; + RotateRootEntity( pRoot, movetime, rotPushMove ); + + // Next generate a list of all entities that could potentially be intersecting with + // any of the children in their new locations... + GenerateBlockingEntityList( ); + + // Now we have a unique list of things that could potentially block our push + // and need to be pushed out of the way. Lets try to push them all out of the way. + // If we fail, undo it all + if (!SpeculativelyCheckRotPush( rotPushMove, pRoot )) + { + CBaseEntity *pBlocker = RegisterBlockage(); + pRoot->SetLocalAngles( angPrevAngles ); + RestoreEntities( ); + return pBlocker; + } + + FinishPush( true, &rotPushMove ); + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Linearly moves the root entity +//----------------------------------------------------------------------------- +void CPhysicsPushedEntities::LinearlyMoveRootEntity( CBaseEntity *pRoot, float movetime, Vector *pAbsPushVector ) +{ + VPROF("CPhysicsPushedEntities::LinearlyMoveRootEntity"); + + // move the pusher to it's final position + Vector move = pRoot->GetLocalVelocity() * movetime; + Vector origin = pRoot->GetLocalOrigin(); + origin += move; + pRoot->SetLocalOrigin( origin ); + + // Store off the abs push vector + *pAbsPushVector = pRoot->GetAbsVelocity() * movetime; +} + + +//----------------------------------------------------------------------------- +// Purpose: Tries to linearly push an entity hierarchy, returns the blocker if any +//----------------------------------------------------------------------------- +CBaseEntity *CPhysicsPushedEntities::PerformLinearPush( CBaseEntity *pRoot, float movetime ) +{ + VPROF("CPhysicsPushedEntities::PerformLinearPush"); + + m_flMoveTime = movetime; + + m_bIsUnblockableByPlayer = (pRoot->GetFlags() & FL_UNBLOCKABLE_BY_PLAYER) ? true : false; + // Build a list of this entity + all its children because we're going to try to move them all + // This will also make sure each entity is linked in the appropriate place + // with correct absboxes + m_rgPusher.RemoveAll(); + SetupAllInHierarchy( pRoot ); + + // save where we started from, in case we're blocked + Vector vecPrevOrigin = pRoot->GetLocalOrigin(); + + // Move the root (and all children) into its new position + Vector vecAbsPush; + LinearlyMoveRootEntity( pRoot, movetime, &vecAbsPush ); + + // Next generate a list of all entities that could potentially be intersecting with + // any of the children in their new locations... + GenerateBlockingEntityListAddBox( vecAbsPush ); + + // Now we have a unique list of things that could potentially block our push + // and need to be pushed out of the way. Lets try to push them all out of the way. + // If we fail, undo it all + if (!SpeculativelyCheckLinearPush( vecAbsPush )) + { + CBaseEntity *pBlocker = RegisterBlockage(); + pRoot->SetLocalOrigin( vecPrevOrigin ); + RestoreEntities(); + return pBlocker; + } + + FinishPush( ); + return NULL; +} + + + +//----------------------------------------------------------------------------- +// +// CBaseEntity methods +// +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Purpose: Called when it's time for a physically moved objects (plats, doors, etc) +// to run it's game code. +// All other entity thinking is done during worldspawn's think +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsDispatchThink( BASEPTR thinkFunc ) +{ + VPROF_ENTER_SCOPE( ( !vprof_scope_entity_thinks.GetBool() ) ? + "CBaseEntity::PhysicsDispatchThink" : + EntityFactoryDictionary()->GetCannonicalName( GetClassname() ) ); + + float thinkLimit = think_limit.GetFloat(); + + // The thinkLimit stuff makes a LOT of calls to Sys_FloatTime, which winds up calling into + // VCR mode so much that the framerate becomes unusable. + if ( VCRGetMode() != VCR_Disabled ) + thinkLimit = 0; + + float startTime = 0.0; + + if ( IsDormant() ) + { + Warning( "Dormant entity %s (%s) is thinking!!\n", GetClassname(), GetDebugName() ); + Assert(0); + } + + if ( thinkLimit ) + { + startTime = engine->Time(); + } + + if ( thinkFunc ) + { + MDLCACHE_CRITICAL_SECTION(); + (this->*thinkFunc)(); + } + + if ( thinkLimit ) + { + // calculate running time of the AI in milliseconds + float time = ( engine->Time() - startTime ) * 1000.0f; + if ( time > thinkLimit ) + { +#if defined( _XBOX ) && !defined( _RETAIL ) + if ( vprof_think_limit.GetBool() ) + { + extern bool g_VProfSignalSpike; + g_VProfSignalSpike = true; + } +#endif + // If its an NPC print out the shedule/task that took so long + CAI_BaseNPC *pNPC = MyNPCPointer(); + if (pNPC && pNPC->GetCurSchedule()) + { + pNPC->ReportOverThinkLimit( time ); + } + else + { +#ifdef _WIN32 + Msg( "%s(%s) thinking for %.02f ms!!!\n", GetClassname(), typeid(this).raw_name(), time ); +#elif POSIX + Msg( "%s(%s) thinking for %.02f ms!!!\n", GetClassname(), typeid(this).name(), time ); +#else +#error "typeinfo" +#endif + } + } + } + + VPROF_EXIT_SCOPE(); +} + +//----------------------------------------------------------------------------- +// Purpose: Does not change the entities velocity at all +// Input : push - +// Output : trace_t +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsCheckSweep( const Vector& vecAbsStart, const Vector &vecAbsDelta, trace_t *pTrace ) +{ + ::PhysicsCheckSweep( this, vecAbsStart, vecAbsDelta, pTrace ); +} + + + +#define MAX_CLIP_PLANES 5 +//----------------------------------------------------------------------------- +// Purpose: The basic solid body movement attempt/clip that slides along multiple planes +// Input : time - Amount of time to try moving for +// *steptrace - if not NULL, the trace results of any vertical wall hit will be stored +// Output : int - the clipflags if the velocity was modified (hit something solid) +// 1 = floor +// 2 = wall / step +// 4 = dead stop +//----------------------------------------------------------------------------- +int CBaseEntity::PhysicsTryMove( float flTime, trace_t *steptrace ) +{ + VPROF("CBaseEntity::PhysicsTryMove"); + + int bumpcount, numbumps; + Vector dir; + float d; + int numplanes; + Vector planes[MAX_CLIP_PLANES]; + Vector primal_velocity, original_velocity, new_velocity; + int i, j; + trace_t trace; + Vector end; + float time_left; + int blocked; + + unsigned int mask = PhysicsSolidMaskForEntity(); + + new_velocity.Init(); + + numbumps = 4; + + Vector vecAbsVelocity = GetAbsVelocity(); + + blocked = 0; + VectorCopy (vecAbsVelocity, original_velocity); + VectorCopy (vecAbsVelocity, primal_velocity); + numplanes = 0; + + time_left = flTime; + + for (bumpcount=0 ; bumpcount 0) + { // actually covered some distance + SetAbsOrigin( trace.endpos ); + VectorCopy (vecAbsVelocity, original_velocity); + numplanes = 0; + } + + if (trace.fraction == 1) + break; // moved the entire distance + + if (!trace.m_pEnt) + { + SetAbsVelocity( vecAbsVelocity ); + Warning( "PhysicsTryMove: !trace.u.ent" ); + Assert(0); + return 4; + } + + if (trace.plane.normal[2] > 0.7) + { + blocked |= 1; // floor + if (CanStandOn( trace.m_pEnt )) + { + // keep track of time when changing ground entity + if (GetGroundEntity() != trace.m_pEnt) + { + SetGroundChangeTime( gpGlobals->curtime + (flTime - (1 - trace.fraction) * time_left) ); + } + + SetGroundEntity( trace.m_pEnt ); + } + } + if (!trace.plane.normal[2]) + { + blocked |= 2; // step + if (steptrace) + *steptrace = trace; // save for player extrafriction + } + + // run the impact function + PhysicsImpact( trace.m_pEnt, trace ); + // Removed by the impact function + if ( IsMarkedForDeletion() || IsEdictFree() ) + break; + + time_left -= time_left * trace.fraction; + + // clipped to another plane + if (numplanes >= MAX_CLIP_PLANES) + { // this shouldn't really happen + SetAbsVelocity(vec3_origin); + return blocked; + } + + VectorCopy (trace.plane.normal, planes[numplanes]); + numplanes++; + + // modify original_velocity so it parallels all of the clip planes + if ( GetMoveType() == MOVETYPE_WALK && (!(GetFlags() & FL_ONGROUND) || GetFriction()!=1) ) // relfect player velocity + { + for ( i = 0; i < numplanes; i++ ) + { + if ( planes[i][2] > 0.7 ) + {// floor or slope + PhysicsClipVelocity( original_velocity, planes[i], new_velocity, 1 ); + VectorCopy( new_velocity, original_velocity ); + } + else + { + PhysicsClipVelocity( original_velocity, planes[i], new_velocity, 1.0 + sv_bounce.GetFloat() * (1-GetFriction()) ); + } + } + + VectorCopy( new_velocity, vecAbsVelocity ); + VectorCopy( new_velocity, original_velocity ); + } + else + { + for (i=0 ; iframetime; + SetAbsVelocity( vecAbsVelocity ); + + Vector vecNewBaseVelocity = GetBaseVelocity(); + vecNewBaseVelocity[2] = 0; + SetBaseVelocity( vecNewBaseVelocity ); + + // Bound velocity + PhysicsCheckVelocity(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Does not change the entities velocity at all +// Input : push - +// Output : trace_t +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsPushEntity( const Vector& push, trace_t *pTrace ) +{ + VPROF("CBaseEntity::PhysicsPushEntity"); + + if ( GetMoveParent() ) + { + Warning( "pushing entity (%s) that has parent (%s)!\n", GetDebugName(), GetMoveParent()->GetDebugName() ); + Assert(0); + } + + // NOTE: absorigin and origin must be equal because there is no moveparent + Vector prevOrigin; + VectorCopy( GetAbsOrigin(), prevOrigin ); + + ::PhysicsCheckSweep( this, prevOrigin, push, pTrace ); + + if ( pTrace->fraction ) + { + SetAbsOrigin( pTrace->endpos ); + + // FIXME(ywb): Should we try to enable this here + // WakeRestingObjects(); + } + + // Passing in the previous abs origin here will cause the relinker + // to test the swept ray from previous to current location for trigger intersections + PhysicsTouchTriggers( &prevOrigin ); + + if ( pTrace->m_pEnt ) + { + PhysicsImpact( pTrace->m_pEnt, *pTrace ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: See if entity is inside another entity, if so, returns true if so, fills in *ppEntity if ppEntity is not NULL +// Input : **ppEntity - optional return pointer to entity we are inside of +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseEntity::PhysicsTestEntityPosition( CBaseEntity **ppEntity /*=NULL*/ ) +{ + VPROF("CBaseEntity::PhysicsTestEntityPosition"); + + trace_t trace; + + unsigned int mask = PhysicsSolidMaskForEntity(); + + Physics_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin(), mask, &trace ); + + if ( trace.startsolid ) + { + if ( ppEntity ) + { + *ppEntity = trace.m_pEnt; + } + return true; + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CBaseEntity::PhysicsPushMove( float movetime ) +{ + VPROF("CBaseEntity::PhysicsPushMove"); + + // If this entity isn't moving, just update the time. + IncrementLocalTime( movetime ); + + if ( GetLocalVelocity() == vec3_origin ) + { + return NULL; + } + + // Now check that the entire hierarchy can rotate into the new location + CBaseEntity *pBlocker = g_pPushedEntities->PerformLinearPush( this, movetime ); + if ( pBlocker ) + { + IncrementLocalTime( -movetime ); + } + return pBlocker; +} + + +//----------------------------------------------------------------------------- +// Purpose: Tries to rotate, returns success or failure +// Input : movetime - +// Output : bool +//----------------------------------------------------------------------------- +CBaseEntity *CBaseEntity::PhysicsPushRotate( float movetime ) +{ + VPROF("CBaseEntity::PhysicsPushRotate"); + + IncrementLocalTime( movetime ); + + // Not rotating + if ( GetLocalAngularVelocity() == vec3_angle ) + { + return NULL; + } + + // Now check that the entire hierarchy can rotate into the new location + CBaseEntity *pBlocker = g_pPushedEntities->PerformRotatePush( this, movetime ); + if ( pBlocker ) + { + IncrementLocalTime( -movetime ); + } + + return pBlocker; +} + + +//----------------------------------------------------------------------------- +// Block of icky shared code from PhysicsParent + PhysicsPusher +//----------------------------------------------------------------------------- +void CBaseEntity::PerformPush( float movetime ) +{ + VPROF("CBaseEntity::PerformPush"); + // NOTE: Use handle index because the previous blocker could have been deleted + int hPrevBlocker = m_pBlocker.ToInt(); + CBaseEntity *pBlocker; + g_pPushedEntities->BeginPush( this ); + if (movetime > 0) + { + if ( GetLocalAngularVelocity() != vec3_angle ) + { + if ( GetLocalVelocity() != vec3_origin ) + { + // NOTE: Both PhysicsPushRotate + PhysicsPushMove + // will attempt to advance local time. Choose the one that's + // the greater of the two from push + move + + // FIXME: Should we really be doing them both simultaneously?? + // FIXME: Choose the *greater* of the two?!? That's strange... + float flInitialLocalTime = m_flLocalTime; + + // moving and rotating, so rotate first, then move + pBlocker = PhysicsPushRotate( movetime ); + if ( !pBlocker ) + { + float flRotateLocalTime = m_flLocalTime; + + // Reset the local time to what it was before we rotated + m_flLocalTime = flInitialLocalTime; + pBlocker = PhysicsPushMove( movetime ); + if ( m_flLocalTime < flRotateLocalTime ) + { + m_flLocalTime = flRotateLocalTime; + } + } + } + else + { + // only rotating + pBlocker = PhysicsPushRotate( movetime ); + } + } + else + { + // only moving + pBlocker = PhysicsPushMove( movetime ); + } + + m_pBlocker = pBlocker; + if (m_pBlocker.ToInt() != hPrevBlocker) + { + if (hPrevBlocker != INVALID_EHANDLE_INDEX) + { + EndBlocked(); + } + if (m_pBlocker) + { + StartBlocked( pBlocker ); + } + } + if (m_pBlocker) + { + Blocked( m_pBlocker ); + } + + // NOTE NOTE: This is here for brutal reasons. + // For MOVETYPE_PUSH objects with VPhysics shadow objects, the move done time + // is handled by CBaseEntity::VPhyicsUpdatePusher, which only gets called if + // the physics system thinks the entity is awake. That will happen if the + // shadow gets updated, but the push code above doesn't update unless the + // move is successful or non-zero. So we must make sure it's awake + if ( VPhysicsGetObject() ) + { + VPhysicsGetObject()->Wake(); + } + } + + // move done is handled by physics if it has any + if ( VPhysicsGetObject() ) + { + // store the list of moved entities for later + // if you actually did an unblocked push that moved entities, and you're using physics (which may block later) + if ( movetime > 0 && !m_pBlocker && GetSolid() == SOLID_VPHYSICS && g_pPushedEntities->CountMovedEntities() > 0 ) + { + // UNDONE: Any reason to want to call this twice before physics runs? + // If so, maybe just append to the list? + Assert( !GetDataObject( PHYSICSPUSHLIST ) ); + physicspushlist_t *pList = (physicspushlist_t *)CreateDataObject( PHYSICSPUSHLIST ); + if ( pList ) + { + g_pPushedEntities->StoreMovedEntities( *pList ); + } + } + } + else + { + if ( m_flMoveDoneTime <= m_flLocalTime && m_flMoveDoneTime > 0 ) + { + SetMoveDoneTime( -1 ); + MoveDone(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: UNDONE: This is only different from PhysicsParent because of the callback to PhysicsVelocity() +// Can we support that callback in push objects as well? +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsPusher( void ) +{ + VPROF("CBaseEntity::PhysicsPusher"); + + // regular thinking + if ( !PhysicsRunThink() ) + return; + + m_flVPhysicsUpdateLocalTime = m_flLocalTime; + + float movetime = GetMoveDoneTime(); + if (movetime > gpGlobals->frametime) + { + movetime = gpGlobals->frametime; + } + + PerformPush( movetime ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: Non moving objects can only think +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsNone( void ) +{ + VPROF("CBaseEntity::PhysicsNone"); + + // regular thinking + PhysicsRunThink(); +} + + +//----------------------------------------------------------------------------- +// Purpose: A moving object that doesn't obey physics +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsNoclip( void ) +{ + VPROF("CBaseEntity::PhysicsNoclip"); + + // regular thinking + if ( !PhysicsRunThink() ) + { + return; + } + + // Apply angular velocity + SimulateAngles( gpGlobals->frametime ); + + Vector origin; + VectorMA( GetLocalOrigin(), gpGlobals->frametime, GetLocalVelocity(), origin ); + SetLocalOrigin( origin ); +} + + +void CBaseEntity::PerformCustomPhysics( Vector *pNewPosition, Vector *pNewVelocity, QAngle *pNewAngles, QAngle *pNewAngVelocity ) +{ + // If you're going to use custom physics, you need to implement this! + Assert(0); +} + + +//----------------------------------------------------------------------------- +// Allows entities to describe their own physics +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsCustom() +{ + VPROF("CBaseEntity::PhysicsCustom"); + PhysicsCheckWater(); + + // regular thinking + if ( !PhysicsRunThink() ) + return; + + // Moving upward, off the ground, or resting on a client/monster, remove FL_ONGROUND + if ( m_vecVelocity[2] > 0 || !GetGroundEntity() || !GetGroundEntity()->IsStandable() ) + { + SetGroundEntity( NULL ); + } + + // NOTE: The entity must set the position, angles, velocity in its custom movement + Vector vecNewPosition = GetAbsOrigin(); + Vector vecNewVelocity = GetAbsVelocity(); + QAngle angNewAngles = GetAbsAngles(); + QAngle angNewAngVelocity = GetLocalAngularVelocity(); + + PerformCustomPhysics( &vecNewPosition, &vecNewVelocity, &angNewAngles, &angNewAngVelocity ); + + // Store off all of the new state information... + SetAbsVelocity( vecNewVelocity ); + SetAbsAngles( angNewAngles ); + SetLocalAngularVelocity( angNewAngVelocity ); + + Vector move; + VectorSubtract( vecNewPosition, GetAbsOrigin(), move ); + + // move origin + trace_t trace; + PhysicsPushEntity( move, &trace ); + + PhysicsCheckVelocity(); + + if (trace.allsolid) + { + // entity is trapped in another solid + // UNDONE: does this entity needs to be removed? + SetAbsVelocity(vec3_origin); + SetLocalAngularVelocity(vec3_angle); + return; + } + + if (IsEdictFree()) + return; + + // check for in water + PhysicsCheckWaterTransition(); +} + +bool g_bTestMoveTypeStepSimulation = true; +ConVar sv_teststepsimulation( "sv_teststepsimulation", "1", 0 ); + +//----------------------------------------------------------------------------- +// Purpose: Until we remove the above cvar, we need to have the entities able +// to dynamically deal with changing their simulation stuff here. +//----------------------------------------------------------------------------- +void CBaseEntity::CheckStepSimulationChanged() +{ + if ( g_bTestMoveTypeStepSimulation != IsSimulatedEveryTick() ) + { + SetSimulatedEveryTick( g_bTestMoveTypeStepSimulation ); + } + + bool hadobject = HasDataObjectType( STEPSIMULATION ); + + if ( g_bTestMoveTypeStepSimulation ) + { + if ( !hadobject ) + { + CreateDataObject( STEPSIMULATION ); + } + } + else + { + if ( hadobject ) + { + DestroyDataObject( STEPSIMULATION ); + } + } +} + + +#define STEP_TELPORTATION_VEL_SQ ( 4096.0f * 4096.0f ) +//----------------------------------------------------------------------------- +// Purpose: Run regular think and latch off angle/origin changes so we can interpolate them on the server to fake simulation +// Input : *step - +//----------------------------------------------------------------------------- +void CBaseEntity::StepSimulationThink( float dt ) +{ + // See if we need to allocate, deallocate step simulation object + CheckStepSimulationChanged(); + + StepSimulationData *step = ( StepSimulationData * )GetDataObject( STEPSIMULATION ); + if ( !step ) + { + PhysicsStepRunTimestep( dt ); + + // Just call the think function directly + PhysicsRunThink( THINK_FIRE_BASE_ONLY ); + } + else + { + // Assume that it's in use + step->m_bOriginActive = true; + step->m_bAnglesActive = true; + + // Reset networked versions of origin and angles + step->m_nLastProcessTickCount = -1; + step->m_vecNetworkOrigin.Init(); + step->m_angNetworkAngles.Init(); + + // Remember old old values + step->m_Previous2 = step->m_Previous; + + // Remember old values + step->m_Previous.nTickCount = gpGlobals->tickcount; + step->m_Previous.vecOrigin = GetStepOrigin(); + QAngle stepAngles = GetStepAngles(); + AngleQuaternion( stepAngles, step->m_Previous.qRotation ); + + // Run simulation + PhysicsStepRunTimestep( dt ); + + // Call the actual think function... + PhysicsRunThink( THINK_FIRE_BASE_ONLY ); + + // do any local processing that's needed + if (GetBaseAnimating() != NULL) + { + GetBaseAnimating()->UpdateStepOrigin(); + } + + // Latch new values to see if external code modifies our position/orientation + step->m_Next.vecOrigin = GetStepOrigin(); + stepAngles = GetStepAngles(); + AngleQuaternion( stepAngles, step->m_Next.qRotation ); + // Also store of non-Quaternion version for simple comparisons + step->m_angNextRotation = GetStepAngles(); + step->m_Next.nTickCount = GetNextThinkTick(); + + // Hack: Add a tick if we are simulating every other tick + if ( CBaseEntity::IsSimulatingOnAlternateTicks() ) + { + ++step->m_Next.nTickCount; + } + + // Check for teleportation/snapping of the origin + if ( dt > 0.0f ) + { + Vector deltaorigin = step->m_Next.vecOrigin - step->m_Previous.vecOrigin; + float velSq = deltaorigin.LengthSqr() / ( dt * dt ); + if ( velSq >= STEP_TELPORTATION_VEL_SQ ) + { + // Deactivate it due to large origin change + step->m_bOriginActive = false; + step->m_bAnglesActive = false; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Monsters freefall when they don't have a ground entity, otherwise +// all movement is done with discrete steps. +// This is also used for objects that have become still on the ground, but +// will fall if the floor is pulled out from under them. +// JAY: Extended this to synchronize movement and thinking wherever possible. +// This allows the client-side interpolation to interpolate animation and simulation +// data at the same time. +// UNDONE: Remove all other cases from this loop - only use MOVETYPE_STEP to simulate +// entities that are currently animating/thinking. +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsStep() +{ + // EVIL HACK: Force these to appear as if they've changed!!! + // The underlying values don't actually change, but we need the network sendproxy on origin/angles + // to get triggered, and that only happens if NetworkStateChanged() appears to have occured. + // Getting them for modify marks them as changed automagically. + m_vecOrigin.GetForModify(); + m_angRotation.GetForModify(); + + // HACK: Make sure that the client latches the networked origin/orientation changes with the current server tick count + // so that we don't get jittery interpolation. All of this is necessary to mimic actual continuous simulation of the underlying + // variables. + SetSimulationTime( gpGlobals->curtime ); + + // Run all but the base think function + PhysicsRunThink( THINK_FIRE_ALL_BUT_BASE ); + + int thinktick = GetNextThinkTick(); + float thinktime = thinktick * TICK_INTERVAL; + + // Is the next think too far out, or non-existent? + // BUGBUG: Interpolation is going to look bad in here. But it should only + // be for dead things - and those should be ragdolls (client-side sim) anyway. + // UNDONE: Remove this and assert? Force MOVETYPE_STEP objs to become MOVETYPE_TOSS when + // they aren't thinking? + // UNDONE: this happens as the first frame for a bunch of things like dynamically created ents. + // can't remove until initial conditions are resolved + float deltaThink = thinktime - gpGlobals->curtime; + if ( thinktime <= 0 || deltaThink > 0.5 ) + { + PhysicsStepRunTimestep( gpGlobals->frametime ); + PhysicsCheckWaterTransition(); + SetLastThink( -1, gpGlobals->curtime ); + UpdatePhysicsShadowToCurrentPosition(gpGlobals->frametime); + PhysicsRelinkChildren(gpGlobals->frametime); + return; + } + + Vector oldOrigin = GetAbsOrigin(); + + // Feed the position delta back from vphysics if enabled + bool updateFromVPhysics = npc_vphysics.GetBool(); + if ( HasDataObjectType(VPHYSICSUPDATEAI) ) + { + vphysicsupdateai_t *pUpdate = static_cast(GetDataObject( VPHYSICSUPDATEAI )); + if ( pUpdate->stopUpdateTime > gpGlobals->curtime ) + { + updateFromVPhysics = true; + } + else + { + float maxAngular; + VPhysicsGetObject()->GetShadowController()->GetMaxSpeed( NULL, &maxAngular ); + VPhysicsGetObject()->GetShadowController()->MaxSpeed( pUpdate->savedShadowControllerMaxSpeed, maxAngular ); + DestroyDataObject(VPHYSICSUPDATEAI); + } + } + + if ( updateFromVPhysics && VPhysicsGetObject() && !GetParent() ) + { + Vector position; + VPhysicsGetObject()->GetShadowPosition( &position, NULL ); + float delta = (GetAbsOrigin() - position).LengthSqr(); + // for now, use a tolerance of 1 inch for these tests + if ( delta < 1 ) + { + // physics is really close, check to see if my current position is valid. + // If so, ignore the physics result. + trace_t tr; + Physics_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin(), PhysicsSolidMaskForEntity(), &tr ); + updateFromVPhysics = tr.startsolid; + } + if ( updateFromVPhysics ) + { + SetAbsOrigin( position ); + PhysicsTouchTriggers(); + } + //NDebugOverlay::Box( position, WorldAlignMins(), WorldAlignMaxs(), 255, 255, 0, 0, 0.0 ); + } + + // not going to think, don't run game physics either + if ( thinktick > gpGlobals->tickcount ) + return; + + // Don't let things stay in the past. + // it is possible to start that way + // by a trigger with a local time. + if ( thinktime < gpGlobals->curtime ) + { + thinktime = gpGlobals->curtime; + } + + // simulate over the timestep + float dt = thinktime - GetLastThink(); + + // Now run step simulator + StepSimulationThink( dt ); + + PhysicsCheckWaterTransition(); + + if ( VPhysicsGetObject() ) + { + if ( !VectorCompare( oldOrigin, GetAbsOrigin() ) ) + { + VPhysicsGetObject()->UpdateShadow( GetAbsOrigin(), vec3_angle, (GetFlags() & FL_FLY) ? true : false, dt ); + } + } + PhysicsRelinkChildren(dt); +} + + +void UTIL_TraceLineFilterEntity( CBaseEntity *pEntity, const Vector &vecAbsStart, const Vector &vecAbsEnd, + unsigned int mask, const int nCollisionGroup, trace_t *ptr ); + +// Check to see what (if anything) this MOVETYPE_STEP entity is standing on +void CBaseEntity::PhysicsStepRecheckGround() +{ + unsigned int mask = PhysicsSolidMaskForEntity(); + // determine if it's on solid ground at all + Vector mins, maxs, point; + int x, y; + trace_t trace; + + VectorAdd (GetAbsOrigin(), WorldAlignMins(), mins); + VectorAdd (GetAbsOrigin(), WorldAlignMaxs(), maxs); + point[2] = mins[2] - 1; + for (x=0 ; x<=1 ; x++) + { + for (y=0 ; y<=1 ; y++) + { + point[0] = x ? maxs[0] : mins[0]; + point[1] = y ? maxs[1] : mins[1]; + + ICollideable *pCollision = GetCollideable(); + + if ( pCollision && IsNPC() ) + { + UTIL_TraceLineFilterEntity( this, point, point, mask, COLLISION_GROUP_NONE, &trace ); + } + else + { + UTIL_TraceLine( point, point, mask, this, COLLISION_GROUP_NONE, &trace ); + } + + if ( trace.startsolid ) + { + SetGroundEntity( trace.m_pEnt ); + return; + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : timestep - +//----------------------------------------------------------------------------- +void CBaseEntity::PhysicsStepRunTimestep( float timestep ) +{ + bool wasonground; + bool inwater; + bool hitsound = false; + float speed, newspeed, control; + float friction; + + PhysicsCheckVelocity(); + + wasonground = ( GetFlags() & FL_ONGROUND ) ? true : false; + + // add gravity except: + // flying monsters + // swimming monsters who are in the water + inwater = PhysicsCheckWater(); + + bool isfalling = false; + + if ( !wasonground ) + { + if ( !( GetFlags() & FL_FLY ) ) + { + if ( !( ( GetFlags() & FL_SWIM ) && ( GetWaterLevel() > 0 ) ) ) + { + if ( GetAbsVelocity()[2] < ( GetCurrentGravity() * -0.1 ) ) + { + hitsound = true; + } + + if ( !inwater ) + { + PhysicsAddHalfGravity( timestep ); + isfalling = true; + } + } + } + } + + if ( !(GetFlags() & FL_STEPMOVEMENT) && + (!VectorCompare(GetAbsVelocity(), vec3_origin) || + !VectorCompare(GetBaseVelocity(), vec3_origin))) + { + Vector vecAbsVelocity = GetAbsVelocity(); + + SetGroundEntity( NULL ); + // apply friction + // let dead monsters who aren't completely onground slide + if ( wasonground ) + { + speed = VectorLength( vecAbsVelocity ); + if (speed) + { + friction = sv_friction.GetFloat() * GetFriction(); + + control = speed < sv_stopspeed.GetFloat() ? sv_stopspeed.GetFloat() : speed; + newspeed = speed - timestep*control*friction; + + if (newspeed < 0) + newspeed = 0; + newspeed /= speed; + + vecAbsVelocity[0] *= newspeed; + vecAbsVelocity[1] *= newspeed; + } + } + + vecAbsVelocity += GetBaseVelocity(); + SetAbsVelocity( vecAbsVelocity ); + + // Apply angular velocity + SimulateAngles( timestep ); + + PhysicsCheckVelocity(); + + PhysicsTryMove( timestep, NULL ); + + PhysicsCheckVelocity(); + + vecAbsVelocity = GetAbsVelocity(); + vecAbsVelocity -= GetBaseVelocity(); + SetAbsVelocity( vecAbsVelocity ); + + PhysicsCheckVelocity(); + + if ( !(GetFlags() & FL_ONGROUND) ) + { + PhysicsStepRecheckGround(); + } + + PhysicsTouchTriggers(); + } + + if (!( GetFlags() & FL_ONGROUND ) && isfalling) + { + PhysicsAddHalfGravity( timestep ); + } +} + +// After this long, if a player isn't updating, then return it's projectiles to server control +#define PLAYER_PACKETS_STOPPED_SO_RETURN_TO_PHYSICS_TIME 1.0f + +void Physics_SimulateEntity( CBaseEntity *pEntity ) +{ + VPROF( ( !vprof_scope_entity_gamephys.GetBool() ) ? + "Physics_SimulateEntity" : + EntityFactoryDictionary()->GetCannonicalName( pEntity->GetClassname() ) ); + + if ( pEntity->edict() ) + { +#if !defined( NO_ENTITY_PREDICTION ) + // Player drives simulation of this entity + if ( pEntity->IsPlayerSimulated() ) + { + // If the player is gone, dropped, crashed, then return + // control to the game code. + CBasePlayer *simulatingPlayer = pEntity->GetSimulatingPlayer(); + if ( simulatingPlayer && + ( simulatingPlayer->GetTimeBase() > gpGlobals->curtime - PLAYER_PACKETS_STOPPED_SO_RETURN_TO_PHYSICS_TIME ) ) + { + // Okay, the guy is still around + return; + } + + pEntity->UnsetPlayerSimulated(); + } +#endif + + MDLCACHE_CRITICAL_SECTION(); + +#if !defined( NO_ENTITY_PREDICTION ) + // If an object was at one point player simulated, but had that status revoked (as just + // above when no packets have arrived in a while ), then we still will assume that the + // owner/player will be predicting the entity locally (even if the game is playing like butt) + // and so we won't spam that player with additional network data such as effects/sounds + // that are theoretically being predicted by the player anyway. + if ( pEntity->m_PredictableID->IsActive() ) + { + CBasePlayer *playerowner = ToBasePlayer( pEntity->GetOwnerEntity() ); + if ( playerowner ) + { + CBasePlayer *pl = ToBasePlayer( UTIL_PlayerByIndex( pEntity->m_PredictableID->GetPlayer() + 1 ) ); + // Is the player who created it still the owner? + if ( pl == playerowner ) + { + // Set up to suppress sending events to owner player + if ( pl->IsPredictingWeapons() ) + { + IPredictionSystem::SuppressHostEvents( playerowner ); + } + } + } + { + VPROF( ( !vprof_scope_entity_gamephys.GetBool() ) ? + "pEntity->PhysicsSimulate" : + EntityFactoryDictionary()->GetCannonicalName( pEntity->GetClassname() ) ); + + // Run entity physics + pEntity->PhysicsSimulate(); + } + + // Restore suppression filter + IPredictionSystem::SuppressHostEvents( NULL ); + } + else +#endif + { + // Run entity physics + pEntity->PhysicsSimulate(); + } + } + else + { + pEntity->PhysicsRunThink(); + } +} +//----------------------------------------------------------------------------- +// Purpose: Runs the main physics simulation loop against all entities ( except players ) +//----------------------------------------------------------------------------- +void Physics_RunThinkFunctions( bool simulating ) +{ + VPROF( "Physics_RunThinkFunctions"); + + g_bTestMoveTypeStepSimulation = sv_teststepsimulation.GetBool(); + + float starttime = gpGlobals->curtime; + // clear all entites freed outside of this loop + gEntList.CleanupDeleteList(); + + if ( !simulating ) + { + // only simulate players + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer ) + { + // Always reset clock to real sv.time + gpGlobals->curtime = starttime; + // Force usercmd processing even though gpGlobals->tickcount isn't incrementing + pPlayer->ForceSimulation(); + Physics_SimulateEntity( pPlayer ); + } + } + } + else + { + UTIL_DisableRemoveImmediate(); + int listMax = SimThink_ListCount(); + listMax = MAX(listMax,1); + CBaseEntity **list = (CBaseEntity **)stackalloc( sizeof(CBaseEntity *) * listMax ); + // iterate through all entities and have them think or simulate + + // UNDONE: This has problems with UTIL_RemoveImmediate() (now disabled during this loop). + // Do we really need UTIL_RemoveImmediate()? + int count = SimThink_ListCopy( list, listMax ); + + //DevMsg(1, "Count: %d\n", count ); + for ( int i = 0; i < count; i++ ) + { + if ( !list[i] ) + continue; + // Always reset clock to real sv.time + gpGlobals->curtime = starttime; + Physics_SimulateEntity( list[i] ); + } + + stackfree( list ); + UTIL_EnableRemoveImmediate(); + } + + gpGlobals->curtime = starttime; +} + diff --git a/sp/src/game/server/physics_npc_solver.cpp b/sp/src/game/server/physics_npc_solver.cpp new file mode 100644 index 00000000..ce7f8434 --- /dev/null +++ b/sp/src/game/server/physics_npc_solver.cpp @@ -0,0 +1,468 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "physics_saverestore.h" +#include "vphysics/friction.h" +#include "ai_basenpc.h" +#include "movevars_shared.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +class CPhysicsNPCSolver : public CLogicalEntity, public IMotionEvent +{ + DECLARE_CLASS( CPhysicsNPCSolver, CLogicalEntity ); +public: + CPhysicsNPCSolver(); + ~CPhysicsNPCSolver(); + DECLARE_DATADESC(); + void Init( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject, bool disableCollisions, float separationTime ); + static CPhysicsNPCSolver *Create( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject, bool disableCollisions, float separationTime ); + + // CBaseEntity + virtual void Spawn(); + virtual void UpdateOnRemove(); + virtual void Think(); + virtual void OnRestore() + { + BaseClass::OnRestore(); + if ( m_allowIntersection ) + { + PhysDisableEntityCollisions( m_hNPC, m_hEntity ); + } + } + + // IMotionEvent + virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + +public: + CPhysicsNPCSolver *m_pNext; +private: + // locals + void ResetCancelTime(); + void BecomePenetrationSolver(); + bool IsIntersecting(); + bool IsContactOnNPCHead( IPhysicsFrictionSnapshot *pSnapshot, IPhysicsObject *pPhysics, CAI_BaseNPC *pNPC ); + bool CheckTouching(); + friend bool NPCPhysics_SolverExists( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject ); + + CHandle m_hNPC; + EHANDLE m_hEntity; + IPhysicsMotionController *m_pController; + float m_separationDuration; + float m_cancelTime; + bool m_allowIntersection; +}; + +LINK_ENTITY_TO_CLASS( physics_npc_solver, CPhysicsNPCSolver ); + +BEGIN_DATADESC( CPhysicsNPCSolver ) + + DEFINE_FIELD( m_hNPC, FIELD_EHANDLE ), + DEFINE_FIELD( m_hEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_separationDuration, FIELD_FLOAT ), + DEFINE_FIELD( m_cancelTime, FIELD_TIME ), + DEFINE_FIELD( m_allowIntersection, FIELD_BOOLEAN ), + DEFINE_PHYSPTR( m_pController ), + //DEFINE_FIELD( m_pNext, FIELD_CLASSPTR ), + +END_DATADESC() + +CEntityClassList g_SolverList; +template <> CPhysicsNPCSolver *CEntityClassList::m_pClassList = NULL; + +bool NPCPhysics_SolverExists( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject ) +{ + CPhysicsNPCSolver *pSolver = g_SolverList.m_pClassList; + while ( pSolver ) + { + if ( pSolver->m_hEntity == pPhysicsObject && pSolver->m_hNPC == pNPC ) + return true; + pSolver = pSolver->m_pNext; + } + + return false; +} + +CPhysicsNPCSolver *CPhysicsNPCSolver::Create( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject, bool disableCollisions, float separationTime ) +{ + CPhysicsNPCSolver *pSolver = (CPhysicsNPCSolver *)CBaseEntity::CreateNoSpawn( "physics_npc_solver", vec3_origin, vec3_angle, NULL ); + pSolver->Init( pNPC, pPhysicsObject, disableCollisions, separationTime ); + pSolver->Spawn(); + //NDebugOverlay::EntityBounds(pNPC, 255, 255, 0, 64, 0.5f ); + return pSolver; +} + +CPhysicsNPCSolver::CPhysicsNPCSolver() +{ + g_SolverList.Insert( this ); +} + +CPhysicsNPCSolver::~CPhysicsNPCSolver() +{ + g_SolverList.Remove( this ); +} + +void CPhysicsNPCSolver::Init( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject, bool disableCollisions, float separationTime ) +{ + m_hNPC = pNPC; + m_hEntity = pPhysicsObject; + m_pController = NULL; + m_separationDuration = separationTime; + m_allowIntersection = disableCollisions; + +} + +void CPhysicsNPCSolver::ResetCancelTime() +{ + m_cancelTime = gpGlobals->curtime + m_separationDuration; + SetNextThink( m_cancelTime ); +} + +void CPhysicsNPCSolver::BecomePenetrationSolver() +{ + CBaseEntity *pEntity = m_hEntity.Get(); + if ( pEntity ) + { + m_allowIntersection = true; + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int listCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + PhysDisableEntityCollisions( m_hNPC, pEntity ); + m_pController = physenv->CreateMotionController( this ); + for ( int i = 0; i < listCount; i++ ) + { + m_pController->AttachObject( pList[i], false ); + pList[i]->Wake(); + } + m_pController->SetPriority( IPhysicsMotionController::HIGH_PRIORITY ); + } +} + +void CPhysicsNPCSolver::Spawn() +{ + if ( m_allowIntersection ) + { + BecomePenetrationSolver(); + } + else + { + m_hEntity->SetNavIgnore(); + } + ResetCancelTime(); +} + +void CPhysicsNPCSolver::UpdateOnRemove() +{ + if ( m_allowIntersection ) + { + physenv->DestroyMotionController( m_pController ); + m_pController = NULL; + PhysEnableEntityCollisions( m_hNPC, m_hEntity ); + } + else + { + if ( m_hEntity.Get() ) + { + m_hEntity->ClearNavIgnore(); + } + } + //NDebugOverlay::EntityBounds(m_hNPC, 0, 255, 0, 64, 0.5f ); + BaseClass::UpdateOnRemove(); +} + +bool CPhysicsNPCSolver::IsIntersecting() +{ + CAI_BaseNPC *pNPC = m_hNPC.Get(); + CBaseEntity *pPhysics = m_hEntity.Get(); + if ( pNPC && pPhysics ) + { + Ray_t ray; + // bloated bounds to force slight separation + Vector mins = pNPC->WorldAlignMins() - Vector(1,1,1); + Vector maxs = pNPC->WorldAlignMaxs() + Vector(1,1,1); + + ray.Init( pNPC->GetAbsOrigin(), pNPC->GetAbsOrigin(), mins, maxs ); + trace_t tr; + enginetrace->ClipRayToEntity( ray, pNPC->PhysicsSolidMaskForEntity(), pPhysics, &tr ); + if ( tr.startsolid ) + return true; + } + return false; +} + +bool CPhysicsNPCSolver::IsContactOnNPCHead( IPhysicsFrictionSnapshot *pSnapshot, IPhysicsObject *pPhysics, CAI_BaseNPC *pNPC ) +{ + float heightCheck = pNPC->GetAbsOrigin().z + pNPC->GetHullMaxs().z; + Vector vel, point; + pPhysics->GetVelocity( &vel, NULL ); + pSnapshot->GetContactPoint( point ); + // don't care if the object is already moving away + if ( vel.LengthSqr() < 10.0f*10.0f ) + { + float topdist = fabs(point.z-heightCheck); + if ( topdist < 2.0f ) + { + return true; + } + } + return false; +} + +bool CPhysicsNPCSolver::CheckTouching() +{ + CAI_BaseNPC *pNPC = m_hNPC.Get(); + if ( !pNPC ) + return false; + + CBaseEntity *pPhysicsEnt = m_hEntity.Get(); + if ( !pPhysicsEnt ) + return false; + + IPhysicsObject *pPhysics = pPhysicsEnt->VPhysicsGetObject(); + IPhysicsObject *pNPCPhysics = pNPC->VPhysicsGetObject(); + if ( !pNPCPhysics || !pPhysics ) + return false; + + IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); + bool found = false; + bool penetrate = false; + + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + if ( pOther == pNPCPhysics ) + { + found = true; + if ( IsContactOnNPCHead(pSnapshot, pPhysics, pNPC ) ) + { + penetrate = true; + pSnapshot->MarkContactForDelete(); + } + break; + } + pSnapshot->NextFrictionData(); + } + pSnapshot->DeleteAllMarkedContacts( true ); + pPhysics->DestroyFrictionSnapshot( pSnapshot ); + + // if the object is penetrating something, check to see if it's intersecting this NPC + // if so, go ahead and switch over to penetration solver mode + if ( !penetrate && (pPhysics->GetGameFlags() & FVPHYSICS_PENETRATING) ) + { + penetrate = IsIntersecting(); + } + + if ( penetrate ) + { + pPhysicsEnt->ClearNavIgnore(); + BecomePenetrationSolver(); + } + + return found; +} + +void CPhysicsNPCSolver::Think() +{ + bool finished = m_allowIntersection ? !IsIntersecting() : !CheckTouching(); + + if ( finished ) + { + UTIL_Remove(this); + return; + } + if ( m_allowIntersection ) + { + IPhysicsObject *pObject = m_hEntity->VPhysicsGetObject(); + if ( !pObject ) + { + UTIL_Remove(this); + return; + } + pObject->Wake(); + } + ResetCancelTime(); +} + +IMotionEvent::simresult_e CPhysicsNPCSolver::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, + float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + if ( IsIntersecting() ) + { + const float PUSH_SPEED = 150.0f; + + if ( pObject->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer ) + { + pPlayer->ForceDropOfCarriedPhysObjects( m_hEntity ); + } + } + + ResetCancelTime(); + angular.Init(); + linear.Init(); + + // Don't push on vehicles because they won't move + if ( pObject->GetGameFlags() & FVPHYSICS_MULTIOBJECT_ENTITY ) + { + if ( m_hEntity->GetServerVehicle() ) + return SIM_NOTHING; + } + + Vector origin, vel; + pObject->GetPosition( &origin, NULL ); + pObject->GetVelocity( &vel, NULL ); + Vector dir = origin - m_hNPC->GetAbsOrigin(); + dir.z = dir.z > 0 ? 0.1f : -0.1f; + VectorNormalize(dir); + AngularImpulse angVel; + angVel.Init(); + + // NOTE: Iterate this object's contact points + // if it can't move in this direction, try sliding along the plane/crease + Vector pushImpulse; + PhysComputeSlideDirection( pObject, dir * PUSH_SPEED, angVel, &pushImpulse, NULL, 0 ); + + dir = pushImpulse; + VectorNormalize(dir); + + if ( DotProduct( vel, dir ) < PUSH_SPEED * 0.5f ) + { + linear = pushImpulse; + if ( pObject->GetContactPoint(NULL,NULL) ) + { + linear.z += GetCurrentGravity(); + } + } + return SIM_GLOBAL_ACCELERATION; + } + return SIM_NOTHING; +} + + +CBaseEntity *NPCPhysics_CreateSolver( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject, bool disableCollisions, float separationDuration ) +{ + if ( disableCollisions ) + { + if ( PhysEntityCollisionsAreDisabled( pNPC, pPhysicsObject ) ) + return NULL; + } + else + { + if ( pPhysicsObject->IsNavIgnored() ) + return NULL; + } + return CPhysicsNPCSolver::Create( pNPC, pPhysicsObject, disableCollisions, separationDuration ); +} + + +class CPhysicsEntitySolver : public CLogicalEntity//, public IMotionEvent +{ + DECLARE_CLASS( CPhysicsEntitySolver, CLogicalEntity ); +public: + DECLARE_DATADESC(); + void Init( CBaseEntity *pMovingEntity, CBaseEntity *pPhysicsBlocker, float separationTime ); + static CPhysicsEntitySolver *Create( CBaseEntity *pMovingEntity, CBaseEntity *pPhysicsBlocker, float separationTime ); + + // CBaseEntity + virtual void Spawn(); + virtual void UpdateOnRemove(); + virtual void Think(); + + // IMotionEvent + //virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + +private: + // locals + void ResetCancelTime(); + void BecomePenetrationSolver(); + //bool IsIntersecting(); + //bool IsTouching(); + + EHANDLE m_hMovingEntity; + EHANDLE m_hPhysicsBlocker; + //IPhysicsMotionController *m_pController; + float m_separationDuration; + float m_cancelTime; + int m_savedCollisionGroup; +}; + +LINK_ENTITY_TO_CLASS( physics_entity_solver, CPhysicsEntitySolver ); + +BEGIN_DATADESC( CPhysicsEntitySolver ) + + DEFINE_FIELD( m_hMovingEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_hPhysicsBlocker, FIELD_EHANDLE ), + DEFINE_FIELD( m_separationDuration, FIELD_FLOAT ), + DEFINE_FIELD( m_cancelTime, FIELD_TIME ), + DEFINE_FIELD( m_savedCollisionGroup, FIELD_INTEGER ), + //DEFINE_PHYSPTR( m_pController ), + +END_DATADESC() + +CPhysicsEntitySolver *CPhysicsEntitySolver::Create( CBaseEntity *pMovingEntity, CBaseEntity *pPhysicsBlocker, float separationTime ) +{ + CPhysicsEntitySolver *pSolver = (CPhysicsEntitySolver *)CBaseEntity::CreateNoSpawn( "physics_entity_solver", vec3_origin, vec3_angle, NULL ); + pSolver->Init( pMovingEntity, pPhysicsBlocker, separationTime ); + pSolver->Spawn(); + //NDebugOverlay::EntityBounds(pNPC, 255, 255, 0, 64, 0.5f ); + return pSolver; +} + +void CPhysicsEntitySolver::Init( CBaseEntity *pMovingEntity, CBaseEntity *pPhysicsBlocker, float separationTime ) +{ + m_hMovingEntity = pMovingEntity; + m_hPhysicsBlocker = pPhysicsBlocker; + //m_pController = NULL; + m_separationDuration = separationTime; +} + +void CPhysicsEntitySolver::Spawn() +{ + SetNextThink( gpGlobals->curtime + m_separationDuration ); + PhysDisableEntityCollisions( m_hMovingEntity, m_hPhysicsBlocker ); + m_savedCollisionGroup = m_hPhysicsBlocker->GetCollisionGroup(); + m_hPhysicsBlocker->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + if ( m_hPhysicsBlocker->VPhysicsGetObject() ) + { + m_hPhysicsBlocker->VPhysicsGetObject()->RecheckContactPoints(); + } +} + +void CPhysicsEntitySolver::Think() +{ + UTIL_Remove(this); +} + +void CPhysicsEntitySolver::UpdateOnRemove() +{ + //physenv->DestroyMotionController( m_pController ); + //m_pController = NULL; + CBaseEntity *pEntity = m_hMovingEntity.Get(); + CBaseEntity *pPhysics = m_hPhysicsBlocker.Get(); + if ( pEntity && pPhysics ) + { + PhysEnableEntityCollisions( pEntity, pPhysics ); + } + if ( pPhysics ) + { + pPhysics->SetCollisionGroup( m_savedCollisionGroup ); + } + BaseClass::UpdateOnRemove(); +} + + +CBaseEntity *EntityPhysics_CreateSolver( CBaseEntity *pMovingEntity, CBaseEntity *pPhysicsObject, bool disableCollisions, float separationDuration ) +{ + if ( PhysEntityCollisionsAreDisabled( pMovingEntity, pPhysicsObject ) ) + return NULL; + + return CPhysicsEntitySolver::Create( pMovingEntity, pPhysicsObject, separationDuration ); +} + diff --git a/sp/src/game/server/physics_npc_solver.h b/sp/src/game/server/physics_npc_solver.h new file mode 100644 index 00000000..12e2e188 --- /dev/null +++ b/sp/src/game/server/physics_npc_solver.h @@ -0,0 +1,19 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PHYSICS_NPC_SOLVER_H +#define PHYSICS_NPC_SOLVER_H +#ifdef _WIN32 +#pragma once +#endif + + +extern CBaseEntity *NPCPhysics_CreateSolver( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject, bool disableCollisions, float separationDuration ); +extern CBaseEntity *EntityPhysics_CreateSolver( CBaseEntity *pMovingEntity, CBaseEntity *pPhysicsBlocker, bool disableCollisions, float separationDuration ); +bool NPCPhysics_SolverExists( CAI_BaseNPC *pNPC, CBaseEntity *pPhysicsObject ); + + +#endif // PHYSICS_NPC_SOLVER_H diff --git a/sp/src/game/server/physics_prop_ragdoll.cpp b/sp/src/game/server/physics_prop_ragdoll.cpp new file mode 100644 index 00000000..f417175f --- /dev/null +++ b/sp/src/game/server/physics_prop_ragdoll.cpp @@ -0,0 +1,1872 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "baseanimating.h" +#include "studio.h" +#include "physics.h" +#include "physics_saverestore.h" +#include "ai_basenpc.h" +#include "vphysics/constraints.h" +#include "datacache/imdlcache.h" +#include "bone_setup.h" +#include "physics_prop_ragdoll.h" +#include "KeyValues.h" +#include "props.h" +#include "RagdollBoogie.h" +#include "AI_Criteria.h" +#include "ragdoll_shared.h" +#include "hierarchy.h" +#ifdef MAPBASE +#include "decals.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef MAPBASE +ConVar ragdoll_autointeractions("ragdoll_autointeractions", "1", FCVAR_NONE, "Controls whether we should rely on hardcoded keyvalues or automatic flesh checks for ragdoll physgun interactions."); +#define IsBody() VPhysicsIsFlesh() +#endif + +//----------------------------------------------------------------------------- +// Forward declarations +//----------------------------------------------------------------------------- +const char *GetMassEquivalent(float flMass); + +#define RAGDOLL_VISUALIZE 0 + +//----------------------------------------------------------------------------- +// ThinkContext +//----------------------------------------------------------------------------- +const char *s_pFadeOutContext = "RagdollFadeOutContext"; +const char *s_pDebrisContext = "DebrisContext"; + +const float ATTACHED_DAMPING_SCALE = 50.0f; + +//----------------------------------------------------------------------------- +// Spawnflags +//----------------------------------------------------------------------------- +#define SF_RAGDOLLPROP_DEBRIS 0x0004 +#define SF_RAGDOLLPROP_USE_LRU_RETIREMENT 0x1000 +#define SF_RAGDOLLPROP_ALLOW_DISSOLVE 0x2000 // Allow this prop to be dissolved +#define SF_RAGDOLLPROP_MOTIONDISABLED 0x4000 +#define SF_RAGDOLLPROP_ALLOW_STRETCH 0x8000 +#define SF_RAGDOLLPROP_STARTASLEEP 0x10000 +#ifdef MAPBASE +#define SF_RAGDOLLPROP_FIXED_CONSTRAINTS 0x20000 +#endif + +//----------------------------------------------------------------------------- +// Networking +//----------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( physics_prop_ragdoll, CRagdollProp ); +LINK_ENTITY_TO_CLASS( prop_ragdoll, CRagdollProp ); +EXTERN_SEND_TABLE(DT_Ragdoll) + +IMPLEMENT_SERVERCLASS_ST(CRagdollProp, DT_Ragdoll) + SendPropArray (SendPropQAngles(SENDINFO_ARRAY(m_ragAngles), 13, 0 ), m_ragAngles), + SendPropArray (SendPropVector(SENDINFO_ARRAY(m_ragPos), -1, SPROP_COORD ), m_ragPos), + SendPropEHandle(SENDINFO( m_hUnragdoll ) ), + SendPropFloat(SENDINFO(m_flBlendWeight), 8, SPROP_ROUNDDOWN, 0.0f, 1.0f ), + SendPropInt(SENDINFO(m_nOverlaySequence), 11), +END_SEND_TABLE() + +#define DEFINE_RAGDOLL_ELEMENT( i ) \ + DEFINE_FIELD( m_ragdoll.list[i].originParentSpace, FIELD_VECTOR ), \ + DEFINE_PHYSPTR( m_ragdoll.list[i].pObject ), \ + DEFINE_PHYSPTR( m_ragdoll.list[i].pConstraint ), \ + DEFINE_FIELD( m_ragdoll.list[i].parentIndex, FIELD_INTEGER ) + +BEGIN_DATADESC(CRagdollProp) +// m_ragdoll (custom handling) + DEFINE_AUTO_ARRAY ( m_ragdoll.boneIndex, FIELD_INTEGER ), + DEFINE_AUTO_ARRAY ( m_ragPos, FIELD_POSITION_VECTOR ), + DEFINE_AUTO_ARRAY ( m_ragAngles, FIELD_VECTOR ), + DEFINE_KEYFIELD(m_anglesOverrideString, FIELD_STRING, "angleOverride" ), + DEFINE_FIELD( m_lastUpdateTickCount, FIELD_INTEGER ), + DEFINE_FIELD( m_allAsleep, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hDamageEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_hKiller, FIELD_EHANDLE ), + + DEFINE_KEYFIELD( m_bStartDisabled, FIELD_BOOLEAN, "StartDisabled" ), + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_FLOAT, "StartRagdollBoogie", InputStartRadgollBoogie ), +#else + DEFINE_INPUTFUNC( FIELD_VOID, "StartRagdollBoogie", InputStartRadgollBoogie ), +#endif + DEFINE_INPUTFUNC( FIELD_VOID, "EnableMotion", InputEnableMotion ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableMotion", InputDisableMotion ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "Wake", InputWake ), + DEFINE_INPUTFUNC( FIELD_VOID, "Sleep", InputSleep ), +#endif + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "FadeAndRemove", InputFadeAndRemove ), + + DEFINE_FIELD( m_hUnragdoll, FIELD_EHANDLE ), + DEFINE_FIELD( m_bFirstCollisionAfterLaunch, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_flBlendWeight, FIELD_FLOAT ), + DEFINE_FIELD( m_nOverlaySequence, FIELD_INTEGER ), + DEFINE_AUTO_ARRAY( m_ragdollMins, FIELD_VECTOR ), + DEFINE_AUTO_ARRAY( m_ragdollMaxs, FIELD_VECTOR ), + + // Physics Influence + DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), + DEFINE_FIELD( m_flFadeOutStartTime, FIELD_TIME ), + DEFINE_FIELD( m_flFadeTime, FIELD_FLOAT), + DEFINE_FIELD( m_strSourceClassName, FIELD_STRING ), + DEFINE_FIELD( m_bHasBeenPhysgunned, FIELD_BOOLEAN ), + + // think functions + DEFINE_THINKFUNC( SetDebrisThink ), + DEFINE_THINKFUNC( ClearFlagsThink ), + DEFINE_THINKFUNC( FadeOutThink ), + + DEFINE_FIELD( m_ragdoll.listCount, FIELD_INTEGER ), + DEFINE_FIELD( m_ragdoll.allowStretch, FIELD_BOOLEAN ), + DEFINE_PHYSPTR( m_ragdoll.pGroup ), + DEFINE_FIELD( m_flDefaultFadeScale, FIELD_FLOAT ), + + //DEFINE_RAGDOLL_ELEMENT( 0 ), + DEFINE_RAGDOLL_ELEMENT( 1 ), + DEFINE_RAGDOLL_ELEMENT( 2 ), + DEFINE_RAGDOLL_ELEMENT( 3 ), + DEFINE_RAGDOLL_ELEMENT( 4 ), + DEFINE_RAGDOLL_ELEMENT( 5 ), + DEFINE_RAGDOLL_ELEMENT( 6 ), + DEFINE_RAGDOLL_ELEMENT( 7 ), + DEFINE_RAGDOLL_ELEMENT( 8 ), + DEFINE_RAGDOLL_ELEMENT( 9 ), + DEFINE_RAGDOLL_ELEMENT( 10 ), + DEFINE_RAGDOLL_ELEMENT( 11 ), + DEFINE_RAGDOLL_ELEMENT( 12 ), + DEFINE_RAGDOLL_ELEMENT( 13 ), + DEFINE_RAGDOLL_ELEMENT( 14 ), + DEFINE_RAGDOLL_ELEMENT( 15 ), + DEFINE_RAGDOLL_ELEMENT( 16 ), + DEFINE_RAGDOLL_ELEMENT( 17 ), + DEFINE_RAGDOLL_ELEMENT( 18 ), + DEFINE_RAGDOLL_ELEMENT( 19 ), + DEFINE_RAGDOLL_ELEMENT( 20 ), + DEFINE_RAGDOLL_ELEMENT( 21 ), + DEFINE_RAGDOLL_ELEMENT( 22 ), + DEFINE_RAGDOLL_ELEMENT( 23 ), + +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +BEGIN_ENT_SCRIPTDESC( CRagdollProp, CBaseAnimating, "Ragdoll physics prop." ) + + DEFINE_SCRIPTFUNC_NAMED( GetSourceClassNameAsCStr, "GetSourceClassName", "Gets the ragdoll's source classname." ) + DEFINE_SCRIPTFUNC( SetSourceClassName, "Sets the ragdoll's source classname." ) + DEFINE_SCRIPTFUNC( HasPhysgunInteraction, "Checks if the ragdoll has the specified interaction." ) + + // TODO: Proper shared ragdoll funcs? + DEFINE_SCRIPTFUNC_NAMED( ScriptGetRagdollObject, "GetRagdollObject", "Gets the ragdoll object of the specified index." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetRagdollObjectCount, "GetRagdollObjectCount", "Gets the number of ragdoll objects on this ragdoll." ) + +END_SCRIPTDESC() +#endif + +//----------------------------------------------------------------------------- +// Disable auto fading under dx7 or when level fades are specified +//----------------------------------------------------------------------------- +void CRagdollProp::DisableAutoFade() +{ + m_flFadeScale = 0; + m_flDefaultFadeScale = 0; +} + + +void CRagdollProp::Spawn( void ) +{ + // Starts out as the default fade scale value + m_flDefaultFadeScale = m_flFadeScale; + + // NOTE: If this fires, then the assert or the datadesc is wrong! (see DEFINE_RAGDOLL_ELEMENT above) + Assert( RAGDOLL_MAX_ELEMENTS == 24 ); + Precache(); + SetModel( STRING( GetModelName() ) ); + + CStudioHdr *pStudioHdr = GetModelPtr( ); + if ( pStudioHdr->flags() & STUDIOHDR_FLAGS_NO_FORCED_FADE ) + { + DisableAutoFade(); + } + else + { + m_flFadeScale = m_flDefaultFadeScale; + } + + matrix3x4_t pBoneToWorld[MAXSTUDIOBONES]; + BaseClass::SetupBones( pBoneToWorld, BONE_USED_BY_ANYTHING ); // FIXME: shouldn't this be a subset of the bones + // this is useless info after the initial conditions are set + SetAbsAngles( vec3_angle ); + int collisionGroup = (m_spawnflags & SF_RAGDOLLPROP_DEBRIS) ? COLLISION_GROUP_DEBRIS : COLLISION_GROUP_NONE; + bool bWake = (m_spawnflags & SF_RAGDOLLPROP_STARTASLEEP) ? false : true; + InitRagdoll( vec3_origin, 0, vec3_origin, pBoneToWorld, pBoneToWorld, 0, collisionGroup, true, bWake ); + m_lastUpdateTickCount = 0; + m_flBlendWeight = 0.0f; + m_nOverlaySequence = -1; + + // Unless specified, do not allow this to be dissolved + if ( HasSpawnFlags( SF_RAGDOLLPROP_ALLOW_DISSOLVE ) == false ) + { + AddEFlags( EFL_NO_DISSOLVE ); + } + + if ( HasSpawnFlags(SF_RAGDOLLPROP_MOTIONDISABLED) ) + { + DisableMotion(); + } + + if( m_bStartDisabled ) + { + AddEffects( EF_NODRAW ); + } +} + +void CRagdollProp::SetSourceClassName( const char *pClassname ) +{ + m_strSourceClassName = MAKE_STRING( pClassname ); +} + + +void CRagdollProp::OnSave( IEntitySaveUtils *pUtils ) +{ + if ( !m_ragdoll.listCount ) + return; + + // Don't save ragdoll element 0, base class saves the pointer in + // m_pPhysicsObject + Assert( m_ragdoll.list[0].parentIndex == -1 ); + Assert( m_ragdoll.list[0].pConstraint == NULL ); + Assert( m_ragdoll.list[0].originParentSpace == vec3_origin ); + Assert( m_ragdoll.list[0].pObject != NULL ); + VPhysicsSetObject( NULL ); // squelch a warning message + VPhysicsSetObject( m_ragdoll.list[0].pObject ); // make sure object zero is saved by CBaseEntity + BaseClass::OnSave( pUtils ); +} + +void CRagdollProp::OnRestore() +{ + // rebuild element 0 since it isn't saved + // NOTE: This breaks the rules - the pointer needs to get fixed in Restore() + m_ragdoll.list[0].pObject = VPhysicsGetObject(); + m_ragdoll.list[0].parentIndex = -1; + m_ragdoll.list[0].originParentSpace.Init(); + + BaseClass::OnRestore(); + if ( !m_ragdoll.listCount ) + return; + + // JAY: Reset collision relationships + RagdollSetupCollisions( m_ragdoll, modelinfo->GetVCollide( GetModelIndex() ), GetModelIndex() ); + VPhysicsUpdate( VPhysicsGetObject() ); +} + +void CRagdollProp::CalcRagdollSize( void ) +{ + CollisionProp()->SetSurroundingBoundsType( USE_HITBOXES ); + CollisionProp()->RemoveSolidFlags( FSOLID_FORCE_WORLD_ALIGNED ); +} + +void CRagdollProp::UpdateOnRemove( void ) +{ + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + if ( m_ragdoll.list[i].pObject ) + { + g_pPhysSaveRestoreManager->ForgetModel( m_ragdoll.list[i].pObject ); + } + } + + // Set to null so that the destructor's call to DestroyObject won't destroy + // m_pObjects[ 0 ] twice since that's the physics object for the prop + VPhysicsSetObject( NULL ); + + RagdollDestroy( m_ragdoll ); + // Chain to base after doing our own cleanup to mimic + // destructor unwind order + BaseClass::UpdateOnRemove(); +} + +CRagdollProp::CRagdollProp( void ) +{ + m_strSourceClassName = NULL_STRING; + m_anglesOverrideString = NULL_STRING; + m_ragdoll.listCount = 0; + Assert( (1<=RAGDOLL_MAX_ELEMENTS ); + m_allAsleep = false; + m_flFadeScale = 1; + m_flDefaultFadeScale = 1; +} + +CRagdollProp::~CRagdollProp( void ) +{ +} + +void CRagdollProp::Precache( void ) +{ + PrecacheModel( STRING( GetModelName() ) ); + BaseClass::Precache(); +} + +int CRagdollProp::ObjectCaps() +{ + return BaseClass::ObjectCaps() | FCAP_WCEDIT_POSITION; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRagdollProp::InitRagdollAnimation() +{ + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0; + SetCycle( 0 ); + + // put into ACT_DIERAGDOLL if it exists, otherwise use sequence 0 + int nSequence = SelectWeightedSequence( ACT_DIERAGDOLL ); + if ( nSequence < 0 ) + { + ResetSequence( 0 ); + } + else + { + ResetSequence( nSequence ); + } +} + + +//----------------------------------------------------------------------------- +// Response system stuff +//----------------------------------------------------------------------------- +IResponseSystem *CRagdollProp::GetResponseSystem() +{ + extern IResponseSystem *g_pResponseSystem; + + // Just use the general NPC response system; we often come from NPCs after all + return g_pResponseSystem; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRagdollProp::ModifyOrAppendCriteria( AI_CriteriaSet& set ) +{ + BaseClass::ModifyOrAppendCriteria( set ); + + if ( m_strSourceClassName != NULL_STRING ) + { + set.RemoveCriteria( "classname" ); + set.AppendCriteria( "classname", STRING(m_strSourceClassName) ); + set.AppendCriteria( "ragdoll", "1" ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRagdollProp::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + m_hPhysicsAttacker = pPhysGunUser; + m_flLastPhysicsInfluenceTime = gpGlobals->curtime; + + // Clear out the classname if we've been physgunned before + // so that the screams, etc. don't happen. Simulate that the first + // major punt or throw has been enough to kill him. + if ( m_bHasBeenPhysgunned ) + { + m_strSourceClassName = NULL_STRING; + } + m_bHasBeenPhysgunned = true; + +#ifdef MAPBASE + if( (ragdoll_autointeractions.GetBool() == true && IsBody()) || HasPhysgunInteraction( "onpickup", "boogie" ) ) +#else + if( HasPhysgunInteraction( "onpickup", "boogie" ) ) +#endif + { + if ( reason == PUNTED_BY_CANNON ) + { + CRagdollBoogie::Create( this, 150, gpGlobals->curtime, 3.0f, SF_RAGDOLL_BOOGIE_ELECTRICAL ); + } + else + { + CRagdollBoogie::Create( this, 150, gpGlobals->curtime, 2.0f, 0.0f ); + } + } + + if ( HasSpawnFlags( SF_RAGDOLLPROP_USE_LRU_RETIREMENT ) ) + { + s_RagdollLRU.MoveToTopOfLRU( this ); + } + + if ( !HasSpawnFlags( SF_PHYSPROP_ENABLE_ON_PHYSCANNON ) ) + return; + + ragdoll_t *pRagdollPhys = GetRagdoll( ); + for ( int j = 0; j < pRagdollPhys->listCount; ++j ) + { + pRagdollPhys->list[j].pObject->Wake(); + pRagdollPhys->list[j].pObject->EnableMotion( true ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRagdollProp::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + CDefaultPlayerPickupVPhysics::OnPhysGunDrop( pPhysGunUser, Reason ); + m_hPhysicsAttacker = pPhysGunUser; + m_flLastPhysicsInfluenceTime = gpGlobals->curtime; + +#ifdef MAPBASE + if( (ragdoll_autointeractions.GetBool() == true && IsBody()) || HasPhysgunInteraction( "onpickup", "boogie" ) ) +#else + if( HasPhysgunInteraction( "onpickup", "boogie" ) ) +#endif + { + CRagdollBoogie::Create( this, 150, gpGlobals->curtime, 3.0f, SF_RAGDOLL_BOOGIE_ELECTRICAL ); + } + + if ( HasSpawnFlags( SF_RAGDOLLPROP_USE_LRU_RETIREMENT ) ) + { + s_RagdollLRU.MoveToTopOfLRU( this ); + } + + // Make sure it's interactive debris for at most 5 seconds + if ( GetCollisionGroup() == COLLISION_GROUP_INTERACTIVE_DEBRIS ) + { + SetContextThink( &CRagdollProp::SetDebrisThink, gpGlobals->curtime + 5, s_pDebrisContext ); + } + + if ( Reason != LAUNCHED_BY_CANNON ) + return; + +#ifdef MAPBASE + if( (ragdoll_autointeractions.GetBool() == true && IsBody()) || HasPhysgunInteraction( "onlaunch", "spin_zaxis" ) ) +#else + if( HasPhysgunInteraction( "onlaunch", "spin_zaxis" ) ) +#endif + { + Vector vecAverageCenter( 0, 0, 0 ); + + // Get the average position, apply forces to produce a spin + int j; + ragdoll_t *pRagdollPhys = GetRagdoll( ); + for ( j = 0; j < pRagdollPhys->listCount; ++j ) + { + Vector vecCenter; + pRagdollPhys->list[j].pObject->GetPosition( &vecCenter, NULL ); + vecAverageCenter += vecCenter; + } + + vecAverageCenter /= pRagdollPhys->listCount; + + Vector vecZAxis( 0, 0, 1 ); + for ( j = 0; j < pRagdollPhys->listCount; ++j ) + { + Vector vecDelta; + pRagdollPhys->list[j].pObject->GetPosition( &vecDelta, NULL ); + vecDelta -= vecAverageCenter; + + Vector vecDir; + CrossProduct( vecZAxis, vecDelta, vecDir ); + vecDir *= 100; + pRagdollPhys->list[j].pObject->AddVelocity( &vecDir, NULL ); + } + } + + PhysSetGameFlags( VPhysicsGetObject(), FVPHYSICS_WAS_THROWN ); + m_bFirstCollisionAfterLaunch = true; +} + + +//----------------------------------------------------------------------------- +// Physics attacker +//----------------------------------------------------------------------------- +CBasePlayer *CRagdollProp::HasPhysicsAttacker( float dt ) +{ + if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) + { + return m_hPhysicsAttacker; + } + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRagdollProp::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + BaseClass::VPhysicsCollision( index, pEvent ); + + CBaseEntity *pHitEntity = pEvent->pEntities[!index]; + if ( pHitEntity == this ) + return; + + // Don't take physics damage from whoever's holding him with the physcannon. + if ( VPhysicsGetObject() && (VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD) ) + { + if ( pHitEntity && (pHitEntity == HasPhysicsAttacker( FLT_MAX )) ) + return; + } + + // Don't bother taking damage from the physics attacker + if ( pHitEntity && HasPhysicsAttacker( 0.5f ) == pHitEntity ) + return; + + if( m_bFirstCollisionAfterLaunch ) + { + HandleFirstCollisionInteractions( index, pEvent ); + } + + if ( m_takedamage != DAMAGE_NO ) + { + int damageType = 0; + float damage = CalculateDefaultPhysicsDamage( index, pEvent, 1.0f, true, damageType ); + if ( damage > 0 ) + { + // Take extra damage after we're punted by the physcannon + if ( m_bFirstCollisionAfterLaunch ) + { + damage *= 10; + } + + CBaseEntity *pHitEntity = pEvent->pEntities[!index]; + if ( !pHitEntity ) + { + // hit world + pHitEntity = GetContainingEntity( INDEXENT(0) ); + } + Vector damagePos; + pEvent->pInternalData->GetContactPoint( damagePos ); + Vector damageForce = pEvent->postVelocity[index] * pEvent->pObjects[index]->GetMass(); + if ( damageForce == vec3_origin ) + { + // This can happen if this entity is motion disabled, and can't move. + // Use the velocity of the entity that hit us instead. + damageForce = pEvent->postVelocity[!index] * pEvent->pObjects[!index]->GetMass(); + } + + // FIXME: this doesn't pass in who is responsible if some other entity "caused" this collision + PhysCallbackDamage( this, CTakeDamageInfo( pHitEntity, pHitEntity, damageForce, damagePos, damage, damageType ), *pEvent, index ); + } + } + + if ( m_bFirstCollisionAfterLaunch ) + { + // Setup the think function to remove the flags + SetThink( &CRagdollProp::ClearFlagsThink ); + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CRagdollProp::HasPhysgunInteraction( const char *pszKeyName, const char *pszValue ) +{ + KeyValues *modelKeyValues = new KeyValues(""); + if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) + { + KeyValues *pkvPropData = modelKeyValues->FindKey("physgun_interactions"); + if ( pkvPropData ) + { + char const *pszBase = pkvPropData->GetString( pszKeyName ); + + if ( pszBase && pszBase[0] && !stricmp( pszBase, pszValue ) ) + { + modelKeyValues->deleteThis(); + return true; + } + } + } + + modelKeyValues->deleteThis(); + return false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CRagdollProp::HandleFirstCollisionInteractions( int index, gamevcollisionevent_t *pEvent ) +{ + IPhysicsObject *pObj = VPhysicsGetObject(); + if ( !pObj) + return; + + if( HasPhysgunInteraction( "onfirstimpact", "break" ) ) + { + // Looks like it's best to break by having the object damage itself. + CTakeDamageInfo info; + + info.SetDamage( m_iHealth ); + info.SetAttacker( this ); + info.SetInflictor( this ); + info.SetDamageType( DMG_GENERIC ); + + Vector vecPosition; + Vector vecVelocity; + + VPhysicsGetObject()->GetVelocity( &vecVelocity, NULL ); + VPhysicsGetObject()->GetPosition( &vecPosition, NULL ); + + info.SetDamageForce( vecVelocity ); + info.SetDamagePosition( vecPosition ); + + TakeDamage( info ); + return; + } + + if( HasPhysgunInteraction( "onfirstimpact", "paintsplat" ) ) + { + IPhysicsObject *pObj = VPhysicsGetObject(); + + Vector vecPos; + pObj->GetPosition( &vecPos, NULL ); + + trace_t tr; + UTIL_TraceLine( vecPos, vecPos + pEvent->preVelocity[0] * 1.5, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + switch( random->RandomInt( 1, 3 ) ) + { + case 1: + UTIL_DecalTrace( &tr, "PaintSplatBlue" ); + break; + + case 2: + UTIL_DecalTrace( &tr, "PaintSplatGreen" ); + break; + + case 3: + UTIL_DecalTrace( &tr, "PaintSplatPink" ); + break; + } + } + +#ifdef MAPBASE + int iVPhysicsFlesh = VPhysicsGetFlesh(); + bool bRagdollAutoInt = (ragdoll_autointeractions.GetBool() == true && iVPhysicsFlesh); + bool bAlienBloodSplat = HasPhysgunInteraction( "onfirstimpact", "alienbloodsplat" ); + if (bRagdollAutoInt && !bAlienBloodSplat) + { + // Alien blood? + bAlienBloodSplat = (iVPhysicsFlesh == CHAR_TEX_ALIENFLESH || iVPhysicsFlesh == CHAR_TEX_ANTLION); + } + + if( bRagdollAutoInt || bAlienBloodSplat || HasPhysgunInteraction( "onfirstimpact", "bloodsplat" ) ) +#else + bool bAlienBloodSplat = HasPhysgunInteraction( "onfirstimpact", "alienbloodsplat" ); + if( bAlienBloodSplat || HasPhysgunInteraction( "onfirstimpact", "bloodsplat" ) ) +#endif + { + IPhysicsObject *pObj = VPhysicsGetObject(); + + Vector vecPos; + pObj->GetPosition( &vecPos, NULL ); + + trace_t tr; + UTIL_TraceLine( vecPos, vecPos + pEvent->preVelocity[0] * 1.5, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + UTIL_BloodDecalTrace( &tr, bAlienBloodSplat ? BLOOD_COLOR_GREEN : BLOOD_COLOR_RED ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRagdollProp::ClearFlagsThink( void ) +{ + PhysClearGameFlags( VPhysicsGetObject(), FVPHYSICS_WAS_THROWN ); + m_bFirstCollisionAfterLaunch = false; + SetThink( NULL ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +AngularImpulse CRagdollProp::PhysGunLaunchAngularImpulse() +{ +#ifdef MAPBASE + if( (ragdoll_autointeractions.GetBool() == true && IsBody()) || HasPhysgunInteraction( "onlaunch", "spin_zaxis" ) ) +#else + if( HasPhysgunInteraction( "onlaunch", "spin_zaxis" ) ) +#endif + { + // Don't add in random angular impulse if this object is supposed to spin in a specific way. + AngularImpulse ang( 0, 0, 0 ); + return ang; + } + + return CDefaultPlayerPickupVPhysics::PhysGunLaunchAngularImpulse(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : activity - +//----------------------------------------------------------------------------- +void CRagdollProp::SetOverlaySequence( Activity activity ) +{ + int seq = SelectWeightedSequence( activity ); + if ( seq < 0 ) + { + m_nOverlaySequence = -1; + } + else + { + m_nOverlaySequence = seq; + } +} + +void CRagdollProp::InitRagdoll( const Vector &forceVector, int forceBone, const Vector &forcePos, matrix3x4_t *pPrevBones, matrix3x4_t *pBoneToWorld, float dt, int collisionGroup, bool activateRagdoll, bool bWakeRagdoll ) +{ + SetCollisionGroup( collisionGroup ); + + // Make sure it's interactive debris for at most 5 seconds + if ( collisionGroup == COLLISION_GROUP_INTERACTIVE_DEBRIS ) + { + SetContextThink( &CRagdollProp::SetDebrisThink, gpGlobals->curtime + 5, s_pDebrisContext ); + } + + SetMoveType( MOVETYPE_VPHYSICS ); + SetSolid( SOLID_VPHYSICS ); + AddSolidFlags( FSOLID_CUSTOMRAYTEST | FSOLID_CUSTOMBOXTEST ); + m_takedamage = DAMAGE_EVENTS_ONLY; + + ragdollparams_t params; + params.pGameData = static_cast( static_cast(this) ); + params.modelIndex = GetModelIndex(); + params.pCollide = modelinfo->GetVCollide( params.modelIndex ); + params.pStudioHdr = GetModelPtr(); + params.forceVector = forceVector; + params.forceBoneIndex = forceBone; + params.forcePosition = forcePos; + params.pCurrentBones = pBoneToWorld; + params.jointFrictionScale = 1.0; + params.allowStretch = HasSpawnFlags(SF_RAGDOLLPROP_ALLOW_STRETCH); +#ifdef MAPBASE + params.fixedConstraints = HasSpawnFlags(SF_RAGDOLLPROP_FIXED_CONSTRAINTS); +#else + params.fixedConstraints = false; +#endif + RagdollCreate( m_ragdoll, params, physenv ); + RagdollApplyAnimationAsVelocity( m_ragdoll, pPrevBones, pBoneToWorld, dt ); + if ( m_anglesOverrideString != NULL_STRING && Q_strlen(m_anglesOverrideString.ToCStr()) > 0 ) + { + char szToken[2048]; + const char *pStr = nexttoken(szToken, STRING(m_anglesOverrideString), ',', sizeof(szToken)); + // anglesOverride is index,angles,index,angles (e.g. "1, 22.5 123.0 0.0, 2, 0 0 0, 3, 0 0 180.0") + while ( szToken[0] != 0 ) + { + int objectIndex = atoi(szToken); + // sanity check to make sure this token is an integer + Assert( atof(szToken) == ((float)objectIndex) ); + pStr = nexttoken(szToken, pStr, ',', sizeof(szToken)); + Assert( szToken[0] ); + if ( objectIndex >= m_ragdoll.listCount ) + { + Warning("Bad ragdoll pose in entity %s, model (%s) at %s, model changed?\n", GetDebugName(), GetModelName().ToCStr(), VecToString(GetAbsOrigin()) ); + } + else if ( szToken[0] != 0 ) + { + QAngle angles; + Assert( objectIndex >= 0 && objectIndex < RAGDOLL_MAX_ELEMENTS ); + UTIL_StringToVector( angles.Base(), szToken ); + int boneIndex = m_ragdoll.boneIndex[objectIndex]; + AngleMatrix( angles, pBoneToWorld[boneIndex] ); + const ragdollelement_t &element = m_ragdoll.list[objectIndex]; + Vector out; + if ( element.parentIndex >= 0 ) + { + int parentBoneIndex = m_ragdoll.boneIndex[element.parentIndex]; + VectorTransform( element.originParentSpace, pBoneToWorld[parentBoneIndex], out ); + } + else + { + out = GetAbsOrigin(); + } + MatrixSetColumn( out, 3, pBoneToWorld[boneIndex] ); + element.pObject->SetPositionMatrix( pBoneToWorld[boneIndex], true ); + } + pStr = nexttoken(szToken, pStr, ',', sizeof(szToken)); + } + } + + if ( activateRagdoll ) + { + MEM_ALLOC_CREDIT(); + RagdollActivate( m_ragdoll, params.pCollide, GetModelIndex(), bWakeRagdoll ); + } + + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + UpdateNetworkDataFromVPhysics( m_ragdoll.list[i].pObject, i ); + g_pPhysSaveRestoreManager->AssociateModel( m_ragdoll.list[i].pObject, GetModelIndex() ); + physcollision->CollideGetAABB( &m_ragdollMins[i], &m_ragdollMaxs[i], m_ragdoll.list[i].pObject->GetCollide(), vec3_origin, vec3_angle ); + } + VPhysicsSetObject( m_ragdoll.list[0].pObject ); + + CalcRagdollSize(); +} + +void CRagdollProp::SetDebrisThink() +{ + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + RecheckCollisionFilter(); +} + +void CRagdollProp::SetDamageEntity( CBaseEntity *pEntity ) +{ + // Damage passing + m_hDamageEntity = pEntity; + + // Set our takedamage to match it + if ( pEntity ) + { + m_takedamage = pEntity->m_takedamage; + } + else + { + m_takedamage = DAMAGE_EVENTS_ONLY; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CRagdollProp::OnTakeDamage( const CTakeDamageInfo &info ) +{ + // If we have a damage entity, we want to pass damage to it. Add the + // Never Ragdoll flag, on the assumption that if the entity dies, we'll + // actually be taking the role of its ragdoll. + if ( m_hDamageEntity.Get() ) + { + CTakeDamageInfo subInfo = info; + subInfo.AddDamageType( DMG_REMOVENORAGDOLL ); + return m_hDamageEntity->OnTakeDamage( subInfo ); + } + + return BaseClass::OnTakeDamage( info ); +} + +//----------------------------------------------------------------------------- +// Purpose: Force all the ragdoll's bone's physics objects to recheck their collision filters +//----------------------------------------------------------------------------- +void CRagdollProp::RecheckCollisionFilter( void ) +{ + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + m_ragdoll.list[i].pObject->RecheckCollisionFilter(); + } +} + + +void CRagdollProp::TraceAttack( const CTakeDamageInfo &info, const Vector &dir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if ( ptr->physicsbone >= 0 && ptr->physicsbone < m_ragdoll.listCount ) + { + VPhysicsSwapObject( m_ragdoll.list[ptr->physicsbone].pObject ); + } + BaseClass::TraceAttack( info, dir, ptr, pAccumulator ); +} + +void CRagdollProp::SetupBones( matrix3x4_t *pBoneToWorld, int boneMask ) +{ + // no ragdoll, fall through to base class + if ( !m_ragdoll.listCount ) + { + BaseClass::SetupBones( pBoneToWorld, boneMask ); + return; + } + + // Not really ideal, but it'll work for now + UpdateModelScale(); + + MDLCACHE_CRITICAL_SECTION(); + CStudioHdr *pStudioHdr = GetModelPtr( ); + bool sim[MAXSTUDIOBONES]; + memset( sim, 0, pStudioHdr->numbones() ); + + int i; + + CBoneAccessor boneaccessor( pBoneToWorld ); + for ( i = 0; i < m_ragdoll.listCount; i++ ) + { + // during restore this may be NULL + if ( !m_ragdoll.list[i].pObject ) + continue; + + if ( RagdollGetBoneMatrix( m_ragdoll, boneaccessor, i ) ) + { + sim[m_ragdoll.boneIndex[i]] = true; + } + } + + mstudiobone_t *pbones = pStudioHdr->pBone( 0 ); + for ( i = 0; i < pStudioHdr->numbones(); i++ ) + { + if ( sim[i] ) + continue; + + if ( !(pStudioHdr->boneFlags(i) & boneMask) ) + continue; + + matrix3x4_t matBoneLocal; + AngleMatrix( pbones[i].rot, pbones[i].pos, matBoneLocal ); + ConcatTransforms( pBoneToWorld[pbones[i].parent], matBoneLocal, pBoneToWorld[i]); + } +} + +bool CRagdollProp::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ +#if 0 + // PERFORMANCE: Use hitboxes for rays instead of vcollides if this is a performance problem + if ( ray.m_IsRay ) + { + return BaseClass::TestCollision( ray, mask, trace ); + } +#endif + + CStudioHdr *pStudioHdr = GetModelPtr( ); + if (!pStudioHdr) + return false; + + // Just iterate all of the elements and trace the box against each one. + // NOTE: This is pretty expensive for small/dense characters + trace_t tr; + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + Vector position; + QAngle angles; + + if( m_ragdoll.list[i].pObject ) + { + m_ragdoll.list[i].pObject->GetPosition( &position, &angles ); + physcollision->TraceBox( ray, m_ragdoll.list[i].pObject->GetCollide(), position, angles, &tr ); + + if ( tr.fraction < trace.fraction ) + { + tr.physicsbone = i; + tr.surface.surfaceProps = m_ragdoll.list[i].pObject->GetMaterialIndex(); + trace = tr; + } + } + else + { + DevWarning("Bogus object in Ragdoll Prop's ragdoll list!\n"); + } + } + + if ( trace.fraction >= 1 ) + { + return false; + } + + return true; +} + + +void CRagdollProp::Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ) +{ + // newAngles is a relative transform for the entity + // But a ragdoll entity has identity orientation by design + // so we compute a relative transform here based on the previous transform + matrix3x4_t startMatrixInv; + MatrixInvert( EntityToWorldTransform(), startMatrixInv ); + matrix3x4_t endMatrix; + MatrixCopy( EntityToWorldTransform(), endMatrix ); + if ( newAngles ) + { + AngleMatrix( *newAngles, endMatrix ); + } + if ( newPosition ) + { + PositionMatrix( *newPosition, endMatrix ); + } + // now endMatrix is the refernce matrix for the entity at the target position + matrix3x4_t xform; + ConcatTransforms( endMatrix, startMatrixInv, xform ); + // now xform is the relative transform the entity must undergo + + // we need to call the base class and it will teleport our vphysics object, + // so set object 0 up and compute the origin/angles for its new position (base implementation has side effects) + VPhysicsSwapObject( m_ragdoll.list[0].pObject ); + matrix3x4_t obj0source, obj0Target; + m_ragdoll.list[0].pObject->GetPositionMatrix( &obj0source ); + ConcatTransforms( xform, obj0source, obj0Target ); + Vector obj0Pos; + QAngle obj0Angles; + MatrixAngles( obj0Target, obj0Angles, obj0Pos ); + BaseClass::Teleport( &obj0Pos, &obj0Angles, newVelocity ); + + for ( int i = 1; i < m_ragdoll.listCount; i++ ) + { + matrix3x4_t matrix, newMatrix; + m_ragdoll.list[i].pObject->GetPositionMatrix( &matrix ); + ConcatTransforms( xform, matrix, newMatrix ); + m_ragdoll.list[i].pObject->SetPositionMatrix( newMatrix, true ); + UpdateNetworkDataFromVPhysics( m_ragdoll.list[i].pObject, i ); + } + // fixup/relink object 0 + UpdateNetworkDataFromVPhysics( m_ragdoll.list[0].pObject, 0 ); +} + +void CRagdollProp::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + if ( m_lastUpdateTickCount == (unsigned int)gpGlobals->tickcount ) + return; + + m_lastUpdateTickCount = gpGlobals->tickcount; + //NetworkStateChanged(); + + matrix3x4_t boneToWorld[MAXSTUDIOBONES]; + QAngle angles; + Vector surroundingMins, surroundingMaxs; + + int i; + for ( i = 0; i < m_ragdoll.listCount; i++ ) + { + CBoneAccessor boneaccessor( boneToWorld ); + if ( RagdollGetBoneMatrix( m_ragdoll, boneaccessor, i ) ) + { + Vector vNewPos; + MatrixAngles( boneToWorld[m_ragdoll.boneIndex[i]], angles, vNewPos ); + m_ragPos.Set( i, vNewPos ); + m_ragAngles.Set( i, angles ); + } + else + { + m_ragPos.GetForModify(i).Init(); + m_ragAngles.GetForModify(i).Init(); + } + } + + // BUGBUG: Use the ragdollmins/maxs to do this instead of the collides + m_allAsleep = RagdollIsAsleep( m_ragdoll ); + + // Don't scream after you've come to rest + if ( m_allAsleep ) + { + m_strSourceClassName = NULL_STRING; + } + else + { + if ( m_ragdoll.pGroup->IsInErrorState() ) + { + RagdollSolveSeparation( m_ragdoll, this ); + } + } + + // Interactive debris converts back to debris when it comes to rest + if ( m_allAsleep && GetCollisionGroup() == COLLISION_GROUP_INTERACTIVE_DEBRIS ) + { + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + RecheckCollisionFilter(); + SetContextThink( NULL, gpGlobals->curtime, s_pDebrisContext ); + } + + Vector vecFullMins, vecFullMaxs; + vecFullMins = m_ragPos[0]; + vecFullMaxs = m_ragPos[0]; + for ( i = 0; i < m_ragdoll.listCount; i++ ) + { + Vector mins, maxs; + matrix3x4_t update; + if ( !m_ragdoll.list[i].pObject ) + { + m_ragdollMins[i].Init(); + m_ragdollMaxs[i].Init(); + continue; + } + m_ragdoll.list[i].pObject->GetPositionMatrix( &update ); + TransformAABB( update, m_ragdollMins[i], m_ragdollMaxs[i], mins, maxs ); + for ( int j = 0; j < 3; j++ ) + { + if ( mins[j] < vecFullMins[j] ) + { + vecFullMins[j] = mins[j]; + } + if ( maxs[j] > vecFullMaxs[j] ) + { + vecFullMaxs[j] = maxs[j]; + } + } + } + + SetAbsOrigin( m_ragPos[0] ); + SetAbsAngles( vec3_angle ); + const Vector &vecOrigin = CollisionProp()->GetCollisionOrigin(); + CollisionProp()->AddSolidFlags( FSOLID_FORCE_WORLD_ALIGNED ); + CollisionProp()->SetSurroundingBoundsType( USE_COLLISION_BOUNDS_NEVER_VPHYSICS ); + SetCollisionBounds( vecFullMins - vecOrigin, vecFullMaxs - vecOrigin ); + CollisionProp()->MarkSurroundingBoundsDirty(); + + PhysicsTouchTriggers(); +} + +int CRagdollProp::VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ) +{ + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + if ( i < listMax ) + { + pList[i] = m_ragdoll.list[i].pObject; + } + } + + return m_ragdoll.listCount; +} + +#ifdef MAPBASE +int CRagdollProp::VPhysicsGetFlesh() +{ + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + for ( int i = 0; i < count; i++ ) + { + int material = pList[i]->GetMaterialIndex(); + const surfacedata_t *pSurfaceData = physprops->GetSurfaceData( material ); + // Is flesh ?, don't allow pickup + if ( pSurfaceData->game.material == CHAR_TEX_ANTLION || pSurfaceData->game.material == CHAR_TEX_FLESH || pSurfaceData->game.material == CHAR_TEX_BLOODYFLESH || pSurfaceData->game.material == CHAR_TEX_ALIENFLESH ) + return pSurfaceData->game.material; + } + return 0; +} +#endif + +void CRagdollProp::UpdateNetworkDataFromVPhysics( IPhysicsObject *pPhysics, int index ) +{ + Assert(index < m_ragdoll.listCount); + + QAngle angles; + Vector vPos; + m_ragdoll.list[index].pObject->GetPosition( &vPos, &angles ); + m_ragPos.Set( index, vPos ); + m_ragAngles.Set( index, angles ); + + // move/relink if root moved + if ( index == 0 ) + { + SetAbsOrigin( m_ragPos[0] ); + PhysicsTouchTriggers(); + } +} + + +//----------------------------------------------------------------------------- +// Fade out due to the LRU telling it do +//----------------------------------------------------------------------------- +#define FADE_OUT_LENGTH 0.5f + +void CRagdollProp::FadeOut( float flDelay, float fadeTime ) +{ + if ( IsFading() ) + return; + + m_flFadeTime = ( fadeTime == -1 ) ? FADE_OUT_LENGTH : fadeTime; + + m_flFadeOutStartTime = gpGlobals->curtime + flDelay; + m_flFadeScale = 0; + SetContextThink( &CRagdollProp::FadeOutThink, gpGlobals->curtime + flDelay + 0.01f, s_pFadeOutContext ); +} + +bool CRagdollProp::IsFading() +{ + return ( GetNextThink( s_pFadeOutContext ) >= gpGlobals->curtime ); +} + +void CRagdollProp::FadeOutThink(void) +{ + float dt = gpGlobals->curtime - m_flFadeOutStartTime; + if ( dt < 0 ) + { + SetContextThink( &CRagdollProp::FadeOutThink, gpGlobals->curtime + 0.1, s_pFadeOutContext ); + } + else if ( dt < m_flFadeTime ) + { + float alpha = 1.0f - dt / m_flFadeTime; + int nFade = (int)(alpha * 255.0f); + m_nRenderMode = kRenderTransTexture; + SetRenderColorA( nFade ); + NetworkStateChanged(); + SetContextThink( &CRagdollProp::FadeOutThink, gpGlobals->curtime + TICK_INTERVAL, s_pFadeOutContext ); + } + else + { + // Necessary to cause it to do the appropriate death cleanup + // Yeah, the player may have nothing to do with it, but + // passing NULL to TakeDamage causes bad things to happen + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + CTakeDamageInfo info( pPlayer, pPlayer, 10000.0, DMG_GENERIC ); + TakeDamage( info ); + UTIL_Remove( this ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CRagdollProp::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + if (m_ragdoll.listCount) + { + float mass = 0; + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + if ( m_ragdoll.list[i].pObject != NULL ) + { + mass += m_ragdoll.list[i].pObject->GetMass(); + } + } + + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr),"Mass: %.2f kg / %.2f lb (%s)", mass, kg2lbs(mass), GetMassEquivalent(mass) ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + } + + return text_offset; +} + +void CRagdollProp::DrawDebugGeometryOverlays() +{ + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + DrawServerHitboxes(); + } + if (m_debugOverlays & OVERLAY_PIVOT_BIT) + { + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + if ( m_ragdoll.list[i].pObject ) + { + float mass = m_ragdoll.list[i].pObject->GetMass(); + Vector pos; + m_ragdoll.list[i].pObject->GetPosition( &pos, NULL ); + CFmtStr str("mass %.1f", mass ); + NDebugOverlay::EntityTextAtPosition( pos, 0, str.Access(), 0, 0, 255, 0, 255 ); + } + } + } + BaseClass::DrawDebugGeometryOverlays(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pOther - +//----------------------------------------------------------------------------- +void CRagdollProp::SetUnragdoll( CBaseAnimating *pOther ) +{ + m_hUnragdoll = pOther; +} + +//=============================================================================================================== +// RagdollPropAttached +//=============================================================================================================== +class CRagdollPropAttached : public CRagdollProp +{ + DECLARE_CLASS( CRagdollPropAttached, CRagdollProp ); +public: + + CRagdollPropAttached() + { + m_bShouldDetach = false; + } + + ~CRagdollPropAttached() + { + physenv->DestroyConstraint( m_pAttachConstraint ); + m_pAttachConstraint = NULL; + } + + void InitRagdollAttached( IPhysicsObject *pAttached, const Vector &forceVector, int forceBone, matrix3x4_t *pPrevBones, matrix3x4_t *pBoneToWorld, float dt, int collisionGroup, CBaseAnimating *pFollow, int boneIndexRoot, const Vector &boneLocalOrigin, int parentBoneAttach, const Vector &worldAttachOrigin ); + void DetachOnNextUpdate(); + void VPhysicsUpdate( IPhysicsObject *pPhysics ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + void Detach(); + CNetworkVar( int, m_boneIndexAttached ); + CNetworkVar( int, m_ragdollAttachedObjectIndex ); + CNetworkVector( m_attachmentPointBoneSpace ); + CNetworkVector( m_attachmentPointRagdollSpace ); + bool m_bShouldDetach; + IPhysicsConstraint *m_pAttachConstraint; +}; + +LINK_ENTITY_TO_CLASS( prop_ragdoll_attached, CRagdollPropAttached ); +EXTERN_SEND_TABLE(DT_Ragdoll_Attached) + +IMPLEMENT_SERVERCLASS_ST(CRagdollPropAttached, DT_Ragdoll_Attached) + SendPropInt( SENDINFO( m_boneIndexAttached ), MAXSTUDIOBONEBITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_ragdollAttachedObjectIndex ), RAGDOLL_INDEX_BITS, SPROP_UNSIGNED ), + SendPropVector(SENDINFO(m_attachmentPointBoneSpace), -1, SPROP_COORD ), + SendPropVector(SENDINFO(m_attachmentPointRagdollSpace), -1, SPROP_COORD ), +END_SEND_TABLE() + +BEGIN_DATADESC(CRagdollPropAttached) + DEFINE_FIELD( m_boneIndexAttached, FIELD_INTEGER ), + DEFINE_FIELD( m_ragdollAttachedObjectIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_attachmentPointBoneSpace, FIELD_VECTOR ), + DEFINE_FIELD( m_attachmentPointRagdollSpace, FIELD_VECTOR ), + DEFINE_FIELD( m_bShouldDetach, FIELD_BOOLEAN ), + DEFINE_PHYSPTR( m_pAttachConstraint ), +END_DATADESC() + + +static void SyncAnimatingWithPhysics( CBaseAnimating *pAnimating ) +{ + IPhysicsObject *pPhysics = pAnimating->VPhysicsGetObject(); + if ( pPhysics ) + { + Vector pos; + pPhysics->GetShadowPosition( &pos, NULL ); + pAnimating->SetAbsOrigin( pos ); + } +} + + +CBaseAnimating *CreateServerRagdollSubmodel( CBaseAnimating *pOwner, const char *pModelName, const Vector &position, const QAngle &angles, int collisionGroup ) +{ + CRagdollProp *pRagdoll = (CRagdollProp *)CBaseEntity::CreateNoSpawn( "prop_ragdoll", position, angles, pOwner ); + pRagdoll->SetModelName( AllocPooledString( pModelName ) ); + pRagdoll->SetModel( STRING(pRagdoll->GetModelName()) ); + matrix3x4_t pBoneToWorld[MAXSTUDIOBONES], pBoneToWorldNext[MAXSTUDIOBONES]; + pRagdoll->ResetSequence( 0 ); + +#ifdef MAPBASE_VSCRIPT + // Hook for pre-spawn ragdolling + if (pOwner->m_ScriptScope.IsInitialized() && CBaseAnimating::g_Hook_OnServerRagdoll.CanRunInScope( pOwner->m_ScriptScope )) + { + // ragdoll, submodel + ScriptVariant_t args[] = { ScriptVariant_t( pRagdoll->GetScriptInstance() ), true }; + CBaseAnimating::g_Hook_OnServerRagdoll.Call( pOwner->m_ScriptScope, NULL, args ); + } +#endif + + // let bone merging do the work of copying everything over for us + pRagdoll->SetParent( pOwner ); + pRagdoll->SetupBones( pBoneToWorld, BONE_USED_BY_ANYTHING ); + // HACKHACK: don't want this parent anymore + pRagdoll->SetParent( NULL ); + + memcpy( pBoneToWorldNext, pBoneToWorld, sizeof(pBoneToWorld) ); + + pRagdoll->InitRagdoll( vec3_origin, -1, vec3_origin, pBoneToWorld, pBoneToWorldNext, 0.1, collisionGroup, true ); + return pRagdoll; +} + + +CBaseEntity *CreateServerRagdoll( CBaseAnimating *pAnimating, int forceBone, const CTakeDamageInfo &info, int collisionGroup, bool bUseLRURetirement ) +{ + if ( info.GetDamageType() & (DMG_VEHICLE|DMG_CRUSH) ) + { + // if the entity was killed by physics or a vehicle, move to the vphysics shadow position before creating the ragdoll. + SyncAnimatingWithPhysics( pAnimating ); + } + CRagdollProp *pRagdoll = (CRagdollProp *)CBaseEntity::CreateNoSpawn( "prop_ragdoll", pAnimating->GetAbsOrigin(), vec3_angle, NULL ); + pRagdoll->CopyAnimationDataFrom( pAnimating ); + pRagdoll->SetOwnerEntity( pAnimating ); + +#ifdef MAPBASE_VSCRIPT + // Hook for pre-spawn ragdolling + if (pAnimating->m_ScriptScope.IsInitialized() && CBaseAnimating::g_Hook_OnServerRagdoll.CanRunInScope( pAnimating->m_ScriptScope )) + { + // ragdoll, submodel + ScriptVariant_t args[] = { ScriptVariant_t( pRagdoll->GetScriptInstance() ), false }; + CBaseAnimating::g_Hook_OnServerRagdoll.Call( pAnimating->m_ScriptScope, NULL, args ); + } +#endif + + pRagdoll->InitRagdollAnimation(); + matrix3x4_t pBoneToWorld[MAXSTUDIOBONES], pBoneToWorldNext[MAXSTUDIOBONES]; + + float dt = 0.1f; + + // Copy over dissolve state... + if ( pAnimating->IsEFlagSet( EFL_NO_DISSOLVE ) ) + { + pRagdoll->AddEFlags( EFL_NO_DISSOLVE ); + } + + // NOTE: This currently is only necessary to prevent manhacks from + // colliding with server ragdolls they kill + pRagdoll->SetKiller( info.GetInflictor() ); + pRagdoll->SetSourceClassName( pAnimating->GetClassname() ); + + // NPC_STATE_DEAD npc's will have their COND_IN_PVS cleared, so this needs to force SetupBones to happen + unsigned short fPrevFlags = pAnimating->GetBoneCacheFlags(); + pAnimating->SetBoneCacheFlags( BCF_NO_ANIMATION_SKIP ); + + // UNDONE: Extract velocity from bones via animation (like we do on the client) + // UNDONE: For now, just move each bone by the total entity velocity if set. + // Get Bones positions before + // Store current cycle + float fSequenceDuration = pAnimating->SequenceDuration( pAnimating->GetSequence() ); + float fSequenceTime = pAnimating->GetCycle() * fSequenceDuration; + + if( fSequenceTime <= dt && fSequenceTime > 0.0f ) + { + // Avoid having negative cycle + dt = fSequenceTime; + } + + float fPreviousCycle = clamp(pAnimating->GetCycle()-( dt * ( 1 / fSequenceDuration ) ),0.f,1.f); + float fCurCycle = pAnimating->GetCycle(); + // Get current bones positions + pAnimating->SetupBones( pBoneToWorldNext, BONE_USED_BY_ANYTHING ); + // Get previous bones positions + pAnimating->SetCycle( fPreviousCycle ); + pAnimating->SetupBones( pBoneToWorld, BONE_USED_BY_ANYTHING ); + // Restore current cycle + pAnimating->SetCycle( fCurCycle ); + + // Reset previous bone flags + pAnimating->ClearBoneCacheFlags( BCF_NO_ANIMATION_SKIP ); + pAnimating->SetBoneCacheFlags( fPrevFlags ); + + Vector vel = pAnimating->GetAbsVelocity(); + if( ( vel.Length() == 0 ) && ( dt > 0 ) ) + { + // Compute animation velocity + CStudioHdr *pstudiohdr = pAnimating->GetModelPtr(); + if ( pstudiohdr ) + { + Vector deltaPos; + QAngle deltaAngles; + if (Studio_SeqMovement( pstudiohdr, + pAnimating->GetSequence(), + fPreviousCycle, + pAnimating->GetCycle(), + pAnimating->GetPoseParameterArray(), + deltaPos, + deltaAngles )) + { + VectorRotate( deltaPos, pAnimating->EntityToWorldTransform(), vel ); + vel /= dt; + } + } + } + + if ( vel.LengthSqr() > 0 ) + { + int numbones = pAnimating->GetModelPtr()->numbones(); + vel *= dt; + for ( int i = 0; i < numbones; i++ ) + { + Vector pos; + MatrixGetColumn( pBoneToWorld[i], 3, pos ); + pos -= vel; + MatrixSetColumn( pos, 3, pBoneToWorld[i] ); + } + } + +#if RAGDOLL_VISUALIZE + pAnimating->DrawRawSkeleton( pBoneToWorld, BONE_USED_BY_ANYTHING, true, 20, false ); + pAnimating->DrawRawSkeleton( pBoneToWorldNext, BONE_USED_BY_ANYTHING, true, 20, true ); +#endif + // Is this a vehicle / NPC collision? + if ( (info.GetDamageType() & DMG_VEHICLE) && pAnimating->MyNPCPointer() ) + { + // init the ragdoll with no forces + pRagdoll->InitRagdoll( vec3_origin, -1, vec3_origin, pBoneToWorld, pBoneToWorldNext, dt, collisionGroup, true ); + + // apply vehicle forces + // Get a list of bones with hitboxes below the plane of impact + int boxList[128]; + Vector normal(0,0,-1); + int count = pAnimating->GetHitboxesFrontside( boxList, ARRAYSIZE(boxList), normal, DotProduct( normal, info.GetDamagePosition() ) ); + + // distribute force over mass of entire character + float massScale = Studio_GetMass(pAnimating->GetModelPtr()); + massScale = clamp( massScale, 1.f, 1.e4f ); + massScale = 1.f / massScale; + + // distribute the force + // BUGBUG: This will hit the same bone twice if it has two hitboxes!!!! + ragdoll_t *pRagInfo = pRagdoll->GetRagdoll(); + for ( int i = 0; i < count; i++ ) + { + int physBone = pAnimating->GetPhysicsBone( pAnimating->GetHitboxBone( boxList[i] ) ); + IPhysicsObject *pPhysics = pRagInfo->list[physBone].pObject; + pPhysics->ApplyForceCenter( info.GetDamageForce() * pPhysics->GetMass() * massScale ); + } + } + else + { + pRagdoll->InitRagdoll( info.GetDamageForce(), forceBone, info.GetDamagePosition(), pBoneToWorld, pBoneToWorldNext, dt, collisionGroup, true ); + } + + // Are we dissolving? + if ( pAnimating->IsDissolving() ) + { + pRagdoll->TransferDissolveFrom( pAnimating ); + } + else if ( bUseLRURetirement ) + { + pRagdoll->AddSpawnFlags( SF_RAGDOLLPROP_USE_LRU_RETIREMENT ); + s_RagdollLRU.MoveToTopOfLRU( pRagdoll ); + } + + // Tracker 22598: If we don't set the OBB mins/maxs to something valid here, then the client will have a zero sized hull + // for the ragdoll for one frame until Vphysics updates the real obb bounds after the first simulation frame. Having + // a zero sized hull makes the ragdoll think it should be faded/alpha'd to zero for a frame, so you get a blink where + // the ragdoll doesn't draw initially. + Vector mins, maxs; + mins = pAnimating->CollisionProp()->OBBMins(); + maxs = pAnimating->CollisionProp()->OBBMaxs(); + pRagdoll->CollisionProp()->SetCollisionBounds( mins, maxs ); + +#ifdef MAPBASE + variant_t variant; + variant.SetEntity(pRagdoll); + pAnimating->FireNamedOutput("OnServerRagdoll", variant, pRagdoll, pAnimating); +#endif + + return pRagdoll; +} + +void CRagdollPropAttached::DetachOnNextUpdate() +{ + m_bShouldDetach = true; +} + +void CRagdollPropAttached::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + if ( m_bShouldDetach ) + { + Detach(); + m_bShouldDetach = false; + } + BaseClass::VPhysicsUpdate( pPhysics ); +} + +void CRagdollPropAttached::Detach() +{ + SetParent(NULL); + SetOwnerEntity( NULL ); + SetAbsAngles( vec3_angle ); + SetMoveType( MOVETYPE_VPHYSICS ); + RemoveSolidFlags( FSOLID_NOT_SOLID ); + physenv->DestroyConstraint( m_pAttachConstraint ); + m_pAttachConstraint = NULL; + const float dampingScale = 1.0f / ATTACHED_DAMPING_SCALE; + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + float damping, rotdamping; + m_ragdoll.list[i].pObject->GetDamping( &damping, &rotdamping ); + damping *= dampingScale; + rotdamping *= dampingScale; + m_ragdoll.list[i].pObject->SetDamping( &damping, &damping ); + } + + // Go non-solid + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + RecheckCollisionFilter(); +} + +void CRagdollPropAttached::InitRagdollAttached( + IPhysicsObject *pAttached, + const Vector &forceVector, + int forceBone, + matrix3x4_t *pPrevBones, + matrix3x4_t *pBoneToWorld, + float dt, + int collisionGroup, + CBaseAnimating *pFollow, + int boneIndexRoot, + const Vector &boneLocalOrigin, + int parentBoneAttach, + const Vector &worldAttachOrigin ) +{ + int ragdollAttachedIndex = 0; + if ( parentBoneAttach > 0 ) + { + CStudioHdr *pStudioHdr = GetModelPtr(); + mstudiobone_t *pBone = pStudioHdr->pBone( parentBoneAttach ); + ragdollAttachedIndex = pBone->physicsbone; + } + + InitRagdoll( forceVector, forceBone, vec3_origin, pPrevBones, pBoneToWorld, dt, collisionGroup, false ); + + IPhysicsObject *pRefObject = m_ragdoll.list[ragdollAttachedIndex].pObject; + + Vector attachmentPointRagdollSpace; + pRefObject->WorldToLocal( &attachmentPointRagdollSpace, worldAttachOrigin ); + + constraint_ragdollparams_t constraint; + constraint.Defaults(); + matrix3x4_t tmp, worldToAttached, worldToReference, constraintToWorld; + + Vector offsetWS; + pAttached->LocalToWorld( &offsetWS, boneLocalOrigin ); + + QAngle followAng = QAngle(0, pFollow->GetAbsAngles().y, 0 ); + AngleMatrix( followAng, offsetWS, constraintToWorld ); + + constraint.axes[0].SetAxisFriction( -2, 2, 20 ); + constraint.axes[1].SetAxisFriction( 0, 0, 0 ); + constraint.axes[2].SetAxisFriction( -15, 15, 20 ); + + // Exaggerate the bone's ability to pull the mass of the ragdoll around + constraint.constraint.bodyMassScale[1] = 50.0f; + + pAttached->GetPositionMatrix( &tmp ); + MatrixInvert( tmp, worldToAttached ); + + pRefObject->GetPositionMatrix( &tmp ); + MatrixInvert( tmp, worldToReference ); + + ConcatTransforms( worldToReference, constraintToWorld, constraint.constraintToReference ); + ConcatTransforms( worldToAttached, constraintToWorld, constraint.constraintToAttached ); + + // for now, just slam this to be the passed in value + MatrixSetColumn( attachmentPointRagdollSpace, 3, constraint.constraintToReference ); + + PhysDisableEntityCollisions( pAttached, m_ragdoll.list[0].pObject ); + m_pAttachConstraint = physenv->CreateRagdollConstraint( pRefObject, pAttached, m_ragdoll.pGroup, constraint ); + + SetParent( pFollow ); + SetOwnerEntity( pFollow ); + + RagdollActivate( m_ragdoll, modelinfo->GetVCollide( GetModelIndex() ), GetModelIndex() ); + + // add a bunch of dampening to the ragdoll + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + float damping, rotdamping; + m_ragdoll.list[i].pObject->GetDamping( &damping, &rotdamping ); + damping *= ATTACHED_DAMPING_SCALE; + rotdamping *= ATTACHED_DAMPING_SCALE; + m_ragdoll.list[i].pObject->SetDamping( &damping, &rotdamping ); + } + + m_boneIndexAttached = boneIndexRoot; + m_ragdollAttachedObjectIndex = ragdollAttachedIndex; + m_attachmentPointBoneSpace = boneLocalOrigin; + + Vector vTemp; + MatrixGetColumn( constraint.constraintToReference, 3, vTemp ); + m_attachmentPointRagdollSpace = vTemp; +} + +CRagdollProp *CreateServerRagdollAttached( CBaseAnimating *pAnimating, const Vector &vecForce, int forceBone, int collisionGroup, IPhysicsObject *pAttached, CBaseAnimating *pParentEntity, int boneAttach, const Vector &originAttached, int parentBoneAttach, const Vector &boneOrigin ) +{ + // Return immediately if the model doesn't have a vcollide + if ( modelinfo->GetVCollide( pAnimating->GetModelIndex() ) == NULL ) + return NULL; + + CRagdollPropAttached *pRagdoll = (CRagdollPropAttached *)CBaseEntity::CreateNoSpawn( "prop_ragdoll_attached", pAnimating->GetAbsOrigin(), vec3_angle, NULL ); + pRagdoll->CopyAnimationDataFrom( pAnimating ); + + pRagdoll->InitRagdollAnimation(); + matrix3x4_t pBoneToWorld[MAXSTUDIOBONES]; + pAnimating->SetupBones( pBoneToWorld, BONE_USED_BY_ANYTHING ); + pRagdoll->InitRagdollAttached( pAttached, vecForce, forceBone, pBoneToWorld, pBoneToWorld, 0.1, collisionGroup, pParentEntity, boneAttach, boneOrigin, parentBoneAttach, originAttached ); + + return pRagdoll; +} + +void DetachAttachedRagdoll( CBaseEntity *pRagdollIn ) +{ + CRagdollPropAttached *pRagdoll = dynamic_cast(pRagdollIn); + + if ( pRagdoll ) + { + pRagdoll->DetachOnNextUpdate(); + } +} + +void DetachAttachedRagdollsForEntity( CBaseEntity *pRagdollParent ) +{ + CUtlVector list; + GetAllChildren( pRagdollParent, list ); + for ( int i = list.Count()-1; i >= 0; --i ) + { + DetachAttachedRagdoll( list[i] ); + } +} + +bool Ragdoll_IsPropRagdoll( CBaseEntity *pEntity ) +{ + if ( dynamic_cast(pEntity) != NULL ) + return true; + return false; +} + +ragdoll_t *Ragdoll_GetRagdoll( CBaseEntity *pEntity ) +{ + CRagdollProp *pProp = dynamic_cast(pEntity); + if ( pProp ) + return pProp->GetRagdoll(); + return NULL; +} + +void CRagdollProp::GetAngleOverrideFromCurrentState( char *pOut, int size ) +{ + pOut[0] = 0; + for ( int i = 0; i < m_ragdoll.listCount; i++ ) + { + if ( i != 0 ) + { + Q_strncat( pOut, ",", size, COPY_ALL_CHARACTERS ); + + } + CFmtStr str("%d,%.2f %.2f %.2f", i, m_ragAngles[i].x, m_ragAngles[i].y, m_ragAngles[i].z ); + Q_strncat( pOut, str, size, COPY_ALL_CHARACTERS ); + } +} + +void CRagdollProp::DisableMotion( void ) +{ + for ( int iRagdoll = 0; iRagdoll < m_ragdoll.listCount; ++iRagdoll ) + { + IPhysicsObject *pPhysicsObject = m_ragdoll.list[ iRagdoll ].pObject; + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->EnableMotion( false ); + } + } +} + +void CRagdollProp::InputStartRadgollBoogie( inputdata_t &inputdata ) +{ + float duration = inputdata.value.Float(); + + if( duration <= 0.0f ) + { + duration = 5.0f; + } + + CRagdollBoogie::Create( this, 100, gpGlobals->curtime, duration, 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Enable physics motion and collision response (on by default) +//----------------------------------------------------------------------------- +void CRagdollProp::InputEnableMotion( inputdata_t &inputdata ) +{ + for ( int iRagdoll = 0; iRagdoll < m_ragdoll.listCount; ++iRagdoll ) + { + IPhysicsObject *pPhysicsObject = m_ragdoll.list[ iRagdoll ].pObject; + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->EnableMotion( true ); + pPhysicsObject->Wake(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Disable any physics motion or collision response +//----------------------------------------------------------------------------- +void CRagdollProp::InputDisableMotion( inputdata_t &inputdata ) +{ + DisableMotion(); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler to start the physics prop simulating. +//----------------------------------------------------------------------------- +void CRagdollProp::InputWake( inputdata_t &inputdata ) +{ + for ( int iRagdoll = 0; iRagdoll < m_ragdoll.listCount; ++iRagdoll ) + { + IPhysicsObject *pPhysicsObject = m_ragdoll.list[ iRagdoll ].pObject; + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->Wake(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler to stop the physics prop simulating. +//----------------------------------------------------------------------------- +void CRagdollProp::InputSleep( inputdata_t &inputdata ) +{ + for ( int iRagdoll = 0; iRagdoll < m_ragdoll.listCount; ++iRagdoll ) + { + IPhysicsObject *pPhysicsObject = m_ragdoll.list[ iRagdoll ].pObject; + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->Sleep(); + } + } +} +#endif + +void CRagdollProp::InputTurnOn( inputdata_t &inputdata ) +{ + RemoveEffects( EF_NODRAW ); +} + +void CRagdollProp::InputTurnOff( inputdata_t &inputdata ) +{ + AddEffects( EF_NODRAW ); +} + +void CRagdollProp::InputFadeAndRemove( inputdata_t &inputdata ) +{ + float flFadeDuration = inputdata.value.Float(); + + if( flFadeDuration == 0.0f ) + flFadeDuration = 1.0f; + + FadeOut( 0.0f, flFadeDuration ); +} + +#ifdef MAPBASE_VSCRIPT +HSCRIPT CRagdollProp::ScriptGetRagdollObject( int iIndex ) +{ + if (iIndex < 0 || iIndex > m_ragdoll.listCount) + { + Warning("%s GetRagdollObject: Index %i not valid (%i objects)\n", GetDebugName(), iIndex, m_ragdoll.listCount); + return NULL; + } + + return g_pScriptVM->RegisterInstance( m_ragdoll.list[iIndex].pObject ); +} + +int CRagdollProp::ScriptGetRagdollObjectCount() +{ + return m_ragdoll.listCount; +} +#endif + +void Ragdoll_GetAngleOverrideString( char *pOut, int size, CBaseEntity *pEntity ) +{ + CRagdollProp *pRagdoll = dynamic_cast(pEntity); + if ( pRagdoll ) + { + pRagdoll->GetAngleOverrideFromCurrentState( pOut, size ); + } +} diff --git a/sp/src/game/server/physics_prop_ragdoll.h b/sp/src/game/server/physics_prop_ragdoll.h new file mode 100644 index 00000000..46e527c3 --- /dev/null +++ b/sp/src/game/server/physics_prop_ragdoll.h @@ -0,0 +1,180 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef PHYSICS_PROP_RAGDOLL_H +#define PHYSICS_PROP_RAGDOLL_H +#ifdef _WIN32 +#pragma once +#endif + +#include "ragdoll_shared.h" +#include "player_pickup.h" + + +//----------------------------------------------------------------------------- +// Purpose: entity class for simple ragdoll physics +//----------------------------------------------------------------------------- + +// UNDONE: Move this to a private header +class CRagdollProp : public CBaseAnimating, public CDefaultPlayerPickupVPhysics +{ + DECLARE_CLASS( CRagdollProp, CBaseAnimating ); +#ifdef MAPBASE_VSCRIPT + DECLARE_ENT_SCRIPTDESC(); +#endif + +public: + CRagdollProp( void ); + ~CRagdollProp( void ); + + virtual void UpdateOnRemove( void ); + + void DrawDebugGeometryOverlays(); + + void Spawn( void ); + void Precache( void ); + + // Disable auto fading under dx7 or when level fades are specified + void DisableAutoFade(); + + int ObjectCaps(); + + DECLARE_SERVERCLASS(); + // Don't treat as a live target + virtual bool IsAlive( void ) { return false; } + + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &dir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + virtual bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); + virtual void Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ); + virtual void SetupBones( matrix3x4_t *pBoneToWorld, int boneMask ); + virtual void VPhysicsUpdate( IPhysicsObject *pPhysics ); + virtual int VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ); +#ifdef MAPBASE + int VPhysicsGetFlesh(); +#endif + + virtual int DrawDebugTextOverlays(void); + + // Response system stuff + virtual IResponseSystem *GetResponseSystem(); + virtual void ModifyOrAppendCriteria( AI_CriteriaSet& set ); + void SetSourceClassName( const char *pClassname ); +#ifdef MAPBASE + const char *GetSourceClassNameAsCStr() { return STRING( m_strSourceClassName ); } +#endif + + // Physics attacker + virtual CBasePlayer *HasPhysicsAttacker( float dt ); + + // locals + void InitRagdollAnimation( void ); + void InitRagdoll( const Vector &forceVector, int forceBone, const Vector &forcePos, matrix3x4_t *pPrevBones, matrix3x4_t *pBoneToWorld, float dt, int collisionGroup, bool activateRagdoll, bool bWakeRagdoll = true ); + + void RecheckCollisionFilter( void ); + void SetDebrisThink(); + void ClearFlagsThink( void ); + inline ragdoll_t *GetRagdoll( void ) { return &m_ragdoll; } + + virtual bool IsRagdoll() { return true; } + + // Damage passing + virtual void SetDamageEntity( CBaseEntity *pEntity ); + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual void OnSave( IEntitySaveUtils *pUtils ); + virtual void OnRestore(); + + // Purpose: CDefaultPlayerPickupVPhysics + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ); + virtual AngularImpulse PhysGunLaunchAngularImpulse(); + bool HasPhysgunInteraction( const char *pszKeyName, const char *pszValue ); + void HandleFirstCollisionInteractions( int index, gamevcollisionevent_t *pEvent ); + + void SetUnragdoll( CBaseAnimating *pOther ); + + void SetBlendWeight( float weight ) { m_flBlendWeight = weight; } + void SetOverlaySequence( Activity activity ); + void FadeOut( float flDelay = 0, float fadeTime = -1 ); + bool IsFading(); + CBaseEntity* GetKiller() { return m_hKiller; } + void SetKiller( CBaseEntity *pKiller ) { m_hKiller = pKiller; } + void GetAngleOverrideFromCurrentState( char *pOut, int size ); + + void DisableMotion( void ); + + // Input/Output + void InputStartRadgollBoogie( inputdata_t &inputdata ); + void InputEnableMotion( inputdata_t &inputdata ); + void InputDisableMotion( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputWake( inputdata_t &inputdata ); + void InputSleep( inputdata_t &inputdata ); +#endif + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputFadeAndRemove( inputdata_t &inputdata ); + +#ifdef MAPBASE_VSCRIPT + HSCRIPT ScriptGetRagdollObject( int iIndex ); + int ScriptGetRagdollObjectCount(); +#endif + + DECLARE_DATADESC(); + +protected: + void CalcRagdollSize( void ); + ragdoll_t m_ragdoll; + +private: + void UpdateNetworkDataFromVPhysics( IPhysicsObject *pPhysics, int index ); + void FadeOutThink(); + + bool m_bStartDisabled; + + CNetworkArray( Vector, m_ragPos, RAGDOLL_MAX_ELEMENTS ); + CNetworkArray( QAngle, m_ragAngles, RAGDOLL_MAX_ELEMENTS ); + + string_t m_anglesOverrideString; + + typedef CHandle CBaseAnimatingHandle; + CNetworkVar( CBaseAnimatingHandle, m_hUnragdoll ); + + + unsigned int m_lastUpdateTickCount; + bool m_allAsleep; + bool m_bFirstCollisionAfterLaunch; + EHANDLE m_hDamageEntity; + EHANDLE m_hKiller; // Who killed me? + CHandle m_hPhysicsAttacker; + float m_flLastPhysicsInfluenceTime; + float m_flFadeOutStartTime; + float m_flFadeTime; + + + string_t m_strSourceClassName; + bool m_bHasBeenPhysgunned; + + // If not 1, then allow underlying sequence to blend in with simulated bone positions + CNetworkVar( float, m_flBlendWeight ); + CNetworkVar( int, m_nOverlaySequence ); + float m_flDefaultFadeScale; + + Vector m_ragdollMins[RAGDOLL_MAX_ELEMENTS]; + Vector m_ragdollMaxs[RAGDOLL_MAX_ELEMENTS]; +}; + +CBaseEntity *CreateServerRagdoll( CBaseAnimating *pAnimating, int forceBone, const CTakeDamageInfo &info, int collisionGroup, bool bUseLRURetirement = false ); +CRagdollProp *CreateServerRagdollAttached( CBaseAnimating *pAnimating, const Vector &vecForce, int forceBone, int collisionGroup, IPhysicsObject *pAttached, CBaseAnimating *pParentEntity, int boneAttach, const Vector &originAttached, int parentBoneAttach, const Vector &boneOrigin ); +void DetachAttachedRagdoll( CBaseEntity *pRagdollIn ); +void DetachAttachedRagdollsForEntity( CBaseEntity *pRagdollParent ); +CBaseAnimating *CreateServerRagdollSubmodel( CBaseAnimating *pOwner, const char *pModelName, const Vector &position, const QAngle &angles, int collisionGroup ); + +bool Ragdoll_IsPropRagdoll( CBaseEntity *pEntity ); +void Ragdoll_GetAngleOverrideString( char *pOut, int size, CBaseEntity *pEntity ); +ragdoll_t *Ragdoll_GetRagdoll( CBaseEntity *pEntity ); + +#endif // PHYSICS_PROP_RAGDOLL_H diff --git a/sp/src/game/server/physobj.cpp b/sp/src/game/server/physobj.cpp new file mode 100644 index 00000000..c486fa80 --- /dev/null +++ b/sp/src/game/server/physobj.cpp @@ -0,0 +1,2349 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "vphysics_interface.h" +#include "physics.h" +#include "vcollide_parse.h" +#include "entitylist.h" +#include "physobj.h" +#include "hierarchy.h" +#include "game.h" +#include "ndebugoverlay.h" +#include "engine/IEngineSound.h" +#include "model_types.h" +#include "props.h" +#include "physics_saverestore.h" +#include "saverestore_utlvector.h" +#include "vphysics/constraints.h" +#include "collisionutils.h" +#include "decals.h" +#include "bone_setup.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar debug_physimpact("debug_physimpact", "0" ); + +const char *GetMassEquivalent(float flMass); + +// This is a physically simulated spring, used to join objects together and create spring forces +// +// NOTE: Springs are not physical in the sense that they only create force, they do not collide with +// anything or have any REAL constraints. They can be stretched infinitely (though this will create +// and infinite force), they can penetrate any other object (or spring). They do not occupy any space. +// + +#define SF_SPRING_ONLYSTRETCH 0x0001 + +class CPhysicsSpring : public CBaseEntity +{ + DECLARE_CLASS( CPhysicsSpring, CBaseEntity ); +public: + CPhysicsSpring(); + ~CPhysicsSpring(); + + void Spawn( void ); + void Activate( void ); + + // Inputs + void InputSetSpringConstant( inputdata_t &inputdata ); + void InputSetSpringDamping( inputdata_t &inputdata ); + void InputSetSpringLength( inputdata_t &inputdata ); + + // Debug + int DrawDebugTextOverlays(void); + void DrawDebugGeometryOverlays(void); + + void GetSpringObjectConnections( string_t nameStart, string_t nameEnd, IPhysicsObject **pStart, IPhysicsObject **pEnd ); + void NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ); + IPhysicsObject *GetStartObject() { return m_pSpring ? m_pSpring->GetStartObject() : NULL; } + IPhysicsObject *GetEndObject() { return m_pSpring ? m_pSpring->GetEndObject() : NULL; } + + DECLARE_DATADESC(); + +private: + IPhysicsSpring *m_pSpring; + bool m_isLocal; + + // These are "template" values used to construct the spring. After creation, they are not needed + float m_tempConstant; + float m_tempLength; // This is the "ideal" length of the spring, not the length it is currently stretched to. + float m_tempDamping; + float m_tempRelativeDamping; + + string_t m_nameAttachStart; + string_t m_nameAttachEnd; + Vector m_start; + Vector m_end; + unsigned int m_teleportTick; +}; + +LINK_ENTITY_TO_CLASS( phys_spring, CPhysicsSpring ); + +BEGIN_DATADESC( CPhysicsSpring ) + + DEFINE_PHYSPTR( m_pSpring ), + + DEFINE_KEYFIELD( m_tempConstant, FIELD_FLOAT, "constant" ), + DEFINE_KEYFIELD( m_tempLength, FIELD_FLOAT, "length" ), + DEFINE_KEYFIELD( m_tempDamping, FIELD_FLOAT, "damping" ), + DEFINE_KEYFIELD( m_tempRelativeDamping, FIELD_FLOAT, "relativedamping" ), + + DEFINE_KEYFIELD( m_nameAttachStart, FIELD_STRING, "attach1" ), + DEFINE_KEYFIELD( m_nameAttachEnd, FIELD_STRING, "attach2" ), + + DEFINE_FIELD( m_start, FIELD_POSITION_VECTOR ), + DEFINE_KEYFIELD( m_end, FIELD_POSITION_VECTOR, "springaxis" ), + DEFINE_FIELD( m_isLocal, FIELD_BOOLEAN ), + + // Not necessary to save... it's only there to make sure +// DEFINE_FIELD( m_teleportTick, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpringConstant", InputSetSpringConstant ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpringLength", InputSetSpringLength ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpringDamping", InputSetSpringDamping ), + +END_DATADESC() + +// debug function - slow, uses dynamic_cast<> - use this to query the attached objects +// physics_debug_entity toggles the constraint system for an object using this +bool GetSpringAttachments( CBaseEntity *pEntity, CBaseEntity *pAttachOut[2], IPhysicsObject *pAttachVPhysics[2] ) +{ + CPhysicsSpring *pSpringEntity = dynamic_cast(pEntity); + if ( pSpringEntity ) + { + IPhysicsObject *pRef = pSpringEntity->GetStartObject(); + pAttachOut[0] = pRef ? static_cast(pRef->GetGameData()) : NULL; + pAttachVPhysics[0] = pRef; + IPhysicsObject *pAttach = pSpringEntity->GetEndObject(); + pAttachOut[1] = pAttach ? static_cast(pAttach->GetGameData()) : NULL; + pAttachVPhysics[1] = pAttach; + return true; + } + return false; +} + + +CPhysicsSpring::CPhysicsSpring( void ) +{ +#ifdef _DEBUG + m_start.Init(); + m_end.Init(); +#endif + m_pSpring = NULL; + m_tempConstant = 150; + m_tempLength = 0; + m_tempDamping = 2.0; + m_tempRelativeDamping = 0.01; + m_isLocal = false; + m_teleportTick = 0xFFFFFFFF; +} + +CPhysicsSpring::~CPhysicsSpring( void ) +{ + if ( m_pSpring ) + { + physenv->DestroySpring( m_pSpring ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPhysicsSpring::InputSetSpringConstant( inputdata_t &inputdata ) +{ + m_tempConstant = inputdata.value.Float(); + m_pSpring->SetSpringConstant(inputdata.value.Float()); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPhysicsSpring::InputSetSpringDamping( inputdata_t &inputdata ) +{ + m_tempDamping = inputdata.value.Float(); + m_pSpring->SetSpringDamping(inputdata.value.Float()); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPhysicsSpring::InputSetSpringLength( inputdata_t &inputdata ) +{ + m_tempLength = inputdata.value.Float(); + m_pSpring->SetSpringLength(inputdata.value.Float()); +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CPhysicsSpring::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf(tempstr,sizeof(tempstr),"Constant: %3.2f",m_tempConstant); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Length: %3.2f",m_tempLength); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Damping: %3.2f",m_tempDamping); + EntityText(text_offset,tempstr,0); + text_offset++; + + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: Override base class to add display of fly direction +// Input : +// Output : +//----------------------------------------------------------------------------- +void CPhysicsSpring::DrawDebugGeometryOverlays(void) +{ + if ( !m_pSpring ) + return; + + // ------------------------------ + // Draw if BBOX is on + // ------------------------------ + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + Vector vStartPos; + Vector vEndPos; + m_pSpring->GetEndpoints( &vStartPos, &vEndPos ); + + Vector vSpringDir = vEndPos - vStartPos; + VectorNormalize(vSpringDir); + + Vector vLength = vStartPos + (vSpringDir*m_tempLength); + + NDebugOverlay::Line(vStartPos, vLength, 0,0,255, false, 0); + NDebugOverlay::Line(vLength, vEndPos, 255,0,0, false, 0); + } + BaseClass::DrawDebugGeometryOverlays(); +} + +bool PointIsNearer( IPhysicsObject *pObject1, const Vector &point1, const Vector &point2 ) +{ + Vector center; + + pObject1->GetPosition( ¢er, 0 ); + + float dist1 = (center - point1).LengthSqr(); + float dist2 = (center - point2).LengthSqr(); + + if ( dist1 < dist2 ) + return true; + + return false; +} + +void CPhysicsSpring::GetSpringObjectConnections( string_t nameStart, string_t nameEnd, IPhysicsObject **pStart, IPhysicsObject **pEnd ) +{ + IPhysicsObject *pStartObject = FindPhysicsObjectByName( STRING(nameStart), this ); + IPhysicsObject *pEndObject = FindPhysicsObjectByName( STRING(nameEnd), this ); + + // Assume the world for missing objects + if ( !pStartObject ) + { + pStartObject = g_PhysWorldObject; + } + else if ( !pEndObject ) + { + // try to sort so that the world is always the start object + pEndObject = pStartObject; + pStartObject = g_PhysWorldObject; + } + else + { + CBaseEntity *pEntity0 = (CBaseEntity *) (pStartObject->GetGameData()); + if ( pEntity0 ) + { + g_pNotify->AddEntity( this, pEntity0 ); + } + + CBaseEntity *pEntity1 = (CBaseEntity *) pEndObject->GetGameData(); + if ( pEntity1 ) + { + g_pNotify->AddEntity( this, pEntity1 ); + } + } + + *pStart = pStartObject; + *pEnd = pEndObject; +} + + +void CPhysicsSpring::Activate( void ) +{ + BaseClass::Activate(); + + // UNDONE: save/restore all data, and only create the spring here + + if ( !m_pSpring ) + { + IPhysicsObject *pStart, *pEnd; + + GetSpringObjectConnections( m_nameAttachStart, m_nameAttachEnd, &pStart, &pEnd ); + + // Needs to connect to real, different objects + if ( (!pStart || !pEnd) || (pStart == pEnd) ) + { + DevMsg("ERROR: Can't init spring %s from \"%s\" to \"%s\"\n", GetDebugName(), STRING(m_nameAttachStart), STRING(m_nameAttachEnd) ); + UTIL_Remove( this ); + return; + } + + // if m_end is not closer to pEnd than m_start, swap + if ( !PointIsNearer( pEnd, m_end, m_start ) ) + { + Vector tmpVec = m_start; + m_start = m_end; + m_end = tmpVec; + } + + // create the spring + springparams_t spring; + spring.constant = m_tempConstant; + spring.damping = m_tempDamping; + spring.naturalLength = m_tempLength; + spring.relativeDamping = m_tempRelativeDamping; + spring.startPosition = m_start; + spring.endPosition = m_end; + spring.useLocalPositions = false; + spring.onlyStretch = HasSpawnFlags( SF_SPRING_ONLYSTRETCH ); + m_pSpring = physenv->CreateSpring( pStart, pEnd, &spring ); + } +} + + +void CPhysicsSpring::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + m_start = GetAbsOrigin(); + if ( m_tempLength <= 0 ) + { + m_tempLength = (m_end - m_start).Length(); + } +} + +void CPhysicsSpring::NotifySystemEvent( CBaseEntity *pNotify, notify_system_event_t eventType, const notify_system_event_params_t ¶ms ) +{ + // don't recurse + if ( eventType != NOTIFY_EVENT_TELEPORT || (unsigned int)gpGlobals->tickcount == m_teleportTick ) + return; + + m_teleportTick = gpGlobals->tickcount; + PhysTeleportConstrainedEntity( pNotify, m_pSpring->GetStartObject(), m_pSpring->GetEndObject(), params.pTeleport->prevOrigin, params.pTeleport->prevAngles, params.pTeleport->physicsRotate ); +} + + +// --------------------------------------------------------------------- +// +// CPhysBox -- physically simulated brush +// +// --------------------------------------------------------------------- + +// SendTable stuff. +IMPLEMENT_SERVERCLASS_ST(CPhysBox, DT_PhysBox) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( func_physbox, CPhysBox ); + +BEGIN_DATADESC( CPhysBox ) + + DEFINE_FIELD( m_hCarryingPlayer, FIELD_EHANDLE ), + + DEFINE_KEYFIELD( m_massScale, FIELD_FLOAT, "massScale" ), + DEFINE_KEYFIELD( m_damageType, FIELD_INTEGER, "Damagetype" ), + DEFINE_KEYFIELD( m_iszOverrideScript, FIELD_STRING, "overridescript" ), + DEFINE_KEYFIELD( m_damageToEnableMotion, FIELD_INTEGER, "damagetoenablemotion" ), + DEFINE_KEYFIELD( m_flForceToEnableMotion, FIELD_FLOAT, "forcetoenablemotion" ), + DEFINE_KEYFIELD( m_angPreferredCarryAngles, FIELD_VECTOR, "preferredcarryangles" ), + DEFINE_KEYFIELD( m_bNotSolidToWorld, FIELD_BOOLEAN, "notsolid" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Wake", InputWake ), + DEFINE_INPUTFUNC( FIELD_VOID, "Sleep", InputSleep ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableMotion", InputEnableMotion ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableMotion", InputDisableMotion ), + DEFINE_INPUTFUNC( FIELD_VOID, "ForceDrop", InputForceDrop ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableFloating", InputDisableFloating ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetDebris", InputSetDebris ), +#endif + + // Function pointers + DEFINE_ENTITYFUNC( BreakTouch ), + + // Outputs + DEFINE_OUTPUT( m_OnDamaged, "OnDamaged" ), + DEFINE_OUTPUT( m_OnAwakened, "OnAwakened" ), + DEFINE_OUTPUT( m_OnMotionEnabled, "OnMotionEnabled" ), + DEFINE_OUTPUT( m_OnPhysGunPickup, "OnPhysGunPickup" ), + DEFINE_OUTPUT( m_OnPhysGunPunt, "OnPhysGunPunt" ), + DEFINE_OUTPUT( m_OnPhysGunOnlyPickup, "OnPhysGunOnlyPickup" ), + DEFINE_OUTPUT( m_OnPhysGunDrop, "OnPhysGunDrop" ), + DEFINE_OUTPUT( m_OnPlayerUse, "OnPlayerUse" ), + +END_DATADESC() + +// UNDONE: Save/Restore needs to take the physics object's properties into account +// UNDONE: Acceleration, velocity, angular velocity, etc. must be preserved +// UNDONE: Many of these quantities are relative to a coordinate frame +// UNDONE: Translate when going across transitions? +// UNDONE: Build transition transformation, and transform data in save/restore for IPhysicsObject +// UNDONE: Angles are saved in the entity, but not propagated back to the IPhysicsObject on restore + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysBox::Spawn( void ) +{ + // Initialize damage modifiers. Must be done before baseclass spawn. + m_flDmgModBullet = func_breakdmg_bullet.GetFloat(); + m_flDmgModClub = func_breakdmg_club.GetFloat(); + m_flDmgModExplosive = func_breakdmg_explosive.GetFloat(); + + ParsePropData(); + + Precache(); + + m_iMaxHealth = ( m_iHealth > 0 ) ? m_iHealth : 1; + + if ( HasSpawnFlags( SF_BREAK_TRIGGER_ONLY ) ) + { + m_takedamage = DAMAGE_EVENTS_ONLY; + AddSpawnFlags( SF_BREAK_DONT_TAKE_PHYSICS_DAMAGE ); + } + else if ( m_iHealth == 0 ) + { + m_takedamage = DAMAGE_EVENTS_ONLY; + AddSpawnFlags( SF_BREAK_DONT_TAKE_PHYSICS_DAMAGE ); + } + else + { + m_takedamage = DAMAGE_YES; + } + + SetMoveType( MOVETYPE_NONE ); + SetAbsVelocity( vec3_origin ); + SetModel( STRING( GetModelName() ) ); + SetSolid( SOLID_VPHYSICS ); + if ( HasSpawnFlags( SF_PHYSBOX_DEBRIS ) ) + { + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } + + if ( HasSpawnFlags( SF_PHYSBOX_NO_ROTORWASH_PUSH ) ) + { + AddEFlags( EFL_NO_ROTORWASH_PUSH ); + } + + if ( m_bNotSolidToWorld ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + CreateVPhysics(); + + m_hCarryingPlayer = NULL; + + SetTouch( &CPhysBox::BreakTouch ); + if ( HasSpawnFlags( SF_BREAK_TRIGGER_ONLY ) ) // Only break on trigger + { + SetTouch( NULL ); + } + + if ( m_impactEnergyScale == 0 ) + { + m_impactEnergyScale = 1.0; + } +} + +// shared from studiomdl, checks for long, thin objects and adds some damping +// to prevent endless rolling due to low inertia +static bool ShouldDampRotation( const CPhysCollide *pCollide ) +{ + Vector mins, maxs; + physcollision->CollideGetAABB( &mins, &maxs, pCollide, vec3_origin, vec3_angle ); + Vector size = maxs-mins; + int largest = 0; + float largeSize = size[0]; + for ( int i = 1; i < 3; i++ ) + { + if ( size[i] > largeSize ) + { + largeSize = size[i]; + largest = i; + } + } + size[largest] = 0; + float len = size.Length(); + if ( len > 0 ) + { + float sizeRatio = largeSize / len; + // HACKHACK: Hardcoded size ratio to induce damping + // This prevents long skinny objects from rolling endlessly + if ( sizeRatio > 9 ) + return true; + } + return false; +} + + +bool CPhysBox::CreateVPhysics() +{ + solid_t tmpSolid; + PhysModelParseSolid( tmpSolid, this, GetModelIndex() ); + if ( m_massScale > 0 ) + { + tmpSolid.params.mass *= m_massScale; + } + + vcollide_t *pVCollide = modelinfo->GetVCollide( GetModelIndex() ); + PhysGetMassCenterOverride( this, pVCollide, tmpSolid ); + PhysSolidOverride( tmpSolid, m_iszOverrideScript ); + if ( tmpSolid.params.rotdamping < 1.0f && ShouldDampRotation(pVCollide->solids[0]) ) + { + tmpSolid.params.rotdamping = 1.0f; + } + IPhysicsObject *pPhysics = VPhysicsInitNormal( GetSolid(), GetSolidFlags(), true, &tmpSolid ); + + if ( m_damageType == 1 ) + { + PhysSetGameFlags( pPhysics, FVPHYSICS_DMG_SLICE ); + } + + // Wake it up if not asleep + if ( !HasSpawnFlags(SF_PHYSBOX_ASLEEP) ) + { + pPhysics->Wake(); + } + + if ( HasSpawnFlags(SF_PHYSBOX_MOTIONDISABLED) || m_damageToEnableMotion > 0 || m_flForceToEnableMotion > 0 ) + { + pPhysics->EnableMotion( false ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPhysBox::ObjectCaps() +{ + int caps = BaseClass::ObjectCaps() | FCAP_WCEDIT_POSITION; + if ( HasSpawnFlags( SF_PHYSBOX_ENABLE_PICKUP_OUTPUT ) ) + { + caps |= FCAP_IMPULSE_USE; + } + else if ( !HasSpawnFlags( SF_PHYSBOX_IGNOREUSE ) ) + { + if ( CBasePlayer::CanPickupObject( this, 35, 128 ) ) + { + caps |= FCAP_IMPULSE_USE; + } + } + +#ifdef MAPBASE + if ( HasSpawnFlags( SF_PHYSBOX_RADIUS_PICKUP ) ) + { + caps |= FCAP_USE_IN_RADIUS; + } +#endif + + return caps; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysBox::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( pPlayer ) + { + if ( HasSpawnFlags( SF_PHYSBOX_ENABLE_PICKUP_OUTPUT ) ) + { + m_OnPlayerUse.FireOutput( this, this ); + } + + if ( !HasSpawnFlags( SF_PHYSBOX_IGNOREUSE ) ) + { + pPlayer->PickupObject( this ); + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CPhysBox::CanBePickedUpByPhyscannon() +{ + if ( HasSpawnFlags( SF_PHYSBOX_NEVER_PICK_UP ) ) + return false; + + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( !pPhysicsObject ) + return false; + + if ( !pPhysicsObject->IsMotionEnabled() && !HasSpawnFlags( SF_PHYSBOX_ENABLE_ON_PHYSCANNON ) ) + return false; + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CPhysBox::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + if (VPhysicsGetObject()) + { + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr),"Mass: %.2f kg / %.2f lb (%s)", VPhysicsGetObject()->GetMass(), kg2lbs(VPhysicsGetObject()->GetMass()), GetMassEquivalent(VPhysicsGetObject()->GetMass())); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + } + + return text_offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that breaks the physics object away from its parent +// and starts it simulating. +//----------------------------------------------------------------------------- +void CPhysBox::InputWake( inputdata_t &inputdata ) +{ + VPhysicsGetObject()->Wake(); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler that breaks the physics object away from its parent +// and stops it simulating. +//----------------------------------------------------------------------------- +void CPhysBox::InputSleep( inputdata_t &inputdata ) +{ + VPhysicsGetObject()->Sleep(); +} + +//----------------------------------------------------------------------------- +// Purpose: Enable physics motion and collision response (on by default) +//----------------------------------------------------------------------------- +void CPhysBox::InputEnableMotion( inputdata_t &inputdata ) +{ + EnableMotion(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysBox::EnableMotion( void ) +{ + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->EnableMotion( true ); + pPhysicsObject->Wake(); + } + + m_damageToEnableMotion = 0; + m_flForceToEnableMotion = 0; + + m_OnMotionEnabled.FireOutput( this, this, 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Disable any physics motion or collision response +//----------------------------------------------------------------------------- +void CPhysBox::InputDisableMotion( inputdata_t &inputdata ) +{ + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->EnableMotion( false ); + } +} + +// Turn off floating simulation (and cost) +void CPhysBox::InputDisableFloating( inputdata_t &inputdata ) +{ + PhysEnableFloating( VPhysicsGetObject(), false ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Adds or removes the debris spawnflag. +//----------------------------------------------------------------------------- +void CPhysBox::InputSetDebris( inputdata_t &inputdata ) +{ + if (inputdata.value.Bool()) + { + AddSpawnFlags(SF_PHYSBOX_DEBRIS); + SetCollisionGroup(COLLISION_GROUP_DEBRIS); + } + else + { + RemoveSpawnFlags(SF_PHYSBOX_DEBRIS); + SetCollisionGroup(COLLISION_GROUP_INTERACTIVE); // Is this the default collision group? + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: If we're being held by the player's hand/physgun, force it to drop us +//----------------------------------------------------------------------------- +void CPhysBox::InputForceDrop( inputdata_t &inputdata ) +{ + if ( m_hCarryingPlayer ) + { + m_hCarryingPlayer->ForceDropOfCarriedPhysObjects(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysBox::Move( const Vector &direction ) +{ + VPhysicsGetObject()->ApplyForceCenter( direction ); +} + +// Update the visible representation of the physic system's representation of this object +void CPhysBox::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + BaseClass::VPhysicsUpdate( pPhysics ); + + // if this is the first time we have moved, fire our target + if ( HasSpawnFlags( SF_PHYSBOX_ASLEEP ) ) + { + if ( !pPhysics->IsAsleep() ) + { + m_OnAwakened.FireOutput(this, this); + FireTargets( STRING(m_target), this, this, USE_TOGGLE, 0 ); + RemoveSpawnFlags( SF_PHYSBOX_ASLEEP ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysBox::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + if ( reason == PUNTED_BY_CANNON ) + { + m_OnPhysGunPunt.FireOutput( pPhysGunUser, this ); + } + + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject && !pPhysicsObject->IsMoveable() ) + { + if ( !HasSpawnFlags( SF_PHYSBOX_ENABLE_ON_PHYSCANNON ) ) + return; + EnableMotion(); + } + + m_OnPhysGunPickup.FireOutput( pPhysGunUser, this ); + + // Are we just being punted? + if ( reason == PUNTED_BY_CANNON ) + return; + + if( reason == PICKED_UP_BY_CANNON ) + { + m_OnPhysGunOnlyPickup.FireOutput( pPhysGunUser, this ); + } + + m_hCarryingPlayer = pPhysGunUser; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysBox::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + BaseClass::OnPhysGunDrop( pPhysGunUser, Reason ); + + m_hCarryingPlayer = NULL; + m_OnPhysGunDrop.FireOutput( pPhysGunUser, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysBox::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + BaseClass::VPhysicsCollision( index, pEvent ); + + IPhysicsObject *pPhysObj = pEvent->pObjects[!index]; + + // If we have a force to enable motion, and we're still disabled, check to see if this should enable us + if ( m_flForceToEnableMotion ) + { + CBaseEntity *pOther = static_cast(pPhysObj->GetGameData()); + + // Don't allow the player to bump an object active if we've requested not to + if ( ( pOther && pOther->IsPlayer() && HasSpawnFlags( SF_PHYSBOX_PREVENT_PLAYER_TOUCH_ENABLE ) ) == false ) + { + // Large enough to enable motion? + float flForce = pEvent->collisionSpeed * pEvent->pObjects[!index]->GetMass(); + if ( flForce >= m_flForceToEnableMotion ) + { + EnableMotion(); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPhysBox::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( IsMarkedForDeletion() ) + return 0; + + // note: if motion is disabled, OnTakeDamage can't apply physics force + int ret = BaseClass::OnTakeDamage( info ); + + if ( info.GetInflictor() ) + { + m_OnDamaged.FireOutput( info.GetAttacker(), this ); + } + + // Have we been broken? If so, abort + if ( GetHealth() <= 0 ) + return ret; + + // If we have a force to enable motion, and we're still disabled, check to see if this should enable us + if ( m_flForceToEnableMotion ) + { + // Large enough to enable motion? + float flForce = info.GetDamageForce().Length(); + if ( flForce >= m_flForceToEnableMotion ) + { + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject ) + { + EnableMotion(); + } + } + } + + // Check our health against the threshold: + if( m_damageToEnableMotion > 0 && GetHealth() < m_damageToEnableMotion ) + { + EnableMotion(); + VPhysicsTakeDamage( info ); + } + + return ret; +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if this physbox has preferred carry angles +//----------------------------------------------------------------------------- +bool CPhysBox::HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ) +{ + return HasSpawnFlags( SF_PHYSBOX_USEPREFERRED ); +} + + +// --------------------------------------------------------------------- +// +// CPhysExplosion -- physically simulated explosion +// +// --------------------------------------------------------------------- +#define SF_PHYSEXPLOSION_NODAMAGE 0x0001 +#define SF_PHYSEXPLOSION_PUSH_PLAYER 0x0002 +#define SF_PHYSEXPLOSION_RADIAL 0x0004 +#define SF_PHYSEXPLOSION_TEST_LOS 0x0008 +#define SF_PHYSEXPLOSION_DISORIENT_PLAYER 0x0010 + +LINK_ENTITY_TO_CLASS( env_physexplosion, CPhysExplosion ); + +BEGIN_DATADESC( CPhysExplosion ) + + DEFINE_KEYFIELD( m_damage, FIELD_FLOAT, "magnitude" ), + DEFINE_KEYFIELD( m_radius, FIELD_FLOAT, "radius" ), + DEFINE_KEYFIELD( m_targetEntityName, FIELD_STRING, "targetentityname" ), + DEFINE_KEYFIELD( m_flInnerRadius, FIELD_FLOAT, "inner_radius" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Explode", InputExplode ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "ExplodeAndRemove", InputExplodeAndRemove ), +#endif + + // Outputs + DEFINE_OUTPUT( m_OnPushedPlayer, "OnPushedPlayer" ), + +END_DATADESC() + + +void CPhysExplosion::Spawn( void ) +{ + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_NONE ); + SetModelName( NULL_STRING ); +} + +float CPhysExplosion::GetRadius( void ) +{ + float radius = m_radius; +#ifdef MAPBASE + if ( radius == 0 ) +#else + if ( radius <= 0 ) +#endif + { + // Use the same radius as combat + radius = m_damage * 2.5; + +#ifdef MAPBASE + if (radius < 0) + radius *= -1; +#endif + } + + return radius; +} + +CBaseEntity *CPhysExplosion::FindEntity( CBaseEntity *pEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + // Filter by name or classname + if ( m_targetEntityName != NULL_STRING ) + { + // Try an explicit name first + CBaseEntity *pTarget = gEntList.FindEntityByName( pEntity, m_targetEntityName, NULL, pActivator, pCaller ); + if ( pTarget != NULL ) + return pTarget; + + // Failing that, try a classname + return gEntList.FindEntityByClassnameWithin( pEntity, STRING(m_targetEntityName), GetAbsOrigin(), GetRadius() ); + } + + // Just find anything in the radius + return gEntList.FindEntityInSphere( pEntity, GetAbsOrigin(), GetRadius() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysExplosion::InputExplode( inputdata_t &inputdata ) +{ + Explode( inputdata.pActivator, inputdata.pCaller ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysExplosion::InputExplodeAndRemove( inputdata_t &inputdata ) +{ + Explode( inputdata.pActivator, inputdata.pCaller ); + UTIL_Remove(this); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysExplosion::Explode( CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + CBaseEntity *pEntity = NULL; + float adjustedDamage, falloff, flDist; + Vector vecSpot, vecOrigin; + + falloff = 1.0 / 2.5; + +#ifdef MAPBASE + // For negative damage handling + float damage = m_damage; + if (damage < 0) + damage *= -1.0f; +#endif + + // iterate on all entities in the vicinity. + // I've removed the traceline heuristic from phys explosions. SO right now they will + // affect entities through walls. (sjb) + // UNDONE: Try tracing world-only? + while ((pEntity = FindEntity( pEntity, pActivator, pCaller )) != NULL) + { + // UNDONE: Ask the object if it should get force if it's not MOVETYPE_VPHYSICS? + if ( pEntity->m_takedamage != DAMAGE_NO && (pEntity->GetMoveType() == MOVETYPE_VPHYSICS || (pEntity->VPhysicsGetObject() /*&& !pEntity->IsPlayer()*/)) ) + { + vecOrigin = GetAbsOrigin(); + + vecSpot = pEntity->BodyTarget( vecOrigin ); + // Squash this down to a circle + if ( HasSpawnFlags( SF_PHYSEXPLOSION_RADIAL ) ) + { + vecOrigin[2] = vecSpot[2]; + } + + // decrease damage for an ent that's farther from the bomb. + flDist = ( vecOrigin - vecSpot ).Length(); + + if( m_radius == 0 || flDist <= m_radius ) + { + if ( HasSpawnFlags( SF_PHYSEXPLOSION_TEST_LOS ) ) + { + Vector vecStartPos = GetAbsOrigin(); + Vector vecEndPos = pEntity->BodyTarget( vecStartPos, false ); + + if ( m_flInnerRadius != 0.0f ) + { + // Find a point on our inner radius sphere to begin from + Vector vecDirToTarget = ( vecEndPos - vecStartPos ); + VectorNormalize( vecDirToTarget ); + vecStartPos = GetAbsOrigin() + ( vecDirToTarget * m_flInnerRadius ); + } + + trace_t tr; + UTIL_TraceLine( vecStartPos, + pEntity->BodyTarget( vecStartPos, false ), + MASK_SOLID_BRUSHONLY, + this, + COLLISION_GROUP_NONE, + &tr ); + + // Shielded + if ( tr.fraction < 1.0f && tr.m_pEnt != pEntity ) + continue; + } + + adjustedDamage = flDist * falloff; +#ifdef MAPBASE + adjustedDamage = damage - adjustedDamage; +#else + adjustedDamage = m_damage - adjustedDamage; +#endif + + if ( adjustedDamage < 1 ) + { + adjustedDamage = 1; + } + + CTakeDamageInfo info( this, this, adjustedDamage, DMG_BLAST ); +#ifdef MAPBASE + // Negative damage handling + Vector vecDir = (vecSpot - vecOrigin); + if (m_damage < 0) + { + vecDir *= -1.0f; + vecOrigin += vecDir; + NDebugOverlay::Cross3D(vecOrigin, 2.0f, 255, 255, 0, true, 1.0f); + } + CalculateExplosiveDamageForce( &info, vecDir, vecOrigin ); +#else + CalculateExplosiveDamageForce( &info, (vecSpot - vecOrigin), vecOrigin ); +#endif + + if ( HasSpawnFlags( SF_PHYSEXPLOSION_PUSH_PLAYER ) ) + { + if ( pEntity->IsPlayer() ) + { + Vector vecPushDir = ( pEntity->BodyTarget( GetAbsOrigin(), false ) - GetAbsOrigin() ); + float dist = VectorNormalize( vecPushDir ); + + float flFalloff = RemapValClamped( dist, m_radius, m_radius*0.75f, 0.0f, 1.0f ); + + if ( HasSpawnFlags( SF_PHYSEXPLOSION_DISORIENT_PLAYER ) ) + { + //Disorient the player + QAngle vecDeltaAngles; + + vecDeltaAngles.x = random->RandomInt( -30, 30 ); + vecDeltaAngles.y = random->RandomInt( -30, 30 ); + vecDeltaAngles.z = 0.0f; + + CBasePlayer *pPlayer = ToBasePlayer( pEntity ); + pPlayer->SnapEyeAngles( GetLocalAngles() + vecDeltaAngles ); + pEntity->ViewPunch( vecDeltaAngles ); + } + +#ifdef MAPBASE + Vector vecPush = (vecPushDir*damage*flFalloff*2.0f); +#else + Vector vecPush = (vecPushDir*m_damage*flFalloff*2.0f); +#endif + if ( pEntity->GetFlags() & FL_BASEVELOCITY ) + { + vecPush = vecPush + pEntity->GetBaseVelocity(); + } + if ( vecPush.z > 0 && (pEntity->GetFlags() & FL_ONGROUND) ) + { + pEntity->SetGroundEntity( NULL ); + Vector origin = pEntity->GetAbsOrigin(); + origin.z += 1.0f; + pEntity->SetAbsOrigin( origin ); + } + + pEntity->SetBaseVelocity( vecPush ); + pEntity->AddFlag( FL_BASEVELOCITY ); + + // Fire an output that the player has been pushed + m_OnPushedPlayer.FireOutput( this, this ); + continue; + } + } + + if ( HasSpawnFlags( SF_PHYSEXPLOSION_NODAMAGE ) ) + { + pEntity->VPhysicsTakeDamage( info ); + } + else + { + pEntity->TakeDamage( info ); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CPhysExplosion::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print magnitude + Q_snprintf(tempstr,sizeof(tempstr)," magnitude: %f", m_damage); + EntityText(text_offset,tempstr,0); + text_offset++; + + // print target entity + Q_snprintf(tempstr,sizeof(tempstr)," limit to: %s", STRING(m_targetEntityName)); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + + +//================================================== +// CPhysImpact +//================================================== + +#define bitsPHYSIMPACT_NOFALLOFF 0x00000001 +#define bitsPHYSIMPACT_INFINITE_LENGTH 0x00000002 +#define bitsPHYSIMPACT_IGNORE_MASS 0x00000004 +#define bitsPHYSIMPACT_IGNORE_NORMAL 0x00000008 + +#define DEFAULT_EXPLODE_DISTANCE 256 +LINK_ENTITY_TO_CLASS( env_physimpact, CPhysImpact ); + +BEGIN_DATADESC( CPhysImpact ) + + DEFINE_KEYFIELD( m_damage, FIELD_FLOAT, "magnitude" ), + DEFINE_KEYFIELD( m_distance, FIELD_FLOAT, "distance" ), + DEFINE_KEYFIELD( m_directionEntityName,FIELD_STRING, "directionentityname" ), + + // Function pointers + DEFINE_FUNCTION( PointAtEntity ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Impact", InputImpact ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysImpact::Activate( void ) +{ + BaseClass::Activate(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysImpact::Spawn( void ) +{ + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_NONE ); + SetModelName( NULL_STRING ); + + //If not targetted, and no distance is set, give it a default value + if ( m_distance == 0 ) + { + m_distance = DEFAULT_EXPLODE_DISTANCE; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysImpact::PointAtEntity( void ) +{ + //If we're not targetted at anything, don't bother + if ( m_directionEntityName == NULL_STRING ) + return; + + UTIL_PointAtNamedEntity( this, m_directionEntityName ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActivator - +// *pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CPhysImpact::InputImpact( inputdata_t &inputdata ) +{ + Vector dir; + trace_t trace; + + //If we have a direction target, setup to point at it + if ( m_directionEntityName != NULL_STRING ) + { + PointAtEntity(); + } + + AngleVectors( GetAbsAngles(), &dir ); + + //Setup our trace information + float dist = HasSpawnFlags( bitsPHYSIMPACT_INFINITE_LENGTH ) ? MAX_TRACE_LENGTH : m_distance; + Vector start = GetAbsOrigin(); + Vector end = start + ( dir * dist ); + + //Trace out + UTIL_TraceLine( start, end, MASK_SHOT, this, COLLISION_GROUP_NONE, &trace ); + if ( trace.startsolid ) + { + // ep1_citadel_04 has a phys_impact just behind another entity, so if we startsolid then + // bump out just a little and retry the trace + Vector startOffset = start + ( dir * 0.1 ); + UTIL_TraceLine( startOffset , end, MASK_SHOT, this, COLLISION_GROUP_NONE, &trace ); + } + + if( debug_physimpact.GetBool() ) + { + NDebugOverlay::Cross3D( start, 24, 255, 255, 255, false, 30 ); + NDebugOverlay::Line( trace.startpos, trace.endpos, 0, 255, 0, false, 30 ); + } + + if ( trace.fraction != 1.0 ) + { + // if inside the object, just go opposite the direction + if ( trace.startsolid ) + { + trace.plane.normal = -dir; + } + CBaseEntity *pEnt = trace.m_pEnt; + + IPhysicsObject *pPhysics = pEnt->VPhysicsGetObject(); + //If the entity is valid, hit it + if ( ( pEnt != NULL ) && ( pPhysics != NULL ) ) + { + CTakeDamageInfo info; + info.SetAttacker( this); + info.SetInflictor( this ); + info.SetDamage( 0 ); + info.SetDamageForce( vec3_origin ); + info.SetDamageType( DMG_GENERIC ); + + pEnt->DispatchTraceAttack( info, dir, &trace ); + ApplyMultiDamage(); + + //Damage falls off unless specified or the ray's length is infinite + float damage = HasSpawnFlags( bitsPHYSIMPACT_NOFALLOFF | bitsPHYSIMPACT_INFINITE_LENGTH ) ? + m_damage : (m_damage * (1.0f-trace.fraction)); + + if ( HasSpawnFlags( bitsPHYSIMPACT_IGNORE_MASS ) ) + { + damage *= pPhysics->GetMass(); + } + + if( debug_physimpact.GetBool() ) + { + NDebugOverlay::Line( trace.endpos, trace.endpos + trace.plane.normal * -128, 255, 0, 0, false, 30 ); + } + + // Legacy entities applied the force along the impact normal, which yielded unpredictable results. + if ( !HasSpawnFlags( bitsPHYSIMPACT_IGNORE_NORMAL ) ) + { + dir = -trace.plane.normal; + } + + pPhysics->ApplyForceOffset( damage * dir * phys_pushscale.GetFloat(), trace.endpos ); + } + } +} + + +class CSimplePhysicsBrush : public CBaseEntity +{ + DECLARE_CLASS( CSimplePhysicsBrush, CBaseEntity ); +public: + void Spawn() + { + SetModel( STRING( GetModelName() ) ); + SetMoveType( MOVETYPE_VPHYSICS ); + SetSolid( SOLID_VPHYSICS ); + m_takedamage = DAMAGE_EVENTS_ONLY; + +#ifdef MAPBASE + // If we don't have an owner entity, it means this wasn't spawned by a phys_convert and this is safe. + if ( !GetOwnerEntity() ) + { + VPhysicsInitNormal( SOLID_VPHYSICS, 0, HasSpawnFlags(SF_PHYSBOX_DEBRIS) ); + if ( HasSpawnFlags(SF_PHYSBOX_ASLEEP) ) + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } +#endif + } +}; + +LINK_ENTITY_TO_CLASS( simple_physics_brush, CSimplePhysicsBrush ); + +class CSimplePhysicsProp : public CBaseProp +{ + DECLARE_CLASS( CSimplePhysicsProp, CBaseProp ); + +public: + void Spawn() + { + BaseClass::Spawn(); + SetMoveType( MOVETYPE_VPHYSICS ); + SetSolid( SOLID_VPHYSICS ); + m_takedamage = DAMAGE_EVENTS_ONLY; + +#ifdef MAPBASE + // If we don't have an owner entity, it means this wasn't spawned by a phys_convert and this is safe. + if ( !GetOwnerEntity() ) + { + VPhysicsInitNormal( SOLID_VPHYSICS, 0, HasSpawnFlags(SF_PHYSPROP_DEBRIS) ); + if ( HasSpawnFlags(SF_PHYSPROP_START_ASLEEP) ) + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } +#endif + } + + int ObjectCaps() + { + int caps = BaseClass::ObjectCaps() | FCAP_WCEDIT_POSITION; + + if ( CBasePlayer::CanPickupObject( this, 35, 128 ) ) + { + caps |= FCAP_IMPULSE_USE; + } + + return caps; + } + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) + { + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( pPlayer ) + { + pPlayer->PickupObject( this ); + } + } +}; + +LINK_ENTITY_TO_CLASS( simple_physics_prop, CSimplePhysicsProp ); + +// UNDONE: Is this worth it?, just recreate the object instead? (that happens when this returns false anyway) +// recreating works, but is more expensive and won't inherit properties (velocity, constraints, etc) +bool TransferPhysicsObject( CBaseEntity *pFrom, CBaseEntity *pTo, bool wakeUp ) +{ + IPhysicsObject *pVPhysics = pFrom->VPhysicsGetObject(); + if ( !pVPhysics || pVPhysics->IsStatic() ) + return false; + + // clear out the pointer so it won't get deleted + pFrom->VPhysicsSwapObject( NULL ); + // remove any AI behavior bound to it + pVPhysics->RemoveShadowController(); + // transfer to the new owner + pTo->VPhysicsSetObject( pVPhysics ); + pVPhysics->SetGameData( (void *)pTo ); + pTo->VPhysicsUpdate( pVPhysics ); + + // may have been temporarily disabled by the old object + pVPhysics->EnableMotion( true ); + pVPhysics->EnableGravity( true ); + + // Update for the new entity solid type + pVPhysics->RecheckCollisionFilter(); + if ( wakeUp ) + { + pVPhysics->Wake(); + } + + return true; +} + +// UNDONE: Move/rename this function +static CBaseEntity *CreateSimplePhysicsObject( CBaseEntity *pEntity, bool createAsleep, bool createAsDebris ) +{ + CBaseEntity *pPhysEntity = NULL; + int modelindex = pEntity->GetModelIndex(); + const model_t *model = modelinfo->GetModel( modelindex ); + if ( model && modelinfo->GetModelType(model) == mod_brush ) + { + pPhysEntity = CreateEntityByName( "simple_physics_brush" ); + } + else + { + pPhysEntity = CreateEntityByName( "simple_physics_prop" ); + } + + pPhysEntity->KeyValue( "model", STRING(pEntity->GetModelName()) ); + pPhysEntity->SetAbsOrigin( pEntity->GetAbsOrigin() ); + pPhysEntity->SetAbsAngles( pEntity->GetAbsAngles() ); +#ifdef MAPBASE + // So the entity knows it's being spawned by a phys_convert + pPhysEntity->SetOwnerEntity( pEntity ); +#endif + pPhysEntity->Spawn(); + if ( !TransferPhysicsObject( pEntity, pPhysEntity, !createAsleep ) ) + { + pPhysEntity->VPhysicsInitNormal( SOLID_VPHYSICS, 0, createAsleep ); + if ( createAsDebris ) + pPhysEntity->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } + return pPhysEntity; +} + +#ifdef MAPBASE +// Creates func_brush and prop_physics instead, because why not? +static CBaseEntity *CreateConventionalPhysicsObject( CBaseEntity *pEntity, bool createAsleep, bool createAsDebris ) +{ + CBaseEntity *pPhysEntity = NULL; + int modelindex = pEntity->GetModelIndex(); + const model_t *model = modelinfo->GetModel( modelindex ); + if ( model && modelinfo->GetModelType(model) == mod_brush ) + { + pPhysEntity = CreateEntityByName( "func_physbox" ); + } + else + { + pPhysEntity = CreateEntityByName( "prop_physics_override" ); + } + + pPhysEntity->KeyValue( "model", STRING(pEntity->GetModelName()) ); + pPhysEntity->SetAbsOrigin( pEntity->GetAbsOrigin() ); + pPhysEntity->SetAbsAngles( pEntity->GetAbsAngles() ); + pPhysEntity->Spawn(); + if ( !TransferPhysicsObject( pEntity, pPhysEntity, !createAsleep ) ) + { + pPhysEntity->VPhysicsInitNormal( SOLID_VPHYSICS, 0, createAsleep ); + if ( createAsDebris ) + pPhysEntity->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } + return pPhysEntity; +} +#endif + +#define SF_CONVERT_ASLEEP 0x0001 +#define SF_CONVERT_AS_DEBRIS 0x0002 + +class CPhysConvert : public CLogicalEntity +{ + DECLARE_CLASS( CPhysConvert, CLogicalEntity ); + +public: + CPhysConvert( void ) : m_flMassOverride( 0.0f ) {}; + COutputEvent m_OnConvert; + + // Input handlers + void InputConvertTarget( inputdata_t &inputdata ); + +#ifdef MAPBASE + enum + { + CONVERT_ENTITYTYPE_SIMPLE, // simple_physics_prop, simple_physics_brush, etc. + CONVERT_ENTITYTYPE_CONVENTIONAL, // prop_physics, func_physbox, etc. + }; +#endif + + DECLARE_DATADESC(); + +private: + string_t m_swapModel; + float m_flMassOverride; +#ifdef MAPBASE + int m_iPhysicsEntityType = CONVERT_ENTITYTYPE_SIMPLE; +#endif +}; + +LINK_ENTITY_TO_CLASS( phys_convert, CPhysConvert ); + +BEGIN_DATADESC( CPhysConvert ) + + DEFINE_KEYFIELD( m_swapModel, FIELD_STRING, "swapmodel" ), + DEFINE_KEYFIELD( m_flMassOverride, FIELD_FLOAT, "massoverride" ), +#ifdef MAPBASE + DEFINE_INPUT( m_iPhysicsEntityType, FIELD_INTEGER, "SetConversionType" ), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "ConvertTarget", InputConvertTarget ), + + // Outputs + DEFINE_OUTPUT( m_OnConvert, "OnConvert"), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that converts our target to a physics object. +//----------------------------------------------------------------------------- +void CPhysConvert::InputConvertTarget( inputdata_t &inputdata ) +{ + bool createAsleep = HasSpawnFlags(SF_CONVERT_ASLEEP); + bool createAsDebris = HasSpawnFlags(SF_CONVERT_AS_DEBRIS); + // Fire output + m_OnConvert.FireOutput( inputdata.pActivator, this ); + + CBaseEntity *entlist[512]; + CBaseEntity *pSwap = gEntList.FindEntityByName( NULL, m_swapModel, NULL, inputdata.pActivator, inputdata.pCaller ); + CBaseEntity *pEntity = NULL; + + int count = 0; + while ( (pEntity = gEntList.FindEntityByName( pEntity, m_target, NULL, inputdata.pActivator, inputdata.pCaller )) != NULL ) + { + entlist[count++] = pEntity; + if ( count >= ARRAYSIZE(entlist) ) + break; + } + + // if we're swapping to model out, don't loop over more than one object + // multiple objects with the same brush model will render, but the dynamic lights + // and decals will be shared between the two instances... + if ( pSwap && count > 0 ) + { + count = 1; + } + + for ( int i = 0; i < count; i++ ) + { + pEntity = entlist[i]; + + // don't convert something that is already physics based + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + Msg( "ERROR phys_convert %s ! Already MOVETYPE_VPHYSICS\n", STRING(pEntity->m_iClassname) ); + continue; + } + + UnlinkFromParent( pEntity ); + + if ( pSwap ) + { + // we can't reuse this physics object, so kill it + pEntity->VPhysicsDestroyObject(); + pEntity->SetModel( STRING(pSwap->GetModelName()) ); + } + + // created phys object, now move hierarchy over +#ifdef MAPBASE + CBaseEntity *pPhys; + switch (m_iPhysicsEntityType) + { + case CONVERT_ENTITYTYPE_CONVENTIONAL: pPhys = CreateConventionalPhysicsObject( pEntity, createAsleep, createAsDebris ); break; + default: pPhys = CreateSimplePhysicsObject( pEntity, createAsleep, createAsDebris ); break; + } +#else + CBaseEntity *pPhys = CreateSimplePhysicsObject( pEntity, createAsleep, createAsDebris ); +#endif + if ( pPhys ) + { + // Override the mass if specified + if ( m_flMassOverride > 0 ) + { + IPhysicsObject *pPhysObj = pPhys->VPhysicsGetObject(); + if ( pPhysObj ) + { + pPhysObj->SetMass( m_flMassOverride ); + } + } + +#ifdef MAPBASE + pPhys->m_nRenderMode = pEntity->m_nRenderMode; + pPhys->m_nRenderFX = pEntity->m_nRenderFX; + const color32 rclr = pEntity->GetRenderColor(); + pPhys->SetRenderColor(rclr.r, rclr.g, rclr.b, rclr.a); + if (pEntity->GetBaseAnimating() /*&& pPhys->GetBaseAnimating()*/) + { + CBaseAnimating *pEntityAnimating = pEntity->GetBaseAnimating(); + CBaseAnimating *pPhysAnimating = pPhys->GetBaseAnimating(); + + pPhysAnimating->m_nSkin = pEntityAnimating->m_nSkin; + pPhysAnimating->m_nBody = pEntityAnimating->m_nBody; + pPhysAnimating->SetModelScale(pEntityAnimating->GetModelScale()); + } +#endif + + pPhys->SetName( pEntity->GetEntityName() ); + UTIL_TransferPoseParameters( pEntity, pPhys ); + TransferChildren( pEntity, pPhys ); + pEntity->AddSolidFlags( FSOLID_NOT_SOLID ); + pEntity->AddEffects( EF_NODRAW ); + UTIL_Remove( pEntity ); + } + } +} + +//============================================================================================================ +// PHYS MAGNET +//============================================================================================================ +#define SF_MAGNET_ASLEEP 0x0001 +#define SF_MAGNET_MOTIONDISABLED 0x0002 +#define SF_MAGNET_SUCK 0x0004 +#define SF_MAGNET_ALLOWROTATION 0x0008 +#define SF_MAGNET_COAST_HACK 0x0010 +#ifdef MAPBASE +#define SF_MAGNET_PREVENT_PICKUP 0x0020 +#endif + +LINK_ENTITY_TO_CLASS( phys_magnet, CPhysMagnet ); + +// BUGBUG: This won't work! Right now you can't save physics pointers inside an embedded type! +BEGIN_SIMPLE_DATADESC( magnetted_objects_t ) + + DEFINE_PHYSPTR( pConstraint ), + DEFINE_FIELD( hEntity, FIELD_EHANDLE ), + +END_DATADESC() + +BEGIN_DATADESC( CPhysMagnet ) + // Outputs + DEFINE_OUTPUT( m_OnMagnetAttach, "OnAttach" ), + DEFINE_OUTPUT( m_OnMagnetDetach, "OnDetach" ), + + // Keys + DEFINE_KEYFIELD( m_massScale, FIELD_FLOAT, "massScale" ), + DEFINE_KEYFIELD( m_iszOverrideScript, FIELD_STRING, "overridescript" ), + DEFINE_KEYFIELD( m_iMaxObjectsAttached, FIELD_INTEGER, "maxobjects" ), + DEFINE_KEYFIELD( m_forceLimit, FIELD_FLOAT, "forcelimit" ), + DEFINE_KEYFIELD( m_torqueLimit, FIELD_FLOAT, "torquelimit" ), + + DEFINE_UTLVECTOR( m_MagnettedEntities, FIELD_EMBEDDED ), + DEFINE_PHYSPTR( m_pConstraintGroup ), + + DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bHasHitSomething, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flTotalMass, FIELD_FLOAT ), + DEFINE_FIELD( m_flRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextSuckTime, FIELD_FLOAT ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: SendProxy that converts the magnet's attached object UtlVector to entindexes +//----------------------------------------------------------------------------- +void SendProxy_MagnetAttachedObjectList( const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CPhysMagnet *pMagnet = (CPhysMagnet*)pData; + + // If this assertion fails, then SendProxyArrayLength_MagnetAttachedArray must have failed. + Assert( iElement < pMagnet->GetNumAttachedObjects() ); + + pOut->m_Int = pMagnet->GetAttachedObject(iElement)->entindex(); +} + + +int SendProxyArrayLength_MagnetAttachedArray( const void *pStruct, int objectID ) +{ + CPhysMagnet *pMagnet = (CPhysMagnet*)pStruct; + return pMagnet->GetNumAttachedObjects(); +} + +IMPLEMENT_SERVERCLASS_ST( CPhysMagnet, DT_PhysMagnet ) + + // ROBIN: Disabled because we don't need it anymore + /* + SendPropArray2( + SendProxyArrayLength_MagnetAttachedArray, + SendPropInt("magnetattached_array_element", 0, 4, 10, SPROP_UNSIGNED, SendProxy_MagnetAttachedObjectList), + 128, + 0, + "magnetattached_array" + ) + */ + +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPhysMagnet::CPhysMagnet( void ) +{ + m_forceLimit = 0; + m_torqueLimit = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPhysMagnet::~CPhysMagnet( void ) +{ + DetachAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysMagnet::Spawn( void ) +{ +#ifdef MAPBASE + // Crashes otherwise + if (GetModelName() == NULL_STRING) + { + Warning("WARNING: %s spawned with no model name\n", GetDebugName()); + UTIL_Remove(this); + return; + } +#endif + + Precache(); + + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_VPHYSICS ); + SetModel( STRING( GetModelName() ) ); + + m_takedamage = DAMAGE_EVENTS_ONLY; + + solid_t tmpSolid; + PhysModelParseSolid( tmpSolid, this, GetModelIndex() ); + if ( m_massScale > 0 ) + { + tmpSolid.params.mass *= m_massScale; + } + PhysSolidOverride( tmpSolid, m_iszOverrideScript ); + VPhysicsInitNormal( GetSolid(), GetSolidFlags(), true, &tmpSolid ); + + // Wake it up if not asleep + if ( !HasSpawnFlags(SF_MAGNET_ASLEEP) ) + { + VPhysicsGetObject()->Wake(); + } + + if ( HasSpawnFlags(SF_MAGNET_MOTIONDISABLED) ) + { + VPhysicsGetObject()->EnableMotion( false ); + } + +#ifdef MAPBASE + if ( HasSpawnFlags(SF_MAGNET_PREVENT_PICKUP) ) + { + PhysSetGameFlags(VPhysicsGetObject(), FVPHYSICS_NO_PLAYER_PICKUP); + } +#endif + + m_bActive = true; + m_pConstraintGroup = NULL; + m_flTotalMass = 0; + m_flNextSuckTime = 0; + + BaseClass::Spawn(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysMagnet::Precache( void ) +{ + PrecacheModel( STRING( GetModelName() ) ); + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysMagnet::Touch( CBaseEntity *pOther ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysMagnet::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + int otherIndex = !index; + CBaseEntity *pOther = pEvent->pEntities[otherIndex]; + + // Ignore triggers + if ( pOther->IsSolidFlagSet( FSOLID_NOT_SOLID ) ) + return; + + m_bHasHitSomething = true; + DoMagnetSuck( pEvent->pEntities[!index] ); + + // Don't pickup if we're not active + if ( !m_bActive ) + return; + + // Hit our maximum? + if ( m_iMaxObjectsAttached && m_iMaxObjectsAttached <= GetNumAttachedObjects() ) + return; + + // This is a hack to solve players (Erik) stacking stuff on their jeeps in coast_01 + // and being screwed when the crane can't pick them up. We need to get rid of the object. + if ( HasSpawnFlags( SF_MAGNET_COAST_HACK ) ) + { + // If the other isn't the jeep, we need to get rid of it + if ( !FClassnameIs( pOther, "prop_vehicle_jeep" ) ) + { + // If it takes damage, destroy it + if ( pOther->m_takedamage != DAMAGE_NO && pOther->m_takedamage != DAMAGE_EVENTS_ONLY ) + { + CTakeDamageInfo info( this, this, pOther->GetHealth(), DMG_GENERIC | DMG_PREVENT_PHYSICS_FORCE ); + pOther->TakeDamage( info ); + } + else if ( pEvent->pObjects[ otherIndex ]->IsMoveable() ) + { + // Otherwise, we're screwed, so just remove it + UTIL_Remove( pOther ); + } + else + { + Warning( "CPhysMagnet %s:%d blocking magnet\n", + pOther->GetClassname(), pOther->entindex() ); + } + return; + } + } + + // Make sure it's made of metal + const surfacedata_t *phit = physprops->GetSurfaceData( pEvent->surfaceProps[otherIndex] ); + char cTexType = phit->game.material; + if ( cTexType != CHAR_TEX_METAL && cTexType != CHAR_TEX_COMPUTER ) + { + // If we don't have a model, we're done. The texture we hit wasn't metal. + if ( !pOther->GetBaseAnimating() ) + return; + + // If we have a model that wants to be metal, even though we hit a non-metal texture, we'll stick to it + if ( Q_strncmp( Studio_GetDefaultSurfaceProps( pOther->GetBaseAnimating()->GetModelPtr() ), "metal", 5 ) ) + return; + } + + IPhysicsObject *pPhysics = pOther->VPhysicsGetObject(); + if ( pPhysics && pOther->GetMoveType() == MOVETYPE_VPHYSICS && pPhysics->IsMoveable() ) + { + // Make sure we haven't already got this sucker on the magnet + int iCount = m_MagnettedEntities.Count(); + for ( int i = 0; i < iCount; i++ ) + { + if ( m_MagnettedEntities[i].hEntity == pOther ) + return; + } + + // We want to cast a long way to ensure our shadow shows up + pOther->SetShadowCastDistance( 2048 ); + + // Create a constraint between the magnet and this sucker + IPhysicsObject *pMagnetPhysObject = VPhysicsGetObject(); + Assert( pMagnetPhysObject ); + + magnetted_objects_t newEntityOnMagnet; + newEntityOnMagnet.hEntity = pOther; + + // Use the right constraint + if ( HasSpawnFlags( SF_MAGNET_ALLOWROTATION ) ) + { + constraint_ballsocketparams_t ballsocket; + ballsocket.Defaults(); + ballsocket.constraint.Defaults(); + ballsocket.constraint.forceLimit = lbs2kg(m_forceLimit); + ballsocket.constraint.torqueLimit = lbs2kg(m_torqueLimit); + + Vector vecCollisionPoint; + pEvent->pInternalData->GetContactPoint( vecCollisionPoint ); + + pMagnetPhysObject->WorldToLocal( &ballsocket.constraintPosition[0], vecCollisionPoint ); + pPhysics->WorldToLocal( &ballsocket.constraintPosition[1], vecCollisionPoint ); + + //newEntityOnMagnet.pConstraint = physenv->CreateBallsocketConstraint( pMagnetPhysObject, pPhysics, m_pConstraintGroup, ballsocket ); + newEntityOnMagnet.pConstraint = physenv->CreateBallsocketConstraint( pMagnetPhysObject, pPhysics, NULL, ballsocket ); + } + else + { + constraint_fixedparams_t fixed; + fixed.Defaults(); + fixed.InitWithCurrentObjectState( pMagnetPhysObject, pPhysics ); + fixed.constraint.Defaults(); + fixed.constraint.forceLimit = lbs2kg(m_forceLimit); + fixed.constraint.torqueLimit = lbs2kg(m_torqueLimit); + + // FIXME: Use the magnet's constraint group. + //newEntityOnMagnet.pConstraint = physenv->CreateFixedConstraint( pMagnetPhysObject, pPhysics, m_pConstraintGroup, fixed ); + newEntityOnMagnet.pConstraint = physenv->CreateFixedConstraint( pMagnetPhysObject, pPhysics, NULL, fixed ); + } + + newEntityOnMagnet.pConstraint->SetGameData( (void *) this ); + m_MagnettedEntities.AddToTail( newEntityOnMagnet ); + + m_flTotalMass += pPhysics->GetMass(); + } + + DoMagnetSuck( pOther ); + + m_OnMagnetAttach.FireOutput( this, this ); + + BaseClass::VPhysicsCollision( index, pEvent ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPhysMagnet::CanBePickedUpByPhyscannon( void ) +{ + if ( HasSpawnFlags( SF_MAGNET_PREVENT_PICKUP ) ) + return false; + + return true; +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysMagnet::DoMagnetSuck( CBaseEntity *pOther ) +{ + if ( !HasSpawnFlags( SF_MAGNET_SUCK ) ) + return; + + if ( !m_bActive ) + return; + + // Don't repeatedly suck + if ( m_flNextSuckTime > gpGlobals->curtime ) + return; + + // Look for physics objects underneath the magnet and suck them onto it + Vector vecCheckPos, vecSuckPoint; + VectorTransform( Vector(0,0,-96), EntityToWorldTransform(), vecCheckPos ); + VectorTransform( Vector(0,0,-64), EntityToWorldTransform(), vecSuckPoint ); + + CBaseEntity *pEntities[20]; + int iNumEntities = UTIL_EntitiesInSphere( pEntities, 20, vecCheckPos, 80.0, 0 ); + for ( int i = 0; i < iNumEntities; i++ ) + { + CBaseEntity *pEntity = pEntities[i]; + if ( !pEntity || pEntity == pOther ) + continue; + + IPhysicsObject *pPhys = pEntity->VPhysicsGetObject(); + if ( pPhys && pEntity->GetMoveType() == MOVETYPE_VPHYSICS && pPhys->GetMass() < 5000 ) + { + // Do we have line of sight to it? + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), pEntity->GetAbsOrigin(), MASK_SHOT, this, 0, &tr ); + if ( tr.fraction == 1.0 || tr.m_pEnt == pEntity ) + { + // Pull it towards the magnet + Vector vecVelocity = (vecSuckPoint - pEntity->GetAbsOrigin()); + VectorNormalize(vecVelocity); + vecVelocity *= 5 * pPhys->GetMass(); + pPhys->AddVelocity( &vecVelocity, NULL ); + } + } + } + + m_flNextSuckTime = gpGlobals->curtime + 2.0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysMagnet::SetConstraintGroup( IPhysicsConstraintGroup *pGroup ) +{ + m_pConstraintGroup = pGroup; +} + +//----------------------------------------------------------------------------- +// Purpose: Make the magnet active +//----------------------------------------------------------------------------- +void CPhysMagnet::InputTurnOn( inputdata_t &inputdata ) +{ + m_bActive = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Make the magnet inactive. Drop everything it's got hooked on. +//----------------------------------------------------------------------------- +void CPhysMagnet::InputTurnOff( inputdata_t &inputdata ) +{ + m_bActive = false; + DetachAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: Toggle the magnet's active state +//----------------------------------------------------------------------------- +void CPhysMagnet::InputToggle( inputdata_t &inputdata ) +{ + if ( m_bActive ) + { + InputTurnOff( inputdata ); + } + else + { + InputTurnOn( inputdata ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: One of our magnet constraints broke +//----------------------------------------------------------------------------- +void CPhysMagnet::ConstraintBroken( IPhysicsConstraint *pConstraint ) +{ + // Find the entity that was constrained and release it + int iCount = m_MagnettedEntities.Count(); + for ( int i = 0; i < iCount; i++ ) + { + if ( m_MagnettedEntities[i].hEntity.Get() != NULL && m_MagnettedEntities[i].pConstraint == pConstraint ) + { + IPhysicsObject *pPhysObject = m_MagnettedEntities[i].hEntity->VPhysicsGetObject(); + + if( pPhysObject != NULL ) + { + m_flTotalMass -= pPhysObject->GetMass(); + } + + m_MagnettedEntities.Remove(i); + break; + } + } + + m_OnMagnetDetach.FireOutput( this, this ); + + physenv->DestroyConstraint( pConstraint ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysMagnet::DetachAll( void ) +{ + // Make sure we haven't already got this sucker on the magnet + int iCount = m_MagnettedEntities.Count(); + for ( int i = 0; i < iCount; i++ ) + { + // Delay a couple seconds to reset to the default shadow cast behavior + if ( m_MagnettedEntities[i].hEntity ) + { + m_MagnettedEntities[i].hEntity->SetShadowCastDistance( 0, 2.0f ); + } + + physenv->DestroyConstraint( m_MagnettedEntities[i].pConstraint ); + } + + m_MagnettedEntities.Purge(); + m_flTotalMass = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPhysMagnet::GetNumAttachedObjects( void ) +{ + return m_MagnettedEntities.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CPhysMagnet::GetTotalMassAttachedObjects( void ) +{ + return m_flTotalMass; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CPhysMagnet::GetAttachedObject( int iIndex ) +{ + Assert( iIndex < GetNumAttachedObjects() ); + + return m_MagnettedEntities[iIndex].hEntity; +} + +class CInfoMassCenter : public CPointEntity +{ + DECLARE_CLASS( CInfoMassCenter, CPointEntity ); +public: + void Spawn( void ) + { + if ( m_target != NULL_STRING ) + { + masscenteroverride_t params; + params.SnapToPoint( m_target, GetAbsOrigin() ); + PhysSetMassCenterOverride( params ); + UTIL_Remove( this ); + } + } +}; +LINK_ENTITY_TO_CLASS( info_mass_center, CInfoMassCenter ); + +// ============================================================= +// point_push +// ============================================================= + +class CPointPush : public CPointEntity +{ +public: + DECLARE_CLASS( CPointPush, CPointEntity ); + + virtual void Activate( void ); + void PushThink( void ); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + inline void PushEntity( CBaseEntity *pTarget ); + + bool m_bEnabled; + float m_flMagnitude; + float m_flRadius; + float m_flInnerRadius; // Inner radius where the push eminates from (on a sphere) +}; + +LINK_ENTITY_TO_CLASS( point_push, CPointPush ); + +BEGIN_DATADESC( CPointPush ) + + DEFINE_THINKFUNC( PushThink ), + + DEFINE_KEYFIELD( m_bEnabled, FIELD_BOOLEAN, "enabled" ), + DEFINE_KEYFIELD( m_flMagnitude, FIELD_FLOAT, "magnitude" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + DEFINE_KEYFIELD( m_flInnerRadius,FIELD_FLOAT, "inner_radius" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + +END_DATADESC(); + +// Spawnflags +#define SF_PUSH_TEST_LOS 0x0001 +#define SF_PUSH_DIRECTIONAL 0x0002 +#define SF_PUSH_NO_FALLOFF 0x0004 +#define SF_PUSH_PLAYER 0x0008 +#define SF_PUSH_PHYSICS 0x0010 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointPush::Activate( void ) +{ + if ( m_bEnabled ) + { + SetThink( &CPointPush::PushThink ); + SetNextThink( gpGlobals->curtime + 0.05f ); + } + + BaseClass::Activate(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pTarget - +//----------------------------------------------------------------------------- +void CPointPush::PushEntity( CBaseEntity *pTarget ) +{ + Vector vecPushDir; + + if ( HasSpawnFlags( SF_PUSH_DIRECTIONAL ) ) + { + GetVectors( &vecPushDir, NULL, NULL ); + } + else + { + vecPushDir = ( pTarget->BodyTarget( GetAbsOrigin(), false ) - GetAbsOrigin() ); + } + + float dist = VectorNormalize( vecPushDir ); + + float flFalloff = ( HasSpawnFlags( SF_PUSH_NO_FALLOFF ) ) ? 1.0f : RemapValClamped( dist, m_flRadius, m_flRadius*0.25f, 0.0f, 1.0f ); + + switch( pTarget->GetMoveType() ) + { + case MOVETYPE_NONE: + case MOVETYPE_PUSH: + case MOVETYPE_NOCLIP: + break; + + case MOVETYPE_VPHYSICS: + { + IPhysicsObject *pPhys = pTarget->VPhysicsGetObject(); + if ( pPhys ) + { + // UNDONE: Assume the velocity is for a 100kg object, scale with mass + pPhys->ApplyForceCenter( m_flMagnitude * flFalloff * 100.0f * vecPushDir * pPhys->GetMass() * gpGlobals->frametime ); + return; + } + } + break; + + case MOVETYPE_STEP: + { + // NPCs cannot be lifted up properly, they need to move in 2D + vecPushDir.z = 0.0f; + + // NOTE: Falls through! + } + + default: + { + Vector vecPush = (m_flMagnitude * vecPushDir * flFalloff); + if ( pTarget->GetFlags() & FL_BASEVELOCITY ) + { + vecPush = vecPush + pTarget->GetBaseVelocity(); + } + if ( vecPush.z > 0 && (pTarget->GetFlags() & FL_ONGROUND) ) + { + pTarget->SetGroundEntity( NULL ); + Vector origin = pTarget->GetAbsOrigin(); + origin.z += 1.0f; + pTarget->SetAbsOrigin( origin ); + } + + pTarget->SetBaseVelocity( vecPush ); + pTarget->AddFlag( FL_BASEVELOCITY ); + } + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointPush::PushThink( void ) +{ + // Get a collection of entities in a radius around us + CBaseEntity *pEnts[256]; + int numEnts = UTIL_EntitiesInSphere( pEnts, 256, GetAbsOrigin(), m_flRadius, 0 ); + for ( int i = 0; i < numEnts; i++ ) + { + // Must be solid + if ( pEnts[i]->IsSolid() == false ) + continue; + + // Cannot be parented (only push parents) + if ( pEnts[i]->GetMoveParent() != NULL ) + continue; + + // Must be moveable + if ( pEnts[i]->GetMoveType() != MOVETYPE_VPHYSICS && + pEnts[i]->GetMoveType() != MOVETYPE_WALK && + pEnts[i]->GetMoveType() != MOVETYPE_STEP ) + continue; + + // If we don't want to push players, don't + if ( pEnts[i]->IsPlayer() && HasSpawnFlags( SF_PUSH_PLAYER ) == false ) + continue; + + // If we don't want to push physics, don't + if ( pEnts[i]->GetMoveType() == MOVETYPE_VPHYSICS && HasSpawnFlags( SF_PUSH_PHYSICS ) == false ) + continue; + + // Test for LOS if asked to + if ( HasSpawnFlags( SF_PUSH_TEST_LOS ) ) + { + Vector vecStartPos = GetAbsOrigin(); + Vector vecEndPos = pEnts[i]->BodyTarget( vecStartPos, false ); + + if ( m_flInnerRadius != 0.0f ) + { + // Find a point on our inner radius sphere to begin from + Vector vecDirToTarget = ( vecEndPos - vecStartPos ); + VectorNormalize( vecDirToTarget ); + vecStartPos = GetAbsOrigin() + ( vecDirToTarget * m_flInnerRadius ); + } + + trace_t tr; + UTIL_TraceLine( vecStartPos, + pEnts[i]->BodyTarget( vecStartPos, false ), + MASK_SOLID_BRUSHONLY, + this, + COLLISION_GROUP_NONE, + &tr ); + + // Shielded + if ( tr.fraction < 1.0f && tr.m_pEnt != pEnts[i] ) + continue; + } + + // Push it along + PushEntity( pEnts[i] ); + } + + // Set us up for the next think + SetNextThink( gpGlobals->curtime + 0.05f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointPush::InputEnable( inputdata_t &inputdata ) +{ + m_bEnabled = true; + SetThink( &CPointPush::PushThink ); + SetNextThink( gpGlobals->curtime + 0.05f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointPush::InputDisable( inputdata_t &inputdata ) +{ + m_bEnabled = false; + SetThink( NULL ); + SetNextThink( gpGlobals->curtime ); +} diff --git a/sp/src/game/server/physobj.h b/sp/src/game/server/physobj.h new file mode 100644 index 00000000..09d333c5 --- /dev/null +++ b/sp/src/game/server/physobj.h @@ -0,0 +1,249 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef PHYSOBJ_H +#define PHYSOBJ_H +#ifdef _WIN32 +#pragma once +#endif + +#ifndef PHYSICS_H +#include "physics.h" +#endif + +#include "entityoutput.h" +#include "func_break.h" +#include "player_pickup.h" + +// --------------------------------------------------------------------- +// +// CPhysBox -- physically simulated brush rectangular solid +// +// --------------------------------------------------------------------- +// Physbox Spawnflags. Start at 0x01000 to avoid collision with CBreakable's +#define SF_PHYSBOX_ASLEEP 0x01000 +#define SF_PHYSBOX_IGNOREUSE 0x02000 +#define SF_PHYSBOX_DEBRIS 0x04000 +#define SF_PHYSBOX_MOTIONDISABLED 0x08000 +#define SF_PHYSBOX_USEPREFERRED 0x10000 +#define SF_PHYSBOX_ENABLE_ON_PHYSCANNON 0x20000 +#define SF_PHYSBOX_NO_ROTORWASH_PUSH 0x40000 // The rotorwash doesn't push these +#define SF_PHYSBOX_ENABLE_PICKUP_OUTPUT 0x80000 +#define SF_PHYSBOX_ALWAYS_PICK_UP 0x100000 // Physcannon can always pick this up, no matter what mass or constraints may apply. +#define SF_PHYSBOX_NEVER_PICK_UP 0x200000 // Physcannon will never be able to pick this up. +#define SF_PHYSBOX_NEVER_PUNT 0x400000 // Physcannon will never be able to punt this object. +#define SF_PHYSBOX_PREVENT_PLAYER_TOUCH_ENABLE 0x800000 // If set, the player will not cause the object to enable its motion when bumped into +#ifdef MAPBASE +#define SF_PHYSBOX_RADIUS_PICKUP 0x1000000 // Allows this object to be picked up in a radius, useful for smaller objects. Based on the prop_physics input +#endif + +// UNDONE: Hook collisions into the physics system to generate touch functions and take damage on falls +// UNDONE: Base class PhysBrush +class CPhysBox : public CBreakable +{ +DECLARE_CLASS( CPhysBox, CBreakable ); + +public: + DECLARE_SERVERCLASS(); + + void Spawn ( void ); + bool CreateVPhysics(); + void Move( const Vector &force ); + virtual int ObjectCaps(); + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + virtual int DrawDebugTextOverlays(void); + + virtual void VPhysicsUpdate( IPhysicsObject *pPhysics ); + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + int OnTakeDamage( const CTakeDamageInfo &info ); + void EnableMotion( void ); + + bool CanBePickedUpByPhyscannon(); + + // IPlayerPickupVPhysics + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ); + + bool HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ); + virtual QAngle PreferredCarryAngles( void ) { return m_angPreferredCarryAngles; } + + // inputs + void InputWake( inputdata_t &inputdata ); + void InputSleep( inputdata_t &inputdata ); + void InputEnableMotion( inputdata_t &inputdata ); + void InputDisableMotion( inputdata_t &inputdata ); + void InputForceDrop( inputdata_t &inputdata ); + void InputDisableFloating( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetDebris( inputdata_t &inputdata ); +#endif + + DECLARE_DATADESC(); + +protected: + int m_damageType; + float m_massScale; + string_t m_iszOverrideScript; + int m_damageToEnableMotion; + float m_flForceToEnableMotion; + QAngle m_angPreferredCarryAngles; + bool m_bNotSolidToWorld; + + // Outputs + COutputEvent m_OnDamaged; + COutputEvent m_OnAwakened; + COutputEvent m_OnMotionEnabled; + COutputEvent m_OnPhysGunPickup; + COutputEvent m_OnPhysGunPunt; + COutputEvent m_OnPhysGunOnlyPickup; + COutputEvent m_OnPhysGunDrop; + COutputEvent m_OnPlayerUse; + + CHandle m_hCarryingPlayer; // Player who's carrying us +}; + +// --------------------------------------------------------------------- +// +// CPhysExplosion -- physically simulated explosion +// +// --------------------------------------------------------------------- +class CPhysExplosion : public CPointEntity +{ +public: + DECLARE_CLASS( CPhysExplosion, CPointEntity ); + + void Spawn ( void ); + void Explode( CBaseEntity *pActivator, CBaseEntity *pCaller ); + + CBaseEntity *FindEntity( CBaseEntity *pEntity, CBaseEntity *pActivator, CBaseEntity *pCaller ); + + int DrawDebugTextOverlays(void); + + // Input handlers + void InputExplode( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputExplodeAndRemove( inputdata_t &inputdata ); +#endif + + DECLARE_DATADESC(); +private: + + float GetRadius( void ); + + float m_damage; + float m_radius; + string_t m_targetEntityName; + float m_flInnerRadius; + + COutputEvent m_OnPushedPlayer; +}; + + +//================================================== +// CPhysImpact +//================================================== + +class CPhysImpact : public CPointEntity +{ +public: + DECLARE_CLASS( CPhysImpact, CPointEntity ); + + void Spawn( void ); + //void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void Activate( void ); + + void InputImpact( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + + void PointAtEntity( void ); + + float m_damage; + float m_distance; + string_t m_directionEntityName; +}; + +//----------------------------------------------------------------------------- +// Purpose: A magnet that creates constraints between itself and anything it touches +//----------------------------------------------------------------------------- + +struct magnetted_objects_t +{ + IPhysicsConstraint *pConstraint; + EHANDLE hEntity; + + DECLARE_SIMPLE_DATADESC(); +}; + +class CPhysMagnet : public CBaseAnimating, public IPhysicsConstraintEvent +{ + DECLARE_CLASS( CPhysMagnet, CBaseAnimating ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CPhysMagnet(); + ~CPhysMagnet(); + + void Spawn( void ); + void Precache( void ); + void Touch( CBaseEntity *pOther ); + void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); +#ifdef MAPBASE + bool CanBePickedUpByPhyscannon( void ); +#endif + void DoMagnetSuck( CBaseEntity *pOther ); + void SetConstraintGroup( IPhysicsConstraintGroup *pGroup ); + + bool IsOn( void ) { return m_bActive; } + int GetNumAttachedObjects( void ); + float GetTotalMassAttachedObjects( void ); + CBaseEntity *GetAttachedObject( int iIndex ); + + // Checking for hitting something + void ResetHasHitSomething( void ) { m_bHasHitSomething = false; } + bool HasHitSomething( void ) { return m_bHasHitSomething; } + + // Inputs + void InputToggle( inputdata_t &inputdata ); + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + + void InputConstraintBroken( inputdata_t &inputdata ); + + void DetachAll( void ); + +// IPhysicsConstraintEvent +public: + void ConstraintBroken( IPhysicsConstraint *pConstraint ); + +protected: + // Outputs + COutputEvent m_OnMagnetAttach; + COutputEvent m_OnMagnetDetach; + + // Keys + float m_massScale; + string_t m_iszOverrideScript; + float m_forceLimit; + float m_torqueLimit; + + CUtlVector< magnetted_objects_t > m_MagnettedEntities; + IPhysicsConstraintGroup *m_pConstraintGroup; + + bool m_bActive; + bool m_bHasHitSomething; + float m_flTotalMass; + float m_flRadius; + float m_flNextSuckTime; + int m_iMaxObjectsAttached; +}; + +#endif // PHYSOBJ_H diff --git a/sp/src/game/server/plasma.cpp b/sp/src/game/server/plasma.cpp new file mode 100644 index 00000000..964cf76f --- /dev/null +++ b/sp/src/game/server/plasma.cpp @@ -0,0 +1,93 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "plasma.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//================================================== +// CPlasma +//================================================== + +//Link the entity +LINK_ENTITY_TO_CLASS( _plasma, CPlasma ); + +//Send datatable +IMPLEMENT_SERVERCLASS_ST( CPlasma, DT_Plasma ) + SendPropFloat( SENDINFO( m_flScale ), 0, SPROP_NOSCALE), + SendPropFloat( SENDINFO( m_flScaleTime ), 0, SPROP_NOSCALE), + SendPropInt( SENDINFO( m_nFlags ), 8, SPROP_UNSIGNED ), + SendPropModelIndex( SENDINFO( m_nPlasmaModelIndex )), + SendPropModelIndex( SENDINFO( m_nPlasmaModelIndex2 )), + SendPropModelIndex( SENDINFO( m_nGlowModelIndex )), +END_SEND_TABLE() + +//Data description +BEGIN_DATADESC( CPlasma ) + + //Client-side + DEFINE_FIELD( m_flScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flScaleTime, FIELD_FLOAT ), + DEFINE_FIELD( m_nFlags, FIELD_INTEGER ), + +// DEFINE_FIELD( m_nPlasmaModelIndex, FIELD_INTEGER ), +// DEFINE_FIELD( m_nPlasmaModelIndex2, FIELD_INTEGER ), +// DEFINE_FIELD( m_nGlowModelIndex, FIELD_INTEGER ), + + //Server-side + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CPlasma::CPlasma( void ) +{ + //Client-side + m_flScale = 0.0f; + m_flScaleTime = 0.0f; + m_nFlags = bitsFIRE_NONE; + m_nPlasmaModelIndex = PrecacheModel( "sprites/plasma1.vmt" ); + m_nPlasmaModelIndex2 = PrecacheModel( "sprites/plasma1.vmt" );//<> + m_nGlowModelIndex = PrecacheModel( "sprites/fire_floor.vmt" ); + //Server-side +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPlasma::~CPlasma( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +void CPlasma::EnableSmoke( int state ) +{ + if ( state ) + { + m_nFlags |= bitsFIRESMOKE_SMOKE; + } + else + { + m_nFlags &= ~bitsFIRESMOKE_SMOKE; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPlasma::Precache( void ) +{ + PrecacheModel( "sprites/plasma1.vmt" ); + PrecacheModel( "sprites/fire_floor.vmt" ); +} diff --git a/sp/src/game/server/plasma.h b/sp/src/game/server/plasma.h new file mode 100644 index 00000000..c723a9a6 --- /dev/null +++ b/sp/src/game/server/plasma.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef __PLASMA__ +#define __PLASMA__ +#pragma once + +#include "fire_smoke.h" + +//================================================== +// CPlasma +//================================================== + +//NOTENOTE: Mirrored in cl_dll/c_plasma.cpp +#define bitsPLASMA_FREE 0x00000002 + +class CPlasma : public CBaseFire +{ +public: + DECLARE_CLASS( CPlasma, CBaseFire ); + + CPlasma( void ); + virtual ~CPlasma( void ); + void EnableSmoke( int state ); + + void Precache( void ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +public: + + //Client-side + CNetworkVar( int, m_nPlasmaModelIndex ); + CNetworkVar( int, m_nPlasmaModelIndex2 ); + CNetworkVar( int, m_nGlowModelIndex ); + + //Server-side +}; + +#endif //__PLASMA__ diff --git a/sp/src/game/server/player.cpp b/sp/src/game/server/player.cpp new file mode 100644 index 00000000..0f26c252 --- /dev/null +++ b/sp/src/game/server/player.cpp @@ -0,0 +1,10148 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Functions dealing with the player. +// +//===========================================================================// + +#include "cbase.h" +#include "const.h" +#include "baseplayer_shared.h" +#include "trains.h" +#include "soundent.h" +#include "gib.h" +#include "shake.h" +#include "decals.h" +#include "gamerules.h" +#include "game.h" +#include "entityapi.h" +#include "entitylist.h" +#include "eventqueue.h" +#include "worldsize.h" +#include "isaverestore.h" +#include "globalstate.h" +#include "basecombatweapon.h" +#include "ai_basenpc.h" +#include "ai_network.h" +#include "ai_node.h" +#include "ai_networkmanager.h" +#include "ammodef.h" +#include "mathlib/mathlib.h" +#include "ndebugoverlay.h" +#include "baseviewmodel.h" +#include "in_buttons.h" +#include "client.h" +#include "team.h" +#include "particle_smokegrenade.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "movehelper_server.h" +#include "igamemovement.h" +#include "saverestoretypes.h" +#include "iservervehicle.h" +#include "movevars_shared.h" +#include "vcollide_parse.h" +#include "player_command.h" +#include "vehicle_base.h" +#include "AI_Criteria.h" +#include "globals.h" +#include "usermessages.h" +#include "gamevars_shared.h" +#include "world.h" +#include "physobj.h" +#include "KeyValues.h" +#include "coordsize.h" +#include "vphysics/player_controller.h" +#include "saverestore_utlvector.h" +#include "hltvdirector.h" +#include "nav_mesh.h" +#include "env_zoom.h" +#include "rumble_shared.h" +#include "gamestats.h" +#include "npcevent.h" +#include "datacache/imdlcache.h" +#include "hintsystem.h" +#include "env_debughistory.h" +#include "fogcontroller.h" +#include "gameinterface.h" +#include "hl2orange.spa.h" +#include "dt_utlvector_send.h" +#include "vote_controller.h" +#include "ai_speech.h" + +#if defined USES_ECON_ITEMS +#include "econ_wearable.h" +#endif + +// NVNT haptic utils +#include "haptics/haptic_utils.h" + +#ifdef HL2_DLL +#include "combine_mine.h" +#include "weapon_physcannon.h" +#ifdef MAPBASE +#include "mapbase/GlobalStrings.h" +#include "mapbase/matchers.h" +#endif +#endif + +#ifdef MAPBASE_VSCRIPT +#include "mapbase/vscript_funcs_shared.h" +#endif + +ConVar autoaim_max_dist( "autoaim_max_dist", "2160" ); // 2160 = 180 feet +ConVar autoaim_max_deflect( "autoaim_max_deflect", "0.99" ); + +#ifdef CSTRIKE_DLL +ConVar spec_freeze_time( "spec_freeze_time", "5.0", FCVAR_CHEAT | FCVAR_REPLICATED, "Time spend frozen in observer freeze cam." ); +ConVar spec_freeze_traveltime( "spec_freeze_traveltime", "0.7", FCVAR_CHEAT | FCVAR_REPLICATED, "Time taken to zoom in to frame a target in observer freeze cam.", true, 0.01, false, 0 ); +#else +ConVar spec_freeze_time( "spec_freeze_time", "4.0", FCVAR_CHEAT | FCVAR_REPLICATED, "Time spend frozen in observer freeze cam." ); +ConVar spec_freeze_traveltime( "spec_freeze_traveltime", "0.4", FCVAR_CHEAT | FCVAR_REPLICATED, "Time taken to zoom in to frame a target in observer freeze cam.", true, 0.01, false, 0 ); +#endif + +ConVar sv_bonus_challenge( "sv_bonus_challenge", "0", FCVAR_REPLICATED, "Set to values other than 0 to select a bonus map challenge type." ); + +static ConVar sv_maxusrcmdprocessticks( "sv_maxusrcmdprocessticks", "24", FCVAR_NOTIFY, "Maximum number of client-issued usrcmd ticks that can be replayed in packet loss conditions, 0 to allow no restrictions" ); + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static ConVar old_armor( "player_old_armor", "0" ); + +static ConVar physicsshadowupdate_render( "physicsshadowupdate_render", "0" ); +bool IsInCommentaryMode( void ); +bool IsListeningToCommentary( void ); + +#if !defined( CSTRIKE_DLL ) +ConVar cl_sidespeed( "cl_sidespeed", "450", FCVAR_REPLICATED | FCVAR_CHEAT ); +ConVar cl_upspeed( "cl_upspeed", "320", FCVAR_REPLICATED | FCVAR_CHEAT ); +ConVar cl_forwardspeed( "cl_forwardspeed", "450", FCVAR_REPLICATED | FCVAR_CHEAT ); +ConVar cl_backspeed( "cl_backspeed", "450", FCVAR_REPLICATED | FCVAR_CHEAT ); +#endif // CSTRIKE_DLL + +// This is declared in the engine, too +ConVar sv_noclipduringpause( "sv_noclipduringpause", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If cheats are enabled, then you can noclip with the game paused (for doing screenshots, etc.)." ); + +extern ConVar sv_maxunlag; +extern ConVar sv_turbophysics; +extern ConVar *sv_maxreplay; + +extern CServerGameDLL g_ServerGameDLL; + +// TIME BASED DAMAGE AMOUNT +// tweak these values based on gameplay feedback: +#define PARALYZE_DURATION 2 // number of 2 second intervals to take damage +#define PARALYZE_DAMAGE 1.0 // damage to take each 2 second interval + +#define NERVEGAS_DURATION 2 +#define NERVEGAS_DAMAGE 5.0 + +#define POISON_DURATION 5 +#define POISON_DAMAGE 2.0 + +#define RADIATION_DURATION 2 +#define RADIATION_DAMAGE 1.0 + +#define ACID_DURATION 2 +#define ACID_DAMAGE 5.0 + +#define SLOWBURN_DURATION 2 +#define SLOWBURN_DAMAGE 1.0 + +#define SLOWFREEZE_DURATION 2 +#define SLOWFREEZE_DAMAGE 1.0 + +//---------------------------------------------------- +// Player Physics Shadow +//---------------------------------------------------- +#define VPHYS_MAX_DISTANCE 2.0 +#define VPHYS_MAX_VEL 10 +#define VPHYS_MAX_DISTSQR (VPHYS_MAX_DISTANCE*VPHYS_MAX_DISTANCE) +#define VPHYS_MAX_VELSQR (VPHYS_MAX_VEL*VPHYS_MAX_VEL) + + +extern bool g_fDrawLines; +int gEvilImpulse101; + +bool gInitHUD = true; + +extern void respawn(CBaseEntity *pEdict, bool fCopyCorpse); +int MapTextureTypeStepType(char chTextureType); +extern void SpawnBlood(Vector vecSpot, const Vector &vecDir, int bloodColor, float flDamage); +extern void AddMultiDamage( const CTakeDamageInfo &info, CBaseEntity *pEntity ); + + +#define CMD_MOSTRECENT 0 + +//#define FLASH_DRAIN_TIME 1.2 //100 units/3 minutes +//#define FLASH_CHARGE_TIME 0.2 // 100 units/20 seconds (seconds per unit) + + +//#define PLAYER_MAX_SAFE_FALL_DIST 20// falling any farther than this many feet will inflict damage +//#define PLAYER_FATAL_FALL_DIST 60// 100% damage inflicted if player falls this many feet +//#define DAMAGE_PER_UNIT_FALLEN (float)( 100 ) / ( ( PLAYER_FATAL_FALL_DIST - PLAYER_MAX_SAFE_FALL_DIST ) * 12 ) +//#define MAX_SAFE_FALL_UNITS ( PLAYER_MAX_SAFE_FALL_DIST * 12 ) + +// player damage adjusters +ConVar sk_player_head( "sk_player_head","2" ); +ConVar sk_player_chest( "sk_player_chest","1" ); +ConVar sk_player_stomach( "sk_player_stomach","1" ); +ConVar sk_player_arm( "sk_player_arm","1" ); +ConVar sk_player_leg( "sk_player_leg","1" ); + +//ConVar player_usercommand_timeout( "player_usercommand_timeout", "10", 0, "After this many seconds without a usercommand from a player, the client is kicked." ); +#ifdef _DEBUG +ConVar sv_player_net_suppress_usercommands( "sv_player_net_suppress_usercommands", "0", FCVAR_CHEAT, "For testing usercommand hacking sideeffects. DO NOT SHIP" ); +#endif // _DEBUG +ConVar sv_player_display_usercommand_errors( "sv_player_display_usercommand_errors", "0", FCVAR_CHEAT, "1 = Display warning when command values are out-of-range. 2 = Spew invalid ranges." ); + +ConVar player_debug_print_damage( "player_debug_print_damage", "0", FCVAR_CHEAT, "When true, print amount and type of all damage received by player to console." ); + +#ifdef MAPBASE +ConVar player_use_visibility_cache( "player_use_visibility_cache", "0", FCVAR_NONE, "Allows the player to use the visibility cache." ); +#endif + + +void CC_GiveCurrentAmmo( void ) +{ + CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); + + if( pPlayer ) + { + CBaseCombatWeapon *pWeapon = pPlayer->GetActiveWeapon(); + + if( pWeapon ) + { + if( pWeapon->UsesPrimaryAmmo() ) + { + int ammoIndex = pWeapon->GetPrimaryAmmoType(); + + if( ammoIndex != -1 ) + { + int giveAmount; + giveAmount = GetAmmoDef()->MaxCarry(ammoIndex); + pPlayer->GiveAmmo( giveAmount, GetAmmoDef()->GetAmmoOfIndex(ammoIndex)->pName ); + } + } + if( pWeapon->UsesSecondaryAmmo() && pWeapon->HasSecondaryAmmo() ) + { + // Give secondary ammo out, as long as the player already has some + // from a presumeably natural source. This prevents players on XBox + // having Combine Balls and so forth in areas of the game that + // were not tested with these items. + int ammoIndex = pWeapon->GetSecondaryAmmoType(); + + if( ammoIndex != -1 ) + { + int giveAmount; + giveAmount = GetAmmoDef()->MaxCarry(ammoIndex); + pPlayer->GiveAmmo( giveAmount, GetAmmoDef()->GetAmmoOfIndex(ammoIndex)->pName ); + } + } + } + } +} +static ConCommand givecurrentammo("givecurrentammo", CC_GiveCurrentAmmo, "Give a supply of ammo for current weapon..\n", FCVAR_CHEAT ); + + +// pl +BEGIN_SIMPLE_DATADESC( CPlayerState ) + // DEFINE_FIELD( netname, FIELD_STRING ), // Don't stomp player name with what's in save/restore + DEFINE_FIELD( v_angle, FIELD_VECTOR ), + DEFINE_FIELD( deadflag, FIELD_BOOLEAN ), + + // this is always set to true on restore, don't bother saving it. + // DEFINE_FIELD( fixangle, FIELD_INTEGER ), + // DEFINE_FIELD( anglechange, FIELD_FLOAT ), + // DEFINE_FIELD( hltv, FIELD_BOOLEAN ), + // DEFINE_FIELD( replay, FIELD_BOOLEAN ), + // DEFINE_FIELD( frags, FIELD_INTEGER ), + // DEFINE_FIELD( deaths, FIELD_INTEGER ), +END_DATADESC() + +// Global Savedata for player +BEGIN_DATADESC( CBasePlayer ) + + DEFINE_EMBEDDED( m_Local ), +#if defined USES_ECON_ITEMS + DEFINE_EMBEDDED( m_AttributeList ), +#endif + DEFINE_UTLVECTOR( m_hTriggerSoundscapeList, FIELD_EHANDLE ), + DEFINE_EMBEDDED( pl ), + + DEFINE_FIELD( m_StuckLast, FIELD_INTEGER ), + + DEFINE_FIELD( m_nButtons, FIELD_INTEGER ), + DEFINE_FIELD( m_afButtonLast, FIELD_INTEGER ), + DEFINE_FIELD( m_afButtonPressed, FIELD_INTEGER ), + DEFINE_FIELD( m_afButtonReleased, FIELD_INTEGER ), + DEFINE_FIELD( m_afButtonDisabled, FIELD_INTEGER ), + DEFINE_FIELD( m_afButtonForced, FIELD_INTEGER ), + + DEFINE_FIELD( m_iFOV, FIELD_INTEGER ), + DEFINE_FIELD( m_iFOVStart, FIELD_INTEGER ), + DEFINE_FIELD( m_flFOVTime, FIELD_TIME ), + DEFINE_FIELD( m_iDefaultFOV,FIELD_INTEGER ), + DEFINE_FIELD( m_flVehicleViewFOV, FIELD_FLOAT ), + + //DEFINE_FIELD( m_fOnTarget, FIELD_BOOLEAN ), // Don't need to restore + DEFINE_FIELD( m_iObserverMode, FIELD_INTEGER ), + DEFINE_FIELD( m_iObserverLastMode, FIELD_INTEGER ), + DEFINE_FIELD( m_hObserverTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_bForcedObserverMode, FIELD_BOOLEAN ), + DEFINE_AUTO_ARRAY( m_szAnimExtension, FIELD_CHARACTER ), +// DEFINE_CUSTOM_FIELD( m_Activity, ActivityDataOps() ), + + DEFINE_FIELD( m_nUpdateRate, FIELD_INTEGER ), + DEFINE_FIELD( m_fLerpTime, FIELD_FLOAT ), + DEFINE_FIELD( m_bLagCompensation, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bPredictWeapons, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_vecAdditionalPVSOrigin, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecCameraPVSOrigin, FIELD_POSITION_VECTOR ), + + DEFINE_FIELD( m_hUseEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_iTrain, FIELD_INTEGER ), + DEFINE_FIELD( m_iRespawnFrames, FIELD_FLOAT ), + DEFINE_FIELD( m_afPhysicsFlags, FIELD_INTEGER ), + DEFINE_FIELD( m_hVehicle, FIELD_EHANDLE ), + + // recreate, don't restore + // DEFINE_FIELD( m_CommandContext, CUtlVector < CCommandContext > ), + //DEFINE_FIELD( m_pPhysicsController, FIELD_POINTER ), + //DEFINE_FIELD( m_pShadowStand, FIELD_POINTER ), + //DEFINE_FIELD( m_pShadowCrouch, FIELD_POINTER ), + //DEFINE_FIELD( m_vphysicsCollisionState, FIELD_INTEGER ), + DEFINE_ARRAY( m_szNetworkIDString, FIELD_CHARACTER, MAX_NETWORKID_LENGTH ), + DEFINE_FIELD( m_oldOrigin, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecSmoothedVelocity, FIELD_VECTOR ), + //DEFINE_FIELD( m_touchedPhysObject, FIELD_BOOLEAN ), + //DEFINE_FIELD( m_bPhysicsWasFrozen, FIELD_BOOLEAN ), + //DEFINE_FIELD( m_iPlayerSound, FIELD_INTEGER ), // Don't restore, set in Precache() + DEFINE_FIELD( m_iTargetVolume, FIELD_INTEGER ), + DEFINE_AUTO_ARRAY( m_rgItems, FIELD_INTEGER ), + //DEFINE_FIELD( m_fNextSuicideTime, FIELD_TIME ), + // DEFINE_FIELD( m_PlayerInfo, CPlayerInfo ), + + DEFINE_FIELD( m_flSwimTime, FIELD_TIME ), + DEFINE_FIELD( m_flDuckTime, FIELD_TIME ), + DEFINE_FIELD( m_flDuckJumpTime, FIELD_TIME ), + + DEFINE_FIELD( m_flSuitUpdate, FIELD_TIME ), + DEFINE_AUTO_ARRAY( m_rgSuitPlayList, FIELD_INTEGER ), + DEFINE_FIELD( m_iSuitPlayNext, FIELD_INTEGER ), + DEFINE_AUTO_ARRAY( m_rgiSuitNoRepeat, FIELD_INTEGER ), + DEFINE_AUTO_ARRAY( m_rgflSuitNoRepeatTime, FIELD_TIME ), + DEFINE_FIELD( m_bPauseBonusProgress, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iBonusProgress, FIELD_INTEGER ), + DEFINE_FIELD( m_iBonusChallenge, FIELD_INTEGER ), + DEFINE_FIELD( m_lastDamageAmount, FIELD_INTEGER ), + DEFINE_FIELD( m_tbdPrev, FIELD_TIME ), + DEFINE_FIELD( m_flStepSoundTime, FIELD_FLOAT ), + DEFINE_ARRAY( m_szNetname, FIELD_CHARACTER, MAX_PLAYER_NAME_LENGTH ), + + //DEFINE_FIELD( m_flgeigerRange, FIELD_FLOAT ), // Don't restore, reset in Precache() + //DEFINE_FIELD( m_flgeigerDelay, FIELD_FLOAT ), // Don't restore, reset in Precache() + //DEFINE_FIELD( m_igeigerRangePrev, FIELD_FLOAT ), // Don't restore, reset in Precache() + //DEFINE_FIELD( m_iStepLeft, FIELD_INTEGER ), // Don't need to restore + //DEFINE_FIELD( m_chTextureType, FIELD_CHARACTER ), // Don't need to restore + //DEFINE_FIELD( m_surfaceProps, FIELD_INTEGER ), // don't need to restore, reset by gamemovement + // DEFINE_FIELD( m_pSurfaceData, surfacedata_t* ), + //DEFINE_FIELD( m_surfaceFriction, FIELD_FLOAT ), + //DEFINE_FIELD( m_chPreviousTextureType, FIELD_CHARACTER ), + + DEFINE_FIELD( m_idrowndmg, FIELD_INTEGER ), + DEFINE_FIELD( m_idrownrestored, FIELD_INTEGER ), + + DEFINE_FIELD( m_nPoisonDmg, FIELD_INTEGER ), + DEFINE_FIELD( m_nPoisonRestored, FIELD_INTEGER ), + + DEFINE_FIELD( m_bitsHUDDamage, FIELD_INTEGER ), + DEFINE_FIELD( m_fInitHUD, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flDeathTime, FIELD_TIME ), + DEFINE_FIELD( m_flDeathAnimTime, FIELD_TIME ), + + //DEFINE_FIELD( m_fGameHUDInitialized, FIELD_BOOLEAN ), // only used in multiplayer games + //DEFINE_FIELD( m_fWeapon, FIELD_BOOLEAN ), // Don't restore, client needs reset + //DEFINE_FIELD( m_iUpdateTime, FIELD_INTEGER ), // Don't need to restore + //DEFINE_FIELD( m_iClientBattery, FIELD_INTEGER ), // Don't restore, client needs reset + //DEFINE_FIELD( m_iClientHideHUD, FIELD_INTEGER ), // Don't restore, client needs reset + //DEFINE_FIELD( m_vecAutoAim, FIELD_VECTOR ), // Don't save/restore - this is recomputed + //DEFINE_FIELD( m_lastx, FIELD_INTEGER ), + //DEFINE_FIELD( m_lasty, FIELD_INTEGER ), + + DEFINE_FIELD( m_iFrags, FIELD_INTEGER ), + DEFINE_FIELD( m_iDeaths, FIELD_INTEGER ), + DEFINE_FIELD( m_bAllowInstantSpawn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flNextDecalTime, FIELD_TIME ), + //DEFINE_AUTO_ARRAY( m_szTeamName, FIELD_STRING ), // mp + + //DEFINE_FIELD( m_iConnected, FIELD_INTEGER ), + // from edict_t + DEFINE_FIELD( m_ArmorValue, FIELD_INTEGER ), + DEFINE_FIELD( m_DmgOrigin, FIELD_VECTOR ), + DEFINE_FIELD( m_DmgTake, FIELD_FLOAT ), + DEFINE_FIELD( m_DmgSave, FIELD_FLOAT ), + DEFINE_FIELD( m_AirFinished, FIELD_TIME ), + DEFINE_FIELD( m_PainFinished, FIELD_TIME ), + + DEFINE_FIELD( m_iPlayerLocked, FIELD_INTEGER ), + + DEFINE_AUTO_ARRAY( m_hViewModel, FIELD_EHANDLE ), + + DEFINE_FIELD( m_flMaxspeed, FIELD_FLOAT ), + DEFINE_FIELD( m_flWaterJumpTime, FIELD_TIME ), + DEFINE_FIELD( m_vecWaterJumpVel, FIELD_VECTOR ), + DEFINE_FIELD( m_nImpulse, FIELD_INTEGER ), + DEFINE_FIELD( m_flSwimSoundTime, FIELD_TIME ), + DEFINE_FIELD( m_vecLadderNormal, FIELD_VECTOR ), + + DEFINE_FIELD( m_flFlashTime, FIELD_TIME ), + DEFINE_FIELD( m_nDrownDmgRate, FIELD_INTEGER ), + DEFINE_FIELD( m_iSuicideCustomKillFlags, FIELD_INTEGER ), + + // NOT SAVED + //DEFINE_FIELD( m_vForcedOrigin, FIELD_VECTOR ), + //DEFINE_FIELD( m_bForceOrigin, FIELD_BOOLEAN ), + //DEFINE_FIELD( m_nTickBase, FIELD_INTEGER ), + //DEFINE_FIELD( m_LastCmd, FIELD_ ), + // DEFINE_FIELD( m_pCurrentCommand, CUserCmd ), + //DEFINE_FIELD( m_bGamePaused, FIELD_BOOLEAN ), + // DEFINE_FIELD( m_iVehicleAnalogBias, FIELD_INTEGER ), + + // m_flVehicleViewFOV + // m_vecVehicleViewOrigin + // m_vecVehicleViewAngles + // m_nVehicleViewSavedFrame + + DEFINE_FIELD( m_bitsDamageType, FIELD_INTEGER ), + DEFINE_AUTO_ARRAY( m_rgbTimeBasedDamage, FIELD_CHARACTER ), + DEFINE_FIELD( m_fLastPlayerTalkTime, FIELD_FLOAT ), + DEFINE_FIELD( m_hLastWeapon, FIELD_EHANDLE ), + +#if !defined( NO_ENTITY_PREDICTION ) + // DEFINE_FIELD( m_SimulatedByThisPlayer, CUtlVector < CHandle < CBaseEntity > > ), +#endif + + DEFINE_FIELD( m_flOldPlayerZ, FIELD_FLOAT ), + DEFINE_FIELD( m_flOldPlayerViewOffsetZ, FIELD_FLOAT ), + DEFINE_FIELD( m_bPlayerUnderwater, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hViewEntity, FIELD_EHANDLE ), + + DEFINE_FIELD( m_hConstraintEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_vecConstraintCenter, FIELD_VECTOR ), + DEFINE_FIELD( m_flConstraintRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_flConstraintWidth, FIELD_FLOAT ), + DEFINE_FIELD( m_flConstraintSpeedFactor, FIELD_FLOAT ), + DEFINE_FIELD( m_hZoomOwner, FIELD_EHANDLE ), + + DEFINE_FIELD( m_flLaggedMovementValue, FIELD_FLOAT ), + + DEFINE_FIELD( m_vNewVPhysicsPosition, FIELD_VECTOR ), + DEFINE_FIELD( m_vNewVPhysicsVelocity, FIELD_VECTOR ), + + DEFINE_FIELD( m_bSinglePlayerGameEnding, FIELD_BOOLEAN ), + DEFINE_ARRAY( m_szLastPlaceName, FIELD_CHARACTER, MAX_PLACE_NAME_LENGTH ), + + DEFINE_FIELD( m_autoKickDisabled, FIELD_BOOLEAN ), + +#ifdef MAPBASE + DEFINE_FIELD( m_bInTriggerFall, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_bDrawPlayerModelExternally, FIELD_BOOLEAN ), +#endif + + // Function Pointers + DEFINE_FUNCTION( PlayerDeathThink ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetHUDVisibility", InputSetHUDVisibility ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetFogController", InputSetFogController ), + DEFINE_INPUTFUNC( FIELD_INPUT, "SetPostProcessController", InputSetPostProcessController ), + DEFINE_INPUTFUNC( FIELD_STRING, "HandleMapEvent", InputHandleMapEvent ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetSuppressAttacks", InputSetSuppressAttacks ), +#endif + + DEFINE_FIELD( m_nNumCrouches, FIELD_INTEGER ), + DEFINE_FIELD( m_bDuckToggled, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flForwardMove, FIELD_FLOAT ), + DEFINE_FIELD( m_flSideMove, FIELD_FLOAT ), + DEFINE_FIELD( m_vecPreviouslyPredictedOrigin, FIELD_POSITION_VECTOR ), + + DEFINE_FIELD( m_nNumCrateHudHints, FIELD_INTEGER ), + + DEFINE_FIELD( m_hPostProcessCtrl, FIELD_EHANDLE ), + + + + // DEFINE_FIELD( m_nBodyPitchPoseParam, FIELD_INTEGER ), + // DEFINE_ARRAY( m_StepSoundCache, StepSoundCache_t, 2 ), + + // DEFINE_UTLVECTOR( m_vecPlayerCmdInfo ), + // DEFINE_UTLVECTOR( m_vecPlayerSimInfo ), +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +// TODO: Better placement? +ScriptHook_t g_Hook_PlayerRunCommand; + +BEGIN_ENT_SCRIPTDESC( CBasePlayer, CBaseCombatCharacter, "The player entity." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptIsPlayerNoclipping, "IsNoclipping", "Returns true if the player is in noclip mode." ) + + DEFINE_SCRIPTFUNC_NAMED( VScriptGetExpresser, "GetExpresser", "Gets a handle for this player's expresser." ) + + DEFINE_SCRIPTFUNC( GetPlayerName, "Gets the player's name." ) + DEFINE_SCRIPTFUNC( GetUserID, "Gets the player's user ID." ) + DEFINE_SCRIPTFUNC_NAMED( GetUserID, "GetPlayerUserId", SCRIPT_HIDE ) + DEFINE_SCRIPTFUNC( GetNetworkIDString, "Gets the player's network (i.e. Steam) ID." ) + + DEFINE_SCRIPTFUNC( FragCount, "Gets the number of frags (kills) this player has in a multiplayer game." ) + DEFINE_SCRIPTFUNC( DeathCount, "Gets the number of deaths this player has had in a multiplayer game." ) + DEFINE_SCRIPTFUNC( IsConnected, "Returns true if this player is connected." ) + DEFINE_SCRIPTFUNC( IsDisconnecting, "Returns true if this player is disconnecting." ) + DEFINE_SCRIPTFUNC( IsSuitEquipped, "Returns true if this player had the HEV suit equipped." ) + + DEFINE_SCRIPTFUNC_NAMED( ArmorValue, "GetArmor", "Gets the player's armor." ) + DEFINE_SCRIPTFUNC_NAMED( SetArmorValue, "SetArmor", "Sets the player's armor." ) + + DEFINE_SCRIPTFUNC( FlashlightIsOn, "Returns true if the flashlight is on." ) + DEFINE_SCRIPTFUNC( FlashlightTurnOn, "Turns on the flashlight." ) + DEFINE_SCRIPTFUNC( FlashlightTurnOff, "Turns off the flashlight." ) + + DEFINE_SCRIPTFUNC( DisableButtons, "Disables the specified button mask." ) + DEFINE_SCRIPTFUNC( EnableButtons, "Enables the specified button mask if it was disabled before." ) + DEFINE_SCRIPTFUNC( ForceButtons, "Forces the specified button mask." ) + DEFINE_SCRIPTFUNC( UnforceButtons, "Unforces the specified button mask if it was forced before." ) + + DEFINE_SCRIPTFUNC( GetButtons, "Gets the player's active buttons." ) + DEFINE_SCRIPTFUNC( GetButtonPressed, "Gets the player's currently pressed buttons." ) + DEFINE_SCRIPTFUNC( GetButtonReleased, "Gets the player's just-released buttons." ) + DEFINE_SCRIPTFUNC( GetButtonLast, "Gets the player's previously active buttons." ) + DEFINE_SCRIPTFUNC( GetButtonDisabled, "Gets the player's currently unusable buttons." ) + DEFINE_SCRIPTFUNC( GetButtonForced, "Gets the player's currently forced buttons." ) + + DEFINE_SCRIPTFUNC( GetFOV, "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetFOVOwner, "GetFOVOwner", "Gets current view owner." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetFOV, "SetFOV", "Sets player FOV regardless of view owner." ) + + DEFINE_SCRIPTFUNC( ViewPunch, "Punches the player's view with the specified vector." ) + DEFINE_SCRIPTFUNC( SetMuzzleFlashTime, "Sets the player's muzzle flash time for AI." ) + DEFINE_SCRIPTFUNC( SetSuitUpdate, "Sets an update for the player's HEV suit." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetAutoaimVector, "GetAutoaimVector", "Gets the player's autoaim shooting direction with the specified scale." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetAutoaimVectorCustomMaxDist, "GetAutoaimVectorCustomMaxDist", "Gets the player's autoaim shooting direction with the specified scale and a custom max distance." ) + DEFINE_SCRIPTFUNC( ShouldAutoaim, "Returns true if the player should be autoaiming." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetEyeForward, "GetEyeForward", "Gets the player's forward eye vector." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetEyeRight, "GetEyeRight", "Gets the player's right eye vector." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetEyeUp, "GetEyeUp", "Gets the player's up eye vector." ) + + // + // Hooks + // + BEGIN_SCRIPTHOOK( g_Hook_PlayerRunCommand, "PlayerRunCommand", FIELD_VOID, "Called when running a player command on the server." ) + DEFINE_SCRIPTHOOK_PARAM( "command", FIELD_HSCRIPT ) + END_SCRIPTHOOK() + +END_SCRIPTDESC(); +#else +BEGIN_ENT_SCRIPTDESC( CBasePlayer, CBaseAnimating, "The player entity." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptIsPlayerNoclipping, "IsNoclipping", "Returns true if the player is in noclip mode." ) +END_SCRIPTDESC(); +#endif + +int giPrecacheGrunt = 0; + +edict_t *CBasePlayer::s_PlayerEdict = NULL; + + +inline bool ShouldRunCommandsInContext( const CCommandContext *ctx ) +{ + // TODO: This should be enabled at some point. If usercmds can run while paused, then + // they can create entities which will never die and it will fill up the entity list. +#ifdef NO_USERCMDS_DURING_PAUSE + return !ctx->paused || sv_noclipduringpause.GetInt(); +#else + return true; +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : CBaseViewModel +//----------------------------------------------------------------------------- +CBaseViewModel *CBasePlayer::GetViewModel( int index /*= 0*/, bool bObserverOK ) +{ + Assert( index >= 0 && index < MAX_VIEWMODELS ); + return m_hViewModel[ index ].Get(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::CreateViewModel( int index /*=0*/ ) +{ + Assert( index >= 0 && index < MAX_VIEWMODELS ); + + if ( GetViewModel( index ) ) + return; + + CBaseViewModel *vm = ( CBaseViewModel * )CreateEntityByName( "viewmodel" ); + if ( vm ) + { + vm->SetAbsOrigin( GetAbsOrigin() ); + vm->SetOwner( this ); + vm->SetIndex( index ); + DispatchSpawn( vm ); + vm->FollowEntity( this ); + m_hViewModel.Set( index, vm ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::DestroyViewModels( void ) +{ + int i; + for ( i = MAX_VIEWMODELS - 1; i >= 0; i-- ) + { + CBaseViewModel *vm = GetViewModel( i ); + if ( !vm ) + continue; + + UTIL_Remove( vm ); + m_hViewModel.Set( i, NULL ); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::CreateHandModel(int index, int iOtherVm) +{ + Assert(index >= 0 && index < MAX_VIEWMODELS && iOtherVm >= 0 && iOtherVm < MAX_VIEWMODELS ); + + if (GetViewModel(index)) + return; + + CBaseViewModel *vm = (CBaseViewModel *)CreateEntityByName("hand_viewmodel"); + if (vm) + { + vm->SetAbsOrigin(GetAbsOrigin()); + vm->SetOwner(this); + vm->SetIndex(index); + DispatchSpawn(vm); + vm->FollowEntity(GetViewModel(iOtherVm), true); + m_hViewModel.Set(index, vm); + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Static member function to create a player of the specified class +// Input : *className - +// *ed - +// Output : CBasePlayer +//----------------------------------------------------------------------------- +CBasePlayer *CBasePlayer::CreatePlayer( const char *className, edict_t *ed ) +{ + CBasePlayer *player; + CBasePlayer::s_PlayerEdict = ed; + player = ( CBasePlayer * )CreateEntityByName( className ); + return player; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +CBasePlayer::CBasePlayer( ) +{ + AddEFlags( EFL_NO_AUTO_EDICT_ATTACH ); + +#ifdef _DEBUG + m_vecAutoAim.Init(); + m_vecAdditionalPVSOrigin.Init(); + m_vecCameraPVSOrigin.Init(); + m_DmgOrigin.Init(); + m_vecLadderNormal.Init(); + + m_oldOrigin.Init(); + m_vecSmoothedVelocity.Init(); +#endif + + if ( s_PlayerEdict ) + { + // take the assigned edict_t and attach it + Assert( s_PlayerEdict != NULL ); + NetworkProp()->AttachEdict( s_PlayerEdict ); + s_PlayerEdict = NULL; + } + + m_flFlashTime = -1; + pl.fixangle = FIXANGLE_ABSOLUTE; + pl.hltv = false; + pl.replay = false; + pl.frags = 0; + pl.deaths = 0; + + m_szNetname[0] = '\0'; + + m_iHealth = 0; + Weapon_SetLast( NULL ); + m_bitsDamageType = 0; + + m_bForceOrigin = false; + m_hVehicle = NULL; + m_pCurrentCommand = NULL; + + // Setup our default FOV + m_iDefaultFOV = g_pGameRules->DefaultFOV(); + + m_hZoomOwner = NULL; + + m_nUpdateRate = 20; // cl_updaterate defualt + m_fLerpTime = 0.1f; // cl_interp default + m_bPredictWeapons = true; + m_bLagCompensation = false; + m_flLaggedMovementValue = 1.0f; + m_StuckLast = 0; + m_impactEnergyScale = 1.0f; + m_fLastPlayerTalkTime = 0.0f; + m_PlayerInfo.SetParent( this ); + + ResetObserverMode(); + + m_surfaceProps = 0; + m_pSurfaceData = NULL; + m_surfaceFriction = 1.0f; + m_chTextureType = 0; + m_chPreviousTextureType = 0; + + m_iSuicideCustomKillFlags = 0; + m_fDelay = 0.0f; + m_fReplayEnd = -1; + m_iReplayEntity = 0; + + m_autoKickDisabled = false; + + m_nNumCrouches = 0; + m_bDuckToggled = false; + m_bPhysicsWasFrozen = false; + + // Used to mask off buttons + m_afButtonDisabled = 0; + m_afButtonForced = 0; + + m_nBodyPitchPoseParam = -1; + m_flForwardMove = 0; + m_flSideMove = 0; + + // NVNT default to no haptics + m_bhasHaptics = false; + + m_vecConstraintCenter = vec3_origin; + + m_flLastUserCommandTime = 0.f; + m_flMovementTimeForUserCmdProcessingRemaining = 0.0f; + + m_hPostProcessCtrl.Set( NULL ); +} + +CBasePlayer::~CBasePlayer( ) +{ + VPhysicsDestroyObject(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CBasePlayer::UpdateOnRemove( void ) +{ + if ( !g_pGameRules->IsMultiplayer() && g_pScriptVM ) + { + g_pScriptVM->SetValue( "player", SCRIPT_VARIANT_NULL ); + } + + VPhysicsDestroyObject(); + + // Remove him from his current team + if ( GetTeam() ) + { + GetTeam()->RemovePlayer( this ); + } + + // Chain at end to mimic destructor unwind order + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : **pvs - +// **pas - +//----------------------------------------------------------------------------- +void CBasePlayer::SetupVisibility( CBaseEntity *pViewEntity, unsigned char *pvs, int pvssize ) +{ + // If we have a viewentity, we don't add the player's origin. + if ( pViewEntity ) + return; + + Vector org; + org = EyePosition(); + + engine->AddOriginToPVS( org ); +} + +int CBasePlayer::UpdateTransmitState() +{ + // always call ShouldTransmit() for players + return SetTransmitState( FL_EDICT_FULLCHECK ); +} + +int CBasePlayer::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + // Allow me to introduce myself to, err, myself. + // I.e., always update the recipient player data even if it's nodraw (first person mode) + if ( pInfo->m_pClientEnt == edict() ) + { + return FL_EDICT_ALWAYS; + } + + // when HLTV/Replay is connected and spectators press +USE, they + // signal that they are recording a interesting scene + // so transmit these 'cameramans' to the HLTV or Replay client + if ( HLTVDirector()->GetCameraMan() == entindex() ) + { + CBaseEntity *pRecipientEntity = CBaseEntity::Instance( pInfo->m_pClientEnt ); + + Assert( pRecipientEntity->IsPlayer() ); + + CBasePlayer *pRecipientPlayer = static_cast( pRecipientEntity ); + if ( pRecipientPlayer->IsHLTV() || + pRecipientPlayer->IsReplay() ) + { + // HACK force calling RecomputePVSInformation to update PVS data + NetworkProp()->AreaNum(); + return FL_EDICT_ALWAYS; + } + } + + // Transmit for a short time after death and our death anim finishes so ragdolls can access reliable player data. + // Note that if m_flDeathAnimTime is never set, as long as m_lifeState is set to LIFE_DEAD after dying, this + // test will act as if the death anim is finished. + if ( IsEffectActive( EF_NODRAW ) || ( IsObserver() && ( gpGlobals->curtime - m_flDeathTime > 0.5 ) && + ( m_lifeState == LIFE_DEAD ) && ( gpGlobals->curtime - m_flDeathAnimTime > 0.5 ) ) ) + { + return FL_EDICT_DONTSEND; + } + + return BaseClass::ShouldTransmit( pInfo ); +} + + +bool CBasePlayer::WantsLagCompensationOnEntity( const CBasePlayer *pPlayer, const CUserCmd *pCmd, const CBitVec *pEntityTransmitBits ) const +{ + //Tony; only check teams in teamplay + if ( gpGlobals->teamplay ) + { + // Team members shouldn't be adjusted unless friendly fire is on. + if ( !friendlyfire.GetInt() && pPlayer->GetTeamNumber() == GetTeamNumber() ) + return false; + } + + // If this entity hasn't been transmitted to us and acked, then don't bother lag compensating it. + if ( pEntityTransmitBits && !pEntityTransmitBits->Get( pPlayer->entindex() ) ) + return false; + + const Vector &vMyOrigin = GetAbsOrigin(); + const Vector &vHisOrigin = pPlayer->GetAbsOrigin(); + + // get max distance player could have moved within max lag compensation time, + // multiply by 1.5 to to avoid "dead zones" (sqrt(2) would be the exact value) + float maxDistance = 1.5 * pPlayer->MaxSpeed() * sv_maxunlag.GetFloat(); + + // If the player is within this distance, lag compensate them in case they're running past us. + if ( vHisOrigin.DistTo( vMyOrigin ) < maxDistance ) + return true; + + // If their origin is not within a 45 degree cone in front of us, no need to lag compensate. + Vector vForward; + AngleVectors( pCmd->viewangles, &vForward ); + + Vector vDiff = vHisOrigin - vMyOrigin; + VectorNormalize( vDiff ); + + float flCosAngle = 0.707107f; // 45 degree angle + if ( vForward.Dot( vDiff ) < flCosAngle ) + return false; + + return true; +} + +void CBasePlayer::PauseBonusProgress( bool bPause ) +{ + m_bPauseBonusProgress = bPause; +} + +void CBasePlayer::SetBonusProgress( int iBonusProgress ) +{ + if ( !m_bPauseBonusProgress ) + m_iBonusProgress = iBonusProgress; +} + +void CBasePlayer::SetBonusChallenge( int iBonusChallenge ) +{ + m_iBonusChallenge = iBonusChallenge; +} + + +//----------------------------------------------------------------------------- +// Sets the view angles +//----------------------------------------------------------------------------- +void CBasePlayer::SnapEyeAngles( const QAngle &viewAngles ) +{ + pl.v_angle = viewAngles; + pl.fixangle = FIXANGLE_ABSOLUTE; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : iSpeed - +// iMax - +// Output : int +//----------------------------------------------------------------------------- +int TrainSpeed(int iSpeed, int iMax) +{ + float fSpeed, fMax; + int iRet = 0; + + fMax = (float)iMax; + fSpeed = iSpeed; + + fSpeed = fSpeed/fMax; + + if (iSpeed < 0) + iRet = TRAIN_BACK; + else if (iSpeed == 0) + iRet = TRAIN_NEUTRAL; + else if (fSpeed < 0.33) + iRet = TRAIN_SLOW; + else if (fSpeed < 0.66) + iRet = TRAIN_MEDIUM; + else + iRet = TRAIN_FAST; + + return iRet; +} + +void CBasePlayer::DeathSound( const CTakeDamageInfo &info ) +{ + // temporarily using pain sounds for death sounds + + // Did we die from falling? + if ( m_bitsDamageType & DMG_FALL ) + { + // They died in the fall. Play a splat sound. + EmitSound( "Player.FallGib" ); + } + else + { + EmitSound( "Player.Death" ); + } + + // play one of the suit death alarms + if ( IsSuitEquipped() ) + { + UTIL_EmitGroupnameSuit(edict(), "HEV_DEAD"); + } +} + +// override takehealth +// bitsDamageType indicates type of damage healed. + +int CBasePlayer::TakeHealth( float flHealth, int bitsDamageType ) +{ + // clear out any damage types we healed. + // UNDONE: generic health should not heal any + // UNDONE: time-based damage + if (m_takedamage) + { + int bitsDmgTimeBased = g_pGameRules->Damage_GetTimeBased(); + m_bitsDamageType &= ~( bitsDamageType & ~bitsDmgTimeBased ); + } + + // I disabled reporting history into the dbghist because it was super spammy. + // But, if you need to reenable it, the code is below in the "else" clause. +#if 1 // #ifdef DISABLE_DEBUG_HISTORY + return BaseClass::TakeHealth (flHealth, bitsDamageType); +#else + const int healingTaken = BaseClass::TakeHealth(flHealth,bitsDamageType); + char buf[256]; + Q_snprintf(buf, 256, "[%f] Player %s healed for %d with damagetype %X\n", gpGlobals->curtime, GetDebugName(), healingTaken, bitsDamageType); + ADD_DEBUG_HISTORY( HISTORY_PLAYER_DAMAGE, buf ); + + return healingTaken; +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Draw all overlays (should be implemented in cascade by subclass to add +// any additional non-text overlays) +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +void CBasePlayer::DrawDebugGeometryOverlays(void) +{ + // -------------------------------------------------------- + // If in buddha mode and dead draw lines to indicate death + // -------------------------------------------------------- + if ((m_debugOverlays & OVERLAY_BUDDHA_MODE) && m_iHealth == 1) + { + Vector vBodyDir = BodyDirection2D( ); + Vector eyePos = EyePosition() + vBodyDir*10.0; + Vector vUp = Vector(0,0,8); + Vector vSide; + CrossProduct( vBodyDir, vUp, vSide); + NDebugOverlay::Line(eyePos+vSide+vUp, eyePos-vSide-vUp, 255,0,0, false, 0); + NDebugOverlay::Line(eyePos+vSide-vUp, eyePos-vSide+vUp, 255,0,0, false, 0); + } + BaseClass::DrawDebugGeometryOverlays(); +} + +//========================================================= +// TraceAttack +//========================================================= +void CBasePlayer::TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + if ( m_takedamage ) + { + CTakeDamageInfo info = inputInfo; + + if ( info.GetAttacker() ) + { + // -------------------------------------------------- + // If an NPC check if friendly fire is disallowed + // -------------------------------------------------- + CAI_BaseNPC *pNPC = info.GetAttacker()->MyNPCPointer(); +#ifdef MAPBASE + if ( pNPC && (pNPC->CapabilitiesGet() & bits_CAP_NO_HIT_PLAYER) && pNPC->IRelationType( this ) > D_FR ) +#else + if ( pNPC && (pNPC->CapabilitiesGet() & bits_CAP_NO_HIT_PLAYER) && pNPC->IRelationType( this ) != D_HT ) +#endif + return; + + // Prevent team damage here so blood doesn't appear + if ( info.GetAttacker()->IsPlayer() ) + { + if ( !g_pGameRules->FPlayerCanTakeDamage( this, info.GetAttacker(), info ) ) + return; + } + } + + SetLastHitGroup( ptr->hitgroup ); + + + switch ( ptr->hitgroup ) + { + case HITGROUP_GENERIC: + break; + case HITGROUP_HEAD: + info.ScaleDamage( sk_player_head.GetFloat() ); + break; + case HITGROUP_CHEST: + info.ScaleDamage( sk_player_chest.GetFloat() ); + break; + case HITGROUP_STOMACH: + info.ScaleDamage( sk_player_stomach.GetFloat() ); + break; + case HITGROUP_LEFTARM: + case HITGROUP_RIGHTARM: + info.ScaleDamage( sk_player_arm.GetFloat() ); + break; + case HITGROUP_LEFTLEG: + case HITGROUP_RIGHTLEG: + info.ScaleDamage( sk_player_leg.GetFloat() ); + break; + default: + break; + } + +#ifdef MAPBASE + + // Damage filter bleed control needs to exist on all DLLs + bool bShouldBleed = +#ifdef HL2_EPISODIC + !g_pGameRules->Damage_ShouldNotBleed( info.GetDamageType() ) && +#endif + DamageFilterAllowsBlood( info ); + + if ( bShouldBleed ) +#else +#ifdef HL2_EPISODIC + // If this damage type makes us bleed, then do so + bool bShouldBleed = !g_pGameRules->Damage_ShouldNotBleed( info.GetDamageType() ); + if ( bShouldBleed ) +#endif +#endif + { + SpawnBlood(ptr->endpos, vecDir, BloodColor(), info.GetDamage());// a little surface blood. + TraceBleed( info.GetDamage(), vecDir, ptr, info.GetDamageType() ); + } + + AddMultiDamage( info, this ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : Do some kind of damage effect for the type of damage +// Input : +// Output : +//------------------------------------------------------------------------------ +void CBasePlayer::DamageEffect(float flDamage, int fDamageType) +{ + if (fDamageType & DMG_CRUSH) + { + //Red damage indicator + color32 red = {128,0,0,128}; + UTIL_ScreenFade( this, red, 1.0f, 0.1f, FFADE_IN ); + } + else if (fDamageType & DMG_DROWN) + { + //Red damage indicator + color32 blue = {0,0,128,128}; + UTIL_ScreenFade( this, blue, 1.0f, 0.1f, FFADE_IN ); + } + else if (fDamageType & DMG_SLASH) + { + // If slash damage shoot some blood + SpawnBlood(EyePosition(), g_vecAttackDir, BloodColor(), flDamage); + } + else if (fDamageType & DMG_PLASMA) + { + // Blue screen fade + color32 blue = {0,0,255,100}; + UTIL_ScreenFade( this, blue, 0.2, 0.4, FFADE_MODULATE ); + + // Very small screen shake + // Both -0.1 and 0.1 map to 0 when converted to integer, so all of these RandomInt + // calls are just expensive ways of returning zero. This code has always been this + // way and has never had any value. clang complains about the conversion from a + // literal floating-point number to an integer. + //ViewPunch(QAngle(random->RandomInt(-0.1,0.1), random->RandomInt(-0.1,0.1), random->RandomInt(-0.1,0.1))); + + // Burn sound + EmitSound( "Player.PlasmaDamage" ); + } + else if (fDamageType & DMG_SONIC) + { + // Sonic damage sound + EmitSound( "Player.SonicDamage" ); + } + else if ( fDamageType & DMG_BULLET ) + { + EmitSound( "Flesh.BulletImpact" ); + } +} + +/* + Take some damage. + NOTE: each call to OnTakeDamage with bitsDamageType set to a time-based damage + type will cause the damage time countdown to be reset. Thus the ongoing effects of poison, radiation + etc are implemented with subsequent calls to OnTakeDamage using DMG_GENERIC. +*/ + +// Old values +#define OLD_ARMOR_RATIO 0.2 // Armor Takes 80% of the damage +#define OLD_ARMOR_BONUS 0.5 // Each Point of Armor is work 1/x points of health + +// New values +#define ARMOR_RATIO 0.2 +#define ARMOR_BONUS 1.0 + +//--------------------------------------------------------- +//--------------------------------------------------------- +bool CBasePlayer::ShouldTakeDamageInCommentaryMode( const CTakeDamageInfo &inputInfo ) +{ + // Only ignore damage when we're listening to a commentary node + if ( !IsListeningToCommentary() ) + return true; + + // Allow SetHealth inputs to kill player. + if ( inputInfo.GetInflictor() == this && inputInfo.GetAttacker() == this ) + return true; + +#ifdef PORTAL + if ( inputInfo.GetDamageType() & DMG_ACID ) + return true; +#endif + + // In commentary, ignore all damage except for falling and leeches + if ( !(inputInfo.GetDamageType() & (DMG_BURN | DMG_PLASMA | DMG_FALL | DMG_CRUSH)) && inputInfo.GetDamageType() != DMG_GENERIC ) + return false; + + // We let DMG_CRUSH pass the check above so that we can check here for stress damage. Deny the CRUSH damage if there is no attacker, + // or if the attacker isn't a BSP model. Therefore, we're allowing any CRUSH damage done by a BSP model. + if ( (inputInfo.GetDamageType() & DMG_CRUSH) && ( inputInfo.GetAttacker() == NULL || !inputInfo.GetAttacker()->IsBSPModel() ) ) + return false; + + return true; +} + +int CBasePlayer::OnTakeDamage( const CTakeDamageInfo &inputInfo ) +{ + // have suit diagnose the problem - ie: report damage type + int bitsDamage = inputInfo.GetDamageType(); + int ffound = true; + int fmajor; + int fcritical; + int fTookDamage; + int ftrivial; + float flRatio; + float flBonus; + float flHealthPrev = m_iHealth; + + CTakeDamageInfo info = inputInfo; + + IServerVehicle *pVehicle = GetVehicle(); + if ( pVehicle ) + { + // Let the vehicle decide if we should take this damage or not + if ( pVehicle->PassengerShouldReceiveDamage( info ) == false ) + return 0; + } + + if ( IsInCommentaryMode() ) + { + if( !ShouldTakeDamageInCommentaryMode( info ) ) + return 0; + } + + if ( GetFlags() & FL_GODMODE ) + return 0; + + if ( m_debugOverlays & OVERLAY_BUDDHA_MODE ) + { + if ((m_iHealth - info.GetDamage()) <= 0) + { + m_iHealth = 1; + return 0; + } + } + + // Early out if there's no damage + if ( !info.GetDamage() ) + return 0; + + if( old_armor.GetBool() ) + { + flBonus = OLD_ARMOR_BONUS; + flRatio = OLD_ARMOR_RATIO; + } + else + { + flBonus = ARMOR_BONUS; + flRatio = ARMOR_RATIO; + } + + if ( ( info.GetDamageType() & DMG_BLAST ) && g_pGameRules->IsMultiplayer() ) + { + // blasts damage armor more. + flBonus *= 2; + } + + // Already dead + if ( !IsAlive() ) + return 0; + // go take the damage first + + + if ( !g_pGameRules->FPlayerCanTakeDamage( this, info.GetAttacker(), inputInfo ) ) + { + // Refuse the damage + return 0; + } + + // print to console if the appropriate cvar is set +#ifdef DISABLE_DEBUG_HISTORY + if (player_debug_print_damage.GetBool() && info.GetDamage() > 0) +#endif + { + char dmgtype[64]; + CTakeDamageInfo::DebugGetDamageTypeString( info.GetDamageType(), dmgtype, 512 ); + char outputString[256]; + Q_snprintf( outputString, 256, "%f: Player %s at [%0.2f %0.2f %0.2f] took %f damage from %s, type %s\n", gpGlobals->curtime, GetDebugName(), + GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, info.GetDamage(), info.GetInflictor()->GetDebugName(), dmgtype ); + + //Msg( "%f: Player %s at [%0.2f %0.2f %0.2f] took %f damage from %s, type %s\n", gpGlobals->curtime, GetDebugName(), + // GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, info.GetDamage(), info.GetInflictor()->GetDebugName(), dmgtype ); + + ADD_DEBUG_HISTORY( HISTORY_PLAYER_DAMAGE, outputString ); +#ifndef DISABLE_DEBUG_HISTORY + if ( player_debug_print_damage.GetBool() ) // if we're not in here just for the debug history +#endif + { + Msg( "%s", outputString); + } + } + + // keep track of amount of damage last sustained + m_lastDamageAmount = info.GetDamage(); + + // Armor. + if (m_ArmorValue && !(info.GetDamageType() & (DMG_FALL | DMG_DROWN | DMG_POISON | DMG_RADIATION)) )// armor doesn't protect against fall or drown damage! + { + float flNew = info.GetDamage() * flRatio; + + float flArmor; + + flArmor = (info.GetDamage() - flNew) * flBonus; + + if( !old_armor.GetBool() ) + { + if( flArmor < 1.0 ) + { + flArmor = 1.0; + } + } + + // Does this use more armor than we have? + if (flArmor > m_ArmorValue) + { + flArmor = m_ArmorValue; + flArmor *= (1/flBonus); + flNew = info.GetDamage() - flArmor; + m_DmgSave = m_ArmorValue; + m_ArmorValue = 0; + } + else + { + m_DmgSave = flArmor; + m_ArmorValue -= flArmor; + } + + info.SetDamage( flNew ); + } + + +#if defined( WIN32 ) && !defined( _X360 ) + // NVNT if player's client has a haptic device send them a user message with the damage. + if(HasHaptics()) + HapticsDamage(this,info); +#endif + + // this cast to INT is critical!!! If a player ends up with 0.5 health, the engine will get that + // as an int (zero) and think the player is dead! (this will incite a clientside screentilt, etc) + + // NOTENOTE: jdw - We are now capable of retaining the mantissa of this damage value and deferring its application + + // info.SetDamage( (int)info.GetDamage() ); + + // Call up to the base class + fTookDamage = BaseClass::OnTakeDamage( info ); + + // Early out if the base class took no damage + if ( !fTookDamage ) + return 0; + + // add to the damage total for clients, which will be sent as a single + // message at the end of the frame + // todo: remove after combining shotgun blasts? + if ( info.GetInflictor() && info.GetInflictor()->edict() ) + m_DmgOrigin = info.GetInflictor()->GetAbsOrigin(); + + m_DmgTake += (int)info.GetDamage(); + + // Reset damage time countdown for each type of time based damage player just sustained + for (int i = 0; i < CDMG_TIMEBASED; i++) + { + // Make sure the damage type is really time-based. + // This is kind of hacky but necessary until we setup DamageType as an enum. + int iDamage = ( DMG_PARALYZE << i ); + if ( ( info.GetDamageType() & iDamage ) && g_pGameRules->Damage_IsTimeBased( iDamage ) ) + { + m_rgbTimeBasedDamage[i] = 0; + } + } + + // Display any effect associate with this damage type + DamageEffect(info.GetDamage(),bitsDamage); + + // how bad is it, doc? + ftrivial = (m_iHealth > 75 || m_lastDamageAmount < 5); + fmajor = (m_lastDamageAmount > 25); + fcritical = (m_iHealth < 30); + + // handle all bits set in this damage message, + // let the suit give player the diagnosis + + // UNDONE: add sounds for types of damage sustained (ie: burn, shock, slash ) + + // UNDONE: still need to record damage and heal messages for the following types + + // DMG_BURN + // DMG_FREEZE + // DMG_BLAST + // DMG_SHOCK + + m_bitsDamageType |= bitsDamage; // Save this so we can report it to the client + m_bitsHUDDamage = -1; // make sure the damage bits get resent + + while (fTookDamage && (!ftrivial || g_pGameRules->Damage_IsTimeBased( bitsDamage ) ) && ffound && bitsDamage) + { + ffound = false; + + if (bitsDamage & DMG_CLUB) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG4", false, SUIT_NEXT_IN_30SEC); // minor fracture + bitsDamage &= ~DMG_CLUB; + ffound = true; + } + if (bitsDamage & (DMG_FALL | DMG_CRUSH)) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG5", false, SUIT_NEXT_IN_30SEC); // major fracture + else + SetSuitUpdate("!HEV_DMG4", false, SUIT_NEXT_IN_30SEC); // minor fracture + + bitsDamage &= ~(DMG_FALL | DMG_CRUSH); + ffound = true; + } + + if (bitsDamage & DMG_BULLET) + { + if (m_lastDamageAmount > 5) + SetSuitUpdate("!HEV_DMG6", false, SUIT_NEXT_IN_30SEC); // blood loss detected + //else + // SetSuitUpdate("!HEV_DMG0", false, SUIT_NEXT_IN_30SEC); // minor laceration + + bitsDamage &= ~DMG_BULLET; + ffound = true; + } + + if (bitsDamage & DMG_SLASH) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG1", false, SUIT_NEXT_IN_30SEC); // major laceration + else + SetSuitUpdate("!HEV_DMG0", false, SUIT_NEXT_IN_30SEC); // minor laceration + + bitsDamage &= ~DMG_SLASH; + ffound = true; + } + + if (bitsDamage & DMG_SONIC) + { + if (fmajor) + SetSuitUpdate("!HEV_DMG2", false, SUIT_NEXT_IN_1MIN); // internal bleeding + bitsDamage &= ~DMG_SONIC; + ffound = true; + } + + if (bitsDamage & (DMG_POISON | DMG_PARALYZE)) + { + if (bitsDamage & DMG_POISON) + { + m_nPoisonDmg += info.GetDamage(); + m_tbdPrev = gpGlobals->curtime; + m_rgbTimeBasedDamage[itbd_PoisonRecover] = 0; + } + + SetSuitUpdate("!HEV_DMG3", false, SUIT_NEXT_IN_1MIN); // blood toxins detected + bitsDamage &= ~( DMG_POISON | DMG_PARALYZE ); + ffound = true; + } + + if (bitsDamage & DMG_ACID) + { + SetSuitUpdate("!HEV_DET1", false, SUIT_NEXT_IN_1MIN); // hazardous chemicals detected + bitsDamage &= ~DMG_ACID; + ffound = true; + } + + if (bitsDamage & DMG_NERVEGAS) + { + SetSuitUpdate("!HEV_DET0", false, SUIT_NEXT_IN_1MIN); // biohazard detected + bitsDamage &= ~DMG_NERVEGAS; + ffound = true; + } + + if (bitsDamage & DMG_RADIATION) + { + SetSuitUpdate("!HEV_DET2", false, SUIT_NEXT_IN_1MIN); // radiation detected + bitsDamage &= ~DMG_RADIATION; + ffound = true; + } + if (bitsDamage & DMG_SHOCK) + { + bitsDamage &= ~DMG_SHOCK; + ffound = true; + } + } + + float flPunch = -2; + + if( hl2_episodic.GetBool() && info.GetAttacker() && !FInViewCone( info.GetAttacker() ) ) + { + if( info.GetDamage() > 10.0f ) + flPunch = -10; + else + flPunch = RandomFloat( -5, -7 ); + } + + m_Local.m_vecPunchAngle.SetX( flPunch ); + + if (fTookDamage && !ftrivial && fmajor && flHealthPrev >= 75) + { + // first time we take major damage... + // turn automedic on if not on + SetSuitUpdate("!HEV_MED1", false, SUIT_NEXT_IN_30MIN); // automedic on + + // give morphine shot if not given recently + SetSuitUpdate("!HEV_HEAL7", false, SUIT_NEXT_IN_30MIN); // morphine shot + } + + if (fTookDamage && !ftrivial && fcritical && flHealthPrev < 75) + { + + // already took major damage, now it's critical... + if (m_iHealth < 6) + SetSuitUpdate("!HEV_HLTH3", false, SUIT_NEXT_IN_10MIN); // near death + else if (m_iHealth < 20) + SetSuitUpdate("!HEV_HLTH2", false, SUIT_NEXT_IN_10MIN); // health critical + + // give critical health warnings + if (!random->RandomInt(0,3) && flHealthPrev < 50) + SetSuitUpdate("!HEV_DMG7", false, SUIT_NEXT_IN_5MIN); //seek medical attention + } + + // if we're taking time based damage, warn about its continuing effects + if (fTookDamage && g_pGameRules->Damage_IsTimeBased( info.GetDamageType() ) && flHealthPrev < 75) + { + if (flHealthPrev < 50) + { + if (!random->RandomInt(0,3)) + SetSuitUpdate("!HEV_DMG7", false, SUIT_NEXT_IN_5MIN); //seek medical attention + } + else + SetSuitUpdate("!HEV_HLTH1", false, SUIT_NEXT_IN_10MIN); // health dropping + } + + // Do special explosion damage effect + if ( bitsDamage & DMG_BLAST ) + { + OnDamagedByExplosion( info ); + } + + return fTookDamage; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &info - +// damageAmount - +//----------------------------------------------------------------------------- +#define MIN_SHOCK_AND_CONFUSION_DAMAGE 30.0f +#define MIN_EAR_RINGING_DISTANCE 240.0f // 20 feet + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &info - +//----------------------------------------------------------------------------- +void CBasePlayer::OnDamagedByExplosion( const CTakeDamageInfo &info ) +{ + float lastDamage = info.GetDamage(); + + float distanceFromPlayer = 9999.0f; + + CBaseEntity *inflictor = info.GetInflictor(); + if ( inflictor ) + { + Vector delta = GetAbsOrigin() - inflictor->GetAbsOrigin(); + distanceFromPlayer = delta.Length(); + } + + bool ear_ringing = distanceFromPlayer < MIN_EAR_RINGING_DISTANCE ? true : false; + bool shock = lastDamage >= MIN_SHOCK_AND_CONFUSION_DAMAGE; + + if ( !shock && !ear_ringing ) + return; + + int effect = shock ? + random->RandomInt( 35, 37 ) : + random->RandomInt( 32, 34 ); + + CSingleUserRecipientFilter user( this ); + enginesound->SetPlayerDSP( user, effect, false ); +} + +//========================================================= +// PackDeadPlayerItems - call this when a player dies to +// pack up the appropriate weapons and ammo items, and to +// destroy anything that shouldn't be packed. +// +// This is pretty brute force :( +//========================================================= +void CBasePlayer::PackDeadPlayerItems( void ) +{ + int iWeaponRules; + int iAmmoRules; + int i; + CBaseCombatWeapon *rgpPackWeapons[ 20 ];// 20 hardcoded for now. How to determine exactly how many weapons we have? + int iPackAmmo[ MAX_AMMO_SLOTS + 1]; + int iPW = 0;// index into packweapons array + int iPA = 0;// index into packammo array + + memset(rgpPackWeapons, NULL, sizeof(rgpPackWeapons) ); + memset(iPackAmmo, -1, sizeof(iPackAmmo) ); + + // get the game rules + iWeaponRules = g_pGameRules->DeadPlayerWeapons( this ); + iAmmoRules = g_pGameRules->DeadPlayerAmmo( this ); + + if ( iWeaponRules == GR_PLR_DROP_GUN_NO && iAmmoRules == GR_PLR_DROP_AMMO_NO ) + { + // nothing to pack. Remove the weapons and return. Don't call create on the box! + RemoveAllItems( true ); + return; + } + +// go through all of the weapons and make a list of the ones to pack + for ( i = 0 ; i < WeaponCount() ; i++ ) + { + // there's a weapon here. Should I pack it? + CBaseCombatWeapon *pPlayerItem = GetWeapon( i ); + if ( pPlayerItem ) + { + switch( iWeaponRules ) + { + case GR_PLR_DROP_GUN_ACTIVE: + if ( GetActiveWeapon() && pPlayerItem == GetActiveWeapon() ) + { + // this is the active item. Pack it. + rgpPackWeapons[ iPW++ ] = pPlayerItem; + } + break; + + case GR_PLR_DROP_GUN_ALL: + rgpPackWeapons[ iPW++ ] = pPlayerItem; + break; + + default: + break; + } + } + } + +// now go through ammo and make a list of which types to pack. + if ( iAmmoRules != GR_PLR_DROP_AMMO_NO ) + { + for ( i = 0 ; i < MAX_AMMO_SLOTS ; i++ ) + { + if ( GetAmmoCount( i ) > 0 ) + { + // player has some ammo of this type. + switch ( iAmmoRules ) + { + case GR_PLR_DROP_AMMO_ALL: + iPackAmmo[ iPA++ ] = i; + break; + + case GR_PLR_DROP_AMMO_ACTIVE: + // WEAPONTODO: Make this work + /* + if ( GetActiveWeapon() && i == GetActiveWeapon()->m_iPrimaryAmmoType ) + { + // this is the primary ammo type for the active weapon + iPackAmmo[ iPA++ ] = i; + } + else if ( GetActiveWeapon() && i == GetActiveWeapon()->m_iSecondaryAmmoType ) + { + // this is the secondary ammo type for the active weapon + iPackAmmo[ iPA++ ] = i; + } + */ + break; + + default: + break; + } + } + } + } + + RemoveAllItems( true );// now strip off everything that wasn't handled by the code above. +} + +void CBasePlayer::RemoveAllItems( bool removeSuit ) +{ + if (GetActiveWeapon()) + { + ResetAutoaim( ); + GetActiveWeapon()->Holster( ); + } + + Weapon_SetLast( NULL ); + RemoveAllWeapons(); + RemoveAllAmmo(); + + if ( removeSuit ) + { + RemoveSuit(); + } + + UpdateClientData(); +} + +//Tony; correct this for base code so that IsDead will be correct accross all games. +bool CBasePlayer::IsDead() const +{ + return m_lifeState != LIFE_ALIVE; +} + +static float DamageForce( const Vector &size, float damage ) +{ + float force = damage * ((32 * 32 * 72.0) / (size.x * size.y * size.z)) * 5; + + if ( force > 1000.0) + { + force = 1000.0; + } + + return force; +} + + +const impactdamagetable_t &CBasePlayer::GetPhysicsImpactDamageTable() +{ + return gDefaultPlayerImpactDamageTable; +} + + +int CBasePlayer::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + // set damage type sustained + m_bitsDamageType |= info.GetDamageType(); + + if ( !BaseClass::OnTakeDamage_Alive( info ) ) + return 0; + + CBaseEntity * attacker = info.GetAttacker(); + + if ( !attacker ) + return 0; + + Vector vecDir = vec3_origin; + if ( info.GetInflictor() ) + { + vecDir = info.GetInflictor()->WorldSpaceCenter() - Vector ( 0, 0, 10 ) - WorldSpaceCenter(); + VectorNormalize( vecDir ); + } + + if ( info.GetInflictor() && (GetMoveType() == MOVETYPE_WALK) && + ( !attacker->IsSolidFlagSet(FSOLID_TRIGGER)) ) + { + Vector force = vecDir * -DamageForce( WorldAlignSize(), info.GetBaseDamage() ); + if ( force.z > 250.0f ) + { + force.z = 250.0f; + } + ApplyAbsVelocityImpulse( force ); + } + + // fire global game event + + IGameEvent * event = gameeventmanager->CreateEvent( "player_hurt" ); + if ( event ) + { + event->SetInt("userid", GetUserID() ); + event->SetInt("health", MAX(0, m_iHealth) ); + event->SetInt("priority", 5 ); // HLTV event priority, not transmitted + + if ( attacker->IsPlayer() ) + { + CBasePlayer *player = ToBasePlayer( attacker ); + event->SetInt("attacker", player->GetUserID() ); // hurt by other player + } + else + { + event->SetInt("attacker", 0 ); // hurt by "world" + } + + gameeventmanager->FireEvent( event ); + } + + // Insert a combat sound so that nearby NPCs hear battle + if ( attacker->IsNPC() ) + { + CSoundEnt::InsertSound( SOUND_COMBAT, GetAbsOrigin(), 512, 0.5, this );//<>//magic number + } + + return 1; +} + + +void CBasePlayer::Event_Killed( const CTakeDamageInfo &info ) +{ + CSound *pSound; + + if ( Hints() ) + { + Hints()->ResetHintTimers(); + } + + g_pGameRules->PlayerKilled( this, info ); + + gamestats->Event_PlayerKilled( this, info ); + + RumbleEffect( RUMBLE_STOP_ALL, 0, RUMBLE_FLAGS_NONE ); + +#if defined( WIN32 ) && !defined( _X360 ) + // NVNT set the drag to zero in the case of underwater death. + HapticSetDrag(this,0); +#endif + ClearUseEntity(); + + // this client isn't going to be thinking for a while, so reset the sound until they respawn + pSound = CSoundEnt::SoundPointerForIndex( CSoundEnt::ClientSoundIndex( edict() ) ); + { + if ( pSound ) + { + pSound->Reset(); + } + } + + // don't let the status bar glitch for players with <0 health. + if (m_iHealth < -99) + { + m_iHealth = 0; + } + + // holster the current weapon + if ( GetActiveWeapon() ) + { + GetActiveWeapon()->Holster(); + } + + SetAnimation( PLAYER_DIE ); + + if ( !IsObserver() ) + { + SetViewOffset( VEC_DEAD_VIEWHEIGHT_SCALED( this ) ); + } + m_lifeState = LIFE_DYING; + + pl.deadflag = true; + AddSolidFlags( FSOLID_NOT_SOLID ); + // force contact points to get flushed if no longer valid + // UNDONE: Always do this on RecheckCollisionFilter() ? + IPhysicsObject *pObject = VPhysicsGetObject(); + if ( pObject ) + { + pObject->RecheckContactPoints(); + } + + SetMoveType( MOVETYPE_FLYGRAVITY ); + SetGroundEntity( NULL ); + + // clear out the suit message cache so we don't keep chattering + SetSuitUpdate(NULL, false, 0); + + // reset FOV + SetFOV( this, 0 ); + + if ( FlashlightIsOn() ) + { + FlashlightTurnOff(); + } + + m_flDeathTime = gpGlobals->curtime; + + ClearLastKnownArea(); + + BaseClass::Event_Killed( info ); +} + +void CBasePlayer::Event_Dying( const CTakeDamageInfo& info ) +{ + // NOT GIBBED, RUN THIS CODE + + DeathSound( info ); + + // The dead body rolls out of the vehicle. + if ( IsInAVehicle() ) + { + LeaveVehicle(); + } + + QAngle angles = GetLocalAngles(); + + angles.x = 0; + angles.z = 0; + + SetLocalAngles( angles ); + + SetThink(&CBasePlayer::PlayerDeathThink); + SetNextThink( gpGlobals->curtime + 0.1f ); + BaseClass::Event_Dying( info ); +} + + +// Set the activity based on an event or current state +void CBasePlayer::SetAnimation( PLAYER_ANIM playerAnim ) +{ + int animDesired; + char szAnim[64]; + + float speed; + + speed = GetAbsVelocity().Length2D(); + + if (GetFlags() & (FL_FROZEN|FL_ATCONTROLS)) + { + speed = 0; + playerAnim = PLAYER_IDLE; + } + + Activity idealActivity = ACT_WALK;// TEMP!!!!! + + // This could stand to be redone. Why is playerAnim abstracted from activity? (sjb) + if (playerAnim == PLAYER_JUMP) + { + idealActivity = ACT_HOP; + } + else if (playerAnim == PLAYER_SUPERJUMP) + { + idealActivity = ACT_LEAP; + } + else if (playerAnim == PLAYER_DIE) + { + if ( m_lifeState == LIFE_ALIVE ) + { + idealActivity = GetDeathActivity(); + } + } + else if (playerAnim == PLAYER_ATTACK1) + { + if ( m_Activity == ACT_HOVER || + m_Activity == ACT_SWIM || + m_Activity == ACT_HOP || + m_Activity == ACT_LEAP || + m_Activity == ACT_DIESIMPLE ) + { + idealActivity = m_Activity; + } + else + { + idealActivity = ACT_RANGE_ATTACK1; + } + } + else if (playerAnim == PLAYER_IDLE || playerAnim == PLAYER_WALK) + { + if ( !( GetFlags() & FL_ONGROUND ) && (m_Activity == ACT_HOP || m_Activity == ACT_LEAP) ) // Still jumping + { + idealActivity = m_Activity; + } + else if ( GetWaterLevel() > 1 ) + { + if ( speed == 0 ) + idealActivity = ACT_HOVER; + else + idealActivity = ACT_SWIM; + } + else + { + idealActivity = ACT_WALK; + } + } + + + if (idealActivity == ACT_RANGE_ATTACK1) + { + if ( GetFlags() & FL_DUCKING ) // crouching + { + Q_strncpy( szAnim, "crouch_shoot_" ,sizeof(szAnim)); + } + else + { + Q_strncpy( szAnim, "ref_shoot_" ,sizeof(szAnim)); + } + Q_strncat( szAnim, m_szAnimExtension ,sizeof(szAnim), COPY_ALL_CHARACTERS ); + animDesired = LookupSequence( szAnim ); + if (animDesired == -1) + animDesired = 0; + + if ( GetSequence() != animDesired || !SequenceLoops() ) + { + SetCycle( 0 ); + } + + // Tracker 24588: In single player when firing own weapon this causes eye and punchangle to jitter + //if (!SequenceLoops()) + //{ + // IncrementInterpolationFrame(); + //} + + SetActivity( idealActivity ); + ResetSequence( animDesired ); + } + else if (idealActivity == ACT_WALK) + { + if (GetActivity() != ACT_RANGE_ATTACK1 || IsActivityFinished()) + { + if ( GetFlags() & FL_DUCKING ) // crouching + { + Q_strncpy( szAnim, "crouch_aim_" ,sizeof(szAnim)); + } + else + { + Q_strncpy( szAnim, "ref_aim_" ,sizeof(szAnim)); + } + Q_strncat( szAnim, m_szAnimExtension,sizeof(szAnim), COPY_ALL_CHARACTERS ); + animDesired = LookupSequence( szAnim ); + if (animDesired == -1) + animDesired = 0; + SetActivity( ACT_WALK ); + } + else + { + animDesired = GetSequence(); + } + } + else + { + if ( GetActivity() == idealActivity) + return; + + SetActivity( idealActivity ); + + animDesired = SelectWeightedSequence( m_Activity ); + + // Already using the desired animation? + if (GetSequence() == animDesired) + return; + + ResetSequence( animDesired ); + SetCycle( 0 ); + return; + } + + // Already using the desired animation? + if (GetSequence() == animDesired) + return; + + //Msg( "Set animation to %d\n", animDesired ); + // Reset to first frame of desired animation + ResetSequence( animDesired ); + SetCycle( 0 ); +} + +/* +=========== +WaterMove +============ +*/ +#ifdef HL2_DLL + +// test for HL2 drowning damage increase (aux power used instead) +#define AIRTIME 7 // lung full of air lasts this many seconds +#define DROWNING_DAMAGE_INITIAL 10 +#define DROWNING_DAMAGE_MAX 10 + +#else + +#define AIRTIME 12 // lung full of air lasts this many seconds +#define DROWNING_DAMAGE_INITIAL 2 +#define DROWNING_DAMAGE_MAX 5 + +#endif + +void CBasePlayer::WaterMove() +{ + if ( ( GetMoveType() == MOVETYPE_NOCLIP ) && !GetMoveParent() ) + { + m_AirFinished = gpGlobals->curtime + AIRTIME; + return; + } + + if ( m_iHealth < 0 || !IsAlive() ) + { + UpdateUnderwaterState(); + return; + } + + // waterlevel 0 - not in water (WL_NotInWater) + // waterlevel 1 - feet in water (WL_Feet) + // waterlevel 2 - waist in water (WL_Waist) + // waterlevel 3 - head in water (WL_Eyes) + + if (GetWaterLevel() != WL_Eyes || CanBreatheUnderwater()) + { + // not underwater + + // play 'up for air' sound + + if (m_AirFinished < gpGlobals->curtime) + { + EmitSound( "Player.DrownStart" ); + } + + m_AirFinished = gpGlobals->curtime + AIRTIME; + m_nDrownDmgRate = DROWNING_DAMAGE_INITIAL; + + // if we took drowning damage, give it back slowly + if (m_idrowndmg > m_idrownrestored) + { + // set drowning damage bit. hack - dmg_drownrecover actually + // makes the time based damage code 'give back' health over time. + // make sure counter is cleared so we start count correctly. + + // NOTE: this actually causes the count to continue restarting + // until all drowning damage is healed. + + m_bitsDamageType |= DMG_DROWNRECOVER; + m_bitsDamageType &= ~DMG_DROWN; + m_rgbTimeBasedDamage[itbd_DrownRecover] = 0; + } + + } + else + { // fully under water + // stop restoring damage while underwater + m_bitsDamageType &= ~DMG_DROWNRECOVER; + m_rgbTimeBasedDamage[itbd_DrownRecover] = 0; + + if (m_AirFinished < gpGlobals->curtime && !(GetFlags() & FL_GODMODE) ) // drown! + { + if (m_PainFinished < gpGlobals->curtime) + { + // take drowning damage + m_nDrownDmgRate += 1; + if (m_nDrownDmgRate > DROWNING_DAMAGE_MAX) + { + m_nDrownDmgRate = DROWNING_DAMAGE_MAX; + } + + OnTakeDamage( CTakeDamageInfo( GetContainingEntity(INDEXENT(0)), GetContainingEntity(INDEXENT(0)), m_nDrownDmgRate, DMG_DROWN ) ); + m_PainFinished = gpGlobals->curtime + 1; + + // track drowning damage, give it back when + // player finally takes a breath + m_idrowndmg += m_nDrownDmgRate; + } + } + else + { + m_bitsDamageType &= ~DMG_DROWN; + } + } + + UpdateUnderwaterState(); +} + + +// true if the player is attached to a ladder +bool CBasePlayer::IsOnLadder( void ) +{ + return (GetMoveType() == MOVETYPE_LADDER); +} + + +float CBasePlayer::GetWaterJumpTime() const +{ + return m_flWaterJumpTime; +} + +void CBasePlayer::SetWaterJumpTime( float flWaterJumpTime ) +{ + m_flWaterJumpTime = flWaterJumpTime; +} + +float CBasePlayer::GetSwimSoundTime( void ) const +{ + return m_flSwimSoundTime; +} + +void CBasePlayer::SetSwimSoundTime( float flSwimSoundTime ) +{ + m_flSwimSoundTime = flSwimSoundTime; +} + +void CBasePlayer::ShowViewPortPanel( const char * name, bool bShow, KeyValues *data ) +{ + CSingleUserRecipientFilter filter( this ); + filter.MakeReliable(); + + int count = 0; + KeyValues *subkey = NULL; + + if ( data ) + { + subkey = data->GetFirstSubKey(); + while ( subkey ) + { + count++; subkey = subkey->GetNextKey(); + } + + subkey = data->GetFirstSubKey(); // reset + } + + UserMessageBegin( filter, "VGUIMenu" ); + WRITE_STRING( name ); // menu name + WRITE_BYTE( bShow?1:0 ); + WRITE_BYTE( count ); + + // write additional data (be careful not more than 192 bytes!) + while ( subkey ) + { + WRITE_STRING( subkey->GetName() ); + WRITE_STRING( subkey->GetString() ); + subkey = subkey->GetNextKey(); + } + MessageEnd(); +} + + +void CBasePlayer::PlayerDeathThink(void) +{ + float flForward; + + SetNextThink( gpGlobals->curtime + 0.1f ); + + if (GetFlags() & FL_ONGROUND) + { + flForward = GetAbsVelocity().Length() - 20; + if (flForward <= 0) + { + SetAbsVelocity( vec3_origin ); + } + else + { + Vector vecNewVelocity = GetAbsVelocity(); + VectorNormalize( vecNewVelocity ); + vecNewVelocity *= flForward; + SetAbsVelocity( vecNewVelocity ); + } + } + + if ( HasWeapons() ) + { + // we drop the guns here because weapons that have an area effect and can kill their user + // will sometimes crash coming back from CBasePlayer::Killed() if they kill their owner because the + // player class sometimes is freed. It's safer to manipulate the weapons once we know + // we aren't calling into any of their code anymore through the player pointer. + PackDeadPlayerItems(); + } + + if (GetModelIndex() && (!IsSequenceFinished()) && (m_lifeState == LIFE_DYING)) + { + StudioFrameAdvance( ); + + m_iRespawnFrames++; + if ( m_iRespawnFrames < 60 ) // animations should be no longer than this + return; + } + + if (m_lifeState == LIFE_DYING) + { + m_lifeState = LIFE_DEAD; + m_flDeathAnimTime = gpGlobals->curtime; + } + + StopAnimation(); + + IncrementInterpolationFrame(); + m_flPlaybackRate = 0.0; + + int fAnyButtonDown = (m_nButtons & ~IN_SCORE); + + // Strip out the duck key from this check if it's toggled + if ( (fAnyButtonDown & IN_DUCK) && GetToggledDuckState()) + { + fAnyButtonDown &= ~IN_DUCK; + } + + // wait for all buttons released + if (m_lifeState == LIFE_DEAD) + { + if (fAnyButtonDown) + return; + + if ( g_pGameRules->FPlayerCanRespawn( this ) ) + { + m_lifeState = LIFE_RESPAWNABLE; + } + + return; + } + +// if the player has been dead for one second longer than allowed by forcerespawn, +// forcerespawn isn't on. Send the player off to an intermission camera until they +// choose to respawn. + if ( g_pGameRules->IsMultiplayer() && ( gpGlobals->curtime > (m_flDeathTime + DEATH_ANIMATION_TIME) ) && !IsObserver() ) + { + // go to dead camera. + StartObserverMode( m_iObserverLastMode ); + } + +// wait for any button down, or mp_forcerespawn is set and the respawn time is up + if (!fAnyButtonDown + && !( g_pGameRules->IsMultiplayer() && forcerespawn.GetInt() > 0 && (gpGlobals->curtime > (m_flDeathTime + 5))) ) + return; + + m_nButtons = 0; + m_iRespawnFrames = 0; + + //Msg( "Respawn\n"); + + respawn( this, !IsObserver() );// don't copy a corpse if we're in deathcam. + SetNextThink( TICK_NEVER_THINK ); +} + +/* + +//========================================================= +// StartDeathCam - find an intermission spot and send the +// player off into observer mode +//========================================================= +void CBasePlayer::StartDeathCam( void ) +{ + CBaseEntity *pSpot, *pNewSpot; + int iRand; + + if ( GetViewOffset() == vec3_origin ) + { + // don't accept subsequent attempts to StartDeathCam() + return; + } + + pSpot = gEntList.FindEntityByClassname( NULL, "info_intermission"); + + if ( pSpot ) + { + // at least one intermission spot in the world. + iRand = random->RandomInt( 0, 3 ); + + while ( iRand > 0 ) + { + pNewSpot = gEntList.FindEntityByClassname( pSpot, "info_intermission"); + + if ( pNewSpot ) + { + pSpot = pNewSpot; + } + + iRand--; + } + + CreateCorpse(); + StartObserverMode( pSpot->GetAbsOrigin(), pSpot->GetAbsAngles() ); + } + else + { + // no intermission spot. Push them up in the air, looking down at their corpse + trace_t tr; + + CreateCorpse(); + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, 128 ), + MASK_PLAYERSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + QAngle angles; + VectorAngles( GetAbsOrigin() - tr.endpos, angles ); + StartObserverMode( tr.endpos, angles ); + return; + } +} */ + +void CBasePlayer::StopObserverMode() +{ + m_bForcedObserverMode = false; + m_afPhysicsFlags &= ~PFLAG_OBSERVER; + + if ( m_iObserverMode == OBS_MODE_NONE ) + return; + + if ( m_iObserverMode > OBS_MODE_DEATHCAM ) + { + m_iObserverLastMode = m_iObserverMode; + } + + m_iObserverMode.Set( OBS_MODE_NONE ); + + ShowViewPortPanel( "specmenu", false ); + ShowViewPortPanel( "specgui", false ); + ShowViewPortPanel( "overview", false ); +} + +bool CBasePlayer::StartObserverMode(int mode) +{ + if ( !IsObserver() ) + { + // set position to last view offset + SetAbsOrigin( GetAbsOrigin() + GetViewOffset() ); + SetViewOffset( vec3_origin ); + } + + Assert( mode > OBS_MODE_NONE ); + + m_afPhysicsFlags |= PFLAG_OBSERVER; + + // Holster weapon immediately, to allow it to cleanup + if ( GetActiveWeapon() ) + GetActiveWeapon()->Holster(); + + // clear out the suit message cache so we don't keep chattering + SetSuitUpdate(NULL, FALSE, 0); + + SetGroundEntity( (CBaseEntity *)NULL ); + + RemoveFlag( FL_DUCKING ); + + AddSolidFlags( FSOLID_NOT_SOLID ); + + SetObserverMode( mode ); + + if ( gpGlobals->eLoadType != MapLoad_Background ) + { + ShowViewPortPanel( "specgui" , ModeWantsSpectatorGUI(mode) ); + } + + // Setup flags + m_Local.m_iHideHUD = HIDEHUD_HEALTH; + m_takedamage = DAMAGE_NO; + + // Become invisible + AddEffects( EF_NODRAW ); + + m_iHealth = 1; + m_lifeState = LIFE_DEAD; // Can't be dead, otherwise movement doesn't work right. + m_flDeathAnimTime = gpGlobals->curtime; + pl.deadflag = true; + + return true; +} + +bool CBasePlayer::SetObserverMode(int mode ) +{ + if ( mode < OBS_MODE_NONE || mode >= NUM_OBSERVER_MODES ) + return false; + + + // check mp_forcecamera settings for dead players + if ( mode > OBS_MODE_FIXED && GetTeamNumber() > TEAM_SPECTATOR ) + { + switch ( mp_forcecamera.GetInt() ) + { + case OBS_ALLOW_ALL : break; // no restrictions + case OBS_ALLOW_TEAM : mode = OBS_MODE_IN_EYE; break; + case OBS_ALLOW_NONE : mode = OBS_MODE_FIXED; break; // don't allow anything + } + } + + if ( m_iObserverMode > OBS_MODE_DEATHCAM ) + { + // remember mode if we were really spectating before + m_iObserverLastMode = m_iObserverMode; + } + + m_iObserverMode = mode; + + switch ( mode ) + { + case OBS_MODE_NONE: + case OBS_MODE_FIXED : + case OBS_MODE_DEATHCAM : + SetFOV( this, 0 ); // Reset FOV + SetViewOffset( vec3_origin ); + SetMoveType( MOVETYPE_NONE ); + break; + + case OBS_MODE_CHASE : + case OBS_MODE_IN_EYE : + // udpate FOV and viewmodels + SetObserverTarget( m_hObserverTarget ); + SetMoveType( MOVETYPE_OBSERVER ); + break; + + //============================================================================= + // HPE_BEGIN: + // [menglish] Added freeze cam to the setter. Uses same setup as the roaming mode + //============================================================================= + + case OBS_MODE_ROAMING : + case OBS_MODE_FREEZECAM : + SetFOV( this, 0 ); // Reset FOV + SetObserverTarget( m_hObserverTarget ); + SetViewOffset( vec3_origin ); + SetMoveType( MOVETYPE_OBSERVER ); + break; + + //============================================================================= + // HPE_END + //============================================================================= + } + + CheckObserverSettings(); + + return true; +} + +int CBasePlayer::GetObserverMode() +{ + return m_iObserverMode; +} + +void CBasePlayer::ForceObserverMode(int mode) +{ + int tempMode = OBS_MODE_ROAMING; + + if ( m_iObserverMode == mode ) + return; + + // don't change last mode if already in forced mode + + if ( m_bForcedObserverMode ) + { + tempMode = m_iObserverLastMode; + } + + SetObserverMode( mode ); + + if ( m_bForcedObserverMode ) + { + m_iObserverLastMode = tempMode; + } + + m_bForcedObserverMode = true; +} + +void CBasePlayer::CheckObserverSettings() +{ + // check if we are in forced mode and may go back to old mode + if ( m_bForcedObserverMode ) + { + CBaseEntity * target = m_hObserverTarget; + + if ( !IsValidObserverTarget(target) ) + { + // if old target is still invalid, try to find valid one + target = FindNextObserverTarget( false ); + } + + if ( target ) + { + // we found a valid target + m_bForcedObserverMode = false; // disable force mode + SetObserverMode( m_iObserverLastMode ); // switch to last mode + SetObserverTarget( target ); // goto target + + // TODO check for HUD icons + return; + } + else + { + // else stay in forced mode, no changes + return; + } + } + + // make sure our last mode is valid + if ( m_iObserverLastMode < OBS_MODE_FIXED ) + { + m_iObserverLastMode = OBS_MODE_ROAMING; + } + + // check if our spectating target is still a valid one + + if ( m_iObserverMode == OBS_MODE_IN_EYE || m_iObserverMode == OBS_MODE_CHASE || m_iObserverMode == OBS_MODE_FIXED ) + { + ValidateCurrentObserverTarget(); + + CBasePlayer *target = ToBasePlayer( m_hObserverTarget.Get() ); + + // for ineye mode we have to copy several data to see exactly the same + + if ( target && m_iObserverMode == OBS_MODE_IN_EYE ) + { + int flagMask = FL_ONGROUND | FL_DUCKING ; + + int flags = target->GetFlags() & flagMask; + + if ( (GetFlags() & flagMask) != flags ) + { + flags |= GetFlags() & (~flagMask); // keep other flags + ClearFlags(); + AddFlag( flags ); + } + + if ( target->GetViewOffset() != GetViewOffset() ) + { + SetViewOffset( target->GetViewOffset() ); + } + } + + // Update the fog. + if ( target ) + { + if ( target->m_Local.m_PlayerFog.m_hCtrl.Get() != m_Local.m_PlayerFog.m_hCtrl.Get() ) + { + m_Local.m_PlayerFog.m_hCtrl.Set( target->m_Local.m_PlayerFog.m_hCtrl.Get() ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::ValidateCurrentObserverTarget( void ) +{ + if ( !IsValidObserverTarget( m_hObserverTarget.Get() ) ) + { + // our target is not valid, try to find new target + CBaseEntity * target = FindNextObserverTarget( false ); + if ( target ) + { + // switch to new valid target + SetObserverTarget( target ); + } + else + { + // couldn't find new target, switch to temporary mode + if ( mp_forcecamera.GetInt() == OBS_ALLOW_ALL ) + { + // let player roam around + ForceObserverMode( OBS_MODE_ROAMING ); + } + else + { + // fix player view right where it is + ForceObserverMode( OBS_MODE_FIXED ); + m_hObserverTarget.Set( NULL ); // no traget to follow + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::AttemptToExitFreezeCam( void ) +{ + StartObserverMode( OBS_MODE_DEATHCAM ); +} + +bool CBasePlayer::StartReplayMode( float fDelay, float fDuration, int iEntity ) +{ + if ( ( sv_maxreplay == NULL ) || ( sv_maxreplay->GetFloat() <= 0 ) ) + return false; + + m_fDelay = fDelay; + m_fReplayEnd = gpGlobals->curtime + fDuration; + m_iReplayEntity = iEntity; + + return true; +} + +void CBasePlayer::StopReplayMode() +{ + m_fDelay = 0.0f; + m_fReplayEnd = -1; + m_iReplayEntity = 0; +} + +int CBasePlayer::GetDelayTicks() +{ + if ( m_fReplayEnd > gpGlobals->curtime ) + { + return TIME_TO_TICKS( m_fDelay ); + } + else + { + if ( m_fDelay > 0.0f ) + StopReplayMode(); + + return 0; + } +} + +int CBasePlayer::GetReplayEntity() +{ + return m_iReplayEntity; +} + +CBaseEntity * CBasePlayer::GetObserverTarget() +{ + return m_hObserverTarget.Get(); +} + +void CBasePlayer::ObserverUse( bool bIsPressed ) +{ +#ifndef _XBOX + if ( !HLTVDirector()->IsActive() ) + return; + + if ( GetTeamNumber() != TEAM_SPECTATOR ) + return; // only pure spectators can play cameraman + + if ( !bIsPressed ) + return; + + bool bIsHLTV = HLTVDirector()->IsActive(); + + if ( bIsHLTV ) + { + int iCameraManIndex = HLTVDirector()->GetCameraMan(); + + if ( iCameraManIndex == 0 ) + { + // turn camera on + HLTVDirector()->SetCameraMan( entindex() ); + } + else if ( iCameraManIndex == entindex() ) + { + // turn camera off + HLTVDirector()->SetCameraMan( 0 ); + } + else + { + ClientPrint( this, HUD_PRINTTALK, "Camera in use by other player." ); + } + } + + /* UTIL_SayText( "Spectator can not USE anything", this ); + + Vector dir,end; + Vector start = GetAbsOrigin(); + + AngleVectors( GetAbsAngles(), &dir ); + VectorNormalize( dir ); + + VectorMA( start, 32.0f, dir, end ); + + trace_t tr; + UTIL_TraceLine( start, end, MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + + if ( tr.fraction == 1.0f ) + return; // no obstacles in spectators way + + VectorMA( start, 128.0f, dir, end ); + + Ray_t ray; + ray.Init( end, start, VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX ); + + UTIL_TraceRay( ray, MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + + if ( tr.startsolid || tr.allsolid ) + return; + + SetAbsOrigin( tr.endpos ); */ +#endif +} + +void CBasePlayer::JumptoPosition(const Vector &origin, const QAngle &angles) +{ + SetAbsOrigin( origin ); + SetAbsVelocity( vec3_origin ); // stop movement + SetLocalAngles( angles ); + SnapEyeAngles( angles ); +} + +bool CBasePlayer::SetObserverTarget(CBaseEntity *target) +{ + if ( !IsValidObserverTarget( target ) ) + return false; + + // set new target + m_hObserverTarget.Set( target ); + + // reset fov to default + SetFOV( this, 0 ); + + if ( m_iObserverMode == OBS_MODE_ROAMING ) + { + Vector dir, end; + Vector start = target->EyePosition(); + + AngleVectors( target->EyeAngles(), &dir ); + VectorNormalize( dir ); + VectorMA( start, -64.0f, dir, end ); + + Ray_t ray; + ray.Init( start, end, VEC_DUCK_HULL_MIN , VEC_DUCK_HULL_MAX ); + + trace_t tr; + UTIL_TraceRay( ray, MASK_PLAYERSOLID, target, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + + JumptoPosition( tr.endpos, target->EyeAngles() ); + } + + return true; +} + +bool CBasePlayer::IsValidObserverTarget(CBaseEntity * target) +{ + if ( target == NULL ) + return false; + + // MOD AUTHORS: Add checks on target here or in derived method + + if ( !target->IsPlayer() ) // only track players + return false; + + CBasePlayer * player = ToBasePlayer( target ); + + /* Don't spec observers or players who haven't picked a class yet + if ( player->IsObserver() ) + return false; */ + + if( player == this ) + return false; // We can't observe ourselves. + + if ( player->IsEffectActive( EF_NODRAW ) ) // don't watch invisible players + return false; + + if ( player->m_lifeState == LIFE_RESPAWNABLE ) // target is dead, waiting for respawn + return false; + + if ( player->m_lifeState == LIFE_DEAD || player->m_lifeState == LIFE_DYING ) + { + if ( (player->m_flDeathTime + DEATH_ANIMATION_TIME ) < gpGlobals->curtime ) + { + return false; // allow watching until 3 seconds after death to see death animation + } + } + + // check forcecamera settings for active players + if ( GetTeamNumber() != TEAM_SPECTATOR ) + { + switch ( mp_forcecamera.GetInt() ) + { + case OBS_ALLOW_ALL : break; + case OBS_ALLOW_TEAM : if ( GetTeamNumber() != target->GetTeamNumber() ) + return false; + break; + case OBS_ALLOW_NONE : return false; + } + } + + return true; // passed all test +} + +int CBasePlayer::GetNextObserverSearchStartPoint( bool bReverse ) +{ + int iDir = bReverse ? -1 : 1; + + int startIndex; + + if ( m_hObserverTarget ) + { + // start using last followed player + startIndex = m_hObserverTarget->entindex(); + } + else + { + // start using own player index + startIndex = this->entindex(); + } + + startIndex += iDir; + if (startIndex > gpGlobals->maxClients) + startIndex = 1; + else if (startIndex < 1) + startIndex = gpGlobals->maxClients; + + return startIndex; +} + +CBaseEntity * CBasePlayer::FindNextObserverTarget(bool bReverse) +{ + // MOD AUTHORS: Modify the logic of this function if you want to restrict the observer to watching + // only a subset of the players. e.g. Make it check the target's team. + +/* if ( m_flNextFollowTime && m_flNextFollowTime > gpGlobals->time ) + { + return; + } + + m_flNextFollowTime = gpGlobals->time + 0.25; + */ // TODO move outside this function + + int startIndex = GetNextObserverSearchStartPoint( bReverse ); + + int currentIndex = startIndex; + int iDir = bReverse ? -1 : 1; + + do + { + CBaseEntity * nextTarget = UTIL_PlayerByIndex( currentIndex ); + + if ( IsValidObserverTarget( nextTarget ) ) + { + return nextTarget; // found next valid player + } + + currentIndex += iDir; + + // Loop through the clients + if (currentIndex > gpGlobals->maxClients) + currentIndex = 1; + else if (currentIndex < 1) + currentIndex = gpGlobals->maxClients; + + } while ( currentIndex != startIndex ); + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if this object can be +used by the player +//----------------------------------------------------------------------------- +bool CBasePlayer::IsUseableEntity( CBaseEntity *pEntity, unsigned int requiredCaps ) +{ + if ( pEntity ) + { + int caps = pEntity->ObjectCaps(); + if ( caps & (FCAP_IMPULSE_USE|FCAP_CONTINUOUS_USE|FCAP_ONOFF_USE|FCAP_DIRECTIONAL_USE) ) + { + if ( (caps & requiredCaps) == requiredCaps ) + { + return true; + } + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBasePlayer::CanPickupObject( CBaseEntity *pObject, float massLimit, float sizeLimit ) +{ + // UNDONE: Make this virtual and move to HL2 player +#ifdef HL2_DLL + //Must be valid + if ( pObject == NULL ) + return false; + + //Must move with physics + if ( pObject->GetMoveType() != MOVETYPE_VPHYSICS ) + return false; + + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pObject->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + + //Must have a physics object + if (!count) + return false; + + float objectMass = 0; + bool checkEnable = false; + for ( int i = 0; i < count; i++ ) + { + objectMass += pList[i]->GetMass(); + if ( !pList[i]->IsMoveable() ) + { + checkEnable = true; + } + if ( pList[i]->GetGameFlags() & FVPHYSICS_NO_PLAYER_PICKUP ) + return false; + if ( pList[i]->IsHinged() ) + return false; + } + + + //Msg( "Target mass: %f\n", pPhys->GetMass() ); + + //Must be under our threshold weight + if ( massLimit > 0 && objectMass > massLimit ) + return false; + + if ( checkEnable ) + { + // Allowing picking up of bouncebombs. + CBounceBomb *pBomb = dynamic_cast(pObject); + if( pBomb ) + return true; + + // Allow pickup of phys props that are motion enabled on player pickup + CPhysicsProp *pProp = dynamic_cast(pObject); + CPhysBox *pBox = dynamic_cast(pObject); + if ( !pProp && !pBox ) + return false; + + if ( pProp && !(pProp->HasSpawnFlags( SF_PHYSPROP_ENABLE_ON_PHYSCANNON )) ) + return false; + + if ( pBox && !(pBox->HasSpawnFlags( SF_PHYSBOX_ENABLE_ON_PHYSCANNON )) ) + return false; + } + + if ( sizeLimit > 0 ) + { + const Vector &size = pObject->CollisionProp()->OBBSize(); + if ( size.x > sizeLimit || size.y > sizeLimit || size.z > sizeLimit ) + return false; + } + + return true; +#else + return false; +#endif +} + +float CBasePlayer::GetHeldObjectMass( IPhysicsObject *pHeldObject ) +{ + return 0; +} + +CBaseEntity *CBasePlayer::GetHeldObject( void ) +{ + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Server side of jumping rules. Most jumping logic is already +// handled in shared gamemovement code. Put stuff here that should +// only be done server side. +//----------------------------------------------------------------------------- +void CBasePlayer::Jump() +{ +} + +void CBasePlayer::Duck( ) +{ + if (m_nButtons & IN_DUCK) + { + if ( m_Activity != ACT_LEAP ) + { + SetAnimation( PLAYER_WALK ); + } + } +} + +// +// ID's player as such. +// +Class_T CBasePlayer::Classify ( void ) +{ + return CLASS_PLAYER; +} + + +void CBasePlayer::ResetFragCount() +{ + m_iFrags = 0; + pl.frags = m_iFrags; +} + +void CBasePlayer::IncrementFragCount( int nCount ) +{ + m_iFrags += nCount; + pl.frags = m_iFrags; +} + +void CBasePlayer::ResetDeathCount() +{ + m_iDeaths = 0; + pl.deaths = m_iDeaths; +} + +void CBasePlayer::IncrementDeathCount( int nCount ) +{ + m_iDeaths += nCount; + pl.deaths = m_iDeaths; +} + +void CBasePlayer::AddPoints( int score, bool bAllowNegativeScore ) +{ + // Positive score always adds + if ( score < 0 ) + { + if ( !bAllowNegativeScore ) + { + if ( m_iFrags < 0 ) // Can't go more negative + return; + + if ( -score > m_iFrags ) // Will this go negative? + { + score = -m_iFrags; // Sum will be 0 + } + } + } + + m_iFrags += score; + pl.frags = m_iFrags; +} + +void CBasePlayer::AddPointsToTeam( int score, bool bAllowNegativeScore ) +{ + if ( GetTeam() ) + { + GetTeam()->AddScore( score ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : int +//----------------------------------------------------------------------------- +int CBasePlayer::GetCommandContextCount( void ) const +{ + return m_CommandContext.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// Output : CCommandContext +//----------------------------------------------------------------------------- +CCommandContext *CBasePlayer::GetCommandContext( int index ) +{ + if ( index < 0 || index >= m_CommandContext.Count() ) + return NULL; + + return &m_CommandContext[ index ]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CCommandContext *CBasePlayer::AllocCommandContext( void ) +{ + int idx = m_CommandContext.AddToTail(); + if ( m_CommandContext.Count() > 1000 ) + { + Assert( 0 ); + } + return &m_CommandContext[ idx ]; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +//----------------------------------------------------------------------------- +void CBasePlayer::RemoveCommandContext( int index ) +{ + m_CommandContext.Remove( index ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::RemoveAllCommandContexts() +{ + m_CommandContext.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes all existing contexts, but leaves the last one around ( or creates it if it doesn't exist -- which would be a bug ) +//----------------------------------------------------------------------------- +CCommandContext *CBasePlayer::RemoveAllCommandContextsExceptNewest( void ) +{ + int count = m_CommandContext.Count(); + int toRemove = count - 1; + if ( toRemove > 0 ) + { + m_CommandContext.RemoveMultiple( 0, toRemove ); + } + + if ( !m_CommandContext.Count() ) + { + Assert( 0 ); + CCommandContext *ctx = AllocCommandContext(); + Q_memset( ctx, 0, sizeof( *ctx ) ); + } + + return &m_CommandContext[ 0 ]; +} + +//----------------------------------------------------------------------------- +// Purpose: Replaces the first nCommands CUserCmds in the context with the ones passed in -- this is used to help meter out CUserCmds over the number of simulation ticks on the server +//----------------------------------------------------------------------------- +void CBasePlayer::ReplaceContextCommands( CCommandContext *ctx, CUserCmd *pCommands, int nCommands ) +{ + // Blow away all of the commands + ctx->cmds.RemoveAll(); + + ctx->numcmds = nCommands; + ctx->totalcmds = nCommands; + ctx->dropped_packets = 0; // meaningless in this context + + // Add them in so the most recent is at slot 0 + for ( int i = nCommands - 1; i >= 0; --i ) + { + ctx->cmds.AddToTail( pCommands[ i ] ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Determine how much time we will be running this frame +// Output : float +//----------------------------------------------------------------------------- +int CBasePlayer::DetermineSimulationTicks( void ) +{ + int command_context_count = GetCommandContextCount(); + + int context_number; + + int simulation_ticks = 0; + + // Determine how much time we will be running this frame and fixup player clock as needed + for ( context_number = 0; context_number < command_context_count; context_number++ ) + { + CCommandContext const *ctx = GetCommandContext( context_number ); + Assert( ctx ); + Assert( ctx->numcmds > 0 ); + Assert( ctx->dropped_packets >= 0 ); + + // Determine how long it will take to run those packets + simulation_ticks += ctx->numcmds + ctx->dropped_packets; + } + + return simulation_ticks; +} + +// 2 ticks ahead or behind current clock means we need to fix clock on client +static ConVar sv_clockcorrection_msecs( "sv_clockcorrection_msecs", "60", 0, "The server tries to keep each player's m_nTickBase withing this many msecs of the server absolute tickcount" ); +static ConVar sv_playerperfhistorycount( "sv_playerperfhistorycount", "60", 0, "Number of samples to maintain in player perf history", true, 1.0f, true, 128.0 ); + +//----------------------------------------------------------------------------- +// Purpose: Based upon amount of time in simulation time, adjust m_nTickBase so that +// we just end at the end of the current frame (so the player is basically on clock +// with the server) +// Input : simulation_ticks - +//----------------------------------------------------------------------------- +void CBasePlayer::AdjustPlayerTimeBase( int simulation_ticks ) +{ + Assert( simulation_ticks >= 0 ); + if ( simulation_ticks < 0 ) + return; + + CPlayerSimInfo *pi = NULL; + if ( sv_playerperfhistorycount.GetInt() > 0 ) + { + while ( m_vecPlayerSimInfo.Count() > sv_playerperfhistorycount.GetInt() ) + { + m_vecPlayerSimInfo.Remove( m_vecPlayerSimInfo.Head() ); + } + + pi = &m_vecPlayerSimInfo[ m_vecPlayerSimInfo.AddToTail() ]; + } + + // Start in the past so that we get to the sv.time that we'll hit at the end of the + // frame, just as we process the final command + + if ( gpGlobals->maxClients == 1 ) + { + // set TickBase so that player simulation tick matches gpGlobals->tickcount after + // all commands have been executed + m_nTickBase = gpGlobals->tickcount - simulation_ticks + gpGlobals->simTicksThisFrame; + } + else // multiplayer + { + float flCorrectionSeconds = clamp( sv_clockcorrection_msecs.GetFloat() / 1000.0f, 0.0f, 1.0f ); + int nCorrectionTicks = TIME_TO_TICKS( flCorrectionSeconds ); + + // Set the target tick flCorrectionSeconds (rounded to ticks) ahead in the future. this way the client can + // alternate around this target tick without getting smaller than gpGlobals->tickcount. + // After running the commands simulation time should be equal or after current gpGlobals->tickcount, + // otherwise the simulation time drops out of the client side interpolated var history window. + + int nIdealFinalTick = gpGlobals->tickcount + nCorrectionTicks; + + int nEstimatedFinalTick = m_nTickBase + simulation_ticks; + + // If client gets ahead of this, we'll need to correct + int too_fast_limit = nIdealFinalTick + nCorrectionTicks; + // If client falls behind this, we'll also need to correct + int too_slow_limit = nIdealFinalTick - nCorrectionTicks; + + // See if we are too fast + if ( nEstimatedFinalTick > too_fast_limit || + nEstimatedFinalTick < too_slow_limit ) + { + int nCorrectedTick = nIdealFinalTick - simulation_ticks + gpGlobals->simTicksThisFrame; + + if ( pi ) + { + pi->m_nTicksCorrected = nCorrectionTicks; + } + + m_nTickBase = nCorrectedTick; + } + } + + if ( pi ) + { + pi->m_flFinalSimulationTime = TICKS_TO_TIME( m_nTickBase + simulation_ticks + gpGlobals->simTicksThisFrame ); + } +} + +void CBasePlayer::RunNullCommand( void ) +{ + CUserCmd cmd; // NULL command + + // Store off the globals.. they're gonna get whacked + float flOldFrametime = gpGlobals->frametime; + float flOldCurtime = gpGlobals->curtime; + + pl.fixangle = FIXANGLE_NONE; + + if ( IsReplay() ) + { + cmd.viewangles = QAngle( 0, 0, 0 ); + } + else + { + cmd.viewangles = EyeAngles(); + } + + float flTimeBase = gpGlobals->curtime; + SetTimeBase( flTimeBase ); + + MoveHelperServer()->SetHost( this ); + PlayerRunCommand( &cmd, MoveHelperServer() ); + + // save off the last good usercmd + SetLastUserCommand( cmd ); + + // Restore the globals.. + gpGlobals->frametime = flOldFrametime; + gpGlobals->curtime = flOldCurtime; + + MoveHelperServer()->SetHost( NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: Note, don't chain to BaseClass::PhysicsSimulate +//----------------------------------------------------------------------------- +void CBasePlayer::PhysicsSimulate( void ) +{ + VPROF_BUDGET( "CBasePlayer::PhysicsSimulate", VPROF_BUDGETGROUP_PLAYER ); + + // If we've got a moveparent, we must simulate that first. + CBaseEntity *pMoveParent = GetMoveParent(); + if (pMoveParent) + { + pMoveParent->PhysicsSimulate(); + } + + // Make sure not to simulate this guy twice per frame + if ( m_nSimulationTick == gpGlobals->tickcount ) + { + return; + } + + m_nSimulationTick = gpGlobals->tickcount; + + // See how many CUserCmds are queued up for running + int simulation_ticks = DetermineSimulationTicks(); + + // If some time will elapse, make sure our clock (m_nTickBase) starts at the correct time + if ( simulation_ticks > 0 ) + { + AdjustPlayerTimeBase( simulation_ticks ); + } + + if ( IsHLTV() || IsReplay() ) + { + // just run a single, empty command to make sure + // all PreThink/PostThink functions are called as usual + Assert ( GetCommandContextCount() == 0 ); + RunNullCommand(); + RemoveAllCommandContexts(); + return; + } + + // Store off true server timestamps + float savetime = gpGlobals->curtime; + float saveframetime = gpGlobals->frametime; + + int command_context_count = GetCommandContextCount(); + + + // Build a list of all available commands + CUtlVector< CUserCmd > vecAvailCommands; + + // Contexts go from oldest to newest + for ( int context_number = 0; context_number < command_context_count; context_number++ ) + { + // Get oldest ( newer are added to tail ) + CCommandContext *ctx = GetCommandContext( context_number ); + if ( !ShouldRunCommandsInContext( ctx ) ) + continue; + + if ( !ctx->cmds.Count() ) + continue; + + int numbackup = ctx->totalcmds - ctx->numcmds; + + // If we haven't dropped too many packets, then run some commands + if ( ctx->dropped_packets < 24 ) + { + int droppedcmds = ctx->dropped_packets; + + // run the last known cmd for each dropped cmd we don't have a backup for + while ( droppedcmds > numbackup ) + { + m_LastCmd.tick_count++; + vecAvailCommands.AddToTail( m_LastCmd ); + droppedcmds--; + } + + // Now run the "history" commands if we still have dropped packets + while ( droppedcmds > 0 ) + { + int cmdnum = ctx->numcmds + droppedcmds - 1; + vecAvailCommands.AddToTail( ctx->cmds[cmdnum] ); + droppedcmds--; + } + } + + // Now run any new command(s). Go backward because the most recent command is at index 0. + for ( int i = ctx->numcmds - 1; i >= 0; i-- ) + { + vecAvailCommands.AddToTail( ctx->cmds[i] ); + } + + // Save off the last good command in case we drop > numbackup packets and need to rerun them + // we'll use this to "guess" at what was in the missing packets + m_LastCmd = ctx->cmds[ CMD_MOSTRECENT ]; + } + + // gpGlobals->simTicksThisFrame == number of ticks remaining to be run, so we should take the last N CUserCmds and postpone them until the next frame + + // If we're running multiple ticks this frame, don't peel off all of the commands, spread them out over + // the server ticks. Use blocks of two in alternate ticks + int commandLimit = CBaseEntity::IsSimulatingOnAlternateTicks() ? 2 : 1; + int commandsToRun = vecAvailCommands.Count(); + if ( gpGlobals->simTicksThisFrame >= commandLimit && vecAvailCommands.Count() > commandLimit ) + { + int commandsToRollOver = MIN( vecAvailCommands.Count(), ( gpGlobals->simTicksThisFrame - 1 ) ); + commandsToRun = vecAvailCommands.Count() - commandsToRollOver; + Assert( commandsToRun >= 0 ); + // Clear all contexts except the last one + if ( commandsToRollOver > 0 ) + { + CCommandContext *ctx = RemoveAllCommandContextsExceptNewest(); + ReplaceContextCommands( ctx, &vecAvailCommands[ commandsToRun ], commandsToRollOver ); + } + else + { + // Clear all contexts + RemoveAllCommandContexts(); + } + } + else + { + // Clear all contexts + RemoveAllCommandContexts(); + } + + float vphysicsArrivalTime = TICK_INTERVAL; + +#ifdef _DEBUG + if ( sv_player_net_suppress_usercommands.GetBool() ) + { + commandsToRun = 0; + } +#endif // _DEBUG + + int numUsrCmdProcessTicksMax = sv_maxusrcmdprocessticks.GetInt(); + if ( gpGlobals->maxClients != 1 && numUsrCmdProcessTicksMax ) // don't apply this filter in SP games + { + // Grant the client some time buffer to execute user commands + m_flMovementTimeForUserCmdProcessingRemaining += TICK_INTERVAL; + + // but never accumulate more than N ticks + if ( m_flMovementTimeForUserCmdProcessingRemaining > numUsrCmdProcessTicksMax * TICK_INTERVAL ) + m_flMovementTimeForUserCmdProcessingRemaining = numUsrCmdProcessTicksMax * TICK_INTERVAL; + } + else + { + // Otherwise we don't care to track time + m_flMovementTimeForUserCmdProcessingRemaining = FLT_MAX; + } + + // Now run the commands + if ( commandsToRun > 0 ) + { + m_flLastUserCommandTime = savetime; + + MoveHelperServer()->SetHost( this ); + + // Suppress predicted events, etc. + if ( IsPredictingWeapons() ) + { + IPredictionSystem::SuppressHostEvents( this ); + } + + for ( int i = 0; i < commandsToRun; ++i ) + { + PlayerRunCommand( &vecAvailCommands[ i ], MoveHelperServer() ); + + // Update our vphysics object. + if ( m_pPhysicsController ) + { + VPROF( "CBasePlayer::PhysicsSimulate-UpdateVPhysicsPosition" ); + // If simulating at 2 * TICK_INTERVAL, add an extra TICK_INTERVAL to position arrival computation + UpdateVPhysicsPosition( m_vNewVPhysicsPosition, m_vNewVPhysicsVelocity, vphysicsArrivalTime ); + vphysicsArrivalTime += TICK_INTERVAL; + } + } + + // Always reset after running commands + IPredictionSystem::SuppressHostEvents( NULL ); + + MoveHelperServer()->SetHost( NULL ); + + // Copy in final origin from simulation + CPlayerSimInfo *pi = NULL; + if ( m_vecPlayerSimInfo.Count() > 0 ) + { + pi = &m_vecPlayerSimInfo[ m_vecPlayerSimInfo.Tail() ]; + pi->m_flTime = Plat_FloatTime(); + pi->m_vecAbsOrigin = GetAbsOrigin(); + pi->m_flGameSimulationTime = gpGlobals->curtime; + pi->m_nNumCmds = commandsToRun; + } + } + + // Restore the true server clock + // FIXME: Should this occur after simulation of children so + // that they are in the timespace of the player? + gpGlobals->curtime = savetime; + gpGlobals->frametime = saveframetime; + +// // Kick the player if they haven't sent a user command in awhile in order to prevent clients +// // from using packet-level manipulation to mess with gamestate. Not sending usercommands seems +// // to have all kinds of bad effects, such as stalling a bunch of Think()'s and gamestate handling. +// // An example from TF: A medic stops sending commands after deploying an uber on another player. +// // As a result, invuln is permanently on the heal target because the maintenance code is stalled. +// if ( GetTimeSinceLastUserCommand() > player_usercommand_timeout.GetFloat() ) +// { +// // If they have an active netchan, they're almost certainly messing with usercommands? +// INetChannelInfo *pNetChanInfo = engine->GetPlayerNetInfo( entindex() ); +// if ( pNetChanInfo && pNetChanInfo->GetTimeSinceLastReceived() < 5.f ) +// { +// engine->ServerCommand( UTIL_VarArgs( "kickid %d %s\n", GetUserID(), "UserCommand Timeout" ) ); +// } +// } +} + +unsigned int CBasePlayer::PhysicsSolidMaskForEntity() const +{ + return MASK_PLAYERSOLID; +} + +//----------------------------------------------------------------------------- +// Purpose: This will force usercmd processing to actually consume commands even if the global tick counter isn't incrementing +//----------------------------------------------------------------------------- +void CBasePlayer::ForceSimulation() +{ + m_nSimulationTick = -1; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *buf - +// totalcmds - +// dropped_packets - +// ignore - +// paused - +// Output : float -- Time in seconds of last movement command +//----------------------------------------------------------------------------- +void CBasePlayer::ProcessUsercmds( CUserCmd *cmds, int numcmds, int totalcmds, + int dropped_packets, bool paused ) +{ + CCommandContext *ctx = AllocCommandContext(); + Assert( ctx ); + + int i; + for ( i = totalcmds - 1; i >= 0; i-- ) + { + CUserCmd *pCmd = &cmds[totalcmds - 1 - i]; + + // Validate values + if ( !IsUserCmdDataValid( pCmd ) ) + { + pCmd->MakeInert(); + } + + ctx->cmds.AddToTail( *pCmd ); + } + ctx->numcmds = numcmds; + ctx->totalcmds = totalcmds, + ctx->dropped_packets = dropped_packets; + ctx->paused = paused; + + // If the server is paused, zero out motion,buttons,view changes + if ( ctx->paused ) + { + bool clear_angles = true; + + // If no clipping and cheats enabled and sv_noclipduringpause enabled, then don't zero out movement part of CUserCmd + if ( GetMoveType() == MOVETYPE_NOCLIP && + sv_cheats->GetBool() && + sv_noclipduringpause.GetBool() ) + { + clear_angles = false; + } + + for ( i = 0; i < ctx->numcmds; i++ ) + { + ctx->cmds[ i ].buttons = 0; + if ( clear_angles ) + { + ctx->cmds[ i ].forwardmove = 0; + ctx->cmds[ i ].sidemove = 0; + ctx->cmds[ i ].upmove = 0; + VectorCopy ( pl.v_angle, ctx->cmds[ i ].viewangles ); + } + } + + ctx->dropped_packets = 0; + } + + // Set global pause state for this player + m_bGamePaused = paused; + + if ( paused ) + { + ForceSimulation(); + // Just run the commands right away if paused + PhysicsSimulate(); + } + + if ( sv_playerperfhistorycount.GetInt() > 0 ) + { + CPlayerCmdInfo pi; + pi.m_flTime = Plat_FloatTime(); + pi.m_nDroppedPackets = dropped_packets; + pi.m_nNumCmds = numcmds; + + while ( m_vecPlayerCmdInfo.Count() >= sv_playerperfhistorycount.GetInt() ) + { + m_vecPlayerCmdInfo.Remove( m_vecPlayerCmdInfo.Head() ); + } + + m_vecPlayerCmdInfo.AddToTail( pi ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Check that command values are reasonable +//----------------------------------------------------------------------------- +bool CBasePlayer::IsUserCmdDataValid( CUserCmd *pCmd ) +{ + if ( IsBot() || IsFakeClient() ) + return true; + + // Maximum difference between client's and server's tick_count + const int nCmdMaxTickDelta = ( 1.f / gpGlobals->interval_per_tick ) * 2.5f; + const int nMinDelta = Max( 0, gpGlobals->tickcount - nCmdMaxTickDelta ); + const int nMaxDelta = gpGlobals->tickcount + nCmdMaxTickDelta; + + bool bValid = ( pCmd->tick_count >= nMinDelta && pCmd->tick_count < nMaxDelta ) && + // Prevent clients from sending invalid view angles to try to get leaf server code to crash + ( pCmd->viewangles.IsValid() && IsEntityQAngleReasonable( pCmd->viewangles ) ) && + // Movement ranges + ( IsFinite( pCmd->forwardmove ) && IsEntityCoordinateReasonable( pCmd->forwardmove ) ) && + ( IsFinite( pCmd->sidemove ) && IsEntityCoordinateReasonable( pCmd->sidemove ) ) && + ( IsFinite( pCmd->upmove ) && IsEntityCoordinateReasonable( pCmd->upmove ) ); + + int nWarningLevel = sv_player_display_usercommand_errors.GetInt(); + if ( !bValid && nWarningLevel > 0 ) + { + DevMsg( "UserCommand out-of-range for userid %i\n", GetUserID() ); + + if ( nWarningLevel == 2 ) + { + DevMsg( " tick_count: %i\n viewangles: %5.2f %5.2f %5.2f \n forward: %5.2f \n side: \t%5.2f \n up: \t%5.2f\n", + pCmd->tick_count, + pCmd->viewangles.x, + pCmd->viewangles.y, + pCmd->viewangles.x, + pCmd->forwardmove, + pCmd->sidemove, + pCmd->upmove ); + } + } + + return bValid; +} + +void CBasePlayer::DumpPerfToRecipient( CBasePlayer *pRecipient, int nMaxRecords ) +{ + if ( !pRecipient ) + return; + + char buf[ 256 ] = { 0 }; + int curpos = 0; + + int nDumped = 0; + Vector prevo( 0, 0, 0 ); + float prevt = 0.0f; + + for ( int i = m_vecPlayerSimInfo.Tail(); i != m_vecPlayerSimInfo.InvalidIndex() ; i = m_vecPlayerSimInfo.Previous( i ) ) + { + const CPlayerSimInfo *pi = &m_vecPlayerSimInfo[ i ]; + + float vel = 0.0f; + + // Note we're walking from newest backward + float dt = prevt - pi->m_flFinalSimulationTime; + if ( nDumped > 0 && dt > 0.0f ) + { + Vector d = pi->m_vecAbsOrigin - prevo; + vel = d.Length() / dt; + } + + char line[ 128 ]; + int len = Q_snprintf( line, sizeof( line ), "%.3f %d %d %.3f %.3f vel %.2f\n", + pi->m_flTime, + pi->m_nNumCmds, + pi->m_nTicksCorrected, + pi->m_flFinalSimulationTime, + pi->m_flGameSimulationTime, + vel ); + + if ( curpos + len > 200 ) + { + ClientPrint( pRecipient, HUD_PRINTCONSOLE, (char const *)buf ); + buf[ 0 ] = 0; + curpos = 0; + } + + Q_strncpy( &buf[ curpos ], line, sizeof( buf ) - curpos ); + curpos += len; + + ++nDumped; + if ( nMaxRecords != -1 && nDumped >= nMaxRecords ) + break; + + prevo = pi->m_vecAbsOrigin; + prevt = pi->m_flFinalSimulationTime; + } + + if ( curpos > 0 ) + { + ClientPrint( pRecipient, HUD_PRINTCONSOLE, buf ); + } + + nDumped = 0; + curpos = 0; + + for ( int i = m_vecPlayerCmdInfo.Tail(); i != m_vecPlayerCmdInfo.InvalidIndex() ; i = m_vecPlayerCmdInfo.Previous( i ) ) + { + const CPlayerCmdInfo *pi = &m_vecPlayerCmdInfo[ i ]; + + char line[ 128 ]; + int len = Q_snprintf( line, sizeof( line ), "%.3f %d %d\n", + pi->m_flTime, + pi->m_nNumCmds, + pi->m_nDroppedPackets ); + + if ( curpos + len > 200 ) + { + ClientPrint( pRecipient, HUD_PRINTCONSOLE, (char const *)buf ); + buf[ 0 ] = 0; + curpos = 0; + } + + Q_strncpy( &buf[ curpos ], line, sizeof( buf ) - curpos ); + curpos += len; + + ++nDumped; + if ( nMaxRecords != -1 && nDumped >= nMaxRecords ) + break; + } + + if ( curpos > 0 ) + { + ClientPrint( pRecipient, HUD_PRINTCONSOLE, buf ); + } +} + +// Duck debouncing code to stop menu changes from disallowing crouch/uncrouch +ConVar xc_crouch_debounce( "xc_crouch_debounce", "0", FCVAR_NONE ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *ucmd - +// *moveHelper - +//----------------------------------------------------------------------------- +void CBasePlayer::PlayerRunCommand(CUserCmd *ucmd, IMoveHelper *moveHelper) +{ + m_touchedPhysObject = false; + + if ( pl.fixangle == FIXANGLE_NONE) + { + VectorCopy ( ucmd->viewangles, pl.v_angle ); + } + + // Handle FL_FROZEN. + // Prevent player moving for some seconds after New Game, so that they pick up everything + if( GetFlags() & FL_FROZEN || + (developer.GetInt() == 0 && gpGlobals->eLoadType == MapLoad_NewGame && gpGlobals->curtime < 3.0 ) ) + { + ucmd->forwardmove = 0; + ucmd->sidemove = 0; + ucmd->upmove = 0; + ucmd->buttons = 0; + ucmd->impulse = 0; + VectorCopy ( pl.v_angle, ucmd->viewangles ); + } + else + { + // Force a duck if we're toggled + if ( GetToggledDuckState() ) + { + // If this is set, we've altered our menu options and need to debounce the duck + if ( xc_crouch_debounce.GetBool() ) + { + ToggleDuck(); + + // Mark it as handled + xc_crouch_debounce.SetValue( 0 ); + } + else + { + ucmd->buttons |= IN_DUCK; + } + } + } + +#ifdef MAPBASE_VSCRIPT + // Movement hook for VScript + if (m_ScriptScope.IsInitialized() && g_Hook_PlayerRunCommand.CanRunInScope(m_ScriptScope)) + { + HSCRIPT hCmd = g_pScriptVM->RegisterInstance( ucmd ); + + // command + ScriptVariant_t args[] = { hCmd }; + g_Hook_PlayerRunCommand.Call( m_ScriptScope, NULL, args ); + + g_pScriptVM->RemoveInstance( hCmd ); + } +#endif + + PlayerMove()->RunCommand(this, ucmd, moveHelper); +} + +//----------------------------------------------------------------------------- +// Purpose: Strips off IN_xxx flags from the player's input +//----------------------------------------------------------------------------- +void CBasePlayer::DisableButtons( int nButtons ) +{ + m_afButtonDisabled |= nButtons; +} + +//----------------------------------------------------------------------------- +// Purpose: Re-enables stripped IN_xxx flags to the player's input +//----------------------------------------------------------------------------- +void CBasePlayer::EnableButtons( int nButtons ) +{ + m_afButtonDisabled &= ~nButtons; +} + +//----------------------------------------------------------------------------- +// Purpose: Strips off IN_xxx flags from the player's input +//----------------------------------------------------------------------------- +void CBasePlayer::ForceButtons( int nButtons ) +{ + m_afButtonForced |= nButtons; +} + +//----------------------------------------------------------------------------- +// Purpose: Re-enables stripped IN_xxx flags to the player's input +//----------------------------------------------------------------------------- +void CBasePlayer::UnforceButtons( int nButtons ) +{ + m_afButtonForced &= ~nButtons; +} + +void CBasePlayer::HandleFuncTrain(void) +{ + if ( m_afPhysicsFlags & PFLAG_DIROVERRIDE ) + AddFlag( FL_ONTRAIN ); + else + RemoveFlag( FL_ONTRAIN ); + + // Train speed control + if (( m_afPhysicsFlags & PFLAG_DIROVERRIDE ) == 0) + { + if (m_iTrain & TRAIN_ACTIVE) + { + m_iTrain = TRAIN_NEW; // turn off train + } + return; + } + + CBaseEntity *pTrain = GetGroundEntity(); + float vel; + + if ( pTrain ) + { + if ( !(pTrain->ObjectCaps() & FCAP_DIRECTIONAL_USE) ) + pTrain = NULL; + } + + if ( !pTrain ) + { + if ( GetActiveWeapon()->ObjectCaps() & FCAP_DIRECTIONAL_USE ) + { + m_iTrain = TRAIN_ACTIVE | TRAIN_NEW; + + if ( m_nButtons & IN_FORWARD ) + { + m_iTrain |= TRAIN_FAST; + } + else if ( m_nButtons & IN_BACK ) + { + m_iTrain |= TRAIN_BACK; + } + else + { + m_iTrain |= TRAIN_NEUTRAL; + } + return; + } + else + { + trace_t trainTrace; + // Maybe this is on the other side of a level transition + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector(0,0,-38), + MASK_PLAYERSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &trainTrace ); + + if ( trainTrace.fraction != 1.0 && trainTrace.m_pEnt ) + pTrain = trainTrace.m_pEnt; + + + if ( !pTrain || !(pTrain->ObjectCaps() & FCAP_DIRECTIONAL_USE) || !pTrain->OnControls(this) ) + { + m_afPhysicsFlags &= ~PFLAG_DIROVERRIDE; + m_iTrain = TRAIN_NEW|TRAIN_OFF; + return; + } + } + } + else if ( !( GetFlags() & FL_ONGROUND ) || pTrain->HasSpawnFlags( SF_TRACKTRAIN_NOCONTROL ) || (m_nButtons & (IN_MOVELEFT|IN_MOVERIGHT) ) ) + { + // Turn off the train if you jump, strafe, or the train controls go dead + m_afPhysicsFlags &= ~PFLAG_DIROVERRIDE; + m_iTrain = TRAIN_NEW|TRAIN_OFF; + return; + } + + SetAbsVelocity( vec3_origin ); + vel = 0; + if ( m_afButtonPressed & IN_FORWARD ) + { + vel = 1; + pTrain->Use( this, this, USE_SET, (float)vel ); + } + else if ( m_afButtonPressed & IN_BACK ) + { + vel = -1; + pTrain->Use( this, this, USE_SET, (float)vel ); + } + + if (vel) + { + m_iTrain = TrainSpeed(pTrain->m_flSpeed, ((CFuncTrackTrain*)pTrain)->GetMaxSpeed()); + m_iTrain |= TRAIN_ACTIVE|TRAIN_NEW; + } +} + + +void CBasePlayer::PreThink(void) +{ + if ( g_fGameOver || m_iPlayerLocked ) + return; // intermission or finale + + if ( Hints() ) + { + Hints()->Update(); + } + + ItemPreFrame( ); + WaterMove(); + + if ( g_pGameRules && g_pGameRules->FAllowFlashlight() ) + m_Local.m_iHideHUD &= ~HIDEHUD_FLASHLIGHT; + else + m_Local.m_iHideHUD |= HIDEHUD_FLASHLIGHT; + + // checks if new client data (for HUD and view control) needs to be sent to the client + UpdateClientData(); + + CheckTimeBasedDamage(); + + CheckSuitUpdate(); + + if ( GetObserverMode() > OBS_MODE_FREEZECAM ) + { + CheckObserverSettings(); // do this each frame + } + + if ( m_lifeState >= LIFE_DYING ) + { + // track where we are in the nav mesh even when dead + UpdateLastKnownArea(); + return; + } + + HandleFuncTrain(); + + if (m_nButtons & IN_JUMP) + { + // If on a ladder, jump off the ladder + // else Jump + Jump(); + } + + // If trying to duck, already ducked, or in the process of ducking + if ((m_nButtons & IN_DUCK) || (GetFlags() & FL_DUCKING) || (m_afPhysicsFlags & PFLAG_DUCKING) ) + Duck(); + + // + // If we're not on the ground, we're falling. Update our falling velocity. + // + if ( !( GetFlags() & FL_ONGROUND ) ) + { + m_Local.m_flFallVelocity = -GetAbsVelocity().z; + } + + // track where we are in the nav mesh + UpdateLastKnownArea(); + + + // StudioFrameAdvance( );//!!!HACKHACK!!! Can't be hit by traceline when not animating? +} + + +/* Time based Damage works as follows: + 1) There are several types of timebased damage: + + #define DMG_PARALYZE (1 << 14) // slows affected creature down + #define DMG_NERVEGAS (1 << 15) // nerve toxins, very bad + #define DMG_POISON (1 << 16) // blood poisioning + #define DMG_RADIATION (1 << 17) // radiation exposure + #define DMG_DROWNRECOVER (1 << 18) // drown recovery + #define DMG_ACID (1 << 19) // toxic chemicals or acid burns + #define DMG_SLOWBURN (1 << 20) // in an oven + + 2) A new hit inflicting tbd restarts the tbd counter - each NPC has an 8bit counter, + per damage type. The counter is decremented every second, so the maximum time + an effect will last is 255/60 = 4.25 minutes. Of course, staying within the radius + of a damaging effect like fire, nervegas, radiation will continually reset the counter to max. + + 3) Every second that a tbd counter is running, the player takes damage. The damage + is determined by the type of tdb. + Paralyze - 1/2 movement rate, 30 second duration. + Nervegas - 5 points per second, 16 second duration = 80 points max dose. + Poison - 2 points per second, 25 second duration = 50 points max dose. + Radiation - 1 point per second, 50 second duration = 50 points max dose. + Drown - 5 points per second, 2 second duration. + Acid/Chemical - 5 points per second, 10 second duration = 50 points max. + Burn - 10 points per second, 2 second duration. + Freeze - 3 points per second, 10 second duration = 30 points max. + + 4) Certain actions or countermeasures counteract the damaging effects of tbds: + + Armor/Heater/Cooler - Chemical(acid),burn, freeze all do damage to armor power, then to body + - recharged by suit recharger + Air In Lungs - drowning damage is done to air in lungs first, then to body + - recharged by poking head out of water + - 10 seconds if swiming fast + Air In SCUBA - drowning damage is done to air in tanks first, then to body + - 2 minutes in tanks. Need new tank once empty. + Radiation Syringe - Each syringe full provides protection vs one radiation dosage + Antitoxin Syringe - Each syringe full provides protection vs one poisoning (nervegas or poison). + Health kit - Immediate stop to acid/chemical, fire or freeze damage. + Radiation Shower - Immediate stop to radiation damage, acid/chemical or fire damage. + + +*/ + +// If player is taking time based damage, continue doing damage to player - +// this simulates the effect of being poisoned, gassed, dosed with radiation etc - +// anything that continues to do damage even after the initial contact stops. +// Update all time based damage counters, and shut off any that are done. + +// The m_bitsDamageType bit MUST be set if any damage is to be taken. +// This routine will detect the initial on value of the m_bitsDamageType +// and init the appropriate counter. Only processes damage every second. + +//#define PARALYZE_DURATION 30 // number of 2 second intervals to take damage +//#define PARALYZE_DAMAGE 0.0 // damage to take each 2 second interval + +//#define NERVEGAS_DURATION 16 +//#define NERVEGAS_DAMAGE 5.0 + +//#define POISON_DURATION 25 +//#define POISON_DAMAGE 2.0 + +//#define RADIATION_DURATION 50 +//#define RADIATION_DAMAGE 1.0 + +//#define ACID_DURATION 10 +//#define ACID_DAMAGE 5.0 + +//#define SLOWBURN_DURATION 2 +//#define SLOWBURN_DAMAGE 1.0 + +//#define SLOWFREEZE_DURATION 1.0 +//#define SLOWFREEZE_DAMAGE 3.0 + +/* */ + + +void CBasePlayer::CheckTimeBasedDamage() +{ + int i; + byte bDuration = 0; + + static float gtbdPrev = 0.0; + + // If we don't have any time based damage return. + if ( !g_pGameRules->Damage_IsTimeBased( m_bitsDamageType ) ) + return; + + // only check for time based damage approx. every 2 seconds + if ( abs( gpGlobals->curtime - m_tbdPrev ) < 2.0 ) + return; + + m_tbdPrev = gpGlobals->curtime; + + for (i = 0; i < CDMG_TIMEBASED; i++) + { + // Make sure the damage type is really time-based. + // This is kind of hacky but necessary until we setup DamageType as an enum. + int iDamage = ( DMG_PARALYZE << i ); + if ( !g_pGameRules->Damage_IsTimeBased( iDamage ) ) + continue; + + + // make sure bit is set for damage type + if ( m_bitsDamageType & iDamage ) + { + switch (i) + { + case itbd_Paralyze: + // UNDONE - flag movement as half-speed + bDuration = PARALYZE_DURATION; + break; + case itbd_NerveGas: +// OnTakeDamage(pev, pev, NERVEGAS_DAMAGE, DMG_GENERIC); + bDuration = NERVEGAS_DURATION; + break; +// case itbd_Poison: +// OnTakeDamage( CTakeDamageInfo( this, this, POISON_DAMAGE, DMG_GENERIC ) ); +// bDuration = POISON_DURATION; +// break; + case itbd_Radiation: +// OnTakeDamage(pev, pev, RADIATION_DAMAGE, DMG_GENERIC); + bDuration = RADIATION_DURATION; + break; + case itbd_DrownRecover: + // NOTE: this hack is actually used to RESTORE health + // after the player has been drowning and finally takes a breath + if (m_idrowndmg > m_idrownrestored) + { + int idif = MIN(m_idrowndmg - m_idrownrestored, 10); + + TakeHealth(idif, DMG_GENERIC); + m_idrownrestored += idif; + } + bDuration = 4; // get up to 5*10 = 50 points back + break; + + case itbd_PoisonRecover: + { + // NOTE: this hack is actually used to RESTORE health + // after the player has been poisoned. + if (m_nPoisonDmg > m_nPoisonRestored) + { + int nDif = MIN(m_nPoisonDmg - m_nPoisonRestored, 10); + TakeHealth(nDif, DMG_GENERIC); + m_nPoisonRestored += nDif; + } + bDuration = 9; // get up to 10*10 = 100 points back + break; + } + + case itbd_Acid: +// OnTakeDamage(pev, pev, ACID_DAMAGE, DMG_GENERIC); + bDuration = ACID_DURATION; +#ifdef MAPBASE + // Prevents ant workers from inducing the Flash Plague, flashing the player's screen every time they take damage henceforth. + // I think people came up with a different name, but I can't bother to look for it right now. + // This fix might prevent other acid damage stuff as well, so it's not episodic-exclusive. + m_bitsDamageType &= ~(DMG_ACID); +#endif + break; + case itbd_SlowBurn: +// OnTakeDamage(pev, pev, SLOWBURN_DAMAGE, DMG_GENERIC); + bDuration = SLOWBURN_DURATION; + break; + case itbd_SlowFreeze: +// OnTakeDamage(pev, pev, SLOWFREEZE_DAMAGE, DMG_GENERIC); + bDuration = SLOWFREEZE_DURATION; + break; + default: + bDuration = 0; + } + + if (m_rgbTimeBasedDamage[i]) + { + // decrement damage duration, detect when done. + if (!m_rgbTimeBasedDamage[i] || --m_rgbTimeBasedDamage[i] == 0) + { + m_rgbTimeBasedDamage[i] = 0; + // if we're done, clear damage bits + m_bitsDamageType &= ~(DMG_PARALYZE << i); + } + } + else + // first time taking this damage type - init damage duration + m_rgbTimeBasedDamage[i] = bDuration; + } + } +} + +/* +THE POWER SUIT + +The Suit provides 3 main functions: Protection, Notification and Augmentation. +Some functions are automatic, some require power. +The player gets the suit shortly after getting off the train in C1A0 and it stays +with him for the entire game. + +Protection + + Heat/Cold + When the player enters a hot/cold area, the heating/cooling indicator on the suit + will come on and the battery will drain while the player stays in the area. + After the battery is dead, the player starts to take damage. + This feature is built into the suit and is automatically engaged. + Radiation Syringe + This will cause the player to be immune from the effects of radiation for N seconds. Single use item. + Anti-Toxin Syringe + This will cure the player from being poisoned. Single use item. + Health + Small (1st aid kits, food, etc.) + Large (boxes on walls) + Armor + The armor works using energy to create a protective field that deflects a + percentage of damage projectile and explosive attacks. After the armor has been deployed, + it will attempt to recharge itself to full capacity with the energy reserves from the battery. + It takes the armor N seconds to fully charge. + +Notification (via the HUD) + +x Health +x Ammo +x Automatic Health Care + Notifies the player when automatic healing has been engaged. +x Geiger counter + Classic Geiger counter sound and status bar at top of HUD + alerts player to dangerous levels of radiation. This is not visible when radiation levels are normal. +x Poison + Armor + Displays the current level of armor. + +Augmentation + + Reanimation (w/adrenaline) + Causes the player to come back to life after he has been dead for 3 seconds. + Will not work if player was gibbed. Single use. + Long Jump + Used by hitting the ??? key(s). Caused the player to further than normal. + SCUBA + Used automatically after picked up and after player enters the water. + Works for N seconds. Single use. + +Things powered by the battery + + Armor + Uses N watts for every M units of damage. + Heat/Cool + Uses N watts for every second in hot/cold area. + Long Jump + Uses N watts for every jump. + Alien Cloak + Uses N watts for each use. Each use lasts M seconds. + Alien Shield + Augments armor. Reduces Armor drain by one half + +*/ + +// if in range of radiation source, ping geiger counter + +#define GEIGERDELAY 0.25 + +void CBasePlayer::UpdateGeigerCounter( void ) +{ + byte range; + + // delay per update ie: don't flood net with these msgs + if (gpGlobals->curtime < m_flgeigerDelay) + return; + + m_flgeigerDelay = gpGlobals->curtime + GEIGERDELAY; + + // send range to radition source to client + range = (byte) clamp(Floor2Int(m_flgeigerRange / 4), 0, 255); + + // This is to make sure you aren't driven crazy by geiger while in the airboat + if ( IsInAVehicle() ) + { + range = clamp( (int)range * 4, 0, 255 ); + } + +#ifdef MAPBASE + // If the geiger is disabled, just use 255 + if (HasSpawnFlags(SF_PLAYER_NO_GEIGER)) + range = 255; +#endif + + if (range != m_igeigerRangePrev) + { + m_igeigerRangePrev = range; + + CSingleUserRecipientFilter user( this ); + user.MakeReliable(); + UserMessageBegin( user, "Geiger" ); + WRITE_BYTE( range ); + MessageEnd(); + } + + // reset counter and semaphore + if (!random->RandomInt(0,3)) + { + m_flgeigerRange = 1000; + } +} + +/* +================ +CheckSuitUpdate + +Play suit update if it's time +================ +*/ + +#define SUITUPDATETIME 3.5 +#define SUITFIRSTUPDATETIME 0.1 + +void CBasePlayer::CheckSuitUpdate() +{ + int i; + int isentence = 0; + int isearch = m_iSuitPlayNext; + + // Ignore suit updates if no suit + if ( !IsSuitEquipped() ) + return; + + // if in range of radiation source, ping geiger counter + UpdateGeigerCounter(); + + if ( g_pGameRules->IsMultiplayer() ) + { + // don't bother updating HEV voice in multiplayer. + return; + } + + if ( gpGlobals->curtime >= m_flSuitUpdate && m_flSuitUpdate > 0) + { + // play a sentence off of the end of the queue + for (i = 0; i < CSUITPLAYLIST; i++) + { + if ((isentence = m_rgSuitPlayList[isearch]) != 0) + break; + + if (++isearch == CSUITPLAYLIST) + isearch = 0; + } + + if (isentence) + { + m_rgSuitPlayList[isearch] = 0; + if (isentence > 0) + { + // play sentence number + + char sentence[512]; + Q_snprintf( sentence, sizeof( sentence ), "!%s", engine->SentenceNameFromIndex( isentence ) ); + UTIL_EmitSoundSuit( edict(), sentence ); + } + else + { + // play sentence group + UTIL_EmitGroupIDSuit(edict(), -isentence); + } + m_flSuitUpdate = gpGlobals->curtime + SUITUPDATETIME; + } + else + // queue is empty, don't check + m_flSuitUpdate = 0; + } +} + +// add sentence to suit playlist queue. if fgroup is true, then +// name is a sentence group (HEV_AA), otherwise name is a specific +// sentence name ie: !HEV_AA0. If iNoRepeat is specified in +// seconds, then we won't repeat playback of this word or sentence +// for at least that number of seconds. + +void CBasePlayer::SetSuitUpdate(const char *name, int fgroup, int iNoRepeatTime) +{ + int i; + int isentence; + int iempty = -1; + + + // Ignore suit updates if no suit + if ( !IsSuitEquipped() ) + return; + + if ( g_pGameRules->IsMultiplayer() ) + { + // due to static channel design, etc. We don't play HEV sounds in multiplayer right now. + return; + } + + // if name == NULL, then clear out the queue + + if (!name) + { + for (i = 0; i < CSUITPLAYLIST; i++) + m_rgSuitPlayList[i] = 0; + return; + } + // get sentence or group number + if (!fgroup) + { + isentence = SENTENCEG_Lookup(name); // Lookup sentence index (not group) by name + if (isentence < 0) + return; + } + else + // mark group number as negative + isentence = -SENTENCEG_GetIndex(name); // Lookup group index by name + + // check norepeat list - this list lets us cancel + // the playback of words or sentences that have already + // been played within a certain time. + + for (i = 0; i < CSUITNOREPEAT; i++) + { + if (isentence == m_rgiSuitNoRepeat[i]) + { + // this sentence or group is already in + // the norepeat list + + if (m_rgflSuitNoRepeatTime[i] < gpGlobals->curtime) + { + // norepeat time has expired, clear it out + m_rgiSuitNoRepeat[i] = 0; + m_rgflSuitNoRepeatTime[i] = 0.0; + iempty = i; + break; + } + else + { + // don't play, still marked as norepeat + return; + } + } + // keep track of empty slot + if (!m_rgiSuitNoRepeat[i]) + iempty = i; + } + + // sentence is not in norepeat list, save if norepeat time was given + + if (iNoRepeatTime) + { + if (iempty < 0) + iempty = random->RandomInt(0, CSUITNOREPEAT-1); // pick random slot to take over + m_rgiSuitNoRepeat[iempty] = isentence; + m_rgflSuitNoRepeatTime[iempty] = iNoRepeatTime + gpGlobals->curtime; + } + + // find empty spot in queue, or overwrite last spot + + m_rgSuitPlayList[m_iSuitPlayNext++] = isentence; + if (m_iSuitPlayNext == CSUITPLAYLIST) + m_iSuitPlayNext = 0; + + if (m_flSuitUpdate <= gpGlobals->curtime) + { + if (m_flSuitUpdate == 0) + // play queue is empty, don't delay too long before playback + m_flSuitUpdate = gpGlobals->curtime + SUITFIRSTUPDATETIME; + else + m_flSuitUpdate = gpGlobals->curtime + SUITUPDATETIME; + } + +} + +//========================================================= +// UpdatePlayerSound - updates the position of the player's +// reserved sound slot in the sound list. +//========================================================= +void CBasePlayer::UpdatePlayerSound ( void ) +{ + int iBodyVolume; + int iVolume; + CSound *pSound; + + pSound = CSoundEnt::SoundPointerForIndex( CSoundEnt::ClientSoundIndex( edict() ) ); + + if ( !pSound ) + { + Msg( "Client lost reserved sound!\n" ); + return; + } + + if (GetFlags() & FL_NOTARGET) + { + pSound->m_iVolume = 0; + return; + } + + // now figure out how loud the player's movement is. + if ( GetFlags() & FL_ONGROUND ) + { + iBodyVolume = GetAbsVelocity().Length(); + + // clamp the noise that can be made by the body, in case a push trigger, + // weapon recoil, or anything shoves the player abnormally fast. + // NOTE: 512 units is a pretty large radius for a sound made by the player's body. + // then again, I think some materials are pretty loud. + if ( iBodyVolume > 512 ) + { + iBodyVolume = 512; + } + } + else + { + iBodyVolume = 0; + } + + if ( m_nButtons & IN_JUMP ) + { + // Jumping is a little louder. + iBodyVolume += 100; + } + + m_iTargetVolume = iBodyVolume; + + // if target volume is greater than the player sound's current volume, we paste the new volume in + // immediately. If target is less than the current volume, current volume is not set immediately to the + // lower volume, rather works itself towards target volume over time. This gives NPCs a much better chance + // to hear a sound, especially if they don't listen every frame. + iVolume = pSound->Volume(); + + if ( m_iTargetVolume > iVolume ) + { + iVolume = m_iTargetVolume; + } + else if ( iVolume > m_iTargetVolume ) + { + iVolume -= 250 * gpGlobals->frametime; + + if ( iVolume < m_iTargetVolume ) + { + iVolume = 0; + } + } + + if ( pSound ) + { + pSound->SetSoundOrigin( GetAbsOrigin() ); + pSound->m_iType = SOUND_PLAYER; + pSound->m_iVolume = iVolume; + } + + // Below are a couple of useful little bits that make it easier to visualize just how much noise the + // player is making. + //Vector forward = UTIL_YawToVector( pl.v_angle.y ); + //UTIL_Sparks( GetAbsOrigin() + forward * iVolume ); + //Msg( "%d/%d\n", iVolume, m_iTargetVolume ); +} + +// This is a glorious hack to find free space when you've crouched into some solid space +// Our crouching collisions do not work correctly for some reason and this is easier +// than fixing the problem :( +void FixPlayerCrouchStuck( CBasePlayer *pPlayer ) +{ + trace_t trace; + + // Move up as many as 18 pixels if the player is stuck. + int i; + Vector org = pPlayer->GetAbsOrigin();; + for ( i = 0; i < 18; i++ ) + { + UTIL_TraceHull( pPlayer->GetAbsOrigin(), pPlayer->GetAbsOrigin(), + VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX, MASK_PLAYERSOLID, pPlayer, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + if ( trace.startsolid ) + { + Vector origin = pPlayer->GetAbsOrigin(); + origin.z += 1.0f; + pPlayer->SetLocalOrigin( origin ); + } + else + return; + } + + pPlayer->SetAbsOrigin( org ); + + for ( i = 0; i < 18; i++ ) + { + UTIL_TraceHull( pPlayer->GetAbsOrigin(), pPlayer->GetAbsOrigin(), + VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX, MASK_PLAYERSOLID, pPlayer, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + if ( trace.startsolid ) + { + Vector origin = pPlayer->GetAbsOrigin(); + origin.z -= 1.0f; + pPlayer->SetLocalOrigin( origin ); + } + else + return; + } +} +#define SMOOTHING_FACTOR 0.9 +extern CMoveData *g_pMoveData; + +// UNDONE: Look and see if the ground entity is in hierarchy with a MOVETYPE_VPHYSICS? +// Behavior in that case is not as good currently when the parent is rideable +bool CBasePlayer::IsRideablePhysics( IPhysicsObject *pPhysics ) +{ + if ( pPhysics ) + { + if ( pPhysics->GetMass() > (VPhysicsGetObject()->GetMass()*2) ) + return true; + } + + return false; +} + +IPhysicsObject *CBasePlayer::GetGroundVPhysics() +{ + CBaseEntity *pGroundEntity = GetGroundEntity(); + if ( pGroundEntity && pGroundEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + IPhysicsObject *pPhysGround = pGroundEntity->VPhysicsGetObject(); + if ( pPhysGround && pPhysGround->IsMoveable() ) + return pPhysGround; + } + return NULL; +} + + +//----------------------------------------------------------------------------- +// For debugging... +//----------------------------------------------------------------------------- +void CBasePlayer::ForceOrigin( const Vector &vecOrigin ) +{ + m_bForceOrigin = true; + m_vForcedOrigin = vecOrigin; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::PostThink() +{ + m_vecSmoothedVelocity = m_vecSmoothedVelocity * SMOOTHING_FACTOR + GetAbsVelocity() * ( 1 - SMOOTHING_FACTOR ); + + if ( !g_fGameOver && !m_iPlayerLocked ) + { + if ( IsAlive() ) + { + // set correct collision bounds (may have changed in player movement code) + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-Bounds" ); + if ( GetFlags() & FL_DUCKING ) + { + SetCollisionBounds( VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX ); + } + else + { + SetCollisionBounds( VEC_HULL_MIN, VEC_HULL_MAX ); + } + VPROF_SCOPE_END(); + + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-Use" ); + // Handle controlling an entity + if ( m_hUseEntity != NULL ) + { + // if they've moved too far from the gun, or deployed another weapon, unuse the gun + if ( m_hUseEntity->OnControls( this ) && + ( !GetActiveWeapon() || GetActiveWeapon()->IsEffectActive( EF_NODRAW ) || + ( GetActiveWeapon()->GetActivity() == ACT_VM_HOLSTER ) + #ifdef PORTAL // Portalgun view model stays up when holding an object -Jeep + || FClassnameIs( GetActiveWeapon(), "weapon_portalgun" ) + #endif //#ifdef PORTAL + ) ) + { + m_hUseEntity->Use( this, this, USE_SET, 2 ); // try fire the gun + } + else + { + // they've moved off the controls + ClearUseEntity(); + } + } + VPROF_SCOPE_END(); + + // do weapon stuff + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-ItemPostFrame" ); + ItemPostFrame(); + VPROF_SCOPE_END(); + + if ( GetFlags() & FL_ONGROUND ) + { + if (m_Local.m_flFallVelocity > 64 && !g_pGameRules->IsMultiplayer()) + { + CSoundEnt::InsertSound ( SOUND_PLAYER, GetAbsOrigin(), m_Local.m_flFallVelocity, 0.2, this ); + // Msg( "fall %f\n", m_Local.m_flFallVelocity ); + } + m_Local.m_flFallVelocity = 0; + } + + // select the proper animation for the player character + VPROF( "CBasePlayer::PostThink-Animation" ); + // If he's in a vehicle, sit down + if ( IsInAVehicle() ) + SetAnimation( PLAYER_IN_VEHICLE ); + else if (!GetAbsVelocity().x && !GetAbsVelocity().y) + SetAnimation( PLAYER_IDLE ); + else if ((GetAbsVelocity().x || GetAbsVelocity().y) && ( GetFlags() & FL_ONGROUND )) + SetAnimation( PLAYER_WALK ); + else if (GetWaterLevel() > 1) + SetAnimation( PLAYER_WALK ); + } + + // Don't allow bogus sequence on player + if ( GetSequence() == -1 ) + { + SetSequence( 0 ); + } + + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-StudioFrameAdvance" ); + StudioFrameAdvance(); + VPROF_SCOPE_END(); + + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-DispatchAnimEvents" ); + DispatchAnimEvents( this ); + VPROF_SCOPE_END(); + + SetSimulationTime( gpGlobals->curtime ); + + //Let the weapon update as well + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-Weapon_FrameUpdate" ); + Weapon_FrameUpdate(); + VPROF_SCOPE_END(); + + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-UpdatePlayerSound" ); + UpdatePlayerSound(); + VPROF_SCOPE_END(); + + if ( m_bForceOrigin ) + { + SetLocalOrigin( m_vForcedOrigin ); + SetLocalAngles( m_Local.m_vecPunchAngle ); + m_Local.m_vecPunchAngle = RandomAngle( -25, 25 ); + m_Local.m_vecPunchAngleVel.Init(); + } + + VPROF_SCOPE_BEGIN( "CBasePlayer::PostThink-PostThinkVPhysics" ); + PostThinkVPhysics(); + VPROF_SCOPE_END(); + } + +#if !defined( NO_ENTITY_PREDICTION ) + // Even if dead simulate entities + SimulatePlayerSimulatedEntities(); +#endif + +} + +// handles touching physics objects +void CBasePlayer::Touch( CBaseEntity *pOther ) +{ + if ( pOther == GetGroundEntity() ) + return; + + if ( pOther->GetMoveType() != MOVETYPE_VPHYSICS || pOther->GetSolid() != SOLID_VPHYSICS || (pOther->GetSolidFlags() & FSOLID_TRIGGER) ) + return; + + IPhysicsObject *pPhys = pOther->VPhysicsGetObject(); + if ( !pPhys || !pPhys->IsMoveable() ) + return; + + SetTouchedPhysics( true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::PostThinkVPhysics( void ) +{ + // Check to see if things are initialized! + if ( !m_pPhysicsController ) + return; + + Vector newPosition = GetAbsOrigin(); + float frametime = gpGlobals->frametime; + if ( frametime <= 0 || frametime > 0.1f ) + frametime = 0.1f; + + IPhysicsObject *pPhysGround = GetGroundVPhysics(); + + if ( !pPhysGround && m_touchedPhysObject && g_pMoveData->m_outStepHeight <= 0.f && (GetFlags() & FL_ONGROUND) ) + { + newPosition = m_oldOrigin + frametime * g_pMoveData->m_outWishVel; + newPosition = (GetAbsOrigin() * 0.5f) + (newPosition * 0.5f); + } + + int collisionState = VPHYS_WALK; + if ( GetMoveType() == MOVETYPE_NOCLIP || GetMoveType() == MOVETYPE_OBSERVER ) + { + collisionState = VPHYS_NOCLIP; + } + else if ( GetFlags() & FL_DUCKING ) + { + collisionState = VPHYS_CROUCH; + } + + if ( collisionState != m_vphysicsCollisionState ) + { + SetVCollisionState( GetAbsOrigin(), GetAbsVelocity(), collisionState ); + } + + if ( !(TouchedPhysics() || pPhysGround) ) + { + float maxSpeed = m_flMaxspeed > 0.0f ? m_flMaxspeed : sv_maxspeed.GetFloat(); + g_pMoveData->m_outWishVel.Init( maxSpeed, maxSpeed, maxSpeed ); + } + + // teleport the physics object up by stepheight (game code does this - reflect in the physics) + if ( g_pMoveData->m_outStepHeight > 0.1f ) + { + if ( g_pMoveData->m_outStepHeight > 4.0f ) + { + VPhysicsGetObject()->SetPosition( GetAbsOrigin(), vec3_angle, true ); + } + else + { + // don't ever teleport into solid + Vector position, end; + VPhysicsGetObject()->GetPosition( &position, NULL ); + end = position; + end.z += g_pMoveData->m_outStepHeight; + trace_t trace; + UTIL_TraceEntity( this, position, end, MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + if ( trace.DidHit() ) + { + g_pMoveData->m_outStepHeight = trace.endpos.z - position.z; + } + m_pPhysicsController->StepUp( g_pMoveData->m_outStepHeight ); + } + m_pPhysicsController->Jump(); + } + g_pMoveData->m_outStepHeight = 0.0f; + + // Store these off because after running the usercmds, it'll pass them + // to UpdateVPhysicsPosition. + m_vNewVPhysicsPosition = newPosition; + m_vNewVPhysicsVelocity = g_pMoveData->m_outWishVel; + + m_oldOrigin = GetAbsOrigin(); +} + +void CBasePlayer::UpdateVPhysicsPosition( const Vector &position, const Vector &velocity, float secondsToArrival ) +{ + bool onground = (GetFlags() & FL_ONGROUND) ? true : false; + IPhysicsObject *pPhysGround = GetGroundVPhysics(); + + // if the object is much heavier than the player, treat it as a local coordinate system + // the player controller will solve movement differently in this case. + if ( !IsRideablePhysics(pPhysGround) ) + { + pPhysGround = NULL; + } + + m_pPhysicsController->Update( position, velocity, secondsToArrival, onground, pPhysGround ); +} + +void CBasePlayer::UpdatePhysicsShadowToCurrentPosition() +{ + UpdateVPhysicsPosition( GetAbsOrigin(), vec3_origin, gpGlobals->frametime ); +} + +void CBasePlayer::UpdatePhysicsShadowToPosition( const Vector &vecAbsOrigin ) +{ + UpdateVPhysicsPosition( vecAbsOrigin, vec3_origin, gpGlobals->frametime ); +} + +Vector CBasePlayer::GetSmoothedVelocity( void ) +{ + if ( IsInAVehicle() ) + { + return GetVehicle()->GetVehicleEnt()->GetSmoothedVelocity(); + } + return m_vecSmoothedVelocity; +} + + +CBaseEntity *g_pLastSpawn = NULL; + + +//----------------------------------------------------------------------------- +// Purpose: Finds a player start entity of the given classname. If any entity of +// of the given classname has the SF_PLAYER_START_MASTER flag set, that +// is the entity that will be returned. Otherwise, the first entity of +// the given classname is returned. +// Input : pszClassName - should be "info_player_start", "info_player_coop", or +// "info_player_deathmatch" +//----------------------------------------------------------------------------- +CBaseEntity *FindPlayerStart(const char *pszClassName) +{ + #define SF_PLAYER_START_MASTER 1 + + CBaseEntity *pStart = gEntList.FindEntityByClassname(NULL, pszClassName); + CBaseEntity *pStartFirst = pStart; + while (pStart != NULL) + { + if (pStart->HasSpawnFlags(SF_PLAYER_START_MASTER)) + { + return pStart; + } + + pStart = gEntList.FindEntityByClassname(pStart, pszClassName); + } + + return pStartFirst; +} + +/* +============ +EntSelectSpawnPoint + +Returns the entity to spawn at + +USES AND SETS GLOBAL g_pLastSpawn +============ +*/ +CBaseEntity *CBasePlayer::EntSelectSpawnPoint() +{ + CBaseEntity *pSpot; + edict_t *player; + + player = edict(); + +// choose a info_player_deathmatch point + if (g_pGameRules->IsCoOp()) + { + pSpot = gEntList.FindEntityByClassname( g_pLastSpawn, "info_player_coop"); + if ( pSpot ) + goto ReturnSpot; + pSpot = gEntList.FindEntityByClassname( g_pLastSpawn, "info_player_start"); + if ( pSpot ) + goto ReturnSpot; + } + else if ( g_pGameRules->IsDeathmatch() ) + { + pSpot = g_pLastSpawn; + // Randomize the start spot + for ( int i = random->RandomInt(1,5); i > 0; i-- ) + pSpot = gEntList.FindEntityByClassname( pSpot, "info_player_deathmatch" ); + if ( !pSpot ) // skip over the null point + pSpot = gEntList.FindEntityByClassname( pSpot, "info_player_deathmatch" ); + + CBaseEntity *pFirstSpot = pSpot; + + do + { + if ( pSpot ) + { + // check if pSpot is valid + if ( g_pGameRules->IsSpawnPointValid( pSpot, this ) ) + { + if ( pSpot->GetLocalOrigin() == vec3_origin ) + { + pSpot = gEntList.FindEntityByClassname( pSpot, "info_player_deathmatch" ); + continue; + } + + // if so, go to pSpot + goto ReturnSpot; + } + } + // increment pSpot + pSpot = gEntList.FindEntityByClassname( pSpot, "info_player_deathmatch" ); + } while ( pSpot != pFirstSpot ); // loop if we're not back to the start + + // we haven't found a place to spawn yet, so kill any guy at the first spawn point and spawn there + if ( pSpot ) + { + CBaseEntity *ent = NULL; + for ( CEntitySphereQuery sphere( pSpot->GetAbsOrigin(), 128 ); (ent = sphere.GetCurrentEntity()) != NULL; sphere.NextEntity() ) + { + // if ent is a client, kill em (unless they are ourselves) + if ( ent->IsPlayer() && !(ent->edict() == player) ) + ent->TakeDamage( CTakeDamageInfo( GetContainingEntity(INDEXENT(0)), GetContainingEntity(INDEXENT(0)), 300, DMG_GENERIC ) ); + } + goto ReturnSpot; + } + } + + // If startspot is set, (re)spawn there. + if ( !gpGlobals->startspot || !strlen(STRING(gpGlobals->startspot))) + { + pSpot = FindPlayerStart( "info_player_start" ); + if ( pSpot ) + goto ReturnSpot; + } + else + { + pSpot = gEntList.FindEntityByName( NULL, gpGlobals->startspot ); + if ( pSpot ) + goto ReturnSpot; + } + +ReturnSpot: + if ( !pSpot ) + { + Warning( "PutClientInServer: no info_player_start on level\n"); + return CBaseEntity::Instance( INDEXENT( 0 ) ); + } + + g_pLastSpawn = pSpot; + return pSpot; +} + +//----------------------------------------------------------------------------- +// Purpose: Called the first time the player's created +//----------------------------------------------------------------------------- +void CBasePlayer::InitialSpawn( void ) +{ + m_iConnected = PlayerConnected; + gamestats->Event_PlayerConnected( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: clear our m_Local.m_TonemapParams to -1. +//----------------------------------------------------------------------------- +void CBasePlayer::ClearTonemapParams( void ) +{ + //Tony; clear all the variables to -1.0 + m_Local.m_TonemapParams.m_flAutoExposureMin = -1.0f; + m_Local.m_TonemapParams.m_flAutoExposureMax = -1.0f; + m_Local.m_TonemapParams.m_flTonemapScale = -1.0f; + m_Local.m_TonemapParams.m_flBloomScale = -1.0f; + m_Local.m_TonemapParams.m_flTonemapRate = -1.0f; +} +void CBasePlayer::InputSetTonemapScale( inputdata_t &inputdata ) +{ + m_Local.m_TonemapParams.m_flTonemapScale = inputdata.value.Float(); +} + +void CBasePlayer::InputSetTonemapRate( inputdata_t &inputdata ) +{ + m_Local.m_TonemapParams.m_flTonemapRate = inputdata.value.Float(); +} +void CBasePlayer::InputSetAutoExposureMin( inputdata_t &inputdata ) +{ + m_Local.m_TonemapParams.m_flAutoExposureMin = inputdata.value.Float(); +} + +void CBasePlayer::InputSetAutoExposureMax( inputdata_t &inputdata ) +{ + m_Local.m_TonemapParams.m_flAutoExposureMax = inputdata.value.Float(); +} + +void CBasePlayer::InputSetBloomScale( inputdata_t &inputdata ) +{ + m_Local.m_TonemapParams.m_flBloomScale = inputdata.value.Float(); +} + +//Tony; restore defaults (set min/max to -1.0 so nothing gets overridden) +void CBasePlayer::InputUseDefaultAutoExposure( inputdata_t &inputdata ) +{ + m_Local.m_TonemapParams.m_flAutoExposureMin = -1.0f; + m_Local.m_TonemapParams.m_flAutoExposureMax = -1.0f; + m_Local.m_TonemapParams.m_flTonemapRate = -1.0f; +} +void CBasePlayer::InputUseDefaultBloomScale( inputdata_t &inputdata ) +{ + m_Local.m_TonemapParams.m_flBloomScale = -1.0f; +} +// void InputSetBloomScaleRange( inputdata_t &inputdata ); + +//----------------------------------------------------------------------------- +// Purpose: Called everytime the player respawns +//----------------------------------------------------------------------------- +void CBasePlayer::Spawn( void ) +{ + // Needs to be done before weapons are given + if ( Hints() ) + { + Hints()->ResetHints(); + } + + //Tony; make sure tonemap params is cleared. + ClearTonemapParams(); + + SetClassname( "player" ); + + // Shared spawning code.. + SharedSpawn(); + + SetSimulatedEveryTick( true ); + SetAnimatedEveryTick( true ); + + m_ArmorValue = SpawnArmorValue(); + SetBlocksLOS( false ); + m_iMaxHealth = m_iHealth; + + // Clear all flags except for FL_FULLEDICT + if ( GetFlags() & FL_FAKECLIENT ) + { + ClearFlags(); + AddFlag( FL_CLIENT | FL_FAKECLIENT ); + } + else + { + ClearFlags(); + AddFlag( FL_CLIENT ); + } + + AddFlag( FL_AIMTARGET ); + + m_AirFinished = gpGlobals->curtime + AIRTIME; + m_nDrownDmgRate = DROWNING_DAMAGE_INITIAL; + + // only preserve the shadow flag + int effects = GetEffects() & EF_NOSHADOW; + SetEffects( effects ); + + IncrementInterpolationFrame(); + + // Initialize the fog and postprocess controllers. + InitFogController(); + InitPostProcessController(); + + m_DmgTake = 0; + m_DmgSave = 0; + m_bitsHUDDamage = -1; + m_bitsDamageType = 0; + m_afPhysicsFlags = 0; + + m_idrownrestored = m_idrowndmg; + + SetFOV( this, 0 ); + + m_flNextDecalTime = 0;// let this player decal as soon as he spawns. + + m_flgeigerDelay = gpGlobals->curtime + 2.0; // wait a few seconds until user-defined message registrations + // are recieved by all clients + + m_flFieldOfView = 0.766;// some NPCs use this to determine whether or not the player is looking at them. + + m_vecAdditionalPVSOrigin = vec3_origin; + m_vecCameraPVSOrigin = vec3_origin; + + if ( !m_fGameHUDInitialized ) + g_pGameRules->SetDefaultPlayerTeam( this ); + +#ifdef MAPBASE + CBaseEntity *pSpawnPoint = g_pGameRules->GetPlayerSpawnSpot( this ); + SpawnedAtPoint( pSpawnPoint ); +#else + g_pGameRules->GetPlayerSpawnSpot( this ); +#endif + + m_Local.m_bDucked = false;// This will persist over round restart if you hold duck otherwise. + m_Local.m_bDucking = false; + SetViewOffset( VEC_VIEW_SCALED( this ) ); + Precache(); + + m_bitsDamageType = 0; + m_bitsHUDDamage = -1; + SetPlayerUnderwater( false ); + + m_iTrain = TRAIN_NEW; + + m_HackedGunPos = Vector( 0, 32, 0 ); + + m_iBonusChallenge = sv_bonus_challenge.GetInt(); + sv_bonus_challenge.SetValue( 0 ); + + if ( m_iPlayerSound == SOUNDLIST_EMPTY ) + { + Msg( "Couldn't alloc player sound slot!\n" ); + } + + SetThink(NULL); + m_fInitHUD = true; + m_fWeapon = false; + m_iClientBattery = -1; + + m_lastx = m_lasty = 0; + + Q_strncpy( m_szLastPlaceName.GetForModify(), "", MAX_PLACE_NAME_LENGTH ); + + CSingleUserRecipientFilter user( this ); + enginesound->SetPlayerDSP( user, 0, false ); + + CreateViewModel(); +#ifdef MAPBASE + CreateHandModel(); +#endif + + SetCollisionGroup( COLLISION_GROUP_PLAYER ); + + // if the player is locked, make sure he stays locked + if ( m_iPlayerLocked ) + { + m_iPlayerLocked = false; + LockPlayerInPlace(); + } + + if ( GetTeamNumber() != TEAM_SPECTATOR ) + { + StopObserverMode(); + } + else + { + StartObserverMode( m_iObserverLastMode ); + } + + StopReplayMode(); + + // Clear any screenfade + color32 nothing = {0,0,0,255}; + UTIL_ScreenFade( this, nothing, 0, 0, FFADE_IN | FFADE_PURGE ); + + g_pGameRules->PlayerSpawn( this ); + + m_flLaggedMovementValue = 1.0f; + m_vecSmoothedVelocity = vec3_origin; + InitVCollision( GetAbsOrigin(), GetAbsVelocity() ); + + if ( !g_pGameRules->IsMultiplayer() && g_pScriptVM ) + { + g_pScriptVM->SetValue( "player", GetScriptInstance() ); + } + +#if !defined( TF_DLL ) + IGameEvent *event = gameeventmanager->CreateEvent( "player_spawn" ); + + if ( event ) + { + event->SetInt("userid", GetUserID() ); + gameeventmanager->FireEvent( event ); + } +#endif + + RumbleEffect( RUMBLE_STOP_ALL, 0, RUMBLE_FLAGS_NONE ); + + // Calculate this immediately + m_nVehicleViewSavedFrame = 0; + + // track where we are in the nav mesh + UpdateLastKnownArea(); + + BaseClass::Spawn(); + + // track where we are in the nav mesh + UpdateLastKnownArea(); + + m_weaponFiredTimer.Invalidate(); +} + +void CBasePlayer::Activate( void ) +{ + BaseClass::Activate(); + + AimTarget_ForceRepopulateList(); + + RumbleEffect( RUMBLE_STOP_ALL, 0, RUMBLE_FLAGS_NONE ); + + // Reset the analog bias. If the player is in a vehicle when the game + // reloads, it will autosense and apply the correct bias. + m_iVehicleAnalogBias = VEHICLE_ANALOG_BIAS_NONE; +} + +void CBasePlayer::Precache( void ) +{ + BaseClass::Precache(); + + + PrecacheScriptSound( "Player.FallGib" ); + PrecacheScriptSound( "Player.Death" ); + PrecacheScriptSound( "Player.PlasmaDamage" ); + PrecacheScriptSound( "Player.SonicDamage" ); + PrecacheScriptSound( "Player.DrownStart" ); + PrecacheScriptSound( "Player.DrownContinue" ); + PrecacheScriptSound( "Player.Wade" ); + PrecacheScriptSound( "Player.AmbientUnderWater" ); + enginesound->PrecacheSentenceGroup( "HEV" ); + + // These are always needed +#ifndef TF_DLL + PrecacheParticleSystem( "slime_splash_01" ); + PrecacheParticleSystem( "slime_splash_02" ); + PrecacheParticleSystem( "slime_splash_03" ); +#endif + + // in the event that the player JUST spawned, and the level node graph + // was loaded, fix all of the node graph pointers before the game starts. + + // !!!BUGBUG - now that we have multiplayer, this needs to be moved! + /* todo - put in better spot and use new ainetowrk stuff + if ( WorldGraph.m_fGraphPresent && !WorldGraph.m_fGraphPointersSet ) + { + if ( !WorldGraph.FSetGraphPointers() ) + { + Msg( "**Graph pointers were not set!\n"); + } + else + { + Msg( "**Graph Pointers Set!\n" ); + } + } + */ + + // SOUNDS / MODELS ARE PRECACHED in ClientPrecache() (game specific) + // because they need to precache before any clients have connected + + // init geiger counter vars during spawn and each time + // we cross a level transition + m_flgeigerRange = 1000; + m_igeigerRangePrev = 1000; + +#if 0 + // @Note (toml 04-19-04): These are saved, used to be slammed here + m_bitsDamageType = 0; + m_bitsHUDDamage = -1; + SetPlayerUnderwter( false ); + + m_iTrain = TRAIN_NEW; +#endif + + m_iClientBattery = -1; + + m_iUpdateTime = 5; // won't update for 1/2 a second + + if ( gInitHUD ) + m_fInitHUD = true; + +} + +//----------------------------------------------------------------------------- +// Purpose: Force this player to immediately respawn +//----------------------------------------------------------------------------- +void CBasePlayer::ForceRespawn( void ) +{ + RemoveAllItems( true ); + + // Reset ground state for airwalk animations + SetGroundEntity( NULL ); + + // Stop any firing that was taking place before respawn. + m_nButtons = 0; + + Spawn(); +} + +int CBasePlayer::Save( ISave &save ) +{ + if ( !BaseClass::Save(save) ) + return 0; + + return 1; +} + + +// Friend class of CBaseEntity to access private member data. +class CPlayerRestoreHelper +{ +public: + + const Vector &GetAbsOrigin( CBaseEntity *pent ) + { + return pent->m_vecAbsOrigin; + } + + const Vector &GetAbsVelocity( CBaseEntity *pent ) + { + return pent->m_vecAbsVelocity; + } +}; + + +int CBasePlayer::Restore( IRestore &restore ) +{ + int status = BaseClass::Restore(restore); + if ( !status ) + return 0; + + CSaveRestoreData *pSaveData = gpGlobals->pSaveData; + // landmark isn't present. + if ( !pSaveData->levelInfo.fUseLandmark ) + { + Msg( "No Landmark:%s\n", pSaveData->levelInfo.szLandmarkName ); + + // default to normal spawn + CBaseEntity *pSpawnSpot = EntSelectSpawnPoint(); + SetLocalOrigin( pSpawnSpot->GetLocalOrigin() + Vector(0,0,1) ); + SetLocalAngles( pSpawnSpot->GetLocalAngles() ); + } + + QAngle newViewAngles = pl.v_angle; + newViewAngles.z = 0; // Clear out roll + SetLocalAngles( newViewAngles ); + SnapEyeAngles( newViewAngles ); + + // Copied from spawn() for now + SetBloodColor( BLOOD_COLOR_RED ); + + // clear this - it will get reset by touching the trigger again + m_afPhysicsFlags &= ~PFLAG_VPHYSICS_MOTIONCONTROLLER; + + if ( GetFlags() & FL_DUCKING ) + { + // Use the crouch HACK + FixPlayerCrouchStuck( this ); + UTIL_SetSize(this, VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX); + m_Local.m_bDucked = true; + } + else + { + m_Local.m_bDucked = false; + UTIL_SetSize(this, VEC_HULL_MIN, VEC_HULL_MAX); + } + + // We need to get at m_vecAbsOrigin as it was restored but can't let it be + // recalculated by a call to GetAbsOrigin because hierarchy isn't fully restored yet, + // so we use this backdoor to get at the private data in CBaseEntity. + CPlayerRestoreHelper helper; + InitVCollision( helper.GetAbsOrigin( this ), helper.GetAbsVelocity( this ) ); + + // success + return 1; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::OnRestore( void ) +{ + BaseClass::OnRestore(); + + + SetViewEntity( m_hViewEntity ); + SetDefaultFOV(m_iDefaultFOV); // force this to reset if zero + + // Calculate this immediately + m_nVehicleViewSavedFrame = 0; + + m_nBodyPitchPoseParam = LookupPoseParameter( "body_pitch" ); + + // HACK: (03/25/09) Then the player goes across a transition it doesn't spawn and register + // it's instance. We're hacking around this for now, but this will go away when we get around to + // having entities cross transitions and keep their script state. + if ( !g_pGameRules->IsMultiplayer() && g_pScriptVM && (gpGlobals->eLoadType == MapLoad_Transition) ) + { + g_pScriptVM->SetValue( "player", GetScriptInstance() ); + } +} + +/* void CBasePlayer::SetTeamName( const char *pTeamName ) +{ + Q_strncpy( m_szTeamName, pTeamName, TEAM_NAME_LENGTH ); +} */ + +void CBasePlayer::SetArmorValue( int value ) +{ + m_ArmorValue = value; +} + +void CBasePlayer::IncrementArmorValue( int nCount, int nMaxValue ) +{ + m_ArmorValue += nCount; + if (nMaxValue > 0) + { + if (m_ArmorValue > nMaxValue) + m_ArmorValue = nMaxValue; + } +} + +// used by the physics gun and game physics... is there a better interface? +void CBasePlayer::SetPhysicsFlag( int nFlag, bool bSet ) +{ + if (bSet) + m_afPhysicsFlags |= nFlag; + else + m_afPhysicsFlags &= ~nFlag; +} + + +void CBasePlayer::NotifyNearbyRadiationSource( float flRange ) +{ + // if player's current geiger counter range is larger + // than range to this trigger hurt, reset player's + // geiger counter range + + if (m_flgeigerRange >= flRange) + m_flgeigerRange = flRange; +} + +void CBasePlayer::AllowImmediateDecalPainting() +{ + m_flNextDecalTime = gpGlobals->curtime; +} + +// Suicide... +void CBasePlayer::CommitSuicide( bool bExplode /*= false*/, bool bForce /*= false*/ ) +{ + MDLCACHE_CRITICAL_SECTION(); + + if( !IsAlive() ) + return; + + // prevent suiciding too often + if ( m_fNextSuicideTime > gpGlobals->curtime && !bForce ) + return; + + // don't let them suicide for 5 seconds after suiciding + m_fNextSuicideTime = gpGlobals->curtime + 5; + + int fDamage = DMG_PREVENT_PHYSICS_FORCE | ( bExplode ? ( DMG_BLAST | DMG_ALWAYSGIB ) : DMG_NEVERGIB ); + + // have the player kill themself + m_iHealth = 0; + CTakeDamageInfo info( this, this, 0, fDamage, m_iSuicideCustomKillFlags ); + Event_Killed( info ); + Event_Dying( info ); + m_iSuicideCustomKillFlags = 0; +} + +// Suicide with style... +void CBasePlayer::CommitSuicide( const Vector &vecForce, bool bExplode /*= false*/, bool bForce /*= false*/ ) +{ + MDLCACHE_CRITICAL_SECTION(); + + // Already dead. + if( !IsAlive() ) + return; + + // Prevent suicides for a time. + if ( m_fNextSuicideTime > gpGlobals->curtime && !bForce ) + return; + + m_fNextSuicideTime = gpGlobals->curtime + 5; + + // Apply the force. + int nHealth = GetHealth(); + + // Kill the player. + CTakeDamageInfo info; + info.SetDamage( nHealth + 10 ); + info.SetAttacker( this ); + info.SetDamageType( bExplode ? DMG_ALWAYSGIB : DMG_GENERIC ); + info.SetDamageForce( vecForce ); + info.SetDamagePosition( WorldSpaceCenter() ); + TakeDamage( info ); +} + +//============================================== +// HasWeapons - do I have any weapons at all? +//============================================== +bool CBasePlayer::HasWeapons( void ) +{ + int i; + + for ( i = 0 ; i < WeaponCount() ; i++ ) + { + if ( GetWeapon(i) ) + { + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &vecForce - +//----------------------------------------------------------------------------- +void CBasePlayer::VelocityPunch( const Vector &vecForce ) +{ + // Clear onground and add velocity. + SetGroundEntity( NULL ); + ApplyAbsVelocityImpulse(vecForce ); +} + + +//-------------------------------------------------------------------------------------------------------------- +// VEHICLES +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Purpose: Whether or not the player is currently able to enter the vehicle +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBasePlayer::CanEnterVehicle( IServerVehicle *pVehicle, int nRole ) +{ + // Must not have a passenger there already + if ( pVehicle->GetPassenger( nRole ) ) + return false; + + // Must be able to holster our current weapon (ie. grav gun!) + if ( pVehicle->IsPassengerUsingStandardWeapons( nRole ) == false ) + { + //Must be able to stow our weapon + CBaseCombatWeapon *pWeapon = GetActiveWeapon(); + if ( ( pWeapon != NULL ) && ( pWeapon->CanHolster() == false ) ) + return false; + } + + // Must be alive + if ( IsAlive() == false ) + return false; + + // Can't be pulled by a barnacle + if ( IsEFlagSet( EFL_IS_BEING_LIFTED_BY_BARNACLE ) ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Put this player in a vehicle +//----------------------------------------------------------------------------- +bool CBasePlayer::GetInVehicle( IServerVehicle *pVehicle, int nRole ) +{ + Assert( NULL == m_hVehicle.Get() ); + Assert( nRole >= 0 ); + + // Make sure we can enter the vehicle + if ( CanEnterVehicle( pVehicle, nRole ) == false ) + return false; + + CBaseEntity *pEnt = pVehicle->GetVehicleEnt(); + Assert( pEnt ); + + // Try to stow weapons + if ( pVehicle->IsPassengerUsingStandardWeapons( nRole ) == false ) + { + CBaseCombatWeapon *pWeapon = GetActiveWeapon(); + if ( pWeapon != NULL ) + { + pWeapon->Holster( NULL ); + } + +#ifndef HL2_DLL + m_Local.m_iHideHUD |= HIDEHUD_WEAPONSELECTION; +#endif + m_Local.m_iHideHUD |= HIDEHUD_INVEHICLE; + } + + if ( !pVehicle->IsPassengerVisible( nRole ) ) + { + AddEffects( EF_NODRAW ); + } + + // Put us in the vehicle + pVehicle->SetPassenger( nRole, this ); + + ViewPunchReset(); + + // Setting the velocity to 0 will cause the IDLE animation to play + SetAbsVelocity( vec3_origin ); + SetMoveType( MOVETYPE_NOCLIP ); + + // This is a hack to fixup the player's stats since they really didn't "cheat" and enter noclip from the console + gamestats->Event_DecrementPlayerEnteredNoClip( this ); + + // Get the seat position we'll be at in this vehicle + Vector vSeatOrigin; + QAngle qSeatAngles; + pVehicle->GetPassengerSeatPoint( nRole, &vSeatOrigin, &qSeatAngles ); + + // Set us to that position + SetAbsOrigin( vSeatOrigin ); + SetAbsAngles( qSeatAngles ); + + // Parent to the vehicle + SetParent( pEnt ); + + SetCollisionGroup( COLLISION_GROUP_IN_VEHICLE ); + + // We cannot be ducking -- do all this before SetPassenger because it + // saves our view offset for restoration when we exit the vehicle. + RemoveFlag( FL_DUCKING ); + SetViewOffset( VEC_VIEW_SCALED( this ) ); + m_Local.m_bDucked = false; + m_Local.m_bDucking = false; + m_Local.m_flDucktime = 0.0f; + m_Local.m_flDuckJumpTime = 0.0f; + m_Local.m_flJumpTime = 0.0f; + + // Turn our toggled duck off + if ( GetToggledDuckState() ) + { + ToggleDuck(); + } + + m_hVehicle = pEnt; + + // Throw an event indicating that the player entered the vehicle. + g_pNotify->ReportNamedEvent( this, "PlayerEnteredVehicle" ); + + m_iVehicleAnalogBias = VEHICLE_ANALOG_BIAS_NONE; + + OnVehicleStart(); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Remove this player from a vehicle +//----------------------------------------------------------------------------- +void CBasePlayer::LeaveVehicle( const Vector &vecExitPoint, const QAngle &vecExitAngles ) +{ + if ( NULL == m_hVehicle.Get() ) + return; + + IServerVehicle *pVehicle = GetVehicle(); + Assert( pVehicle ); + + int nRole = pVehicle->GetPassengerRole( this ); + Assert( nRole >= 0 ); + + SetParent( NULL ); + + // Find the first non-blocked exit point: + Vector vNewPos = GetAbsOrigin(); + QAngle qAngles = GetAbsAngles(); + if ( vecExitPoint == vec3_origin ) + { + // FIXME: this might fail to find a safe exit point!! + pVehicle->GetPassengerExitPoint( nRole, &vNewPos, &qAngles ); + } + else + { + vNewPos = vecExitPoint; + qAngles = vecExitAngles; + } + OnVehicleEnd( vNewPos ); + SetAbsOrigin( vNewPos ); + SetAbsAngles( qAngles ); + // Clear out any leftover velocity + SetAbsVelocity( vec3_origin ); + + qAngles[ROLL] = 0; + SnapEyeAngles( qAngles ); + +#ifndef HL2_DLL + m_Local.m_iHideHUD &= ~HIDEHUD_WEAPONSELECTION; +#endif + + m_Local.m_iHideHUD &= ~HIDEHUD_INVEHICLE; + + RemoveEffects( EF_NODRAW ); + + SetMoveType( MOVETYPE_WALK ); + SetCollisionGroup( COLLISION_GROUP_PLAYER ); + + if ( VPhysicsGetObject() ) + { + VPhysicsGetObject()->SetPosition( vNewPos, vec3_angle, true ); + } + + m_hVehicle = NULL; + pVehicle->SetPassenger(nRole, NULL); + + // Re-deploy our weapon + if ( IsAlive() ) + { + if ( GetActiveWeapon() && GetActiveWeapon()->IsWeaponVisible() == false ) + { + GetActiveWeapon()->Deploy(); + ShowCrosshair( true ); + } + } + + // Just cut all of the rumble effects. + RumbleEffect( RUMBLE_STOP_ALL, 0, RUMBLE_FLAGS_NONE ); +} + + +//============================================== +// !!!UNDONE:ultra temporary SprayCan entity to apply +// decal frame at a time. For PreAlpha CD +//============================================== +class CSprayCan : public CPointEntity +{ +public: + DECLARE_CLASS( CSprayCan, CPointEntity ); + + void Spawn ( CBasePlayer *pOwner ); + void Think( void ); + + virtual void Precache(); + + virtual int ObjectCaps( void ) { return FCAP_DONT_SAVE; } +}; + +LINK_ENTITY_TO_CLASS( spraycan, CSprayCan ); +PRECACHE_REGISTER( spraycan ); + +void CSprayCan::Spawn ( CBasePlayer *pOwner ) +{ + SetLocalOrigin( pOwner->WorldSpaceCenter() + Vector ( 0 , 0 , 32 ) ); + SetLocalAngles( pOwner->EyeAngles() ); + SetOwnerEntity( pOwner ); + SetNextThink( gpGlobals->curtime ); + EmitSound( "SprayCan.Paint" ); +} + +void CSprayCan::Precache() +{ + BaseClass::Precache(); + + PrecacheScriptSound( "SprayCan.Paint" ); +} + +void CSprayCan::Think( void ) +{ + CBasePlayer *pPlayer = ToBasePlayer( GetOwnerEntity() ); + if ( pPlayer ) + { + int playernum = pPlayer->entindex(); + + Vector forward; + trace_t tr; + + AngleVectors( GetAbsAngles(), &forward ); + UTIL_TraceLine ( GetAbsOrigin(), GetAbsOrigin() + forward * 128, + MASK_SOLID_BRUSHONLY, pPlayer, COLLISION_GROUP_NONE, & tr); + + UTIL_PlayerDecalTrace( &tr, playernum ); + } + + // Just painted last custom frame. + UTIL_Remove( this ); +} + +class CBloodSplat : public CPointEntity +{ +public: + DECLARE_CLASS( CBloodSplat, CPointEntity ); + + void Spawn ( CBaseEntity *pOwner ); + void Think ( void ); +}; + +void CBloodSplat::Spawn ( CBaseEntity *pOwner ) +{ + SetLocalOrigin( pOwner->WorldSpaceCenter() + Vector ( 0 , 0 , 32 ) ); + SetLocalAngles( pOwner->GetLocalAngles() ); + SetOwnerEntity( pOwner ); + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CBloodSplat::Think( void ) +{ + trace_t tr; + + if ( g_Language.GetInt() != LANGUAGE_GERMAN ) + { + CBasePlayer *pPlayer; + pPlayer = ToBasePlayer( GetOwnerEntity() ); + + Vector forward; + AngleVectors( GetAbsAngles(), &forward ); + UTIL_TraceLine ( GetAbsOrigin(), GetAbsOrigin() + forward * 128, + MASK_SOLID_BRUSHONLY, pPlayer, COLLISION_GROUP_NONE, & tr); + + UTIL_BloodDecalTrace( &tr, BLOOD_COLOR_RED ); + } + UTIL_Remove( this ); +} + +//============================================== + +//----------------------------------------------------------------------------- +// Purpose: Create and give the named item to the player. Then return it. +//----------------------------------------------------------------------------- +CBaseEntity *CBasePlayer::GiveNamedItem( const char *pszName, int iSubType ) +{ + // If I already own this type don't create one + if ( Weapon_OwnsThisType(pszName, iSubType) ) + return NULL; + + // Msg( "giving %s\n", pszName ); + + EHANDLE pent; + + pent = CreateEntityByName(pszName); + if ( pent == NULL ) + { + Msg( "NULL Ent in GiveNamedItem!\n" ); + return NULL; + } + + pent->SetLocalOrigin( GetLocalOrigin() ); + pent->AddSpawnFlags( SF_NORESPAWN ); + + CBaseCombatWeapon *pWeapon = dynamic_cast( (CBaseEntity*)pent ); + if ( pWeapon ) + { + pWeapon->SetSubType( iSubType ); + } + + DispatchSpawn( pent ); + +#ifdef MAPBASE + if ( pWeapon ) + { + for (int i=0;iGetSlot() == pWeapon->GetSlot() && m_hMyWeapons[i]->GetPosition() == pWeapon->GetPosition() ) + { + // Make sure it matches the subtype + if ( m_hMyWeapons[i]->GetSubType() == iSubType ) + { + // Don't use this weapon if the slot is already occupied + UTIL_Remove( pWeapon ); + return NULL; + } + } + } + } +#endif + + if ( pent != NULL && !(pent->IsMarkedForDeletion()) ) + { + pent->Touch( this ); + } + + return pent; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the nearest COLLIBALE entity in front of the player +// that has a clear line of sight with the given classname +// Input : +// Output : +//----------------------------------------------------------------------------- +CBaseEntity *FindEntityClassForward( CBasePlayer *pMe, char *classname ) +{ + trace_t tr; + + Vector forward; + pMe->EyeVectors( &forward ); + UTIL_TraceLine(pMe->EyePosition(), + pMe->EyePosition() + forward * MAX_COORD_RANGE, + MASK_SOLID, pMe, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 && tr.DidHitNonWorldEntity() ) + { + CBaseEntity *pHit = tr.m_pEnt; + if (FClassnameIs( pHit,classname ) ) + { + return pHit; + } + } + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the nearest COLLIBALE entity in front of the player +// that has a clear line of sight. If HULL is true, the trace will +// hit the collision hull of entities. Otherwise, the trace will hit +// hitboxes. +// Input : +// Output : +//----------------------------------------------------------------------------- +CBaseEntity *FindEntityForward( CBasePlayer *pMe, bool fHull ) +{ + if ( pMe ) + { + trace_t tr; + Vector forward; + int mask; + + if( fHull ) + { + mask = MASK_SOLID; + } + else + { + mask = MASK_SHOT; + } + + pMe->EyeVectors( &forward ); + UTIL_TraceLine(pMe->EyePosition(), + pMe->EyePosition() + forward * MAX_COORD_RANGE, + mask, pMe, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 && tr.DidHitNonWorldEntity() ) + { + return tr.m_pEnt; + } + } + return NULL; + +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the nearest entity in front of the player of the given +// classname, preferring collidable entities, but allows selection of +// enities that are on the other side of walls or objects +// +// Input : +// Output : +//----------------------------------------------------------------------------- +CBaseEntity *FindPickerEntityClass( CBasePlayer *pPlayer, char *classname ) +{ + // First try to trace a hull to an entity + CBaseEntity *pEntity = FindEntityClassForward( pPlayer, classname ); + + // If that fails just look for the nearest facing entity + if (!pEntity) + { + Vector forward; + Vector origin; + pPlayer->EyeVectors( &forward ); + origin = pPlayer->WorldSpaceCenter(); + pEntity = gEntList.FindEntityClassNearestFacing( origin, forward,0.95,classname); + } + return pEntity; +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the nearest entity in front of the player, preferring +// collidable entities, but allows selection of enities that are +// on the other side of walls or objects +// Input : +// Output : +//----------------------------------------------------------------------------- +CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ) +{ + MDLCACHE_CRITICAL_SECTION(); + + // First try to trace a hull to an entity + CBaseEntity *pEntity = FindEntityForward( pPlayer, true ); + + // If that fails just look for the nearest facing entity + if (!pEntity) + { + Vector forward; + Vector origin; + pPlayer->EyeVectors( &forward ); + origin = pPlayer->WorldSpaceCenter(); + pEntity = gEntList.FindEntityNearestFacing( origin, forward,0.95); + } + return pEntity; +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the nearest node in front of the player +// Input : +// Output : +//----------------------------------------------------------------------------- +CAI_Node *FindPickerAINode( CBasePlayer *pPlayer, NodeType_e nNodeType ) +{ + Vector forward; + Vector origin; + + pPlayer->EyeVectors( &forward ); + origin = pPlayer->EyePosition(); + return g_pAINetworkManager->GetEditOps()->FindAINodeNearestFacing( origin, forward,0.90, nNodeType); +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the nearest link in front of the player +// Input : +// Output : +//----------------------------------------------------------------------------- +CAI_Link *FindPickerAILink( CBasePlayer* pPlayer ) +{ + Vector forward; + Vector origin; + + pPlayer->EyeVectors( &forward ); + origin = pPlayer->EyePosition(); + return g_pAINetworkManager->GetEditOps()->FindAILinkNearestFacing( origin, forward,0.90); +} + +/* +=============== +ForceClientDllUpdate + +When recording a demo, we need to have the server tell us the entire client state +so that the client side .dll can behave correctly. +Reset stuff so that the state is transmitted. +=============== +*/ +void CBasePlayer::ForceClientDllUpdate( void ) +{ + m_iClientBattery = -1; + m_iTrain |= TRAIN_NEW; // Force new train message. + m_fWeapon = false; // Force weapon send + + // Force all HUD data to be resent to client + m_fInitHUD = true; + + // Now force all the necessary messages + // to be sent. + UpdateClientData(); + + UTIL_RestartAmbientSounds(); // MOTODO that updates the sounds for everybody +} + +/* +============ +ImpulseCommands +============ +*/ + +void CBasePlayer::ImpulseCommands( ) +{ + trace_t tr; + + int iImpulse = (int)m_nImpulse; + switch (iImpulse) + { + case 100: + // temporary flashlight for level designers + if ( FlashlightIsOn() ) + { + FlashlightTurnOff(); + } + else + { + FlashlightTurnOn(); + } + break; + + case 200: + if ( sv_cheats->GetBool() ) + { + CBaseCombatWeapon *pWeapon; + + pWeapon = GetActiveWeapon(); +#ifdef MAPBASE + if (!pWeapon) + return; +#endif + + if( pWeapon->IsEffectActive( EF_NODRAW ) ) + { + pWeapon->Deploy(); + } + else + { + pWeapon->Holster(); + } + } + break; + + case 201:// paint decal + + if ( gpGlobals->curtime < m_flNextDecalTime ) + { + // too early! + break; + } + + { + Vector forward; + EyeVectors( &forward ); + UTIL_TraceLine ( EyePosition(), + EyePosition() + forward * 128, + MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, & tr); + } + + if ( tr.fraction != 1.0 ) + {// line hit something, so paint a decal + m_flNextDecalTime = gpGlobals->curtime + decalfrequency.GetFloat(); + CSprayCan *pCan = CREATE_UNSAVED_ENTITY( CSprayCan, "spraycan" ); + pCan->Spawn( this ); + +#ifdef CSTRIKE_DLL + //============================================================================= + // HPE_BEGIN: + // [pfreese] Fire off a game event - the Counter-Strike stats manager listens + // to these achievements for one of the CS achievements. + //============================================================================= + + IGameEvent * event = gameeventmanager->CreateEvent( "player_decal" ); + if ( event ) + { + event->SetInt("userid", GetUserID() ); + gameeventmanager->FireEvent( event ); + } + + //============================================================================= + // HPE_END + //============================================================================= +#endif + } + + break; + + case 202:// player jungle sound + if ( gpGlobals->curtime < m_flNextDecalTime ) + { + // too early! + break; + + } + + EntityMessageBegin( this ); + WRITE_BYTE( PLAY_PLAYER_JINGLE ); + MessageEnd(); + + m_flNextDecalTime = gpGlobals->curtime + decalfrequency.GetFloat(); + break; + + default: + // check all of the cheat impulse commands now + CheatImpulseCommands( iImpulse ); + break; + } + + m_nImpulse = 0; +} + +#ifdef HL2_EPISODIC + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static void CreateJalopy( CBasePlayer *pPlayer ) +{ + // Cheat to create a jeep in front of the player + Vector vecForward; + AngleVectors( pPlayer->EyeAngles(), &vecForward ); + CBaseEntity *pJeep = (CBaseEntity *)CreateEntityByName( "prop_vehicle_jeep" ); + if ( pJeep ) + { + Vector vecOrigin = pPlayer->GetAbsOrigin() + vecForward * 256 + Vector(0,0,64); + QAngle vecAngles( 0, pPlayer->GetAbsAngles().y - 90, 0 ); + pJeep->SetAbsOrigin( vecOrigin ); + pJeep->SetAbsAngles( vecAngles ); + pJeep->KeyValue( "model", "models/vehicle.mdl" ); + pJeep->KeyValue( "solid", "6" ); + pJeep->KeyValue( "targetname", "jeep" ); + pJeep->KeyValue( "vehiclescript", "scripts/vehicles/jalopy.txt" ); + DispatchSpawn( pJeep ); + pJeep->Activate(); + pJeep->Teleport( &vecOrigin, &vecAngles, NULL ); + } +} + +void CC_CH_CreateJalopy( void ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( !pPlayer ) + return; + CreateJalopy( pPlayer ); +} + +static ConCommand ch_createjalopy("ch_createjalopy", CC_CH_CreateJalopy, "Spawn jalopy in front of the player.", FCVAR_CHEAT); + +#endif // HL2_EPISODIC + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static void CreateJeep( CBasePlayer *pPlayer ) +{ + // Cheat to create a jeep in front of the player + Vector vecForward; + AngleVectors( pPlayer->EyeAngles(), &vecForward ); + //Tony; in sp sdk, we have prop_vehicle_hl2buggy; because episode 2 modified the jeep code to turn it into the jalopy instead of the regular buggy +#if defined ( HL2_EPISODIC ) + CBaseEntity *pJeep = (CBaseEntity *)CreateEntityByName( "prop_vehicle_hl2buggy" ); +#else + CBaseEntity *pJeep = (CBaseEntity *)CreateEntityByName( "prop_vehicle_jeep" ); +#endif + if ( pJeep ) + { + Vector vecOrigin = pPlayer->GetAbsOrigin() + vecForward * 256 + Vector(0,0,64); + QAngle vecAngles( 0, pPlayer->GetAbsAngles().y - 90, 0 ); + pJeep->SetAbsOrigin( vecOrigin ); + pJeep->SetAbsAngles( vecAngles ); + pJeep->KeyValue( "model", "models/buggy.mdl" ); + pJeep->KeyValue( "solid", "6" ); +#if defined ( HL2_EPISODIC ) + pJeep->KeyValue( "targetname", "hl2buggy" ); +#else + pJeep->KeyValue( "targetname", "jeep" ); +#endif + pJeep->KeyValue( "vehiclescript", "scripts/vehicles/jeep_test.txt" ); + DispatchSpawn( pJeep ); + pJeep->Activate(); + pJeep->Teleport( &vecOrigin, &vecAngles, NULL ); + } +} + + +void CC_CH_CreateJeep( void ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( !pPlayer ) + return; + CreateJeep( pPlayer ); +} + +static ConCommand ch_createjeep("ch_createjeep", CC_CH_CreateJeep, "Spawn jeep in front of the player.", FCVAR_CHEAT); + + +//----------------------------------------------------------------------------- +// Create an airboat in front of the specified player +//----------------------------------------------------------------------------- +static void CreateAirboat( CBasePlayer *pPlayer ) +{ + // Cheat to create a jeep in front of the player + Vector vecForward; + AngleVectors( pPlayer->EyeAngles(), &vecForward ); + CBaseEntity *pJeep = ( CBaseEntity* )CreateEntityByName( "prop_vehicle_airboat" ); + if ( pJeep ) + { + Vector vecOrigin = pPlayer->GetAbsOrigin() + vecForward * 256 + Vector( 0,0,64 ); + QAngle vecAngles( 0, pPlayer->GetAbsAngles().y - 90, 0 ); + pJeep->SetAbsOrigin( vecOrigin ); + pJeep->SetAbsAngles( vecAngles ); + pJeep->KeyValue( "model", "models/airboat.mdl" ); + pJeep->KeyValue( "solid", "6" ); + pJeep->KeyValue( "targetname", "airboat" ); + pJeep->KeyValue( "vehiclescript", "scripts/vehicles/airboat.txt" ); + DispatchSpawn( pJeep ); + pJeep->Activate(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CC_CH_CreateAirboat( void ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( !pPlayer ) + return; + + CreateAirboat( pPlayer ); + +} + +static ConCommand ch_createairboat( "ch_createairboat", CC_CH_CreateAirboat, "Spawn airboat in front of the player.", FCVAR_CHEAT ); + + +//========================================================= +//========================================================= +void CBasePlayer::CheatImpulseCommands( int iImpulse ) +{ +#if !defined( HLDEMO_BUILD ) + if ( !sv_cheats->GetBool() ) + { + return; + } + + CBaseEntity *pEntity; + trace_t tr; + + switch ( iImpulse ) + { + case 76: + { + if (!giPrecacheGrunt) + { + giPrecacheGrunt = 1; + Msg( "You must now restart to use Grunt-o-matic.\n"); + } + else + { + Vector forward = UTIL_YawToVector( EyeAngles().y ); + Create("NPC_human_grunt", GetLocalOrigin() + forward * 128, GetLocalAngles()); + } + break; + } + + case 81: + + GiveNamedItem( "weapon_cubemap" ); + break; + + case 82: + // Cheat to create a jeep in front of the player + CreateJeep( this ); + break; + + case 83: + // Cheat to create a airboat in front of the player + CreateAirboat( this ); + break; + + case 101: + gEvilImpulse101 = true; + + EquipSuit(); + + // Give the player everything! + GiveAmmo( 255, "Pistol"); + GiveAmmo( 255, "AR2"); + GiveAmmo( 5, "AR2AltFire"); + GiveAmmo( 255, "SMG1"); + GiveAmmo( 255, "Buckshot"); + GiveAmmo( 3, "smg1_grenade"); + GiveAmmo( 3, "rpg_round"); + GiveAmmo( 5, "grenade"); + GiveAmmo( 32, "357" ); + GiveAmmo( 16, "XBowBolt" ); +#ifdef HL2_EPISODIC + GiveAmmo( 5, "Hopwire" ); +#endif + GiveNamedItem( "weapon_smg1" ); + GiveNamedItem( "weapon_frag" ); + GiveNamedItem( "weapon_crowbar" ); + GiveNamedItem( "weapon_pistol" ); + GiveNamedItem( "weapon_ar2" ); + GiveNamedItem( "weapon_shotgun" ); + GiveNamedItem( "weapon_physcannon" ); + GiveNamedItem( "weapon_bugbait" ); + GiveNamedItem( "weapon_rpg" ); + GiveNamedItem( "weapon_357" ); + GiveNamedItem( "weapon_crossbow" ); +#ifdef HL2_EPISODIC + // GiveNamedItem( "weapon_magnade" ); +#endif + if ( GetHealth() < 100 ) + { + TakeHealth( 25, DMG_GENERIC ); + } + + gEvilImpulse101 = false; + + break; + + case 102: + // Gibbage!!! + CGib::SpawnRandomGibs( this, 1, GIB_HUMAN ); + break; + + case 103: + // What the hell are you doing? + pEntity = FindEntityForward( this, true ); + if ( pEntity ) + { + CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); + if ( pNPC ) + pNPC->ReportAIState(); + } + break; + + case 106: + // Give me the classname and targetname of this entity. + pEntity = FindEntityForward( this, true ); + if ( pEntity ) + { + Msg( "Classname: %s", pEntity->GetClassname() ); + + if ( pEntity->GetEntityName() != NULL_STRING ) + { + Msg( " - Name: %s\n", STRING( pEntity->GetEntityName() ) ); + } + else + { + Msg( " - Name: No Targetname\n" ); + } + + if ( pEntity->m_iParent != NULL_STRING ) + Msg( "Parent: %s\n", STRING(pEntity->m_iParent) ); + + Msg( "Model: %s\n", STRING( pEntity->GetModelName() ) ); + if ( pEntity->m_iGlobalname != NULL_STRING ) + Msg( "Globalname: %s\n", STRING(pEntity->m_iGlobalname) ); + } + break; + + case 107: + { + trace_t tr; + + edict_t *pWorld = engine->PEntityOfEntIndex( 0 ); + + Vector start = EyePosition(); + Vector forward; + EyeVectors( &forward ); + Vector end = start + forward * 1024; + UTIL_TraceLine( start, end, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.m_pEnt ) + pWorld = tr.m_pEnt->edict(); + + const char *pTextureName = tr.surface.name; + + if ( pTextureName ) + Msg( "Texture: %s\n", pTextureName ); + } + break; + + // + // Sets the debug NPC to be the NPC under the crosshair. + // + case 108: + { + pEntity = FindEntityForward( this, true ); + if ( pEntity ) + { + CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); + if ( pNPC != NULL ) + { + Msg( "Debugging %s (0x%p)\n", pNPC->GetClassname(), pNPC ); + CAI_BaseNPC::SetDebugNPC( pNPC ); + } + } + break; + } + + case 195:// show shortest paths for entire level to nearest node + { + Create("node_viewer_fly", GetLocalOrigin(), GetLocalAngles()); + } + break; + case 196:// show shortest paths for entire level to nearest node + { + Create("node_viewer_large", GetLocalOrigin(), GetLocalAngles()); + } + break; + case 197:// show shortest paths for entire level to nearest node + { + Create("node_viewer_human", GetLocalOrigin(), GetLocalAngles()); + } + break; + case 202:// Random blood splatter + { + Vector forward; + EyeVectors( &forward ); + UTIL_TraceLine ( EyePosition(), + EyePosition() + forward * 128, + MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, & tr); + + if ( tr.fraction != 1.0 ) + {// line hit something, so paint a decal + CBloodSplat *pBlood = CREATE_UNSAVED_ENTITY( CBloodSplat, "bloodsplat" ); + pBlood->Spawn( this ); + } + } + break; + case 203:// remove creature. + pEntity = FindEntityForward( this, true ); + if ( pEntity ) + { + UTIL_Remove( pEntity ); +// if ( pEntity->m_takedamage ) +// pEntity->SetThink(SUB_Remove); + } + break; + } +#endif // HLDEMO_BUILD +} + + +bool CBasePlayer::ClientCommand( const CCommand &args ) +{ + const char *cmd = args[0]; +#ifdef _DEBUG + if( stricmp( cmd, "test_SmokeGrenade" ) == 0 ) + { + if ( sv_cheats && sv_cheats->GetBool() ) + { + ParticleSmokeGrenade *pSmoke = dynamic_cast( CreateEntityByName(PARTICLESMOKEGRENADE_ENTITYNAME) ); + if ( pSmoke ) + { + Vector vForward; + AngleVectors( GetLocalAngles(), &vForward ); + vForward.z = 0; + VectorNormalize( vForward ); + + pSmoke->SetLocalOrigin( GetLocalOrigin() + vForward * 100 ); + pSmoke->SetFadeTime(25, 30); // Fade out between 25 seconds and 30 seconds. + pSmoke->Activate(); + pSmoke->SetLifetime(30); + pSmoke->FillVolume(); + + return true; + } + } + } + else +#endif // _DEBUG + if( stricmp( cmd, "vehicleRole" ) == 0 ) + { + // Get the vehicle role value. + if ( args.ArgC() == 2 ) + { + // Check to see if a player is in a vehicle. + if ( IsInAVehicle() ) + { + int nRole = atoi( args[1] ); + IServerVehicle *pVehicle = GetVehicle(); + if ( pVehicle ) + { + // Only switch roles if role is empty! + if ( !pVehicle->GetPassenger( nRole ) ) + { + LeaveVehicle(); + GetInVehicle( pVehicle, nRole ); + } + } + } + + return true; + } + } + else if ( HandleVoteCommands( args ) ) + { + return true; + } + else if ( stricmp( cmd, "spectate" ) == 0 ) // join spectator team & start observer mode + { + if ( GetTeamNumber() == TEAM_SPECTATOR ) + return true; + + ConVarRef mp_allowspectators( "mp_allowspectators" ); + if ( mp_allowspectators.IsValid() ) + { + if ( ( mp_allowspectators.GetBool() == false ) && !IsHLTV() && !IsReplay() ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Be_Spectator" ); + return true; + } + } + + if ( !IsDead() ) + { + CommitSuicide(); // kill player + } + + RemoveAllItems( true ); + + ChangeTeam( TEAM_SPECTATOR ); + + StartObserverMode( OBS_MODE_ROAMING ); + return true; + } + else if ( stricmp( cmd, "spec_mode" ) == 0 ) // new observer mode + { + int mode; + + if ( GetObserverMode() == OBS_MODE_FREEZECAM ) + { + AttemptToExitFreezeCam(); + return true; + } + + // not allowed to change spectator modes when mp_fadetoblack is being used + if ( mp_fadetoblack.GetBool() ) + { + if ( GetTeamNumber() > TEAM_SPECTATOR ) + return true; + } + + // check for parameters. + if ( args.ArgC() >= 2 ) + { + mode = atoi( args[1] ); + + if ( mode < OBS_MODE_IN_EYE || mode > LAST_PLAYER_OBSERVERMODE ) + mode = OBS_MODE_IN_EYE; + } + else + { + // switch to next spec mode if no parameter given + mode = GetObserverMode() + 1; + + if ( mode > LAST_PLAYER_OBSERVERMODE ) + { + mode = OBS_MODE_IN_EYE; + } + else if ( mode < OBS_MODE_IN_EYE ) + { + mode = OBS_MODE_ROAMING; + } + + } + + // don't allow input while player or death cam animation + if ( GetObserverMode() > OBS_MODE_DEATHCAM ) + { + // set new spectator mode, don't allow OBS_MODE_NONE + if ( !SetObserverMode( mode ) ) + ClientPrint( this, HUD_PRINTCONSOLE, "#Spectator_Mode_Unkown"); + else + engine->ClientCommand( edict(), "cl_spec_mode %d", mode ); + } + else + { + // remember spectator mode for later use + m_iObserverLastMode = mode; + engine->ClientCommand( edict(), "cl_spec_mode %d", mode ); + } + + return true; + } + else if ( stricmp( cmd, "spec_next" ) == 0 ) // chase next player + { + if ( GetObserverMode() > OBS_MODE_FIXED ) + { + // set new spectator mode + CBaseEntity * target = FindNextObserverTarget( false ); + if ( target ) + { + SetObserverTarget( target ); + } + } + else if ( GetObserverMode() == OBS_MODE_FREEZECAM ) + { + AttemptToExitFreezeCam(); + } + + return true; + } + else if ( stricmp( cmd, "spec_prev" ) == 0 ) // chase prevoius player + { + if ( GetObserverMode() > OBS_MODE_FIXED ) + { + // set new spectator mode + CBaseEntity * target = FindNextObserverTarget( true ); + if ( target ) + { + SetObserverTarget( target ); + } + } + else if ( GetObserverMode() == OBS_MODE_FREEZECAM ) + { + AttemptToExitFreezeCam(); + } + + return true; + } + + else if ( stricmp( cmd, "spec_player" ) == 0 ) // chase next player + { + if ( GetObserverMode() > OBS_MODE_FIXED && args.ArgC() == 2 ) + { + int index = atoi( args[1] ); + + CBasePlayer * target; + + if ( index == 0 ) + { + target = UTIL_PlayerByName( args[1] ); + } + else + { + target = UTIL_PlayerByIndex( index ); + } + + if ( IsValidObserverTarget( target ) ) + { + SetObserverTarget( target ); + } + } + + return true; + } + + else if ( stricmp( cmd, "spec_goto" ) == 0 ) // chase next player + { + if ( ( GetObserverMode() == OBS_MODE_FIXED || + GetObserverMode() == OBS_MODE_ROAMING ) && + args.ArgC() == 6 ) + { + Vector origin; + origin.x = atof( args[1] ); + origin.y = atof( args[2] ); + origin.z = atof( args[3] ); + + QAngle angle; + angle.x = atof( args[4] ); + angle.y = atof( args[5] ); + angle.z = 0.0f; + +#ifdef MAPBASE + #define SPECGOTO_MAX_VALUE 0xFFFF/2.0f + + // This could crash the game somehow if not checked.. Thanks to Nairda. + if (abs(angle.x) <= 360.0f && abs(angle.y) <= 360.0f && abs(origin.x) < SPECGOTO_MAX_VALUE && + abs(origin.y) < SPECGOTO_MAX_VALUE && abs(origin.z) < SPECGOTO_MAX_VALUE) + { + JumptoPosition(origin, angle); + } + else + { + engine->ClientPrintf(edict(), "spec_goto: Out-of-bounds"); + } +#else + JumptoPosition( origin, angle ); +#endif + } + + return true; + } + else if ( stricmp( cmd, "playerperf" ) == 0 ) + { + int nRecip = entindex(); + if ( args.ArgC() >= 2 ) + { + nRecip = clamp( Q_atoi( args.Arg( 1 ) ), 1, gpGlobals->maxClients ); + } + int nRecords = -1; // all + if ( args.ArgC() >= 3 ) + { + nRecords = MAX( Q_atoi( args.Arg( 2 ) ), 1 ); + } + + CBasePlayer *pl = UTIL_PlayerByIndex( nRecip ); + if ( pl ) + { + pl->DumpPerfToRecipient( this, nRecords ); + } + return true; + } + + return false; +} + +extern bool UTIL_ItemCanBeTouchedByPlayer( CBaseEntity *pItem, CBasePlayer *pPlayer ); + +//----------------------------------------------------------------------------- +// Purpose: Player reacts to bumping a weapon. +// Input : pWeapon - the weapon that the player bumped into. +// Output : Returns true if player picked up the weapon +//----------------------------------------------------------------------------- +bool CBasePlayer::BumpWeapon( CBaseCombatWeapon *pWeapon ) +{ + CBaseCombatCharacter *pOwner = pWeapon->GetOwner(); + + // Can I have this weapon type? + if ( !IsAllowedToPickupWeapons() ) + return false; + + if ( pOwner || !Weapon_CanUse( pWeapon ) || !g_pGameRules->CanHavePlayerItem( this, pWeapon ) ) + { + if ( gEvilImpulse101 ) + { + UTIL_Remove( pWeapon ); + } + return false; + } + + // Act differently in the episodes + if ( hl2_episodic.GetBool() ) + { + // Don't let the player touch the item unless unobstructed + if ( !UTIL_ItemCanBeTouchedByPlayer( pWeapon, this ) && !gEvilImpulse101 ) + return false; + } + else + { + // Don't let the player fetch weapons through walls (use MASK_SOLID so that you can't pickup through windows) +#ifdef MAPBASE + if( (pWeapon->FVisible( this, MASK_SOLID ) == false && !(GetFlags() & FL_NOTARGET)) && !HasSpawnFlags(SF_WEAPON_ALWAYS_TOUCHABLE) ) +#else + if( pWeapon->FVisible( this, MASK_SOLID ) == false && !(GetFlags() & FL_NOTARGET) ) +#endif + return false; + } + + // ---------------------------------------- + // If I already have it just take the ammo + // ---------------------------------------- + if (Weapon_OwnsThisType( pWeapon->GetClassname(), pWeapon->GetSubType())) + { + if( Weapon_EquipAmmoOnly( pWeapon ) ) + { + // Only remove me if I have no ammo left +#ifdef MAPBASE + if ( pWeapon->HasPrimaryAmmo() || pWeapon->HasSecondaryAmmo() ) +#else + if ( pWeapon->HasPrimaryAmmo() ) +#endif + return false; + + UTIL_Remove( pWeapon ); + return true; + } + else + { + return false; + } + } +#ifdef MAPBASE + // -------------------------------------------------------------------------------- + // If we own a weapon in the same position take the ammo but leave the weapon behind + // -------------------------------------------------------------------------------- + if (!pWeapon->HasSpawnFlags(SF_WEAPON_USED)) // Make sure we're being used and not being bumped + { + for (int i=0;iGetSlot() == m_hMyWeapons[i]->GetSlot() && + pWeapon->GetPosition() == m_hMyWeapons[i]->GetPosition()) + { + //Weapon_EquipAmmoOnly( pWeapon ); + + // I'm too lazy to make my own version of Weapon_EquipAmmoOnly that doesn't check if we already have the weapon first + int primaryGiven = (pWeapon->UsesClipsForAmmo1()) ? pWeapon->m_iClip1 : pWeapon->GetPrimaryAmmoCount(); + int secondaryGiven = (pWeapon->UsesClipsForAmmo2()) ? pWeapon->m_iClip2 : pWeapon->GetSecondaryAmmoCount(); + + int takenPrimary = GiveAmmo( primaryGiven, pWeapon->m_iPrimaryAmmoType); + int takenSecondary = GiveAmmo( secondaryGiven, pWeapon->m_iSecondaryAmmoType); + + if( pWeapon->UsesClipsForAmmo1() ) + { + pWeapon->m_iClip1 -= takenPrimary; + } + else + { + pWeapon->SetPrimaryAmmoCount( pWeapon->GetPrimaryAmmoCount() - takenPrimary ); + } + + if( pWeapon->UsesClipsForAmmo2() ) + { + pWeapon->m_iClip2 -= takenSecondary; + } + else + { + pWeapon->SetSecondaryAmmoCount( pWeapon->GetSecondaryAmmoCount() - takenSecondary ); + } + + return false; + } + } + } +#endif + // ------------------------- + // Otherwise take the weapon + // ------------------------- +#ifndef MAPBASE + else +#endif + { + pWeapon->CheckRespawn(); + + pWeapon->AddSolidFlags( FSOLID_NOT_SOLID ); + pWeapon->AddEffects( EF_NODRAW ); + + Weapon_Equip( pWeapon ); + if ( IsInAVehicle() ) + { + pWeapon->Holster(); + } + else + { +#ifdef HL2_DLL + + if ( IsX360() ) + { + CFmtStr hint; + hint.sprintf( "#valve_hint_select_%s", pWeapon->GetClassname() ); + UTIL_HudHintText( this, hint.Access() ); + } + +#ifndef MAPBASE // See CBasePlayer::Weapon_Equip. + // Always switch to a newly-picked up weapon + if ( !PlayerHasMegaPhysCannon() ) + { + // If it uses clips, load it full. (this is the first time you've picked up this type of weapon) + if ( pWeapon->UsesClipsForAmmo1() ) + { + pWeapon->m_iClip1 = pWeapon->GetMaxClip1(); + } + + Weapon_Switch( pWeapon ); + } +#endif +#endif + } + return true; + } +} + + +bool CBasePlayer::RemovePlayerItem( CBaseCombatWeapon *pItem ) +{ + if (GetActiveWeapon() == pItem) + { + ResetAutoaim( ); + pItem->Holster( ); + pItem->SetNextThink( TICK_NEVER_THINK );; // crowbar may be trying to swing again, etc + pItem->SetThink( NULL ); + } + + if ( m_hLastWeapon.Get() == pItem ) + { + Weapon_SetLast( NULL ); + } + + return Weapon_Detach( pItem ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Hides or shows the player's view model. The "r_drawviewmodel" cvar +// can still hide the viewmodel even if this is set to true. +// Input : bShow - true to show, false to hide the view model. +//----------------------------------------------------------------------------- +void CBasePlayer::ShowViewModel(bool bShow) +{ + m_Local.m_bDrawViewmodel = bShow; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : bDraw - +//----------------------------------------------------------------------------- +void CBasePlayer::ShowCrosshair( bool bShow ) +{ + if ( bShow ) + { + m_Local.m_iHideHUD &= ~HIDEHUD_CROSSHAIR; + } + else + { + m_Local.m_iHideHUD |= HIDEHUD_CROSSHAIR; + } +} + +//----------------------------------------------------------------------------- +// Used by vscript to determine if the player is noclipping +//----------------------------------------------------------------------------- +bool CBasePlayer::ScriptIsPlayerNoclipping(void) +{ + return (GetMoveType() == MOVETYPE_NOCLIP); +} + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +HSCRIPT CBasePlayer::VScriptGetExpresser() +{ + HSCRIPT hScript = NULL; + CAI_Expresser *pExpresser = GetExpresser(); + if (pExpresser) + { + hScript = g_pScriptVM->RegisterInstance( pExpresser ); + } + + return hScript; +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +QAngle CBasePlayer::BodyAngles() +{ + return EyeAngles(); +} + +//------------------------------------------------------------------------------ +// Purpose : Add noise to BodyTarget() to give enemy a better chance of +// getting a clear shot when the player is peeking above a hole +// or behind a ladder (eventually the randomly-picked point +// along the spine will be one that is exposed above the hole or +// between rungs of a ladder.) +// Input : +// Output : +//------------------------------------------------------------------------------ +Vector CBasePlayer::BodyTarget( const Vector &posSrc, bool bNoisy ) +{ + if ( IsInAVehicle() ) + { + return GetVehicle()->GetVehicleEnt()->BodyTarget( posSrc, bNoisy ); + } + if (bNoisy) + { + return GetAbsOrigin() + (GetViewOffset() * random->RandomFloat( 0.7, 1.0 )); + } + else + { + return EyePosition(); + } +}; + +/* +========================================================= + UpdateClientData + +resends any changed player HUD info to the client. +Called every frame by PlayerPreThink +Also called at start of demo recording and playback by +ForceClientDllUpdate to ensure the demo gets messages +reflecting all of the HUD state info. +========================================================= +*/ +void CBasePlayer::UpdateClientData( void ) +{ + CSingleUserRecipientFilter user( this ); + user.MakeReliable(); + + if (m_fInitHUD) + { + m_fInitHUD = false; + gInitHUD = false; + + UserMessageBegin( user, "ResetHUD" ); + WRITE_BYTE( 0 ); + MessageEnd(); + + if ( !m_fGameHUDInitialized ) + { + g_pGameRules->InitHUD( this ); + InitHUD(); + m_fGameHUDInitialized = true; + if ( g_pGameRules->IsMultiplayer() ) + { + variant_t value; + g_EventQueue.AddEvent( "game_player_manager", "OnPlayerJoin", value, 0, this, this ); + } + } + + variant_t value; + g_EventQueue.AddEvent( "game_player_manager", "OnPlayerSpawn", value, 0, this, this ); + } + + // HACKHACK -- send the message to display the game title + CWorld *world = GetWorldEntity(); + if ( world && world->GetDisplayTitle() ) + { + UserMessageBegin( user, "GameTitle" ); + MessageEnd(); + world->SetDisplayTitle( false ); + } + + if (m_ArmorValue != m_iClientBattery) + { + m_iClientBattery = m_ArmorValue; + + // send "battery" update message + if ( usermessages->LookupUserMessage( "Battery" ) != -1 ) + { + UserMessageBegin( user, "Battery" ); + WRITE_SHORT( (int)m_ArmorValue); + MessageEnd(); + } + } + +#if 0 // BYE BYE!! + // Update Flashlight + if ((m_flFlashLightTime) && (m_flFlashLightTime <= gpGlobals->curtime)) + { + if (FlashlightIsOn()) + { + if (m_iFlashBattery) + { + m_flFlashLightTime = FLASH_DRAIN_TIME + gpGlobals->curtime; + m_iFlashBattery--; + + if (!m_iFlashBattery) + FlashlightTurnOff(); + } + } + else + { + if (m_iFlashBattery < 100) + { + m_flFlashLightTime = FLASH_CHARGE_TIME + gpGlobals->curtime; + m_iFlashBattery++; + } + else + m_flFlashLightTime = 0; + } + } +#endif + + CheckTrainUpdate(); + + // Update all the items + for ( int i = 0; i < WeaponCount(); i++ ) + { + if ( GetWeapon(i) ) // each item updates it's successors + GetWeapon(i)->UpdateClientData( this ); + } + + // update the client with our poison state + m_Local.m_bPoisoned = ( m_bitsDamageType & DMG_POISON ) + && ( m_nPoisonDmg > m_nPoisonRestored ) + && ( m_iHealth < 100 ); + + // Check if the bonus progress HUD element should be displayed + if ( m_iBonusChallenge == 0 && m_iBonusProgress == 0 && !( m_Local.m_iHideHUD & HIDEHUD_BONUS_PROGRESS ) ) + m_Local.m_iHideHUD |= HIDEHUD_BONUS_PROGRESS; + if ( ( m_iBonusChallenge != 0 )&& ( m_Local.m_iHideHUD & HIDEHUD_BONUS_PROGRESS ) ) + m_Local.m_iHideHUD &= ~HIDEHUD_BONUS_PROGRESS; + + // Let any global rules update the HUD, too + g_pGameRules->UpdateClientData( this ); +} + +void CBasePlayer::RumbleEffect( unsigned char index, unsigned char rumbleData, unsigned char rumbleFlags ) +{ + if( !IsAlive() ) + return; + + CSingleUserRecipientFilter filter( this ); + filter.MakeReliable(); + + UserMessageBegin( filter, "Rumble" ); + WRITE_BYTE( index ); + WRITE_BYTE( rumbleData ); + WRITE_BYTE( rumbleFlags ); + MessageEnd(); +} + +void CBasePlayer::EnableControl(bool fControl) +{ + if (!fControl) + AddFlag( FL_FROZEN ); + else + RemoveFlag( FL_FROZEN ); + +} + +void CBasePlayer::CheckTrainUpdate( void ) +{ + if ( ( m_iTrain & TRAIN_NEW ) ) + { + CSingleUserRecipientFilter user( this ); + user.MakeReliable(); + + // send "Train" update message + UserMessageBegin( user, "Train" ); + WRITE_BYTE(m_iTrain & 0xF); + MessageEnd(); + + m_iTrain &= ~TRAIN_NEW; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether the player should autoaim or not +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBasePlayer::ShouldAutoaim( void ) +{ + // cannot be in multiplayer + if ( gpGlobals->maxClients > 1 ) + return false; + + // autoaiming is only for easy and medium skill + return ( IsX360() || !g_pGameRules->IsSkillLevel(SKILL_HARD) ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CBasePlayer::GetAutoaimVector( float flScale ) +{ + autoaim_params_t params; + + params.m_fScale = flScale; + params.m_fMaxDist = autoaim_max_dist.GetFloat(); + + GetAutoaimVector( params ); + return params.m_vecAutoAimDir; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CBasePlayer::GetAutoaimVector( float flScale, float flMaxDist ) +{ + autoaim_params_t params; + + params.m_fScale = flScale; + params.m_fMaxDist = flMaxDist; + + GetAutoaimVector( params ); + return params.m_vecAutoAimDir; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBasePlayer::GetAutoaimVector( autoaim_params_t ¶ms ) +{ + // Assume autoaim will not be assisting. + params.m_bAutoAimAssisting = false; + + if ( ( ShouldAutoaim() == false ) || ( params.m_fScale == AUTOAIM_SCALE_DIRECT_ONLY ) ) + { + Vector forward; + AngleVectors( EyeAngles() + m_Local.m_vecPunchAngle, &forward ); + + params.m_vecAutoAimDir = forward; + params.m_hAutoAimEntity.Set(NULL); + params.m_vecAutoAimPoint = vec3_invalid; + params.m_bAutoAimAssisting = false; + return; + } + + Vector vecSrc = Weapon_ShootPosition( ); + + m_vecAutoAim.Init( 0.0f, 0.0f, 0.0f ); + + QAngle angles = AutoaimDeflection( vecSrc, params ); + + // update ontarget if changed + if ( !g_pGameRules->AllowAutoTargetCrosshair() ) + m_fOnTarget = false; + + if (angles.x > 180) + angles.x -= 360; + if (angles.x < -180) + angles.x += 360; + if (angles.y > 180) + angles.y -= 360; + if (angles.y < -180) + angles.y += 360; + + if (angles.x > 25) + angles.x = 25; + if (angles.x < -25) + angles.x = -25; + if (angles.y > 12) + angles.y = 12; + if (angles.y < -12) + angles.y = -12; + + Vector forward; + + if( IsInAVehicle() && g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE ) + { + m_vecAutoAim = angles; + AngleVectors( EyeAngles() + m_vecAutoAim, &forward ); + } + else + { + // always use non-sticky autoaim + m_vecAutoAim = angles * 0.9f; + AngleVectors( EyeAngles() + m_Local.m_vecPunchAngle + m_vecAutoAim, &forward ); + } + + params.m_vecAutoAimDir = forward; +} + +//----------------------------------------------------------------------------- +// Targets represent themselves to autoaim as a viewplane-parallel disc with +// a radius specified by the target. The player then modifies this radius +// to achieve more or less aggressive aiming assistance +//----------------------------------------------------------------------------- +float CBasePlayer::GetAutoaimScore( const Vector &eyePosition, const Vector &viewDir, const Vector &vecTarget, CBaseEntity *pTarget, float fScale, CBaseCombatWeapon *pActiveWeapon ) +{ + float radiusSqr; + float targetRadius = pTarget->GetAutoAimRadius() * fScale; + + if( pActiveWeapon != NULL ) + targetRadius *= pActiveWeapon->WeaponAutoAimScale(); + + float targetRadiusSqr = Square( targetRadius ); + + Vector vecNearestPoint = PointOnLineNearestPoint( eyePosition, eyePosition + viewDir * 8192, vecTarget ); + Vector vecDiff = vecTarget - vecNearestPoint; + + radiusSqr = vecDiff.LengthSqr(); + + if( radiusSqr <= targetRadiusSqr ) + { + float score; + + score = 1.0f - (radiusSqr / targetRadiusSqr); + + Assert( score >= 0.0f && score <= 1.0f ); + return score; + } + + // 0 means no score- doesn't qualify for autoaim. + return 0.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &vecSrc - +// flDist - +// flDelta - +// Output : Vector +//----------------------------------------------------------------------------- +QAngle CBasePlayer::AutoaimDeflection( Vector &vecSrc, autoaim_params_t ¶ms ) +{ + float bestscore; + float score; + QAngle eyeAngles; + Vector bestdir; + CBaseEntity *bestent; + trace_t tr; + Vector v_forward, v_right, v_up; + + if ( ShouldAutoaim() == false ) + { + m_fOnTarget = false; + return vec3_angle; + } + + eyeAngles = EyeAngles(); + AngleVectors( eyeAngles + m_Local.m_vecPunchAngle + m_vecAutoAim, &v_forward, &v_right, &v_up ); + + // try all possible entities + bestdir = v_forward; + bestscore = 0.0f; + bestent = NULL; + + //Reset this data + m_fOnTarget = false; + params.m_bOnTargetNatural = false; + + CBaseEntity *pIgnore = NULL; + + if( IsInAVehicle() ) + { + pIgnore = GetVehicleEntity(); + } + + CTraceFilterSkipTwoEntities traceFilter( this, pIgnore, COLLISION_GROUP_NONE ); + + UTIL_TraceLine( vecSrc, vecSrc + bestdir * MAX_COORD_FLOAT, MASK_SHOT, &traceFilter, &tr ); + + CBaseEntity *pEntHit = tr.m_pEnt; + + if ( pEntHit && pEntHit->m_takedamage != DAMAGE_NO && pEntHit->GetHealth() > 0 ) + { + // don't look through water + if (!((GetWaterLevel() != 3 && pEntHit->GetWaterLevel() == 3) || (GetWaterLevel() == 3 && pEntHit->GetWaterLevel() == 0))) + { + if( pEntHit->ShouldAttractAutoAim(this) ) + { + bool bAimAtThis = true; + + if( pEntHit->IsNPC() && g_pGameRules->GetAutoAimMode() > AUTOAIM_NONE ) + { + int iRelationType = GetDefaultRelationshipDisposition( pEntHit->Classify() ); + + if( iRelationType != D_HT ) + { + bAimAtThis = false; + } + } + + if( bAimAtThis ) + { + if ( pEntHit->GetFlags() & FL_AIMTARGET ) + { + m_fOnTarget = true; + } + + // Player is already on target naturally, don't autoaim. + // Fill out the autoaim_params_t struct, though. + params.m_hAutoAimEntity.Set(pEntHit); + params.m_vecAutoAimDir = bestdir; + params.m_vecAutoAimPoint = tr.endpos; + params.m_bAutoAimAssisting = false; + params.m_bOnTargetNatural = true; + return vec3_angle; + } + } + + //Fall through and look for an autoaim ent. + } + } + + int count = AimTarget_ListCount(); + if ( count ) + { + CBaseEntity **pList = (CBaseEntity **)stackalloc( sizeof(CBaseEntity *) * count ); + AimTarget_ListCopy( pList, count ); + + for ( int i = 0; i < count; i++ ) + { + Vector center; + Vector dir; + CBaseEntity *pEntity = pList[i]; + + // Don't autoaim at anything that doesn't want to be. + if( !pEntity->ShouldAttractAutoAim(this) ) + continue; + + // Don't shoot yourself + if ( pEntity == this ) + continue; + + if ( (pEntity->IsNPC() && !pEntity->IsAlive()) || !pEntity->edict() ) + continue; + + if ( !g_pGameRules->ShouldAutoAim( this, pEntity->edict() ) ) + continue; + + // don't look through water + if ((GetWaterLevel() != 3 && pEntity->GetWaterLevel() == 3) || (GetWaterLevel() == 3 && pEntity->GetWaterLevel() == 0)) + continue; + + if( pEntity->MyNPCPointer() ) + { + // If this entity is an NPC, only aim if it is an enemy. + if ( IRelationType( pEntity ) != D_HT ) + { + if ( !pEntity->IsPlayer() && !g_pGameRules->IsDeathmatch()) + // Msg( "friend\n"); + continue; + } + } + + // Don't autoaim at the noisy bodytarget, this makes the autoaim crosshair wobble. + //center = pEntity->BodyTarget( vecSrc, false ); + center = pEntity->WorldSpaceCenter(); + + dir = (center - vecSrc); + + float dist = dir.Length2D(); + VectorNormalize( dir ); + + // Skip if out of range. + if( dist > params.m_fMaxDist ) + continue; + + float dot = DotProduct (dir, v_forward ); + + // make sure it's in front of the player + if( dot < 0 ) + continue; + + if( !(pEntity->GetFlags() & FL_FLY) ) + { + // Refuse to take wild shots at targets far from reticle. + if( GetActiveWeapon() != NULL && dot < GetActiveWeapon()->GetMaxAutoAimDeflection() ) + { + // Be lenient if the player is looking down, though. 30 degrees through 90 degrees of pitch. + // (90 degrees is looking down at player's own 'feet'. Looking straight ahead is 0 degrees pitch. + // This was done for XBox to make it easier to fight headcrabs around the player's feet. + if( eyeAngles.x < 30.0f || eyeAngles.x > 90.0f || g_pGameRules->GetAutoAimMode() != AUTOAIM_ON_CONSOLE ) + { + continue; + } + } + } + + score = GetAutoaimScore(vecSrc, v_forward, pEntity->GetAutoAimCenter(), pEntity, params.m_fScale, GetActiveWeapon() ); + + if( score <= bestscore ) + { + continue; + } + + UTIL_TraceLine( vecSrc, center, MASK_SHOT, &traceFilter, &tr ); + + if (tr.fraction != 1.0 && tr.m_pEnt != pEntity ) + { + // Msg( "hit %s, can't see %s\n", STRING( tr.u.ent->classname ), STRING( pEdict->classname ) ); + continue; + } + + // This is the best candidate so far. + bestscore = score; + bestent = pEntity; + bestdir = dir; + } + if ( bestent ) + { + QAngle bestang; + + VectorAngles( bestdir, bestang ); + + if( IsInAVehicle() ) + { + bestang -= EyeAngles(); + } + else + { + bestang -= EyeAngles() - m_Local.m_vecPunchAngle; + } + + m_fOnTarget = true; + + // Autoaim detected a target for us. Aim automatically at its bodytarget. + params.m_hAutoAimEntity.Set(bestent); + params.m_vecAutoAimDir = bestdir; + params.m_vecAutoAimPoint = bestent->BodyTarget( vecSrc, false ); + params.m_bAutoAimAssisting = true; + + return bestang; + } + } + + return vec3_angle; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::ResetAutoaim( void ) +{ + if (m_vecAutoAim.x != 0 || m_vecAutoAim.y != 0) + { + m_vecAutoAim = QAngle( 0, 0, 0 ); + engine->CrosshairAngle( edict(), 0, 0 ); + } + m_fOnTarget = false; +} + +// ========================================================================== +// > Weapon stuff +// ========================================================================== + +//----------------------------------------------------------------------------- +// Purpose: Override base class, player can always use weapon +// Input : A weapon +// Output : true or false +//----------------------------------------------------------------------------- +bool CBasePlayer::Weapon_CanUse( CBaseCombatWeapon *pWeapon ) +{ + return true; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Override to clear dropped weapon from the hud +//----------------------------------------------------------------------------- +void CBasePlayer::Weapon_Drop( CBaseCombatWeapon *pWeapon, const Vector *pvecTarget /* = NULL */, const Vector *pVelocity /* = NULL */ ) +{ + bool bWasActiveWeapon = false; + if ( pWeapon == GetActiveWeapon() ) + { + bWasActiveWeapon = true; + } + + if ( pWeapon ) + { + if ( bWasActiveWeapon ) + { + pWeapon->SendWeaponAnim( ACT_VM_IDLE ); + } + } + + BaseClass::Weapon_Drop( pWeapon, pvecTarget, pVelocity ); + + if ( bWasActiveWeapon ) + { + if (!SwitchToNextBestWeapon( NULL )) + { + CBaseViewModel *vm = GetViewModel(); + if ( vm ) + { + vm->AddEffects( EF_NODRAW ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : weaponSlot - +//----------------------------------------------------------------------------- +void CBasePlayer::Weapon_DropSlot( int weaponSlot ) +{ + CBaseCombatWeapon *pWeapon; + + // Check for that slot being occupied already + for ( int i=0; i < MAX_WEAPONS; i++ ) + { + pWeapon = GetWeapon( i ); + + if ( pWeapon != NULL ) + { + // If the slots match, it's already occupied + if ( pWeapon->GetSlot() == weaponSlot ) + { + Weapon_Drop( pWeapon, NULL, NULL ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Override to add weapon to the hud +//----------------------------------------------------------------------------- +void CBasePlayer::Weapon_Equip( CBaseCombatWeapon *pWeapon ) +{ + BaseClass::Weapon_Equip( pWeapon ); + +#ifdef MAPBASE + // So, I discovered that BumpWeapon seems to have some deprecated code. + // It automatically switches the player to all new weapons. Sounds normal, right? + // Except that's *also* handled here. Since the BumpWeapon code implied the player could pick up weapons while carrying the mega physcannon, + // I assumed it was some old, deprecated code and, since I needed to remove a piece of (also deprecated) code in it, I decided to remove it entirely + // and hand weapon switching to this alone. Seems straightforward, right? + // + // Well, it turns out, this code was more complicated than I thought and used various weights and stuff to determine if a weapon was worth automatically switching to. + // It doesn't automatically switch to most of the weapons. Even though I seem to be right about that old code being deprecated, + // I got irritated and...uh...replaced the correct Weapon_Equip code with the old deprecated code from BumpWeapon. + // + // Trust me. It was hard and pointless to get used to. You'll thank me later. + + if ( !PlayerHasMegaPhysCannon() ) + { + Weapon_Switch( pWeapon ); + } +#else + bool bShouldSwitch = g_pGameRules->FShouldSwitchWeapon( this, pWeapon ); + +#ifdef HL2_DLL + if ( bShouldSwitch == false && PhysCannonGetHeldEntity( GetActiveWeapon() ) == pWeapon && + Weapon_OwnsThisType( pWeapon->GetClassname(), pWeapon->GetSubType()) ) + { + bShouldSwitch = true; + } +#endif//HL2_DLL + + // should we switch to this item? + if ( bShouldSwitch ) + { + Weapon_Switch( pWeapon ); + } +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Activity CBasePlayer::Weapon_TranslateActivity( Activity baseAct, bool *pRequired ) +{ +#ifdef HL2_DLL + // HAAAAAAAAAAAAAACKS! + if (GetActiveWeapon()) + { + int translated = baseAct; + int iActOffset = (baseAct - ACT_HL2MP_IDLE); + + string_t iszClassname = GetActiveWeapon()->m_iClassname; + if (iszClassname == gm_isz_class_Pistol || iszClassname == gm_isz_class_357) + translated = (ACT_HL2MP_IDLE_PISTOL + iActOffset); + else if (iszClassname == gm_isz_class_SMG1) + translated = (ACT_HL2MP_IDLE_SMG1 + iActOffset); + else if (iszClassname == gm_isz_class_AR2) + translated = (ACT_HL2MP_IDLE_AR2 + iActOffset); + else if (iszClassname == gm_isz_class_Shotgun) + translated = (ACT_HL2MP_IDLE_SHOTGUN + iActOffset); + else if (iszClassname == gm_isz_class_RPG) + translated = (ACT_HL2MP_IDLE_RPG + iActOffset); + else if (iszClassname == gm_isz_class_Grenade) + translated = (ACT_HL2MP_IDLE_GRENADE + iActOffset); + else if (iszClassname == gm_isz_class_Physcannon) + translated = (ACT_HL2MP_IDLE_PHYSGUN + iActOffset); + else if (iszClassname == gm_isz_class_Crossbow) + translated = (ACT_HL2MP_IDLE_CROSSBOW + iActOffset); + else if (iszClassname == gm_isz_class_Crowbar || iszClassname == gm_isz_class_Stunstick) + translated = (ACT_HL2MP_IDLE_MELEE + iActOffset); + + if (translated != baseAct) + return (Activity)translated; + } +#endif + + return BaseClass::Weapon_TranslateActivity( baseAct, pRequired ); +} +#endif + + +//========================================================= +// HasNamedPlayerItem Does the player already have this item? +//========================================================= +CBaseEntity *CBasePlayer::HasNamedPlayerItem( const char *pszItemName ) +{ + for ( int i = 0 ; i < WeaponCount() ; i++ ) + { + if ( !GetWeapon(i) ) + continue; + + if ( FStrEq( pszItemName, GetWeapon(i)->GetClassname() ) ) + { + return GetWeapon(i); + } + } + + return NULL; +} + +#if defined USES_ECON_ITEMS +//----------------------------------------------------------------------------- +// Purpose: Add this wearable to the players' equipment list. +//----------------------------------------------------------------------------- +void CBasePlayer::EquipWearable( CEconWearable *pItem ) +{ + Assert( pItem ); + + if ( pItem ) + { + m_hMyWearables.AddToHead( pItem ); + pItem->Equip( this ); + } + +#ifdef DEBUG + // Double check list integrity. + for ( int i = m_hMyWearables.Count()-1; i >= 0; --i ) + { + Assert( m_hMyWearables[i] != NULL ); + } + // Networked Vector has a max size of MAX_WEARABLES_SENT_FROM_SERVER, should never have more then 7 wearables + // in public + // Search for : RecvPropUtlVector( RECVINFO_UTLVECTOR( m_hMyWearables ), MAX_WEARABLES_SENT_FROM_SERVER, RecvPropEHandle(NULL, 0, 0) ), + Assert( m_hMyWearables.Count() <= MAX_WEARABLES_SENT_FROM_SERVER ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Remove this wearable from the player's equipment list. +//----------------------------------------------------------------------------- +void CBasePlayer::RemoveWearable( CEconWearable *pItem ) +{ + Assert( pItem ); + + for ( int i = m_hMyWearables.Count()-1; i >= 0; --i ) + { + CEconWearable *pWearable = m_hMyWearables[i]; + if ( pWearable == pItem ) + { + pItem->UnEquip( this ); + UTIL_Remove( pWearable ); + m_hMyWearables.Remove( i ); + break; + } + + // Integrety is failing, remove NULLs + if ( !pWearable ) + { + m_hMyWearables.Remove( i ); + break; + } + } + +#ifdef DEBUG + // Double check list integrity. + for ( int i = m_hMyWearables.Count()-1; i >= 0; --i ) + { + Assert( m_hMyWearables[i] != NULL ); + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::PlayWearableAnimsForPlaybackEvent( wearableanimplayback_t iPlayback ) +{ + // Tell all our wearables to play their animations + FOR_EACH_VEC( m_hMyWearables, i ) + { + if ( m_hMyWearables[i] ) + { + m_hMyWearables[i]->PlayAnimForPlaybackEvent( iPlayback ); + } + } +} +#endif // USES_ECON_ITEMS + +#ifdef MAPBASE +bool CBasePlayer::ShouldUseVisibilityCache( CBaseEntity *pEntity ) +{ + // In CBaseEntity::FVisible(), players are allowed to see through CONTENTS_BLOCKLOS, which is used for + // nodraw, block LOS brushes, etc. This is so some code doesn't erronesouly assume the player can't see + // an entity (when the player can, in fact, see it) and therefore do something the player is not supposed to see. + // + // However, to reduce the number of traces FVisible() runs, CBaseCombatCharacter uses a "visibility cache" shared + // by all entities derived from it. The player is normally a part of this visibility cache, so when it runs a trace + // through a CONTENTS_BLOCKLOS surface, the visibility cache assumes entities can now see through it and therefore + // NPCs to see through the brush which should normally block their LOS. + // + // This solution stops the player from using the visibility cache altogether, toggled by a convar. + return player_use_visibility_cache.GetBool(); +} +#endif + +//================================================================================ +// TEAM HANDLING +//================================================================================ +//----------------------------------------------------------------------------- +// Purpose: Put the player in the specified team +//----------------------------------------------------------------------------- + +void CBasePlayer::ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent) +{ + if ( !GetGlobalTeam( iTeamNum ) ) + { + Warning( "CBasePlayer::ChangeTeam( %d ) - invalid team index.\n", iTeamNum ); + return; + } + + // if this is our current team, just abort + if ( iTeamNum == GetTeamNumber() ) + { + return; + } + + // Immediately tell all clients that he's changing team. This has to be done + // first, so that all user messages that follow as a result of the team change + // come after this one, allowing the client to be prepared for them. + IGameEvent * event = gameeventmanager->CreateEvent( "player_team" ); + if ( event ) + { + event->SetInt("userid", GetUserID() ); + event->SetInt("team", iTeamNum ); + event->SetInt("oldteam", GetTeamNumber() ); + event->SetInt("disconnect", IsDisconnecting()); + event->SetInt("autoteam", bAutoTeam ); + event->SetInt("silent", bSilent ); + event->SetString("name", GetPlayerName() ); + + gameeventmanager->FireEvent( event ); + } + + // Remove him from his current team + if ( GetTeam() ) + { + GetTeam()->RemovePlayer( this ); + } + + // Are we being added to a team? + if ( iTeamNum ) + { + GetGlobalTeam( iTeamNum )->AddPlayer( this ); + } + + BaseClass::ChangeTeam( iTeamNum ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: Locks a player to the spot; they can't move, shoot, or be hurt +//----------------------------------------------------------------------------- +void CBasePlayer::LockPlayerInPlace( void ) +{ + if ( m_iPlayerLocked ) + return; + + AddFlag( FL_GODMODE | FL_FROZEN ); + SetMoveType( MOVETYPE_NONE ); + m_iPlayerLocked = true; + + // force a client data update, so that anything that has been done to + // this player previously this frame won't get delayed in being sent + UpdateClientData(); +} + +//----------------------------------------------------------------------------- +// Purpose: Unlocks a previously locked player +//----------------------------------------------------------------------------- +void CBasePlayer::UnlockPlayer( void ) +{ + if ( !m_iPlayerLocked ) + return; + + RemoveFlag( FL_GODMODE | FL_FROZEN ); + SetMoveType( MOVETYPE_WALK ); + m_iPlayerLocked = false; +} + +bool CBasePlayer::ClearUseEntity() +{ + if ( m_hUseEntity != NULL ) + { + // Stop controlling the train/object + // TODO: Send HUD Update + m_hUseEntity->Use( this, this, USE_OFF, 0 ); + m_hUseEntity = NULL; + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::HideViewModels( void ) +{ + for ( int i = 0 ; i < MAX_VIEWMODELS; i++ ) + { + CBaseViewModel *vm = GetViewModel( i ); + if ( !vm ) + continue; + + vm->SetWeaponModel( NULL, NULL ); + } +} + +class CStripWeapons : public CPointEntity +{ + DECLARE_CLASS( CStripWeapons, CPointEntity ); +public: + void InputStripWeapons(inputdata_t &data); + void InputStripWeaponsAndSuit(inputdata_t &data); + + void StripWeapons(inputdata_t &data, bool stripSuit); + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( player_weaponstrip, CStripWeapons ); + +BEGIN_DATADESC( CStripWeapons ) + DEFINE_INPUTFUNC( FIELD_VOID, "Strip", InputStripWeapons ), + DEFINE_INPUTFUNC( FIELD_VOID, "StripWeaponsAndSuit", InputStripWeaponsAndSuit ), +END_DATADESC() + + +void CStripWeapons::InputStripWeapons(inputdata_t &data) +{ + StripWeapons(data, false); +} + +void CStripWeapons::InputStripWeaponsAndSuit(inputdata_t &data) +{ + StripWeapons(data, true); +} + +void CStripWeapons::StripWeapons(inputdata_t &data, bool stripSuit) +{ + CBasePlayer *pPlayer = NULL; + + if ( data.pActivator && data.pActivator->IsPlayer() ) + { + pPlayer = (CBasePlayer *)data.pActivator; + } + else if ( !g_pGameRules->IsDeathmatch() ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + + if ( pPlayer ) + { + pPlayer->RemoveAllItems( stripSuit ); + } +} + + +class CRevertSaved : public CPointEntity +{ + DECLARE_CLASS( CRevertSaved, CPointEntity ); +public: + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void LoadThink( void ); + + DECLARE_DATADESC(); + + inline float Duration( void ) { return m_Duration; } + inline float HoldTime( void ) { return m_HoldTime; } + inline float LoadTime( void ) { return m_loadTime; } + + inline void SetDuration( float duration ) { m_Duration = duration; } + inline void SetHoldTime( float hold ) { m_HoldTime = hold; } + inline void SetLoadTime( float time ) { m_loadTime = time; } + + //Inputs + void InputReload(inputdata_t &data); + +#ifdef HL1_DLL + void MessageThink( void ); + inline float MessageTime( void ) { return m_messageTime; } + inline void SetMessageTime( float time ) { m_messageTime = time; } +#endif + +private: + + float m_loadTime; + float m_Duration; + float m_HoldTime; + +#ifdef HL1_DLL + string_t m_iszMessage; + float m_messageTime; +#endif +}; + +LINK_ENTITY_TO_CLASS( player_loadsaved, CRevertSaved ); + +BEGIN_DATADESC( CRevertSaved ) + +#ifdef HL1_DLL + DEFINE_KEYFIELD( m_iszMessage, FIELD_STRING, "message" ), + DEFINE_KEYFIELD( m_messageTime, FIELD_FLOAT, "messagetime" ), // These are not actual times, but durations, so save as floats + + DEFINE_FUNCTION( MessageThink ), +#endif + + DEFINE_KEYFIELD( m_loadTime, FIELD_FLOAT, "loadtime" ), + DEFINE_KEYFIELD( m_Duration, FIELD_FLOAT, "duration" ), + DEFINE_KEYFIELD( m_HoldTime, FIELD_FLOAT, "holdtime" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Reload", InputReload ), + + + // Function Pointers + DEFINE_FUNCTION( LoadThink ), + +END_DATADESC() + +CBaseEntity *CreatePlayerLoadSave( Vector vOrigin, float flDuration, float flHoldTime, float flLoadTime ) +{ + CRevertSaved *pRevertSaved = (CRevertSaved *) CreateEntityByName( "player_loadsaved" ); + + if ( pRevertSaved == NULL ) + return NULL; + + UTIL_SetOrigin( pRevertSaved, vOrigin ); + + pRevertSaved->Spawn(); + pRevertSaved->SetDuration( flDuration ); + pRevertSaved->SetHoldTime( flHoldTime ); + pRevertSaved->SetLoadTime( flLoadTime ); + + return pRevertSaved; +} + + + +void CRevertSaved::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + UTIL_ScreenFadeAll( m_clrRender, Duration(), HoldTime(), FFADE_OUT ); + SetNextThink( gpGlobals->curtime + LoadTime() ); + SetThink( &CRevertSaved::LoadThink ); + + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( pPlayer ) + { + //Adrian: Setting this flag so we can't move or save a game. + pPlayer->pl.deadflag = true; + pPlayer->AddFlag( (FL_NOTARGET|FL_FROZEN) ); + + // clear any pending autosavedangerous + g_ServerGameDLL.m_fAutoSaveDangerousTime = 0.0f; + g_ServerGameDLL.m_fAutoSaveDangerousMinHealthToCommit = 0.0f; + } +} + +void CRevertSaved::InputReload( inputdata_t &inputdata ) +{ + UTIL_ScreenFadeAll( m_clrRender, Duration(), HoldTime(), FFADE_OUT ); + +#ifdef HL1_DLL + SetNextThink( gpGlobals->curtime + MessageTime() ); + SetThink( &CRevertSaved::MessageThink ); +#else + SetNextThink( gpGlobals->curtime + LoadTime() ); + SetThink( &CRevertSaved::LoadThink ); +#endif + + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( pPlayer ) + { + //Adrian: Setting this flag so we can't move or save a game. + pPlayer->pl.deadflag = true; + pPlayer->AddFlag( (FL_NOTARGET|FL_FROZEN) ); + + // clear any pending autosavedangerous + g_ServerGameDLL.m_fAutoSaveDangerousTime = 0.0f; + g_ServerGameDLL.m_fAutoSaveDangerousMinHealthToCommit = 0.0f; + } +} + +#ifdef HL1_DLL +void CRevertSaved::MessageThink( void ) +{ + UTIL_ShowMessageAll( STRING( m_iszMessage ) ); + float nextThink = LoadTime() - MessageTime(); + if ( nextThink > 0 ) + { + SetNextThink( gpGlobals->curtime + nextThink ); + SetThink( &CRevertSaved::LoadThink ); + } + else + LoadThink(); +} +#endif + + +void CRevertSaved::LoadThink( void ) +{ + if ( !gpGlobals->deathmatch ) + { + engine->ServerCommand("reload\n"); + } +} + +#define SF_SPEED_MOD_SUPPRESS_WEAPONS (1<<0) // Take away weapons +#define SF_SPEED_MOD_SUPPRESS_HUD (1<<1) // Take away the HUD +#define SF_SPEED_MOD_SUPPRESS_JUMP (1<<2) +#define SF_SPEED_MOD_SUPPRESS_DUCK (1<<3) +#define SF_SPEED_MOD_SUPPRESS_USE (1<<4) +#define SF_SPEED_MOD_SUPPRESS_SPEED (1<<5) +#define SF_SPEED_MOD_SUPPRESS_ATTACK (1<<6) +#define SF_SPEED_MOD_SUPPRESS_ZOOM (1<<7) +#ifdef MAPBASE +// Needs to be inverse because suppressing the flashlight is already default behavior +// and we don't want to break compatibility for existing speedmods +#define SF_SPEED_MOD_DONT_SUPPRESS_FLASHLIGHT (1<<8) +#endif + +class CMovementSpeedMod : public CPointEntity +{ + DECLARE_CLASS( CMovementSpeedMod, CPointEntity ); +public: + void InputSpeedMod(inputdata_t &data); + +#ifdef MAPBASE + void InputEnable(inputdata_t &data); + void InputDisable(inputdata_t &data); + + void InputSetAdditionalButtons(inputdata_t &data); +#endif + +private: + int GetDisabledButtonMask( void ); + +#ifdef MAPBASE + int m_iAdditionalButtons; +#endif + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( player_speedmod, CMovementSpeedMod ); + +BEGIN_DATADESC( CMovementSpeedMod ) + DEFINE_INPUTFUNC( FIELD_FLOAT, "ModifySpeed", InputSpeedMod ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_KEYFIELD( m_iAdditionalButtons, FIELD_INTEGER, "AdditionalButtons" ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetAdditionalButtons", InputSetAdditionalButtons ), +#endif +END_DATADESC() + +int CMovementSpeedMod::GetDisabledButtonMask( void ) +{ + int nMask = 0; + + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_JUMP ) ) + { + nMask |= IN_JUMP; + } + + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_DUCK ) ) + { + nMask |= IN_DUCK; + } + + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_USE ) ) + { + nMask |= IN_USE; + } + + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_SPEED ) ) + { + nMask |= IN_SPEED; + } + + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_ATTACK ) ) + { + nMask |= (IN_ATTACK|IN_ATTACK2); + } + + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_ZOOM ) ) + { + nMask |= IN_ZOOM; + } + +#ifdef MAPBASE + if ( m_iAdditionalButtons != 0 ) + { + nMask |= m_iAdditionalButtons; + } +#endif + + return nMask; +} + +void CMovementSpeedMod::InputSpeedMod(inputdata_t &data) +{ + CBasePlayer *pPlayer = NULL; + + if ( data.pActivator && data.pActivator->IsPlayer() ) + { + pPlayer = (CBasePlayer *)data.pActivator; + } + else if ( !g_pGameRules->IsDeathmatch() ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + + if ( pPlayer ) + { + if ( data.value.Float() != 1.0f ) + { + // Holster weapon immediately, to allow it to cleanup + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_WEAPONS ) ) + { + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->Weapon_SetLast( pPlayer->GetActiveWeapon() ); + pPlayer->GetActiveWeapon()->Holster(); + pPlayer->ClearActiveWeapon(); + } + + pPlayer->HideViewModels(); + } + +#ifdef MAPBASE + if ( !HasSpawnFlags( SF_SPEED_MOD_DONT_SUPPRESS_FLASHLIGHT ) ) + { +#endif + // Turn off the flashlight + if ( pPlayer->FlashlightIsOn() ) + { + pPlayer->FlashlightTurnOff(); + } + + // Disable the flashlight's further use + pPlayer->SetFlashlightEnabled( false ); +#ifdef MAPBASE + } +#endif + pPlayer->DisableButtons( GetDisabledButtonMask() ); + + // Hide the HUD + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_HUD ) ) + { + pPlayer->m_Local.m_iHideHUD |= HIDEHUD_ALL; + } + } + else + { + // Bring the weapon back + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_WEAPONS ) && pPlayer->GetActiveWeapon() == NULL ) + { + pPlayer->SetActiveWeapon( pPlayer->Weapon_GetLast() ); + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->Deploy(); + } + } + +#ifdef MAPBASE + if ( !HasSpawnFlags( SF_SPEED_MOD_DONT_SUPPRESS_FLASHLIGHT ) ) + { +#endif + // Allow the flashlight again + pPlayer->SetFlashlightEnabled( true ); +#ifdef MAPBASE + } +#endif + pPlayer->EnableButtons( GetDisabledButtonMask() ); + + // Restore the HUD + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_HUD ) ) + { + pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_ALL; + } + } + + pPlayer->SetLaggedMovementValue( data.value.Float() ); + } +} + +#ifdef MAPBASE +void CMovementSpeedMod::InputEnable(inputdata_t &data) +{ + CBasePlayer *pPlayer = NULL; + + if ( data.pActivator && data.pActivator->IsPlayer() ) + { + pPlayer = (CBasePlayer *)data.pActivator; + } + else if ( !g_pGameRules->IsDeathmatch() ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + + if ( pPlayer ) + { + // Holster weapon immediately, to allow it to cleanup + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_WEAPONS ) ) + { + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->Weapon_SetLast( pPlayer->GetActiveWeapon() ); + pPlayer->GetActiveWeapon()->Holster(); + pPlayer->ClearActiveWeapon(); + } + + pPlayer->HideViewModels(); + } + + // Turn off the flashlight + if ( pPlayer->FlashlightIsOn() ) + { + pPlayer->FlashlightTurnOff(); + } + + // Disable the flashlight's further use + pPlayer->SetFlashlightEnabled( false ); + pPlayer->DisableButtons( GetDisabledButtonMask() ); + + // Hide the HUD + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_HUD ) ) + { + pPlayer->m_Local.m_iHideHUD |= HIDEHUD_ALL; + } + } +} + +void CMovementSpeedMod::InputDisable(inputdata_t &data) +{ + CBasePlayer *pPlayer = NULL; + + if ( data.pActivator && data.pActivator->IsPlayer() ) + { + pPlayer = (CBasePlayer *)data.pActivator; + } + else if ( !g_pGameRules->IsDeathmatch() ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + + if ( pPlayer ) + { + // Bring the weapon back + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_WEAPONS ) && pPlayer->GetActiveWeapon() == NULL ) + { + pPlayer->SetActiveWeapon( pPlayer->Weapon_GetLast() ); + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->Deploy(); + } + } + + // Allow the flashlight again + pPlayer->SetFlashlightEnabled( true ); + pPlayer->EnableButtons( GetDisabledButtonMask() ); + + // Restore the HUD + if ( HasSpawnFlags( SF_SPEED_MOD_SUPPRESS_HUD ) ) + { + pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_ALL; + } + } +} + +void CMovementSpeedMod::InputSetAdditionalButtons(inputdata_t &data) +{ + CBasePlayer *pPlayer = NULL; + + if ( data.pActivator && data.pActivator->IsPlayer() ) + { + pPlayer = (CBasePlayer *)data.pActivator; + } + else if ( !g_pGameRules->IsDeathmatch() ) + { + pPlayer = UTIL_GetLocalPlayer(); + } + + bool bAlreadyDisabled = false; + if ( pPlayer ) + { + bAlreadyDisabled = (pPlayer->m_afButtonDisabled & GetDisabledButtonMask()) != 0; + } + + m_iAdditionalButtons = data.value.Int(); + + // If we were already disabling buttons, re-disable them + if ( bAlreadyDisabled ) + { + // We should probably do something better than this. + pPlayer->m_afButtonForced = GetDisabledButtonMask(); + } +} +#endif + +#ifdef MAPBASE +class CLogicPlayerInfo : public CPointEntity +{ + DECLARE_CLASS( CLogicPlayerInfo, CPointEntity ); +public: + void InputGetPlayerInfo( inputdata_t &inputdata ); + void InputGetPlayerByID( inputdata_t &inputdata ); + void InputGetPlayerByName( inputdata_t &inputdata ); + + void GetPlayerInfo( CBasePlayer *pPlayer ); + + COutputInt m_OutUserID; + COutputString m_OutPlayerName; + COutputEHANDLE m_OutPlayerEntity; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( logic_playerinfo, CLogicPlayerInfo ); + +BEGIN_DATADESC( CLogicPlayerInfo ) + DEFINE_INPUTFUNC( FIELD_EHANDLE, "GetPlayerInfo", InputGetPlayerInfo ), + DEFINE_INPUTFUNC( FIELD_STRING, "GetPlayerByID", InputGetPlayerByID ), + DEFINE_INPUTFUNC( FIELD_STRING, "GetPlayerByName", InputGetPlayerByName ), + + DEFINE_OUTPUT( m_OutUserID, "OutUserID" ), + DEFINE_OUTPUT( m_OutPlayerName, "OutPlayerName" ), + DEFINE_OUTPUT( m_OutPlayerEntity, "OutPlayerEntity" ), +END_DATADESC() + + +void CLogicPlayerInfo::InputGetPlayerInfo( inputdata_t &inputdata ) +{ + CBasePlayer *pPlayer = ToBasePlayer(inputdata.value.Entity()); + + // If there was no entity to begin with, try the local player + if (!pPlayer && !inputdata.value.Entity()) + pPlayer = UTIL_GetLocalPlayer(); + + if (pPlayer) + GetPlayerInfo( pPlayer ); +} + +void CLogicPlayerInfo::InputGetPlayerByID( inputdata_t &inputdata ) +{ + for (int i = 1; i < gpGlobals->maxClients; i++) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if (pPlayer) + { + if (Matcher_NamesMatch( inputdata.value.String(), UTIL_VarArgs("%i", pPlayer->GetUserID()) )) + { + GetPlayerInfo( pPlayer ); + return; + } + } + } +} + +void CLogicPlayerInfo::InputGetPlayerByName( inputdata_t &inputdata ) +{ + for (int i = 1; i < gpGlobals->maxClients; i++) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if (pPlayer) + { + if (Matcher_NamesMatch( inputdata.value.String(), pPlayer->GetPlayerName() )) + { + GetPlayerInfo( pPlayer ); + return; + } + } + } +} + +void CLogicPlayerInfo::GetPlayerInfo( CBasePlayer *pPlayer ) +{ + m_OutUserID.Set( pPlayer->GetUserID(), pPlayer, this ); + + m_OutPlayerName.Set( AllocPooledString(pPlayer->GetPlayerName()), pPlayer, this ); + + m_OutPlayerEntity.Set( pPlayer, pPlayer, this ); +} +#endif + + +void SendProxy_CropFlagsToPlayerFlagBitsLength( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID) +{ + int mask = (1<m_Int = ( data & mask ); +} + +#ifdef MAPBASE +// Needs to shift bits since network table only sends the player ones +void SendProxy_ShiftPlayerSpawnflags( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ) +{ + int *pInt = (int *)pVarData; + + pOut->m_Int = (*pInt) >> 16; +} +#endif + +// -------------------------------------------------------------------------------- // +// SendTable for CPlayerState. +// -------------------------------------------------------------------------------- // + + BEGIN_SEND_TABLE_NOBASE(CPlayerState, DT_PlayerState) + SendPropInt (SENDINFO(deadflag), 1, SPROP_UNSIGNED ), + END_SEND_TABLE() + +// -------------------------------------------------------------------------------- // +// This data only gets sent to clients that ARE this player entity. +// -------------------------------------------------------------------------------- // + + BEGIN_SEND_TABLE_NOBASE( CBasePlayer, DT_LocalPlayerExclusive ) + + SendPropDataTable ( SENDINFO_DT(m_Local), &REFERENCE_SEND_TABLE(DT_Local) ), + +// If HL2_DLL is defined, then baseflex.cpp already sends these. +#ifndef HL2_DLL + SendPropFloat ( SENDINFO_VECTORELEM(m_vecViewOffset, 0), 8, SPROP_ROUNDDOWN, -32.0, 32.0f), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecViewOffset, 1), 8, SPROP_ROUNDDOWN, -32.0, 32.0f), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecViewOffset, 2), 20, SPROP_CHANGES_OFTEN, 0.0f, 256.0f), +#endif + + SendPropFloat ( SENDINFO(m_flFriction), 8, SPROP_ROUNDDOWN, 0.0f, 4.0f), + + SendPropArray3 ( SENDINFO_ARRAY3(m_iAmmo), SendPropInt( SENDINFO_ARRAY(m_iAmmo), -1, SPROP_VARINT | SPROP_UNSIGNED ) ), + + SendPropInt ( SENDINFO( m_fOnTarget ), 2, SPROP_UNSIGNED ), + + SendPropInt ( SENDINFO( m_nTickBase ), -1, SPROP_CHANGES_OFTEN ), + SendPropInt ( SENDINFO( m_nNextThinkTick ) ), + + SendPropEHandle ( SENDINFO( m_hLastWeapon ) ), + SendPropEHandle ( SENDINFO( m_hGroundEntity ), SPROP_CHANGES_OFTEN ), + + SendPropFloat ( SENDINFO_VECTORELEM(m_vecVelocity, 0), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecVelocity, 1), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecVelocity, 2), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + +#if PREDICTION_ERROR_CHECK_LEVEL > 1 + SendPropVector ( SENDINFO( m_vecBaseVelocity ), -1, SPROP_COORD ), +#else + SendPropVector ( SENDINFO( m_vecBaseVelocity ), 20, 0, -1000, 1000 ), +#endif + + SendPropEHandle ( SENDINFO( m_hConstraintEntity)), + SendPropVector ( SENDINFO( m_vecConstraintCenter), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO( m_flConstraintRadius ), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO( m_flConstraintWidth ), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO( m_flConstraintSpeedFactor ), 0, SPROP_NOSCALE ), + + SendPropFloat ( SENDINFO( m_flDeathTime ), 0, SPROP_NOSCALE ), + + SendPropInt ( SENDINFO( m_nWaterLevel ), 2, SPROP_UNSIGNED ), + SendPropFloat ( SENDINFO( m_flLaggedMovementValue ), 0, SPROP_NOSCALE ), + +#ifdef MAPBASE + // Transmitted from the server for internal player spawnflags. + // See baseplayer_shared.h for more details. + SendPropInt ( SENDINFO( m_spawnflags ), 3, SPROP_UNSIGNED, SendProxy_ShiftPlayerSpawnflags ), + + SendPropBool ( SENDINFO( m_bDrawPlayerModelExternally ) ), +#endif + + END_SEND_TABLE() + + +// -------------------------------------------------------------------------------- // +// DT_BasePlayer sendtable. +// -------------------------------------------------------------------------------- // + +#if defined USES_ECON_ITEMS + EXTERN_SEND_TABLE(DT_AttributeList); +#endif + + IMPLEMENT_SERVERCLASS_ST( CBasePlayer, DT_BasePlayer ) + +#if defined USES_ECON_ITEMS + SendPropDataTable(SENDINFO_DT(m_AttributeList), &REFERENCE_SEND_TABLE(DT_AttributeList)), +#endif + + SendPropDataTable(SENDINFO_DT(pl), &REFERENCE_SEND_TABLE(DT_PlayerState), SendProxy_DataTableToDataTable), + + SendPropEHandle(SENDINFO(m_hVehicle)), + SendPropEHandle(SENDINFO(m_hUseEntity)), + SendPropInt (SENDINFO(m_iHealth), -1, SPROP_VARINT | SPROP_CHANGES_OFTEN ), + SendPropInt (SENDINFO(m_lifeState), 3, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_iBonusProgress), 15 ), + SendPropInt (SENDINFO(m_iBonusChallenge), 4 ), + SendPropFloat (SENDINFO(m_flMaxspeed), 12, SPROP_ROUNDDOWN, 0.0f, 2048.0f ), // CL + SendPropInt (SENDINFO(m_fFlags), PLAYER_FLAG_BITS, SPROP_UNSIGNED|SPROP_CHANGES_OFTEN, SendProxy_CropFlagsToPlayerFlagBitsLength ), + SendPropInt (SENDINFO(m_iObserverMode), 3, SPROP_UNSIGNED ), + SendPropEHandle (SENDINFO(m_hObserverTarget) ), + SendPropInt (SENDINFO(m_iFOV), 8, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_iFOVStart), 8, SPROP_UNSIGNED ), + SendPropFloat (SENDINFO(m_flFOVTime) ), + SendPropInt (SENDINFO(m_iDefaultFOV), 8, SPROP_UNSIGNED ), + SendPropEHandle (SENDINFO(m_hZoomOwner) ), + SendPropArray ( SendPropEHandle( SENDINFO_ARRAY( m_hViewModel ) ), m_hViewModel ), + SendPropString (SENDINFO(m_szLastPlaceName) ), + + // Postprocess data + SendPropEHandle( SENDINFO( m_hPostProcessCtrl ) ), + +#if defined USES_ECON_ITEMS + SendPropUtlVector( SENDINFO_UTLVECTOR( m_hMyWearables ), MAX_WEARABLES_SENT_FROM_SERVER, SendPropEHandle( NULL, 0 ) ), +#endif // USES_ECON_ITEMS + + // Data that only gets sent to the local player. + SendPropDataTable( "localdata", 0, &REFERENCE_SEND_TABLE(DT_LocalPlayerExclusive), SendProxy_SendLocalDataTable ), + + END_SEND_TABLE() + +//============================================================================= +// +// Player Physics Shadow Code +// + +void CBasePlayer::SetupVPhysicsShadow( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity, CPhysCollide *pStandModel, const char *pStandHullName, CPhysCollide *pCrouchModel, const char *pCrouchHullName ) +{ + solid_t solid; + Q_strncpy( solid.surfaceprop, "player", sizeof(solid.surfaceprop) ); + solid.params = g_PhysDefaultObjectParams; + solid.params.mass = 85.0f; + solid.params.inertia = 1e24f; + solid.params.enableCollisions = false; + //disable drag + solid.params.dragCoefficient = 0; + // create standing hull + m_pShadowStand = PhysModelCreateCustom( this, pStandModel, GetLocalOrigin(), GetLocalAngles(), pStandHullName, false, &solid ); + m_pShadowStand->SetCallbackFlags( CALLBACK_GLOBAL_COLLISION | CALLBACK_SHADOW_COLLISION ); + + // create crouchig hull + m_pShadowCrouch = PhysModelCreateCustom( this, pCrouchModel, GetLocalOrigin(), GetLocalAngles(), pCrouchHullName, false, &solid ); + m_pShadowCrouch->SetCallbackFlags( CALLBACK_GLOBAL_COLLISION | CALLBACK_SHADOW_COLLISION ); + + // default to stand + VPhysicsSetObject( m_pShadowStand ); + + // tell physics lists I'm a shadow controller object + PhysAddShadow( this ); + m_pPhysicsController = physenv->CreatePlayerController( m_pShadowStand ); + m_pPhysicsController->SetPushMassLimit( 350.0f ); + m_pPhysicsController->SetPushSpeedLimit( 50.0f ); + + // Give the controller a valid position so it doesn't do anything rash. + UpdatePhysicsShadowToPosition( vecAbsOrigin ); + + // init state + if ( GetFlags() & FL_DUCKING ) + { + SetVCollisionState( vecAbsOrigin, vecAbsVelocity, VPHYS_CROUCH ); + } + else + { + SetVCollisionState( vecAbsOrigin, vecAbsVelocity, VPHYS_WALK ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Empty, just want to keep the baseentity version from being called +// current so we don't kick up dust, etc. +//----------------------------------------------------------------------------- +void CBasePlayer::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + float savedImpact = m_impactEnergyScale; + + // HACKHACK: Reduce player's stress by 1/8th + m_impactEnergyScale *= 0.125f; + ApplyStressDamage( pPhysics, true ); + m_impactEnergyScale = savedImpact; +} + +//----------------------------------------------------------------------------- +// Purpose: Allow bots etc to use slightly different solid masks +//----------------------------------------------------------------------------- +unsigned int CBasePlayer::PlayerSolidMask( bool brushOnly ) const +{ + if ( brushOnly ) + { + return MASK_PLAYERSOLID_BRUSHONLY; + } + + return MASK_PLAYERSOLID; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::VPhysicsShadowUpdate( IPhysicsObject *pPhysics ) +{ + if ( sv_turbophysics.GetBool() ) + return; + + Vector newPosition; + + bool physicsUpdated = m_pPhysicsController->GetShadowPosition( &newPosition, NULL ) > 0 ? true : false; + + // UNDONE: If the player is penetrating, but the player's game collisions are not stuck, teleport the physics shadow to the game position + if ( pPhysics->GetGameFlags() & FVPHYSICS_PENETRATING ) + { + CUtlVector list; + PhysGetListOfPenetratingEntities( this, list ); + for ( int i = list.Count()-1; i >= 0; --i ) + { + // filter out anything that isn't simulated by vphysics + // UNDONE: Filter out motion disabled objects? + if ( list[i]->GetMoveType() == MOVETYPE_VPHYSICS ) + { + // I'm currently stuck inside a moving object, so allow vphysics to + // apply velocity to the player in order to separate these objects + m_touchedPhysObject = true; + } + + // if it's an NPC, tell them that the player is intersecting them + CAI_BaseNPC *pNPC = list[i]->MyNPCPointer(); + if ( pNPC ) + { + pNPC->PlayerPenetratingVPhysics(); + } + } + } + + bool bCheckStuck = false; + if ( m_afPhysicsFlags & PFLAG_GAMEPHYSICS_ROTPUSH ) + { + bCheckStuck = true; + m_afPhysicsFlags &= ~PFLAG_GAMEPHYSICS_ROTPUSH; + } + if ( m_pPhysicsController->IsInContact() || (m_afPhysicsFlags & PFLAG_VPHYSICS_MOTIONCONTROLLER) ) + { + m_touchedPhysObject = true; + } + + if ( IsFollowingPhysics() ) + { + m_touchedPhysObject = true; + } + + if ( GetMoveType() == MOVETYPE_NOCLIP || pl.deadflag ) + { + m_oldOrigin = GetAbsOrigin(); + return; + } + + if ( phys_timescale.GetFloat() == 0.0f ) + { + physicsUpdated = false; + } + + if ( !physicsUpdated ) + return; + + IPhysicsObject *pPhysGround = GetGroundVPhysics(); + + Vector newVelocity; + pPhysics->GetPosition( &newPosition, 0 ); + m_pPhysicsController->GetShadowVelocity( &newVelocity ); + // assume vphysics gave us back a position without penetration + Vector lastValidPosition = newPosition; + + if ( physicsshadowupdate_render.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), WorldAlignMins(), WorldAlignMaxs(), 255, 0, 0, 24, 15.0f ); + NDebugOverlay::Box( newPosition, WorldAlignMins(), WorldAlignMaxs(), 0,0,255, 24, 15.0f); + // NDebugOverlay::Box( newPosition, WorldAlignMins(), WorldAlignMaxs(), 0,0,255, 24, .01f); + } + + Vector tmp = GetAbsOrigin() - newPosition; + if ( !m_touchedPhysObject && !(GetFlags() & FL_ONGROUND) ) + { + tmp.z *= 0.5f; // don't care about z delta as much + } + + float dist = tmp.LengthSqr(); + float deltaV = (newVelocity - GetAbsVelocity()).LengthSqr(); + + float maxDistErrorSqr = VPHYS_MAX_DISTSQR; + float maxVelErrorSqr = VPHYS_MAX_VELSQR; + if ( IsRideablePhysics(pPhysGround) ) + { + maxDistErrorSqr *= 0.25; + maxVelErrorSqr *= 0.25; + } + + // player's physics was frozen, try moving to the game's simulated position if possible + if ( m_pPhysicsController->WasFrozen() ) + { + m_bPhysicsWasFrozen = true; + // check my position (physics object could have simulated into my position + // physics is not very far away, check my position + trace_t trace; + UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin(), MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + if ( !trace.startsolid ) + return; + + // The physics shadow position is probably not in solid, try to move from there to the desired position + UTIL_TraceEntity( this, newPosition, GetAbsOrigin(), MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + if ( !trace.startsolid ) + { + // found a valid position between the two? take it. + SetAbsOrigin( trace.endpos ); + UpdateVPhysicsPosition(trace.endpos, vec3_origin, 0); + return; + } + + } + if ( dist >= maxDistErrorSqr || deltaV >= maxVelErrorSqr || (pPhysGround && !m_touchedPhysObject) ) + { + if ( m_touchedPhysObject || pPhysGround ) + { + // BUGBUG: Rewrite this code using fixed timestep + if ( deltaV >= maxVelErrorSqr && !m_bPhysicsWasFrozen ) + { + Vector dir = GetAbsVelocity(); + float len = VectorNormalize(dir); + float dot = DotProduct( newVelocity, dir ); + if ( dot > len ) + { + dot = len; + } + else if ( dot < -len ) + { + dot = -len; + } + + VectorMA( newVelocity, -dot, dir, newVelocity ); + + if ( m_afPhysicsFlags & PFLAG_VPHYSICS_MOTIONCONTROLLER ) + { + float val = Lerp( 0.1f, len, dot ); + VectorMA( newVelocity, val - len, dir, newVelocity ); + } + + if ( !IsRideablePhysics(pPhysGround) ) + { + if ( !(m_afPhysicsFlags & PFLAG_VPHYSICS_MOTIONCONTROLLER ) && IsSimulatingOnAlternateTicks() ) + { + newVelocity *= 0.5f; + } + ApplyAbsVelocityImpulse( newVelocity ); + } + } + + trace_t trace; + UTIL_TraceEntity( this, newPosition, newPosition, MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + if ( !trace.allsolid && !trace.startsolid ) + { + SetAbsOrigin( newPosition ); + } + } + else + { + bCheckStuck = true; + } + } + else + { + if ( m_touchedPhysObject ) + { + // check my position (physics object could have simulated into my position + // physics is not very far away, check my position + trace_t trace; + UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin(), + MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + + // is current position ok? + if ( trace.allsolid || trace.startsolid ) + { + // no use the final stuck check to move back to old if this stuck fix didn't work + bCheckStuck = true; + lastValidPosition = m_oldOrigin; + SetAbsOrigin( newPosition ); + } + } + } + + if ( bCheckStuck ) + { + trace_t trace; + UTIL_TraceEntity( this, GetAbsOrigin(), GetAbsOrigin(), MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + + // current position is not ok, fixup + if ( trace.allsolid || trace.startsolid ) + { + // STUCK!?!?! + //Warning( "Checkstuck failed. Stuck on %s!!\n", trace.m_pEnt->GetClassname() ); + SetAbsOrigin( lastValidPosition ); + } + } + m_oldOrigin = GetAbsOrigin(); + m_bPhysicsWasFrozen = false; +} + +// recreate physics on save/load, don't try to save the state! +bool CBasePlayer::ShouldSavePhysics() +{ + return false; +} + +void CBasePlayer::RefreshCollisionBounds( void ) +{ + BaseClass::RefreshCollisionBounds(); + + InitVCollision( GetAbsOrigin(), GetAbsVelocity() ); + SetViewOffset( VEC_VIEW_SCALED( this ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::InitVCollision( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity ) +{ + // Cleanup any old vphysics stuff. + VPhysicsDestroyObject(); + + // in turbo physics players dont have a physics shadow + if ( sv_turbophysics.GetBool() ) + return; + + CPhysCollide *pModel = PhysCreateBbox( VEC_HULL_MIN_SCALED( this ), VEC_HULL_MAX_SCALED( this ) ); + CPhysCollide *pCrouchModel = PhysCreateBbox( VEC_DUCK_HULL_MIN_SCALED( this ), VEC_DUCK_HULL_MAX_SCALED( this ) ); + + SetupVPhysicsShadow( vecAbsOrigin, vecAbsVelocity, pModel, "player_stand", pCrouchModel, "player_crouch" ); +} + + +void CBasePlayer::VPhysicsDestroyObject() +{ + // Since CBasePlayer aliases its pointer to the physics object, tell CBaseEntity to + // clear out its physics object pointer so we don't wind up deleting one of + // the aliased objects twice. + VPhysicsSetObject( NULL ); + + PhysRemoveShadow( this ); + + if ( m_pPhysicsController ) + { + physenv->DestroyPlayerController( m_pPhysicsController ); + m_pPhysicsController = NULL; + } + + if ( m_pShadowStand ) + { + m_pShadowStand->EnableCollisions( false ); + PhysDestroyObject( m_pShadowStand ); + m_pShadowStand = NULL; + } + if ( m_pShadowCrouch ) + { + m_pShadowCrouch->EnableCollisions( false ); + PhysDestroyObject( m_pShadowCrouch ); + m_pShadowCrouch = NULL; + } + + BaseClass::VPhysicsDestroyObject(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::SetVCollisionState( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity, int collisionState ) +{ + m_vphysicsCollisionState = collisionState; + switch( collisionState ) + { + case VPHYS_WALK: + m_pShadowStand->SetPosition( vecAbsOrigin, vec3_angle, true ); + m_pShadowStand->SetVelocity( &vecAbsVelocity, NULL ); + m_pShadowCrouch->EnableCollisions( false ); + m_pPhysicsController->SetObject( m_pShadowStand ); + VPhysicsSwapObject( m_pShadowStand ); + m_pShadowStand->EnableCollisions( true ); + break; + + case VPHYS_CROUCH: + m_pShadowCrouch->SetPosition( vecAbsOrigin, vec3_angle, true ); + m_pShadowCrouch->SetVelocity( &vecAbsVelocity, NULL ); + m_pShadowStand->EnableCollisions( false ); + m_pPhysicsController->SetObject( m_pShadowCrouch ); + VPhysicsSwapObject( m_pShadowCrouch ); + m_pShadowCrouch->EnableCollisions( true ); + break; + + case VPHYS_NOCLIP: + m_pShadowCrouch->EnableCollisions( false ); + m_pShadowStand->EnableCollisions( false ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBasePlayer::GetFOV( void ) +{ + int nDefaultFOV; + + // The vehicle's FOV wins if we're asking for a default value + if ( GetVehicle() ) + { + CacheVehicleView(); + nDefaultFOV = ( m_flVehicleViewFOV == 0 ) ? GetDefaultFOV() : (int) m_flVehicleViewFOV; + } + else + { + nDefaultFOV = GetDefaultFOV(); + } + + int fFOV = ( m_iFOV == 0 ) ? nDefaultFOV : m_iFOV; + + // If it's immediate, just do it + if ( m_Local.m_flFOVRate == 0.0f ) + return fFOV; + + float deltaTime = (float)( gpGlobals->curtime - m_flFOVTime ) / m_Local.m_flFOVRate; + + if ( deltaTime >= 1.0f ) + { + //If we're past the zoom time, just take the new value and stop lerping + m_iFOVStart = fFOV; + } + else + { + fFOV = SimpleSplineRemapValClamped( deltaTime, 0.0f, 1.0f, m_iFOVStart, fFOV ); + } + + return fFOV; +} + + +//----------------------------------------------------------------------------- +// Get the current FOV used for network computations +// Choose the smallest FOV, as it will open the largest number of portals +//----------------------------------------------------------------------------- +int CBasePlayer::GetFOVForNetworking( void ) +{ + int nDefaultFOV; + + // The vehicle's FOV wins if we're asking for a default value + if ( GetVehicle() ) + { + CacheVehicleView(); + nDefaultFOV = ( m_flVehicleViewFOV == 0 ) ? GetDefaultFOV() : (int) m_flVehicleViewFOV; + } + else + { + nDefaultFOV = GetDefaultFOV(); + } + + int fFOV = ( m_iFOV == 0 ) ? nDefaultFOV : m_iFOV; + + // If it's immediate, just do it + if ( m_Local.m_flFOVRate == 0.0f ) + return fFOV; + + if ( gpGlobals->curtime - m_flFOVTime < m_Local.m_flFOVRate ) + { + fFOV = MIN( fFOV, m_iFOVStart ); + } + return fFOV; +} + + +float CBasePlayer::GetFOVDistanceAdjustFactorForNetworking() +{ + float defaultFOV = (float)GetDefaultFOV(); + float localFOV = (float)GetFOVForNetworking(); + + if ( localFOV == defaultFOV || defaultFOV < 0.001f ) + return 1.0f; + + // If FOV is lower, then we're "zoomed" in and this will give a factor < 1 so apparent LOD distances can be + // shorted accordingly + return localFOV / defaultFOV; +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the default FOV for the player if nothing else is going on +// Input : FOV - the new base FOV for this player +//----------------------------------------------------------------------------- +void CBasePlayer::SetDefaultFOV( int FOV ) +{ + m_iDefaultFOV = ( FOV == 0 ) ? g_pGameRules->DefaultFOV() : FOV; +} + +#ifdef MAPBASE_VSCRIPT +void CBasePlayer::ScriptSetFOV(int iFOV, float flRate) +{ + m_iFOVStart = GetFOV(); + + m_flFOVTime = gpGlobals->curtime; + m_iFOV = iFOV; + + m_Local.m_flFOVRate = flRate; +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: // static func +// Input : set - +//----------------------------------------------------------------------------- +void CBasePlayer::ModifyOrAppendPlayerCriteria( AI_CriteriaSet& set ) +{ + // Append our health + set.AppendCriteria( "playerhealth", UTIL_VarArgs( "%i", GetHealth() ) ); + float healthfrac = 0.0f; + if ( GetMaxHealth() > 0 ) + { + healthfrac = (float)GetHealth() / (float)GetMaxHealth(); + } + + set.AppendCriteria( "playerhealthfrac", UTIL_VarArgs( "%.3f", healthfrac ) ); + + CBaseCombatWeapon *weapon = GetActiveWeapon(); + if ( weapon ) + { + set.AppendCriteria( "playerweapon", weapon->GetClassname() ); + } + else + { + set.AppendCriteria( "playerweapon", "none" ); + } + + // Append current activity name + set.AppendCriteria( "playeractivity", CAI_BaseNPC::GetActivityName( GetActivity() ) ); + + set.AppendCriteria( "playerspeed", UTIL_VarArgs( "%.3f", GetAbsVelocity().Length() ) ); + + AppendContextToCriteria( set, "player" ); +} + + +const QAngle& CBasePlayer::GetPunchAngle() +{ + return m_Local.m_vecPunchAngle.Get(); +} + + +void CBasePlayer::SetPunchAngle( const QAngle &punchAngle ) +{ + m_Local.m_vecPunchAngle = punchAngle; + + if ( IsAlive() ) + { + int index = entindex(); + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( pPlayer && i != index && pPlayer->GetObserverTarget() == this && pPlayer->GetObserverMode() == OBS_MODE_IN_EYE ) + { + pPlayer->SetPunchAngle( punchAngle ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Apply a movement constraint to the player +//----------------------------------------------------------------------------- +void CBasePlayer::ActivateMovementConstraint( CBaseEntity *pEntity, const Vector &vecCenter, float flRadius, float flConstraintWidth, float flSpeedFactor ) +{ + m_hConstraintEntity = pEntity; + m_vecConstraintCenter = vecCenter; + m_flConstraintRadius = flRadius; + m_flConstraintWidth = flConstraintWidth; + m_flConstraintSpeedFactor = flSpeedFactor; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::DeactivateMovementConstraint( ) +{ + m_hConstraintEntity = NULL; + m_flConstraintRadius = 0.0f; + m_vecConstraintCenter = vec3_origin; +} + +//----------------------------------------------------------------------------- +// Perhaps a poorly-named function. This function traces against the supplied +// NPC's hitboxes (instead of hull). If the trace hits a different NPC, the +// new NPC is selected. Otherwise, the supplied NPC is determined to be the +// one the citizen wants. This function allows the selection of a citizen over +// another citizen's shoulder, which is impossible without tracing against +// hitboxes instead of the hull (sjb) +//----------------------------------------------------------------------------- +CBaseEntity *CBasePlayer::DoubleCheckUseNPC( CBaseEntity *pNPC, const Vector &vecSrc, const Vector &vecDir ) +{ + trace_t tr; + + UTIL_TraceLine( vecSrc, vecSrc + vecDir * 1024, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if( tr.m_pEnt != NULL && tr.m_pEnt->MyNPCPointer() && tr.m_pEnt != pNPC ) + { + // Player is selecting a different NPC through some negative space + // in the first NPC's hitboxes (between legs, over shoulder, etc). + return tr.m_pEnt; + } + + return pNPC; +} + + +bool CBasePlayer::IsBot() const +{ + return (GetFlags() & FL_FAKECLIENT) != 0; +} + +bool CBasePlayer::IsFakeClient() const +{ + return (GetFlags() & FL_FAKECLIENT) != 0; +} + +void CBasePlayer::EquipSuit( bool bPlayEffects ) +{ + m_Local.m_bWearingSuit = true; +} + +void CBasePlayer::RemoveSuit( void ) +{ + m_Local.m_bWearingSuit = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &tr - +// nDamageType - +//----------------------------------------------------------------------------- +void CBasePlayer::DoImpactEffect( trace_t &tr, int nDamageType ) +{ + if ( GetActiveWeapon() ) + { + GetActiveWeapon()->DoImpactEffect( tr, nDamageType ); + return; + } + + BaseClass::DoImpactEffect( tr, nDamageType ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::InputSetHealth( inputdata_t &inputdata ) +{ + int iNewHealth = inputdata.value.Int(); + int iDelta = abs(GetHealth() - iNewHealth); + if ( iNewHealth > GetHealth() ) + { + TakeHealth( iDelta, DMG_GENERIC ); + } + else if ( iNewHealth < GetHealth() ) + { + // Strip off and restore armor so that it doesn't absorb any of this damage. + int armor = m_ArmorValue; + m_ArmorValue = 0; + TakeDamage( CTakeDamageInfo( this, this, iDelta, DMG_GENERIC ) ); + m_ArmorValue = armor; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::InputHandleMapEvent( inputdata_t &inputdata ) +{ + Internal_HandleMapEvent( inputdata ); +} + +//----------------------------------------------------------------------------- +// Purpose: Hides or displays the HUD +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBasePlayer::InputSetHUDVisibility( inputdata_t &inputdata ) +{ + bool bEnable = inputdata.value.Bool(); + + if ( bEnable ) + { + m_Local.m_iHideHUD &= ~HIDEHUD_ALL; + } + else + { + m_Local.m_iHideHUD |= HIDEHUD_ALL; + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBasePlayer::InputSetSuppressAttacks( inputdata_t &inputdata ) +{ + inputdata.value.Bool() ? + AddSpawnFlags( SF_PLAYER_SUPPRESS_FIRING ) : + RemoveSpawnFlags( SF_PLAYER_SUPPRESS_FIRING ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Set the fog controller data per player. +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CBasePlayer::InputSetFogController( inputdata_t &inputdata ) +{ + // Find the fog controller with the given name. + CFogController *pFogController = dynamic_cast( gEntList.FindEntityByName( NULL, inputdata.value.String() ) ); + if ( pFogController ) + { + m_Local.m_PlayerFog.m_hCtrl.Set( pFogController ); + } +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CBasePlayer::InitFogController( void ) +{ + // Setup with the default master controller. + m_Local.m_PlayerFog.m_hCtrl = FogSystem()->GetMasterFogController(); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CBasePlayer::InitPostProcessController( void ) +{ + // Setup with the default master controller. + m_hPostProcessCtrl = PostProcessSystem()->GetMasterPostProcessController(); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CBasePlayer::InputSetPostProcessController( inputdata_t& inputdata ) +{ + // Find the postprocess controller with the given name. + CPostProcessController* pController = NULL; + if (inputdata.value.FieldType() == FIELD_EHANDLE) + { + pController = dynamic_cast(inputdata.value.Entity().Get()); + } + else + { + pController = dynamic_cast(gEntList.FindEntityByName( NULL, inputdata.value.String() )); + } + + if (pController) + { + m_hPostProcessCtrl.Set( pController ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEntity - +//----------------------------------------------------------------------------- +void CBasePlayer::SetViewEntity( CBaseEntity *pEntity ) +{ + m_hViewEntity = pEntity; + + if ( m_hViewEntity ) + { + engine->SetView( edict(), m_hViewEntity->edict() ); + } + else + { + engine->SetView( edict(), edict() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Looks at the player's reserve ammo and also all his weapons for any ammo +// of the specified type +// Input : nAmmoIndex - ammo to look for +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBasePlayer::HasAnyAmmoOfType( int nAmmoIndex ) +{ + // Must be a valid index + if ( nAmmoIndex < 0 ) + return false; + + // If we have some in reserve, we're already done + if ( GetAmmoCount( nAmmoIndex ) ) + return true; + + CBaseCombatWeapon *pWeapon; + + // Check all held weapons + for ( int i=0; i < MAX_WEAPONS; i++ ) + { + pWeapon = GetWeapon( i ); + + if ( !pWeapon ) + continue; + + // We must use clips and use this sort of ammo + if ( pWeapon->UsesClipsForAmmo1() && pWeapon->GetPrimaryAmmoType() == nAmmoIndex ) + { + // If we have any ammo, we're done + if ( pWeapon->HasPrimaryAmmo() ) + return true; + } + + // We'll check both clips for the same ammo type, just in case + if ( pWeapon->UsesClipsForAmmo2() && pWeapon->GetSecondaryAmmoType() == nAmmoIndex ) + { + if ( pWeapon->HasSecondaryAmmo() ) + return true; + } + } + + // We're completely without this type of ammo + return false; +} + +bool CBasePlayer::HandleVoteCommands( const CCommand &args ) +{ + if( g_voteController == NULL ) + return false; + + if( FStrEq( args[0], "Vote" ) ) + { + if( args.ArgC() < 2 ) + return true; + + const char *arg2 = args[1]; + char szResultString[MAX_COMMAND_LENGTH]; + + CVoteController::TryCastVoteResult nTryResult = g_voteController->TryCastVote( entindex(), arg2 ); + switch( nTryResult ) + { + case CVoteController::CAST_OK: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "Voting %s.\n", arg2 ); + break; + } + case CVoteController::CAST_FAIL_SERVER_DISABLE: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "Vote failed: server disabled.\n" ); + break; + } + case CVoteController::CAST_FAIL_NO_ACTIVE_ISSUE: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "A vote has not been called.\n" ); + break; + } + case CVoteController::CAST_FAIL_TEAM_RESTRICTED: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "Vote failed: team restricted.\n" ); + break; + } + case CVoteController::CAST_FAIL_NO_CHANGES: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "Vote failed: no changing vote.\n" ); + break; + } + case CVoteController::CAST_FAIL_DUPLICATE: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "Vote failed: already voting %s.\n", arg2 ); + break; + } + case CVoteController::CAST_FAIL_VOTE_CLOSED: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "Vote failed: voting closed.\n" ); + break; + } + case CVoteController::CAST_FAIL_SYSTEM_ERROR: + default: + { + Q_snprintf( szResultString, MAX_COMMAND_LENGTH, "Vote failed: system error.\n" ); + break; + } + } + + DevMsg( "%s", szResultString ); + + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// return a string version of the players network (i.e steam) ID. +// +//----------------------------------------------------------------------------- +const char *CBasePlayer::GetNetworkIDString() +{ + //Tony; bots don't have network id's, and this can potentially crash, especially with plugins creating them. + if (IsBot()) + return "__BOT__"; + + //Tony; if networkidstring is null for any reason, the strncpy will crash! + if (!m_szNetworkIDString) + return "NULLID"; + + const char *pStr = engine->GetPlayerNetworkIDString( edict() ); + Q_strncpy( m_szNetworkIDString, pStr ? pStr : "", sizeof(m_szNetworkIDString) ); + return m_szNetworkIDString; +} + +//----------------------------------------------------------------------------- +// Assign the player a name +//----------------------------------------------------------------------------- +void CBasePlayer::SetPlayerName( const char *name ) +{ + Assert( name ); + + if ( name ) + { + Assert( strlen(name) > 0 ); + + Q_strncpy( m_szNetname, name, sizeof(m_szNetname) ); + } +} + +//----------------------------------------------------------------------------- +// sets the "don't autokick me" flag on a player +//----------------------------------------------------------------------------- +class DisableAutokick +{ +public: + DisableAutokick( int userID ) + { + m_userID = userID; + } + + bool operator()( CBasePlayer *player ) + { + if ( player->GetUserID() == m_userID ) + { + Msg( "autokick is disabled for %s\n", player->GetPlayerName() ); + player->DisableAutoKick( true ); + return false; // don't need to check other players + } + + return true; // keep looking at other players + } + +private: + int m_userID; +}; + +//----------------------------------------------------------------------------- +// sets the "don't autokick me" flag on a player +//----------------------------------------------------------------------------- +CON_COMMAND( mp_disable_autokick, "Prevents a userid from being auto-kicked" ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() != 2 ) + { + Msg( "Usage: mp_disable_autokick \n" ); + return; + } + + int userID = atoi( args[1] ); + DisableAutokick disable( userID ); + ForEachPlayer( disable ); +} + +//----------------------------------------------------------------------------- +// Purpose: Toggle between the duck being on and off +//----------------------------------------------------------------------------- +void CBasePlayer::ToggleDuck( void ) +{ + // Toggle the state + m_bDuckToggled = !m_bDuckToggled; +} + +//----------------------------------------------------------------------------- +// Just tells us how far the stick is from the center. No directional info +//----------------------------------------------------------------------------- +float CBasePlayer::GetStickDist() +{ + Vector2D controlStick; + + controlStick.x = m_flForwardMove; + controlStick.y = m_flSideMove; + + return controlStick.Length(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlayer::HandleAnimEvent( animevent_t *pEvent ) +{ + if ((pEvent->type & AE_TYPE_NEWEVENTSYSTEM) && (pEvent->type & AE_TYPE_SERVER)) + { + if ( pEvent->event == AE_RAGDOLL ) + { + // Convert to ragdoll immediately + CreateRagdollEntity(); + BecomeRagdollOnClient( vec3_origin ); + + // Force the player to start death thinking + SetThink(&CBasePlayer::PlayerDeathThink); + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + } + + BaseClass::HandleAnimEvent( pEvent ); +} + +//----------------------------------------------------------------------------- +// CPlayerInfo functions (simple passthroughts to get around the CBasePlayer multiple inheritence limitation) +//----------------------------------------------------------------------------- +const char *CPlayerInfo::GetName() +{ + Assert( m_pParent ); + return m_pParent->GetPlayerName(); +} + +int CPlayerInfo::GetUserID() +{ + Assert( m_pParent ); + return engine->GetPlayerUserId( m_pParent->edict() ); +} + +const char *CPlayerInfo::GetNetworkIDString() +{ + Assert( m_pParent ); + return m_pParent->GetNetworkIDString(); +} + +int CPlayerInfo::GetTeamIndex() +{ + Assert( m_pParent ); + return m_pParent->GetTeamNumber(); +} + +void CPlayerInfo::ChangeTeam( int iTeamNum ) +{ + Assert( m_pParent ); + m_pParent->ChangeTeam(iTeamNum); +} + +int CPlayerInfo::GetFragCount() +{ + Assert( m_pParent ); + return m_pParent->FragCount(); +} + +int CPlayerInfo::GetDeathCount() +{ + Assert( m_pParent ); + return m_pParent->DeathCount(); +} + +bool CPlayerInfo::IsConnected() +{ + Assert( m_pParent ); + return m_pParent->IsConnected(); +} + +int CPlayerInfo::GetArmorValue() +{ + Assert( m_pParent ); + return m_pParent->ArmorValue(); +} + +bool CPlayerInfo::IsHLTV() +{ + Assert( m_pParent ); + return m_pParent->IsHLTV(); +} + +bool CPlayerInfo::IsReplay() +{ +#ifdef TF_DLL // FIXME: Need run-time check for whether replay is enabled + Assert( m_pParent ); + return m_pParent->IsReplay(); +#else + return false; +#endif +} + +bool CPlayerInfo::IsPlayer() +{ + Assert( m_pParent ); + return m_pParent->IsPlayer(); +} + +bool CPlayerInfo::IsFakeClient() +{ + Assert( m_pParent ); + return m_pParent->IsFakeClient(); +} + +bool CPlayerInfo::IsDead() +{ + Assert( m_pParent ); + return m_pParent->IsDead(); +} + +bool CPlayerInfo::IsInAVehicle() +{ + Assert( m_pParent ); + return m_pParent->IsInAVehicle(); +} + +bool CPlayerInfo::IsObserver() +{ + Assert( m_pParent ); + return m_pParent->IsObserver(); +} + +const Vector CPlayerInfo::GetAbsOrigin() +{ + Assert( m_pParent ); + return m_pParent->GetAbsOrigin(); +} + +const QAngle CPlayerInfo::GetAbsAngles() +{ + Assert( m_pParent ); + return m_pParent->GetAbsAngles(); +} + +const Vector CPlayerInfo::GetPlayerMins() +{ + Assert( m_pParent ); + return m_pParent->GetPlayerMins(); +} + +const Vector CPlayerInfo::GetPlayerMaxs() +{ + Assert( m_pParent ); + return m_pParent->GetPlayerMaxs(); +} + +const char *CPlayerInfo::GetWeaponName() +{ + Assert( m_pParent ); + CBaseCombatWeapon *weap = m_pParent->GetActiveWeapon(); + if ( !weap ) + { + return NULL; + } + return weap->GetName(); +} + +const char *CPlayerInfo::GetModelName() +{ + Assert( m_pParent ); + return m_pParent->GetModelName().ToCStr(); +} + +const int CPlayerInfo::GetHealth() +{ + Assert( m_pParent ); + return m_pParent->GetHealth(); +} + +const int CPlayerInfo::GetMaxHealth() +{ + Assert( m_pParent ); + return m_pParent->GetMaxHealth(); +} + + + + + +void CPlayerInfo::SetAbsOrigin( Vector & vec ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + m_pParent->SetAbsOrigin(vec); + } +} + +void CPlayerInfo::SetAbsAngles( QAngle & ang ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + m_pParent->SetAbsAngles(ang); + } +} + +void CPlayerInfo::RemoveAllItems( bool removeSuit ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + m_pParent->RemoveAllItems(removeSuit); + } +} + +void CPlayerInfo::SetActiveWeapon( const char *WeaponName ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + CBaseCombatWeapon *weap = m_pParent->Weapon_Create( WeaponName ); + if ( weap ) + { + m_pParent->Weapon_Equip(weap); + m_pParent->Weapon_Switch(weap); + } + } +} + +void CPlayerInfo::SetLocalOrigin( const Vector& origin ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + m_pParent->SetLocalOrigin(origin); + } +} + +const Vector CPlayerInfo::GetLocalOrigin( void ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + Vector origin = m_pParent->GetLocalOrigin(); + return origin; + } + else + { + return Vector( 0, 0, 0 ); + } +} + +void CPlayerInfo::SetLocalAngles( const QAngle& angles ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + m_pParent->SetLocalAngles( angles ); + } +} + +const QAngle CPlayerInfo::GetLocalAngles( void ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + return m_pParent->GetLocalAngles(); + } + else + { + return QAngle(); + } +} + +bool CPlayerInfo::IsEFlagSet( int nEFlagMask ) +{ + Assert( m_pParent ); + if ( m_pParent->IsBot() ) + { + return m_pParent->IsEFlagSet(nEFlagMask); + } + return false; +} + +void CPlayerInfo::RunPlayerMove( CBotCmd *ucmd ) +{ + if ( m_pParent->IsBot() ) + { + Assert( m_pParent ); + CUserCmd cmd; + cmd.buttons = ucmd->buttons; + cmd.command_number = ucmd->command_number; + cmd.forwardmove = ucmd->forwardmove; + cmd.hasbeenpredicted = ucmd->hasbeenpredicted; + cmd.impulse = ucmd->impulse; + cmd.mousedx = ucmd->mousedx; + cmd.mousedy = ucmd->mousedy; + cmd.random_seed = ucmd->random_seed; + cmd.sidemove = ucmd->sidemove; + cmd.tick_count = ucmd->tick_count; + cmd.upmove = ucmd->upmove; + cmd.viewangles = ucmd->viewangles; + cmd.weaponselect = ucmd->weaponselect; + cmd.weaponsubtype = ucmd->weaponsubtype; + + // Store off the globals.. they're gonna get whacked + float flOldFrametime = gpGlobals->frametime; + float flOldCurtime = gpGlobals->curtime; + + m_pParent->SetTimeBase( gpGlobals->curtime ); + + MoveHelperServer()->SetHost( m_pParent ); + m_pParent->PlayerRunCommand( &cmd, MoveHelperServer() ); + + // save off the last good usercmd + m_pParent->SetLastUserCommand( cmd ); + + // Clear out any fixangle that has been set + m_pParent->pl.fixangle = FIXANGLE_NONE; + + // Restore the globals.. + gpGlobals->frametime = flOldFrametime; + gpGlobals->curtime = flOldCurtime; + MoveHelperServer()->SetHost( NULL ); + } +} + +void CPlayerInfo::SetLastUserCommand( const CBotCmd &ucmd ) +{ + if ( m_pParent->IsBot() ) + { + Assert( m_pParent ); + CUserCmd cmd; + cmd.buttons = ucmd.buttons; + cmd.command_number = ucmd.command_number; + cmd.forwardmove = ucmd.forwardmove; + cmd.hasbeenpredicted = ucmd.hasbeenpredicted; + cmd.impulse = ucmd.impulse; + cmd.mousedx = ucmd.mousedx; + cmd.mousedy = ucmd.mousedy; + cmd.random_seed = ucmd.random_seed; + cmd.sidemove = ucmd.sidemove; + cmd.tick_count = ucmd.tick_count; + cmd.upmove = ucmd.upmove; + cmd.viewangles = ucmd.viewangles; + cmd.weaponselect = ucmd.weaponselect; + cmd.weaponsubtype = ucmd.weaponsubtype; + + m_pParent->SetLastUserCommand(cmd); + } +} + + +CBotCmd CPlayerInfo::GetLastUserCommand() +{ + CBotCmd cmd; + const CUserCmd *ucmd = m_pParent->GetLastUserCommand(); + if ( ucmd ) + { + cmd.buttons = ucmd->buttons; + cmd.command_number = ucmd->command_number; + cmd.forwardmove = ucmd->forwardmove; + cmd.hasbeenpredicted = ucmd->hasbeenpredicted; + cmd.impulse = ucmd->impulse; + cmd.mousedx = ucmd->mousedx; + cmd.mousedy = ucmd->mousedy; + cmd.random_seed = ucmd->random_seed; + cmd.sidemove = ucmd->sidemove; + cmd.tick_count = ucmd->tick_count; + cmd.upmove = ucmd->upmove; + cmd.viewangles = ucmd->viewangles; + cmd.weaponselect = ucmd->weaponselect; + cmd.weaponsubtype = ucmd->weaponsubtype; + } + return cmd; +} + +// Notify that I've killed some other entity. (called from Victim's Event_Killed). +void CBasePlayer::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + BaseClass::Event_KilledOther( pVictim, info ); + if ( pVictim != this ) + { + gamestats->Event_PlayerKilledOther( this, pVictim, info ); + } + else + { + gamestats->Event_PlayerSuicide( this ); + } +} + +void CBasePlayer::SetModel( const char *szModelName ) +{ + BaseClass::SetModel( szModelName ); + m_nBodyPitchPoseParam = LookupPoseParameter( "body_pitch" ); +} + +void CBasePlayer::SetBodyPitch( float flPitch ) +{ + if ( m_nBodyPitchPoseParam >= 0 ) + { + SetPoseParameter( m_nBodyPitchPoseParam, flPitch ); + } +} + +void CBasePlayer::AdjustDrownDmg( int nAmount ) +{ + m_idrowndmg += nAmount; + if ( m_idrowndmg < m_idrownrestored ) + { + m_idrowndmg = m_idrownrestored; + } +} + + + +#if !defined(NO_STEAM) +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBasePlayer::GetSteamID( CSteamID *pID ) +{ + const CSteamID *pClientID = engine->GetClientSteamID( edict() ); + if ( pClientID ) + { + *pID = *pClientID; + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +uint64 CBasePlayer::GetSteamIDAsUInt64( void ) +{ + CSteamID steamIDForPlayer; + if ( GetSteamID( &steamIDForPlayer ) ) + return steamIDForPlayer.ConvertToUint64(); + return 0; +} +#endif // NO_STEAM \ No newline at end of file diff --git a/sp/src/game/server/player.h b/sp/src/game/server/player.h new file mode 100644 index 00000000..03709ade --- /dev/null +++ b/sp/src/game/server/player.h @@ -0,0 +1,1627 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#ifndef PLAYER_H +#define PLAYER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "basecombatcharacter.h" +#include "usercmd.h" +#include "playerlocaldata.h" +#include "PlayerState.h" +#include "game/server/iplayerinfo.h" +#include "hintsystem.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "util_shared.h" + +#if defined USES_ECON_ITEMS +#include "game_item_schema.h" +#include "econ_item_view.h" +#endif + +// For queuing and processing usercmds +class CCommandContext +{ +public: + CUtlVector< CUserCmd > cmds; + + int numcmds; + int totalcmds; + int dropped_packets; + bool paused; +}; + +// Info about last 20 or so updates to the +class CPlayerCmdInfo +{ +public: + CPlayerCmdInfo() : + m_flTime( 0.0f ), m_nNumCmds( 0 ), m_nDroppedPackets( 0 ) + { + } + + // realtime of sample + float m_flTime; + // # of CUserCmds in this update + int m_nNumCmds; + // # of dropped packets on the link + int m_nDroppedPackets; +}; + +class CPlayerSimInfo +{ +public: + CPlayerSimInfo() : + m_flTime( 0.0f ), m_nNumCmds( 0 ), m_nTicksCorrected( 0 ), m_flFinalSimulationTime( 0.0f ), m_flGameSimulationTime( 0.0f ), m_flServerFrameTime( 0.0f ), m_vecAbsOrigin( 0, 0, 0 ) + { + } + + // realtime of sample + float m_flTime; + // # of CUserCmds in this update + int m_nNumCmds; + // If clock needed correction, # of ticks added/removed + int m_nTicksCorrected; // +ve or -ve + // player's m_flSimulationTime at end of frame + float m_flFinalSimulationTime; + float m_flGameSimulationTime; + // estimate of server perf + float m_flServerFrameTime; + Vector m_vecAbsOrigin; +}; +//----------------------------------------------------------------------------- +// Forward declarations: +//----------------------------------------------------------------------------- +class CBaseCombatWeapon; +class CBaseViewModel; +class CTeam; +class IPhysicsPlayerController; +class IServerVehicle; +class CUserCmd; +class CFuncLadder; +class CNavArea; +class CHintSystem; +class CAI_Expresser; + +#if defined USES_ECON_ITEMS +class CEconWearable; +#endif // USES_ECON_ITEMS + +// for step sounds +struct surfacedata_t; + +// !!!set this bit on guns and stuff that should never respawn. +#define SF_NORESPAWN ( 1 << 30 ) + +// +// Player PHYSICS FLAGS bits +// +enum PlayerPhysFlag_e +{ + PFLAG_DIROVERRIDE = ( 1<<0 ), // override the player's directional control (trains, physics gun, etc.) + PFLAG_DUCKING = ( 1<<1 ), // In the process of ducking, but totally squatted yet + PFLAG_USING = ( 1<<2 ), // Using a continuous entity + PFLAG_OBSERVER = ( 1<<3 ), // player is locked in stationary cam mode. Spectators can move, observers can't. + PFLAG_VPHYSICS_MOTIONCONTROLLER = ( 1<<4 ), // player is physically attached to a motion controller + PFLAG_GAMEPHYSICS_ROTPUSH = (1<<5), // game physics did a rotating push that we may want to override with vphysics + + // If you add another flag here check that you aren't + // overwriting phys flags in the HL2 of TF2 player classes +}; + +// +// generic player +// +//----------------------------------------------------- +//This is Half-Life player entity +//----------------------------------------------------- +#define CSUITPLAYLIST 4 // max of 4 suit sentences queued up at any time +#define SUIT_REPEAT_OK 0 + +#define SUIT_NEXT_IN_30SEC 30 +#define SUIT_NEXT_IN_1MIN 60 +#define SUIT_NEXT_IN_5MIN 300 +#define SUIT_NEXT_IN_10MIN 600 +#define SUIT_NEXT_IN_30MIN 1800 +#define SUIT_NEXT_IN_1HOUR 3600 + +#define CSUITNOREPEAT 32 + +#define TEAM_NAME_LENGTH 16 + +// constant items +#define ITEM_HEALTHKIT 1 +#define ITEM_BATTERY 4 + +#define AUTOAIM_2DEGREES 0.0348994967025 +#define AUTOAIM_5DEGREES 0.08715574274766 +#define AUTOAIM_8DEGREES 0.1391731009601 +#define AUTOAIM_10DEGREES 0.1736481776669 +#define AUTOAIM_20DEGREES 0.3490658503989 + +// useful cosines +#define DOT_1DEGREE 0.9998476951564 +#define DOT_2DEGREE 0.9993908270191 +#define DOT_3DEGREE 0.9986295347546 +#define DOT_4DEGREE 0.9975640502598 +#define DOT_5DEGREE 0.9961946980917 +#define DOT_6DEGREE 0.9945218953683 +#define DOT_7DEGREE 0.9925461516413 +#define DOT_8DEGREE 0.9902680687416 +#define DOT_9DEGREE 0.9876883405951 +#define DOT_10DEGREE 0.9848077530122 +#define DOT_15DEGREE 0.9659258262891 +#define DOT_20DEGREE 0.9396926207859 +#define DOT_25DEGREE 0.9063077870367 +#define DOT_30DEGREE 0.866025403784 +#define DOT_45DEGREE 0.707106781187 +enum +{ + VPHYS_WALK = 0, + VPHYS_CROUCH, + VPHYS_NOCLIP, +}; + + +enum PlayerConnectedState +{ + PlayerConnected, + PlayerDisconnecting, + PlayerDisconnected, +}; + +extern bool gInitHUD; +extern ConVar *sv_cheats; + +class CBasePlayer; +class CPlayerInfo : public IBotController, public IPlayerInfo +{ +public: + CPlayerInfo () { m_pParent = NULL; } + ~CPlayerInfo () {} + void SetParent( CBasePlayer *parent ) { m_pParent = parent; } + + // IPlayerInfo interface + virtual const char *GetName(); + virtual int GetUserID(); + virtual const char *GetNetworkIDString(); + virtual int GetTeamIndex(); + virtual void ChangeTeam( int iTeamNum ); + virtual int GetFragCount(); + virtual int GetDeathCount(); + virtual bool IsConnected(); + virtual int GetArmorValue(); + + virtual bool IsHLTV(); + virtual bool IsReplay(); + virtual bool IsPlayer(); + virtual bool IsFakeClient(); + virtual bool IsDead(); + virtual bool IsInAVehicle(); + virtual bool IsObserver(); + virtual const Vector GetAbsOrigin(); + virtual const QAngle GetAbsAngles(); + virtual const Vector GetPlayerMins(); + virtual const Vector GetPlayerMaxs(); + virtual const char *GetWeaponName(); + virtual const char *GetModelName(); + virtual const int GetHealth(); + virtual const int GetMaxHealth(); + + // bot specific functions + virtual void SetAbsOrigin( Vector & vec ); + virtual void SetAbsAngles( QAngle & ang ); + virtual void RemoveAllItems( bool removeSuit ); + virtual void SetActiveWeapon( const char *WeaponName ); + virtual void SetLocalOrigin( const Vector& origin ); + virtual const Vector GetLocalOrigin( void ); + virtual void SetLocalAngles( const QAngle& angles ); + virtual const QAngle GetLocalAngles( void ); + virtual bool IsEFlagSet( int nEFlagMask ); + + virtual void RunPlayerMove( CBotCmd *ucmd ); + virtual void SetLastUserCommand( const CBotCmd &cmd ); + + virtual CBotCmd GetLastUserCommand(); + +private: + CBasePlayer *m_pParent; +}; + +class CBasePlayer : public CBaseCombatCharacter +{ +public: + DECLARE_CLASS( CBasePlayer, CBaseCombatCharacter ); +protected: + // HACK FOR BOTS + friend class CBotManager; + static edict_t *s_PlayerEdict; // must be set before calling constructor +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + // script description + DECLARE_ENT_SCRIPTDESC(); + + CBasePlayer(); + ~CBasePlayer(); + + // IPlayerInfo passthrough (because we can't do multiple inheritance) + IPlayerInfo *GetPlayerInfo() { return &m_PlayerInfo; } + IBotController *GetBotController() { return &m_PlayerInfo; } + + virtual void SetModel( const char *szModelName ); + void SetBodyPitch( float flPitch ); + + virtual void UpdateOnRemove( void ); + + static CBasePlayer *CreatePlayer( const char *className, edict_t *ed ); + + virtual void CreateViewModel( int viewmodelindex = 0 ); + CBaseViewModel *GetViewModel( int viewmodelindex = 0, bool bObserverOK = true ); + void HideViewModels( void ); + void DestroyViewModels( void ); + +#ifdef MAPBASE + virtual void CreateHandModel( int viewmodelindex = 1, int iOtherVm = 0 ); +#endif + + CPlayerState *PlayerData( void ) { return &pl; } + + int RequiredEdictIndex( void ) { return ENTINDEX(edict()); } + + void LockPlayerInPlace( void ); + void UnlockPlayer( void ); + + virtual void DrawDebugGeometryOverlays(void); + + // Networking is about to update this entity, let it override and specify it's own pvs + virtual void SetupVisibility( CBaseEntity *pViewEntity, unsigned char *pvs, int pvssize ); + virtual int UpdateTransmitState(); + virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo ); + + // Returns true if this player wants pPlayer to be moved back in time when this player runs usercmds. + // Saves a lot of overhead on the server if we can cull out entities that don't need to lag compensate + // (like team members, entities out of our PVS, etc). + virtual bool WantsLagCompensationOnEntity( const CBasePlayer *pPlayer, const CUserCmd *pCmd, const CBitVec *pEntityTransmitBits ) const; + + virtual void Spawn( void ); + virtual void Activate( void ); + virtual void SharedSpawn(); // Shared between client and server. + virtual void ForceRespawn( void ); + +#ifdef MAPBASE + // For the logic_playerproxy output + virtual void SpawnedAtPoint( CBaseEntity *pSpawnPoint ) {} +#endif + + virtual void InitialSpawn( void ); + virtual void InitHUD( void ) {} + virtual void ShowViewPortPanel( const char * name, bool bShow = true, KeyValues *data = NULL ); + + virtual void PlayerDeathThink( void ); + + virtual void Jump( void ); + virtual void Duck( void ); + + const char *GetTracerType( void ); + void MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ); + void DoImpactEffect( trace_t &tr, int nDamageType ); + +#if !defined( NO_ENTITY_PREDICTION ) + void AddToPlayerSimulationList( CBaseEntity *other ); + void RemoveFromPlayerSimulationList( CBaseEntity *other ); + void SimulatePlayerSimulatedEntities( void ); + void ClearPlayerSimulationList( void ); +#endif + + // Physics simulation (player executes it's usercmd's here) + virtual void PhysicsSimulate( void ); + + // Forces processing of usercmds (e.g., even if game is paused, etc.) + void ForceSimulation(); + + virtual unsigned int PhysicsSolidMaskForEntity( void ) const; + + virtual void PreThink( void ); + virtual void PostThink( void ); + virtual int TakeHealth( float flHealth, int bitsDamageType ); + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + bool ShouldTakeDamageInCommentaryMode( const CTakeDamageInfo &inputInfo ); + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual void DamageEffect(float flDamage, int fDamageType); + + virtual void OnDamagedByExplosion( const CTakeDamageInfo &info ); + + void PauseBonusProgress( bool bPause = true ); + void SetBonusProgress( int iBonusProgress ); + void SetBonusChallenge( int iBonusChallenge ); + + int GetBonusProgress() const { return m_iBonusProgress; } + int GetBonusChallenge() const { return m_iBonusChallenge; } + + virtual Vector EyePosition( ); // position of eyes + const QAngle &EyeAngles( ); + void EyePositionAndVectors( Vector *pPosition, Vector *pForward, Vector *pRight, Vector *pUp ); + virtual const QAngle &LocalEyeAngles(); // Direction of eyes + void EyeVectors( Vector *pForward, Vector *pRight = NULL, Vector *pUp = NULL ); + void CacheVehicleView( void ); // Calculate and cache the position of the player in the vehicle + + // Sets the view angles + void SnapEyeAngles( const QAngle &viewAngles ); + + virtual QAngle BodyAngles(); + virtual Vector BodyTarget( const Vector &posSrc, bool bNoisy); + virtual bool ShouldFadeOnDeath( void ) { return FALSE; } + + virtual const impactdamagetable_t &GetPhysicsImpactDamageTable(); + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + // Notifier that I've killed some other entity. (called from Victim's Event_Killed). + virtual void Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ); + + virtual void Event_Dying( const CTakeDamageInfo &info ); + + bool IsHLTV( void ) const { return pl.hltv; } + bool IsReplay( void ) const { return pl.replay; } + virtual bool IsPlayer( void ) const { return true; } // Spectators return TRUE for this, use IsObserver to separate cases + virtual bool IsNetClient( void ) const { return true; } // Bots should return FALSE for this, they can't receive NET messages + // Spectators should return TRUE for this + + virtual bool IsFakeClient( void ) const; + + // Get the client index (entindex-1). + int GetClientIndex() { return ENTINDEX( edict() ) - 1; } + + // returns the player name + const char * GetPlayerName() { return m_szNetname; } + void SetPlayerName( const char *name ); + + int GetUserID() { return engine->GetPlayerUserId( edict() ); } + const char * GetNetworkIDString(); + virtual const Vector GetPlayerMins( void ) const; // uses local player + virtual const Vector GetPlayerMaxs( void ) const; // uses local player + + + void VelocityPunch( const Vector &vecForce ); + void ViewPunch( const QAngle &angleOffset ); + void ViewPunchReset( float tolerance = 0 ); + void ShowViewModel( bool bShow ); + void ShowCrosshair( bool bShow ); + + bool ScriptIsPlayerNoclipping(void); + +#ifdef MAPBASE_VSCRIPT + HSCRIPT VScriptGetExpresser(); + + int GetButtons() { return m_nButtons; } + int GetButtonPressed() { return m_afButtonPressed; } + int GetButtonReleased() { return m_afButtonReleased; } + int GetButtonLast() { return m_afButtonLast; } + int GetButtonDisabled() { return m_afButtonDisabled; } + int GetButtonForced() { return m_afButtonForced; } + + const Vector& ScriptGetEyeForward() { static Vector vecForward; EyeVectors( &vecForward, NULL, NULL ); return vecForward; } + const Vector& ScriptGetEyeRight() { static Vector vecRight; EyeVectors( NULL, &vecRight, NULL ); return vecRight; } + const Vector& ScriptGetEyeUp() { static Vector vecUp; EyeVectors( NULL, NULL, &vecUp ); return vecUp; } +#endif + + // View model prediction setup + void CalcView( Vector &eyeOrigin, QAngle &eyeAngles, float &zNear, float &zFar, float &fov ); + + // Handle view smoothing when going up stairs + void SmoothViewOnStairs( Vector& eyeOrigin ); + virtual float CalcRoll (const QAngle& angles, const Vector& velocity, float rollangle, float rollspeed); + void CalcViewRoll( QAngle& eyeAngles ); + + virtual int Save( ISave &save ); + virtual int Restore( IRestore &restore ); + virtual bool ShouldSavePhysics(); + virtual void OnRestore( void ); + + virtual void PackDeadPlayerItems( void ); + virtual void RemoveAllItems( bool removeSuit ); + bool IsDead() const; +#ifdef CSTRIKE_DLL + virtual bool IsRunning( void ) const { return false; } // bot support under cstrike (AR) +#endif + + bool HasPhysicsFlag( unsigned int flag ) { return (m_afPhysicsFlags & flag) != 0; } + + // Weapon stuff + virtual Vector Weapon_ShootPosition( ); + virtual bool Weapon_CanUse( CBaseCombatWeapon *pWeapon ); + virtual void Weapon_Equip( CBaseCombatWeapon *pWeapon ); + virtual void Weapon_Drop( CBaseCombatWeapon *pWeapon, const Vector *pvecTarget /* = NULL */, const Vector *pVelocity /* = NULL */ ); + virtual bool Weapon_Switch( CBaseCombatWeapon *pWeapon, int viewmodelindex = 0 ); // Switch to given weapon if has ammo (false if failed) + virtual void Weapon_SetLast( CBaseCombatWeapon *pWeapon ); + virtual bool Weapon_ShouldSetLast( CBaseCombatWeapon *pOldWeapon, CBaseCombatWeapon *pNewWeapon ) { return true; } + virtual bool Weapon_ShouldSelectItem( CBaseCombatWeapon *pWeapon ); + void Weapon_DropSlot( int weaponSlot ); + CBaseCombatWeapon *Weapon_GetLast( void ) { return m_hLastWeapon.Get(); } +#ifdef MAPBASE + virtual Activity Weapon_TranslateActivity( Activity baseAct, bool *pRequired = NULL ); +#endif + + virtual void OnMyWeaponFired( CBaseCombatWeapon *weapon ); // call this when this player fires a weapon to allow other systems to react + virtual float GetTimeSinceWeaponFired( void ) const; // returns the time, in seconds, since this player fired a weapon + virtual bool IsFiringWeapon( void ) const; // return true if this player is currently firing their weapon + + bool HasAnyAmmoOfType( int nAmmoIndex ); + + // JOHN: sends custom messages if player HUD data has changed (eg health, ammo) + virtual void UpdateClientData( void ); + void RumbleEffect( unsigned char index, unsigned char rumbleData, unsigned char rumbleFlags ); + + // Player is moved across the transition by other means + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + virtual void Precache( void ); + bool IsOnLadder( void ); + virtual void ExitLadder() {} + virtual surfacedata_t *GetLadderSurface( const Vector &origin ); + + virtual void SetFlashlightEnabled( bool bState ) { }; + virtual int FlashlightIsOn( void ) { return false; } + virtual void FlashlightTurnOn( void ) { }; + virtual void FlashlightTurnOff( void ) { }; + virtual bool IsIlluminatedByFlashlight( CBaseEntity *pEntity, float *flReturnDot ) {return false; } + + void UpdatePlayerSound ( void ); + virtual void UpdateStepSound( surfacedata_t *psurface, const Vector &vecOrigin, const Vector &vecVelocity ); + virtual void PlayStepSound( Vector &vecOrigin, surfacedata_t *psurface, float fvol, bool force ); + virtual const char *GetOverrideStepSound( const char *pszBaseStepSoundName ) { return pszBaseStepSoundName; } + virtual void GetStepSoundVelocities( float *velwalk, float *velrun ); + virtual void SetStepSoundTime( stepsoundtimes_t iStepSoundTime, bool bWalking ); + virtual void DeathSound( const CTakeDamageInfo &info ); + virtual const char* GetSceneSoundToken( void ) { return ""; } + + virtual void OnEmitFootstepSound( const CSoundParameters& params, const Vector& vecOrigin, float fVolume ) {} + + Class_T Classify ( void ); + virtual void SetAnimation( PLAYER_ANIM playerAnim ); + void SetWeaponAnimType( const char *szExtention ); + + // custom player functions + virtual void ImpulseCommands( void ); + virtual void CheatImpulseCommands( int iImpulse ); + virtual bool ClientCommand( const CCommand &args ); + + void NotifySinglePlayerGameEnding() { m_bSinglePlayerGameEnding = true; } + bool IsSinglePlayerGameEnding() { return m_bSinglePlayerGameEnding == true; } + + bool HandleVoteCommands( const CCommand &args ); + + // Observer functions + virtual bool StartObserverMode(int mode); // true, if successful + virtual void StopObserverMode( void ); // stop spectator mode + virtual bool ModeWantsSpectatorGUI( int iMode ) { return true; } + virtual bool SetObserverMode(int mode); // sets new observer mode, returns true if successful + virtual int GetObserverMode( void ); // returns observer mode or OBS_NONE + virtual bool SetObserverTarget(CBaseEntity * target); + virtual void ObserverUse( bool bIsPressed ); // observer pressed use + virtual CBaseEntity *GetObserverTarget( void ); // returns players targer or NULL + virtual CBaseEntity *FindNextObserverTarget( bool bReverse ); // returns next/prev player to follow or NULL + virtual int GetNextObserverSearchStartPoint( bool bReverse ); // Where we should start looping the player list in a FindNextObserverTarget call + virtual bool IsValidObserverTarget(CBaseEntity * target); // true, if player is allowed to see this target + virtual void CheckObserverSettings(); // checks, if target still valid (didn't die etc) + virtual void JumptoPosition(const Vector &origin, const QAngle &angles); + virtual void ForceObserverMode(int mode); // sets a temporary mode, force because of invalid targets + virtual void ResetObserverMode(); // resets all observer related settings + virtual void ValidateCurrentObserverTarget( void ); // Checks the current observer target, and moves on if it's not valid anymore + virtual void AttemptToExitFreezeCam( void ); + + virtual bool StartReplayMode( float fDelay, float fDuration, int iEntity ); + virtual void StopReplayMode(); + virtual int GetDelayTicks(); + virtual int GetReplayEntity(); + + virtual void CreateCorpse( void ) { } + virtual CBaseEntity *EntSelectSpawnPoint( void ); + + // Vehicles + virtual bool IsInAVehicle( void ) const; + bool CanEnterVehicle( IServerVehicle *pVehicle, int nRole ); + virtual bool GetInVehicle( IServerVehicle *pVehicle, int nRole ); + virtual void LeaveVehicle( const Vector &vecExitPoint = vec3_origin, const QAngle &vecExitAngles = vec3_angle ); + int GetVehicleAnalogControlBias() { return m_iVehicleAnalogBias; } + void SetVehicleAnalogControlBias( int bias ) { m_iVehicleAnalogBias = bias; } + + // override these for + virtual void OnVehicleStart() {} + virtual void OnVehicleEnd( Vector &playerDestPosition ) {} + IServerVehicle *GetVehicle(); + CBaseEntity *GetVehicleEntity( void ); + bool UsingStandardWeaponsInVehicle( void ); + + void AddPoints( int score, bool bAllowNegativeScore ); + void AddPointsToTeam( int score, bool bAllowNegativeScore ); + virtual bool BumpWeapon( CBaseCombatWeapon *pWeapon ); + bool RemovePlayerItem( CBaseCombatWeapon *pItem ); + CBaseEntity *HasNamedPlayerItem( const char *pszItemName ); + bool HasWeapons( void );// do I have ANY weapons? + virtual void SelectLastItem(void); + virtual void SelectItem( const char *pstr, int iSubType = 0 ); + void ItemPreFrame( void ); + virtual void ItemPostFrame( void ); + virtual CBaseEntity *GiveNamedItem( const char *szName, int iSubType = 0 ); + void EnableControl(bool fControl); + virtual void CheckTrainUpdate( void ); + void AbortReload( void ); + + void SendAmmoUpdate(void); + + void WaterMove( void ); + float GetWaterJumpTime() const; + void SetWaterJumpTime( float flWaterJumpTime ); + float GetSwimSoundTime( void ) const; + void SetSwimSoundTime( float flSwimSoundTime ); + + virtual void SetPlayerUnderwater( bool state ); + void UpdateUnderwaterState( void ); + bool IsPlayerUnderwater( void ) { return m_bPlayerUnderwater; } + + virtual bool CanBreatheUnderwater() const { return false; } + virtual void PlayerUse( void ); + virtual void PlayUseDenySound() {} + + virtual CBaseEntity *FindUseEntity( void ); + virtual bool IsUseableEntity( CBaseEntity *pEntity, unsigned int requiredCaps ); + bool ClearUseEntity(); + CBaseEntity *DoubleCheckUseNPC( CBaseEntity *pNPC, const Vector &vecSrc, const Vector &vecDir ); + + + // physics interactions + // mass/size limit set to zero for none + static bool CanPickupObject( CBaseEntity *pObject, float massLimit, float sizeLimit ); + virtual void PickupObject( CBaseEntity *pObject, bool bLimitMassAndSize = true ) {} + virtual void ForceDropOfCarriedPhysObjects( CBaseEntity *pOnlyIfHoldindThis = NULL ) {} + virtual float GetHeldObjectMass( IPhysicsObject *pHeldObject ); + virtual CBaseEntity *GetHeldObject( void ); + + virtual void CheckSuitUpdate(); + void SetSuitUpdate(const char *name, int fgroup, int iNoRepeat); + virtual void UpdateGeigerCounter( void ); + void CheckTimeBasedDamage( void ); + + void ResetAutoaim( void ); + + virtual Vector GetAutoaimVector( float flScale ); + virtual Vector GetAutoaimVector( float flScale, float flMaxDist ); + virtual void GetAutoaimVector( autoaim_params_t ¶ms ); +#ifdef MAPBASE_VSCRIPT + Vector ScriptGetAutoaimVector( float flScale ) { return GetAutoaimVector( flScale ); } + Vector ScriptGetAutoaimVectorCustomMaxDist( float flScale, float flMaxDist ) { return GetAutoaimVector( flScale, flMaxDist ); } +#endif + + float GetAutoaimScore( const Vector &eyePosition, const Vector &viewDir, const Vector &vecTarget, CBaseEntity *pTarget, float fScale, CBaseCombatWeapon *pActiveWeapon ); + QAngle AutoaimDeflection( Vector &vecSrc, autoaim_params_t ¶ms ); + virtual bool ShouldAutoaim( void ); + void SetTargetInfo( Vector &vecSrc, float flDist ); + + void SetViewEntity( CBaseEntity *pEntity ); + CBaseEntity *GetViewEntity( void ) { return m_hViewEntity; } + + virtual void ForceClientDllUpdate( void ); // Forces all client .dll specific data to be resent to client. + + void DeathMessage( CBaseEntity *pKiller ); + + virtual void ProcessUsercmds( CUserCmd *cmds, int numcmds, int totalcmds, + int dropped_packets, bool paused ); + bool IsUserCmdDataValid( CUserCmd *pCmd ); + + void AvoidPhysicsProps( CUserCmd *pCmd ); + + // Run a user command. The default implementation calls ::PlayerRunCommand. In TF, this controls a vehicle if + // the player is in one. + virtual void PlayerRunCommand(CUserCmd *ucmd, IMoveHelper *moveHelper); + void RunNullCommand(); + CUserCmd * GetCurrentCommand( void ) { return m_pCurrentCommand; } + float GetTimeSinceLastUserCommand( void ) { return ( !IsConnected() || IsFakeClient() || IsBot() ) ? 0.f : gpGlobals->curtime - m_flLastUserCommandTime; } + + // Team Handling + virtual void ChangeTeam( int iTeamNum ) { ChangeTeam(iTeamNum,false, false); } + virtual void ChangeTeam( int iTeamNum, bool bAutoTeam, bool bSilent ); + + // say/sayteam allowed? + virtual bool CanHearAndReadChatFrom( CBasePlayer *pPlayer ) { return true; } + virtual bool CanSpeak( void ) { return true; } + + audioparams_t &GetAudioParams() { return m_Local.m_audio; } + + virtual void ModifyOrAppendPlayerCriteria( AI_CriteriaSet& set ); + + const QAngle& GetPunchAngle(); + void SetPunchAngle( const QAngle &punchAngle ); + + virtual void DoMuzzleFlash(); + + const char *GetLastKnownPlaceName( void ) const { return m_szLastPlaceName; } // return the last nav place name the player occupied + + virtual void CheckChatText( char *p, int bufsize ) {} + + virtual void CreateRagdollEntity( void ) { return; } + + virtual void HandleAnimEvent( animevent_t *pEvent ); + + virtual bool ShouldAnnounceAchievement( void ){ return true; } + +#if defined USES_ECON_ITEMS + // Wearables + virtual void EquipWearable( CEconWearable *pItem ); + virtual void RemoveWearable( CEconWearable *pItem ); + void PlayWearableAnimsForPlaybackEvent( wearableanimplayback_t iPlayback ); +#endif + +#ifdef MAPBASE + bool ShouldUseVisibilityCache( CBaseEntity *pEntity ); +#endif + +public: + // Player Physics Shadow + void SetupVPhysicsShadow( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity, CPhysCollide *pStandModel, const char *pStandHullName, CPhysCollide *pCrouchModel, const char *pCrouchHullName ); + IPhysicsPlayerController* GetPhysicsController() { return m_pPhysicsController; } + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + void VPhysicsUpdate( IPhysicsObject *pPhysics ); + virtual void VPhysicsShadowUpdate( IPhysicsObject *pPhysics ); + virtual bool IsFollowingPhysics( void ) { return false; } + bool IsRideablePhysics( IPhysicsObject *pPhysics ); + IPhysicsObject *GetGroundVPhysics(); + + virtual void Touch( CBaseEntity *pOther ); + void SetTouchedPhysics( bool bTouch ); + bool TouchedPhysics( void ); + Vector GetSmoothedVelocity( void ); + + virtual void RefreshCollisionBounds( void ); + virtual void InitVCollision( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity ); + virtual void VPhysicsDestroyObject(); + void SetVCollisionState( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity, int collisionState ); + void PostThinkVPhysics( void ); + virtual void UpdatePhysicsShadowToCurrentPosition(); + void UpdatePhysicsShadowToPosition( const Vector &vecAbsOrigin ); + void UpdateVPhysicsPosition( const Vector &position, const Vector &velocity, float secondsToArrival ); + + // Hint system + virtual CHintSystem *Hints( void ) { return NULL; } + bool ShouldShowHints( void ) { return Hints() ? Hints()->ShouldShowHints() : false; } + void SetShowHints( bool bShowHints ) { if (Hints()) Hints()->SetShowHints( bShowHints ); } + bool HintMessage( int hint, bool bForce = false ) { return Hints() ? Hints()->HintMessage( hint, bForce ) : false; } + void HintMessage( const char *pMessage ) { if (Hints()) Hints()->HintMessage( pMessage ); } + void StartHintTimer( int iHintID ) { if (Hints()) Hints()->StartHintTimer( iHintID ); } + void StopHintTimer( int iHintID ) { if (Hints()) Hints()->StopHintTimer( iHintID ); } + void RemoveHintTimer( int iHintID ) { if (Hints()) Hints()->RemoveHintTimer( iHintID ); } + + // Accessor methods + int FragCount() const { return m_iFrags; } + int DeathCount() const { return m_iDeaths;} + bool IsConnected() const { return m_iConnected != PlayerDisconnected; } + bool IsDisconnecting() const { return m_iConnected == PlayerDisconnecting; } + bool IsSuitEquipped() const { return m_Local.m_bWearingSuit; } + virtual int ArmorValue() const { return m_ArmorValue; } + bool HUDNeedsRestart() const { return m_fInitHUD; } + float MaxSpeed() const { return m_flMaxspeed; } + Activity GetActivity( ) const { return m_Activity; } + inline void SetActivity( Activity eActivity ) { m_Activity = eActivity; } + bool IsPlayerLockedInPlace() const { return m_iPlayerLocked != 0; } + bool IsObserver() const { return (m_afPhysicsFlags & PFLAG_OBSERVER) != 0; } + bool IsOnTarget() const { return m_fOnTarget; } + float MuzzleFlashTime() const { return m_flFlashTime; } + float PlayerDrownTime() const { return m_AirFinished; } + + int GetObserverMode() const { return m_iObserverMode; } + CBaseEntity *GetObserverTarget() const { return m_hObserverTarget; } + + // Round gamerules + virtual bool IsReadyToPlay( void ) { return true; } + virtual bool IsReadyToSpawn( void ) { return true; } + virtual bool ShouldGainInstantSpawn( void ) { return false; } + virtual void ResetPerRoundStats( void ) { return; } + void AllowInstantSpawn( void ) { m_bAllowInstantSpawn = true; } + + virtual void ResetScores( void ) { ResetFragCount(); ResetDeathCount(); } + void ResetFragCount(); + void IncrementFragCount( int nCount ); + + void ResetDeathCount(); + void IncrementDeathCount( int nCount ); + + void SetArmorValue( int value ); + void IncrementArmorValue( int nCount, int nMaxValue = -1 ); + + void SetConnected( PlayerConnectedState iConnected ) { m_iConnected = iConnected; } + virtual void EquipSuit( bool bPlayEffects = true ); + virtual void RemoveSuit( void ); + void SetMaxSpeed( float flMaxSpeed ) { m_flMaxspeed = flMaxSpeed; } + + void NotifyNearbyRadiationSource( float flRange ); + + void SetAnimationExtension( const char *pExtension ); + + void SetAdditionalPVSOrigin( const Vector &vecOrigin ); + void SetCameraPVSOrigin( const Vector &vecOrigin ); + void SetMuzzleFlashTime( float flTime ); + void SetUseEntity( CBaseEntity *pUseEntity ); + CBaseEntity *GetUseEntity(); + + virtual float GetPlayerMaxSpeed(); + + // Used to set private physics flags PFLAG_* + void SetPhysicsFlag( int nFlag, bool bSet ); + + void AllowImmediateDecalPainting(); + + // Suicide... + virtual void CommitSuicide( bool bExplode = false, bool bForce = false ); + virtual void CommitSuicide( const Vector &vecForce, bool bExplode = false, bool bForce = false ); + + // For debugging... + void ForceOrigin( const Vector &vecOrigin ); + + // Bot accessors... + void SetTimeBase( float flTimeBase ); + float GetTimeBase() const; + void SetLastUserCommand( const CUserCmd &cmd ); + const CUserCmd *GetLastUserCommand( void ); + + virtual bool IsBot() const; // IMPORTANT: This returns true for ANY type of bot. If your game uses different, incompatible types of bots check your specific bot type before casting + virtual bool IsBotOfType( int botType ) const; // return true if this player is a bot of the specific type (zero is invalid) + virtual int GetBotType( void ) const; // return a unique int representing the type of bot instance this is + + bool IsPredictingWeapons( void ) const; + int CurrentCommandNumber() const; + const CUserCmd *GetCurrentUserCommand() const; + + int GetFOV( void ); // Get the current FOV value + int GetDefaultFOV( void ) const; // Default FOV if not specified otherwise + int GetFOVForNetworking( void ); // Get the current FOV used for network computations + bool SetFOV( CBaseEntity *pRequester, int FOV, float zoomRate = 0.0f, int iZoomStart = 0 ); // Alters the base FOV of the player (must have a valid requester) +#ifdef MAPBASE_VSCRIPT + void ScriptSetFOV(int iFOV, float flSpeed); // Overrides player FOV, ignores zoom owner + HSCRIPT ScriptGetFOVOwner() { return ToHScript(m_hZoomOwner); } +#endif + void SetDefaultFOV( int FOV ); // Sets the base FOV if nothing else is affecting it by zooming + CBaseEntity *GetFOVOwner( void ) { return m_hZoomOwner; } + float GetFOVDistanceAdjustFactor(); // shared between client and server + float GetFOVDistanceAdjustFactorForNetworking(); + + int GetImpulse( void ) const { return m_nImpulse; } + + // Movement constraints + void ActivateMovementConstraint( CBaseEntity *pEntity, const Vector &vecCenter, float flRadius, float flConstraintWidth, float flSpeedFactor ); + void DeactivateMovementConstraint( ); + + // talk control + void NotePlayerTalked() { m_fLastPlayerTalkTime = gpGlobals->curtime; } + float LastTimePlayerTalked() { return m_fLastPlayerTalkTime; } + + void DisableButtons( int nButtons ); + void EnableButtons( int nButtons ); + void ForceButtons( int nButtons ); + void UnforceButtons( int nButtons ); + + //--------------------------------- + // Inputs + //--------------------------------- + void InputSetHealth( inputdata_t &inputdata ); + void InputSetHUDVisibility( inputdata_t &inputdata ); + void InputHandleMapEvent( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetSuppressAttacks( inputdata_t &inputdata ); +#endif + + surfacedata_t *GetSurfaceData( void ) { return m_pSurfaceData; } + void SetLadderNormal( Vector vecLadderNormal ) { m_vecLadderNormal = vecLadderNormal; } + + // Here so that derived classes can use the expresser + virtual CAI_Expresser *GetExpresser() { return NULL; }; + +#if !defined(NO_STEAM) + //---------------------------- + // Steam handling + bool GetSteamID( CSteamID *pID ); + uint64 GetSteamIDAsUInt64( void ); +#endif + + float GetRemainingMovementTimeForUserCmdProcessing() const { return m_flMovementTimeForUserCmdProcessingRemaining; } + float ConsumeMovementTimeForUserCmdProcessing( float flTimeNeeded ) + { + if ( m_flMovementTimeForUserCmdProcessingRemaining <= 0.0f ) + { + return 0.0f; + } + else if ( flTimeNeeded > m_flMovementTimeForUserCmdProcessingRemaining + FLT_EPSILON ) + { + float flResult = m_flMovementTimeForUserCmdProcessingRemaining; + m_flMovementTimeForUserCmdProcessingRemaining = 0.0f; + return flResult; + } + else + { + m_flMovementTimeForUserCmdProcessingRemaining -= flTimeNeeded; + if ( m_flMovementTimeForUserCmdProcessingRemaining < 0.0f ) + m_flMovementTimeForUserCmdProcessingRemaining = 0.0f; + return flTimeNeeded; + } + } + +private: + // How much of a movement time buffer can we process from this user? + float m_flMovementTimeForUserCmdProcessingRemaining; + + // For queueing up CUserCmds and running them from PhysicsSimulate + int GetCommandContextCount( void ) const; + CCommandContext *GetCommandContext( int index ); + CCommandContext *AllocCommandContext( void ); + void RemoveCommandContext( int index ); + void RemoveAllCommandContexts( void ); + CCommandContext *RemoveAllCommandContextsExceptNewest( void ); + void ReplaceContextCommands( CCommandContext *ctx, CUserCmd *pCommands, int nCommands ); + + int DetermineSimulationTicks( void ); + void AdjustPlayerTimeBase( int simulation_ticks ); + +public: + + + + // Used by gamemovement to check if the entity is stuck. + int m_StuckLast; + + // FIXME: Make these protected or private! + + // This player's data that should only be replicated to + // the player and not to other players. + CNetworkVarEmbedded( CPlayerLocalData, m_Local ); + +#if defined USES_ECON_ITEMS + CNetworkVarEmbedded( CAttributeList, m_AttributeList ); +#endif + + void InitFogController( void ); + void InputSetFogController( inputdata_t &inputdata ); + + CNetworkHandle( CPostProcessController, m_hPostProcessCtrl ); // active postprocessing controller + void InitPostProcessController( void ); + void InputSetPostProcessController( inputdata_t& inputdata ); + + // Used by env_soundscape_triggerable to manage when the player is touching multiple + // soundscape triggers simultaneously. + // The one at the HEAD of the list is always the current soundscape for the player. + CUtlVector m_hTriggerSoundscapeList; + + // Player data that's sometimes needed by the engine + CNetworkVarEmbedded( CPlayerState, pl ); + + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_fFlags ); + + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_vecViewOffset ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_flFriction ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_iAmmo ); + + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_hGroundEntity ); + + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_lifeState ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_iHealth ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_vecBaseVelocity ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_nNextThinkTick ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_vecVelocity ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_nWaterLevel ); + + int m_nButtons; + int m_afButtonPressed; + int m_afButtonReleased; + int m_afButtonLast; + int m_afButtonDisabled; // A mask of input flags that are cleared automatically + int m_afButtonForced; // These are forced onto the player's inputs + + CNetworkVar( bool, m_fOnTarget ); //Is the crosshair on a target? + + char m_szAnimExtension[32]; + + int m_nUpdateRate; // user snapshot rate cl_updaterate + float m_fLerpTime; // users cl_interp + bool m_bLagCompensation; // user wants lag compenstation + bool m_bPredictWeapons; // user has client side predicted weapons + + float GetDeathTime( void ) { return m_flDeathTime; } + + void ClearZoomOwner( void ); + + void SetPreviouslyPredictedOrigin( const Vector &vecAbsOrigin ); + const Vector &GetPreviouslyPredictedOrigin() const; + float GetFOVTime( void ){ return m_flFOVTime; } + + void AdjustDrownDmg( int nAmount ); + +#if defined USES_ECON_ITEMS + CEconWearable *GetWearable( int i ) { return m_hMyWearables[i]; } + int GetNumWearables( void ) { return m_hMyWearables.Count(); } +#endif + +#ifdef MAPBASE + bool m_bInTriggerFall; +#endif + +private: + + Activity m_Activity; + +protected: + + void CalcPlayerView( Vector& eyeOrigin, QAngle& eyeAngles, float& fov ); + void CalcVehicleView( IServerVehicle *pVehicle, Vector& eyeOrigin, QAngle& eyeAngles, + float& zNear, float& zFar, float& fov ); + void CalcObserverView( Vector& eyeOrigin, QAngle& eyeAngles, float& fov ); + void CalcViewModelView( const Vector& eyeOrigin, const QAngle& eyeAngles); + + virtual void Internal_HandleMapEvent( inputdata_t &inputdata ){} + + // FIXME: Make these private! (tf_player uses them) + + // Secondary point to derive PVS from when zoomed in with binoculars/sniper rifle. The PVS is + // a merge of the standing origin and this additional origin + Vector m_vecAdditionalPVSOrigin; + // Extra PVS origin if we are using a camera object + Vector m_vecCameraPVSOrigin; + + CNetworkHandle( CBaseEntity, m_hUseEntity ); // the player is currently controlling this entity because of +USE latched, NULL if no entity + + int m_iTrain; // Train control position + + float m_iRespawnFrames; // used in PlayerDeathThink() to make sure players can always respawn + unsigned int m_afPhysicsFlags; // physics flags - set when 'normal' physics should be revisited or overriden + + // Vehicles + CNetworkHandle( CBaseEntity, m_hVehicle ); + + int m_iVehicleAnalogBias; + + void UpdateButtonState( int nUserCmdButtonMask ); + + bool m_bPauseBonusProgress; + CNetworkVar( int, m_iBonusProgress ); + CNetworkVar( int, m_iBonusChallenge ); + + int m_lastDamageAmount; // Last damage taken + + Vector m_DmgOrigin; + float m_DmgTake; + float m_DmgSave; + int m_bitsDamageType; // what types of damage has player taken + int m_bitsHUDDamage; // Damage bits for the current fame. These get sent to the hud via gmsgDamage + + CNetworkVar( float, m_flDeathTime ); // the time at which the player died (used in PlayerDeathThink()) + float m_flDeathAnimTime; // the time at which the player finished their death anim (used in PlayerDeathThink() and ShouldTransmit()) + + CNetworkVar( int, m_iObserverMode ); // if in spectator mode != 0 + CNetworkVar( int, m_iFOV ); // field of view + CNetworkVar( int, m_iDefaultFOV ); // default field of view + CNetworkVar( int, m_iFOVStart ); // What our FOV started at + CNetworkVar( float, m_flFOVTime ); // Time our FOV change started + + int m_iObserverLastMode; // last used observer mode + CNetworkHandle( CBaseEntity, m_hObserverTarget ); // entity handle to m_iObserverTarget + bool m_bForcedObserverMode; // true, player was forced by invalid targets to switch mode + + CNetworkHandle( CBaseEntity, m_hZoomOwner ); //This is a pointer to the entity currently controlling the player's zoom + //Only this entity can change the zoom state once it has ownership + + float m_tbdPrev; // Time-based damage timer + int m_idrowndmg; // track drowning damage taken + int m_idrownrestored; // track drowning damage restored + int m_nPoisonDmg; // track recoverable poison damage taken + int m_nPoisonRestored; // track poison damage restored + // NOTE: bits damage type appears to only be used for time-based damage + BYTE m_rgbTimeBasedDamage[CDMG_TIMEBASED]; + + // Player Physics Shadow + int m_vphysicsCollisionState; + + virtual int SpawnArmorValue( void ) const { return 0; } + + float m_fNextSuicideTime; // the time after which the player can next use the suicide command + int m_iSuicideCustomKillFlags; + + // Replay mode + float m_fDelay; // replay delay in seconds + float m_fReplayEnd; // time to stop replay mode + int m_iReplayEntity; // follow this entity in replay + +private: + void HandleFuncTrain(); + +// DATA +private: + CUtlVector< CCommandContext > m_CommandContext; + // Player Physics Shadow + +protected: //used to be private, but need access for portal mod (Dave Kircher) + IPhysicsPlayerController *m_pPhysicsController; + IPhysicsObject *m_pShadowStand; + IPhysicsObject *m_pShadowCrouch; + Vector m_oldOrigin; + Vector m_vecSmoothedVelocity; + bool m_touchedPhysObject; + bool m_bPhysicsWasFrozen; + +private: + + int m_iPlayerSound;// the index of the sound list slot reserved for this player + int m_iTargetVolume;// ideal sound volume. + + int m_rgItems[MAX_ITEMS]; + + // these are time-sensitive things that we keep track of + float m_flSwimTime; // how long player has been underwater + float m_flDuckTime; // how long we've been ducking + float m_flDuckJumpTime; + + float m_flSuitUpdate; // when to play next suit update + int m_rgSuitPlayList[CSUITPLAYLIST];// next sentencenum to play for suit update + int m_iSuitPlayNext; // next sentence slot for queue storage; + int m_rgiSuitNoRepeat[CSUITNOREPEAT]; // suit sentence no repeat list + float m_rgflSuitNoRepeatTime[CSUITNOREPEAT]; // how long to wait before allowing repeat + + float m_flgeigerRange; // range to nearest radiation source + float m_flgeigerDelay; // delay per update of range msg to client + int m_igeigerRangePrev; + + bool m_fInitHUD; // True when deferred HUD restart msg needs to be sent + bool m_fGameHUDInitialized; + bool m_fWeapon; // Set this to FALSE to force a reset of the current weapon HUD info + + int m_iUpdateTime; // stores the number of frame ticks before sending HUD update messages + int m_iClientBattery; // the Battery currently known by the client. If this changes, send a new + + // Autoaim data + QAngle m_vecAutoAim; + int m_lastx, m_lasty; // These are the previous update's crosshair angles, DON"T SAVE/RESTORE + + int m_iFrags; + int m_iDeaths; + + float m_flNextDecalTime;// next time this player can spray a decal + + // Team Handling + // char m_szTeamName[TEAM_NAME_LENGTH]; + + // Multiplayer handling + PlayerConnectedState m_iConnected; + + // from edict_t + // CBasePlayer doesn't send this but CCSPlayer does. + CNetworkVarForDerived( int, m_ArmorValue ); + float m_AirFinished; + float m_PainFinished; + + // player locking + int m_iPlayerLocked; + +protected: + // the player's personal view model + typedef CHandle CBaseViewModelHandle; + CNetworkArray( CBaseViewModelHandle, m_hViewModel, MAX_VIEWMODELS ); + + // Last received usercmd (in case we drop a lot of packets ) + CUserCmd m_LastCmd; + CUserCmd *m_pCurrentCommand; + + float m_flStepSoundTime; // time to check for next footstep sound + + bool m_bAllowInstantSpawn; + +#if defined USES_ECON_ITEMS + // Wearables + CUtlVector > m_hMyWearables; +#endif + +private: + +// Replicated to all clients + CNetworkVar( float, m_flMaxspeed ); + +// Not transmitted + float m_flWaterJumpTime; // used to be called teleport_time + Vector m_vecWaterJumpVel; + int m_nImpulse; + float m_flSwimSoundTime; + Vector m_vecLadderNormal; + + float m_flFlashTime; + int m_nDrownDmgRate; // Drowning damage in points per second without air. + + int m_nNumCrouches; // Number of times we've crouched (for hinting) + bool m_bDuckToggled; // If true, the player is crouching via a toggle + +public: + bool GetToggledDuckState( void ) { return m_bDuckToggled; } + void ToggleDuck( void ); + float GetStickDist( void ); + + float m_flForwardMove; + float m_flSideMove; + int m_nNumCrateHudHints; + +#ifdef MAPBASE + CNetworkVar( bool, m_bDrawPlayerModelExternally ); +#endif + +private: + + // Used in test code to teleport the player to random locations in the map. + Vector m_vForcedOrigin; + bool m_bForceOrigin; + + // Clients try to run on their own realtime clock, this is this client's clock + CNetworkVar( int, m_nTickBase ); + + bool m_bGamePaused; + float m_fLastPlayerTalkTime; + + CNetworkVar( CBaseCombatWeaponHandle, m_hLastWeapon ); + +#if !defined( NO_ENTITY_PREDICTION ) + CUtlVector< CHandle< CBaseEntity > > m_SimulatedByThisPlayer; +#endif + + float m_flOldPlayerZ; + float m_flOldPlayerViewOffsetZ; + + bool m_bPlayerUnderwater; + + EHANDLE m_hViewEntity; + + // Movement constraints + CNetworkHandle( CBaseEntity, m_hConstraintEntity ); + CNetworkVector( m_vecConstraintCenter ); + CNetworkVar( float, m_flConstraintRadius ); + CNetworkVar( float, m_flConstraintWidth ); + CNetworkVar( float, m_flConstraintSpeedFactor ); + + friend class CPlayerMove; + friend class CPlayerClass; + + // Player name + char m_szNetname[MAX_PLAYER_NAME_LENGTH]; + +protected: + // HACK FOR TF2 Prediction + friend class CTFGameMovementRecon; + friend class CGameMovement; + friend class CTFGameMovement; + friend class CHL1GameMovement; + friend class CCSGameMovement; + friend class CHL2GameMovement; + friend class CDODGameMovement; + friend class CPortalGameMovement; + + // Accessors for gamemovement + bool IsDucked( void ) const { return m_Local.m_bDucked; } + bool IsDucking( void ) const { return m_Local.m_bDucking; } + float GetStepSize( void ) const { return m_Local.m_flStepSize; } + + CNetworkVar( float, m_flLaggedMovementValue ); + + // These are generated while running usercmds, then given to UpdateVPhysicsPosition after running all queued commands. + Vector m_vNewVPhysicsPosition; + Vector m_vNewVPhysicsVelocity; + + Vector m_vecVehicleViewOrigin; // Used to store the calculated view of the player while riding in a vehicle + QAngle m_vecVehicleViewAngles; // Vehicle angles + float m_flVehicleViewFOV; // FOV of the vehicle driver + int m_nVehicleViewSavedFrame; // Used to mark which frame was the last one the view was calculated for + + Vector m_vecPreviouslyPredictedOrigin; // Used to determine if non-gamemovement game code has teleported, or tweaked the player's origin + int m_nBodyPitchPoseParam; + + CNetworkString( m_szLastPlaceName, MAX_PLACE_NAME_LENGTH ); + + char m_szNetworkIDString[MAX_NETWORKID_LENGTH]; + CPlayerInfo m_PlayerInfo; + + // Texture names and surface data, used by CGameMovement + int m_surfaceProps; + surfacedata_t* m_pSurfaceData; + float m_surfaceFriction; + char m_chTextureType; + char m_chPreviousTextureType; // Separate from m_chTextureType. This is cleared if the player's not on the ground. + + bool m_bSinglePlayerGameEnding; + +public: + + float GetLaggedMovementValue( void ){ return m_flLaggedMovementValue; } + void SetLaggedMovementValue( float flValue ) { m_flLaggedMovementValue = flValue; } + + inline bool IsAutoKickDisabled( void ) const; + inline void DisableAutoKick( bool disabled ); + + void DumpPerfToRecipient( CBasePlayer *pRecipient, int nMaxRecords ); + // NVNT returns true if user has a haptic device + virtual bool HasHaptics(){return m_bhasHaptics;} + // NVNT sets weather a user should receive haptic device messages. + virtual void SetHaptics(bool has) { m_bhasHaptics = has;} +private: + // NVNT member variable holding if this user is using a haptic device. + bool m_bhasHaptics; + + bool m_autoKickDisabled; + + struct StepSoundCache_t + { + StepSoundCache_t() : m_usSoundNameIndex( 0 ) {} + CSoundParameters m_SoundParameters; + unsigned short m_usSoundNameIndex; + }; + // One for left and one for right side of step + StepSoundCache_t m_StepSoundCache[ 2 ]; + + CUtlLinkedList< CPlayerSimInfo > m_vecPlayerSimInfo; + CUtlLinkedList< CPlayerCmdInfo > m_vecPlayerCmdInfo; + + IntervalTimer m_weaponFiredTimer; + + // Store the last time we successfully processed a usercommand + float m_flLastUserCommandTime; + +public: + virtual unsigned int PlayerSolidMask( bool brushOnly = false ) const; // returns the solid mask for the given player, so bots can have a more-restrictive set + +private: + // + //Tony; new tonemap controller changes, specifically for multiplayer. + // + void ClearTonemapParams(); //Tony; we need to clear our tonemap params every time we spawn to -1, if we trigger an input, the values will be set again. +public: + void InputSetTonemapScale( inputdata_t &inputdata ); //Set m_Local. +// void InputBlendTonemapScale( inputdata_t &inputdata ); //TODO; this should be calculated on the client, if we use it; perhaps an entity message would suffice? .. hmm.. + void InputSetTonemapRate( inputdata_t &inputdata ); + void InputSetAutoExposureMin( inputdata_t &inputdata ); + void InputSetAutoExposureMax( inputdata_t &inputdata ); + void InputSetBloomScale( inputdata_t &inputdata ); + + //Tony; restore defaults (set min/max to -1.0 so nothing gets overridden) + void InputUseDefaultAutoExposure( inputdata_t &inputdata ); + void InputUseDefaultBloomScale( inputdata_t &inputdata ); + +}; + +typedef CHandle CBasePlayerHandle; + +EXTERN_SEND_TABLE(DT_BasePlayer) + + + +//----------------------------------------------------------------------------- +// Inline methods +//----------------------------------------------------------------------------- +inline bool CBasePlayer::IsBotOfType( int botType ) const +{ + // bot type of zero is invalid + return ( GetBotType() != 0 ) && ( GetBotType() == botType ); +} + +inline int CBasePlayer::GetBotType( void ) const +{ + return 0; +} + +inline bool CBasePlayer::IsAutoKickDisabled( void ) const +{ + return m_autoKickDisabled; +} + +inline void CBasePlayer::DisableAutoKick( bool disabled ) +{ + m_autoKickDisabled = disabled; +} + +inline void CBasePlayer::SetAdditionalPVSOrigin( const Vector &vecOrigin ) +{ + m_vecAdditionalPVSOrigin = vecOrigin; +} + +inline void CBasePlayer::SetCameraPVSOrigin( const Vector &vecOrigin ) +{ + m_vecCameraPVSOrigin = vecOrigin; +} + +inline void CBasePlayer::SetMuzzleFlashTime( float flTime ) +{ + m_flFlashTime = flTime; +} + +inline void CBasePlayer::SetUseEntity( CBaseEntity *pUseEntity ) +{ + m_hUseEntity = pUseEntity; +} + +inline CBaseEntity *CBasePlayer::GetUseEntity() +{ + return m_hUseEntity; +} + +// Bot accessors... +inline void CBasePlayer::SetTimeBase( float flTimeBase ) +{ + m_nTickBase = TIME_TO_TICKS( flTimeBase ); +} + +inline void CBasePlayer::SetLastUserCommand( const CUserCmd &cmd ) +{ + m_LastCmd = cmd; +} + +inline CUserCmd const *CBasePlayer::GetLastUserCommand( void ) +{ + return &m_LastCmd; +} + +inline bool CBasePlayer::IsPredictingWeapons( void ) const +{ + return m_bPredictWeapons; +} + +inline int CBasePlayer::CurrentCommandNumber() const +{ + Assert( m_pCurrentCommand ); + return m_pCurrentCommand->command_number; +} + +inline const CUserCmd *CBasePlayer::GetCurrentUserCommand() const +{ + Assert( m_pCurrentCommand ); + return m_pCurrentCommand; +} + +inline IServerVehicle *CBasePlayer::GetVehicle() +{ + CBaseEntity *pVehicleEnt = m_hVehicle.Get(); + return pVehicleEnt ? pVehicleEnt->GetServerVehicle() : NULL; +} + +inline CBaseEntity *CBasePlayer::GetVehicleEntity() +{ + return m_hVehicle.Get(); +} + +inline bool CBasePlayer::IsInAVehicle( void ) const +{ + return ( NULL != m_hVehicle.Get() ) ? true : false; +} + +inline void CBasePlayer::SetTouchedPhysics( bool bTouch ) +{ + m_touchedPhysObject = bTouch; +} + +inline bool CBasePlayer::TouchedPhysics( void ) +{ + return m_touchedPhysObject; +} + +inline void CBasePlayer::OnMyWeaponFired( CBaseCombatWeapon *weapon ) +{ + m_weaponFiredTimer.Start(); +} + +inline float CBasePlayer::GetTimeSinceWeaponFired( void ) const +{ + return m_weaponFiredTimer.GetElapsedTime(); +} + +inline bool CBasePlayer::IsFiringWeapon( void ) const +{ + return m_weaponFiredTimer.HasStarted() && m_weaponFiredTimer.IsLessThen( 1.0f ); +} + + + +//----------------------------------------------------------------------------- +// Converts an entity to a player +//----------------------------------------------------------------------------- +inline CBasePlayer *ToBasePlayer( CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() ) + return NULL; +#if _DEBUG + return dynamic_cast( pEntity ); +#else + return static_cast( pEntity ); +#endif +} + +inline const CBasePlayer *ToBasePlayer( const CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() ) + return NULL; +#if _DEBUG + return dynamic_cast( pEntity ); +#else + return static_cast( pEntity ); +#endif +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * DEPRECATED: Use CollectPlayers() instead. + * Iterate over all active players in the game, invoking functor on each. + * If functor returns false, stop iteration and return false. + */ +template < typename Functor > +bool ForEachPlayer( Functor &func ) +{ + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (FNullEnt( player->edict() )) + continue; + + if (!player->IsPlayer()) + continue; + + if( !player->IsConnected() ) + continue; + + if (func( player ) == false) + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------------------------- +/** + * The interface for an iterative player functor + */ +class IPlayerFunctor +{ +public: + virtual void OnBeginIteration( void ) { } // invoked once before iteration begins + + virtual bool operator() ( CBasePlayer *player ) = 0; + + virtual void OnEndIteration( bool allElementsIterated ) { } // invoked once after iteration is complete whether successful or not +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * DEPRECATED: Use CollectPlayers() instead. + * Specialization of ForEachPlayer template for IPlayerFunctors + */ +template <> +inline bool ForEachPlayer( IPlayerFunctor &func ) +{ + func.OnBeginIteration(); + + bool isComplete = true; + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (FNullEnt( player->edict() )) + continue; + + if (!player->IsPlayer()) + continue; + + if( !player->IsConnected() ) + continue; + + if (func( player ) == false) + { + isComplete = false; + break; + } + } + + func.OnEndIteration( isComplete ); + + return isComplete; +} + +//-------------------------------------------------------------------------------------------------------------- +// +// Collect all valid, connected players into given vector. +// Returns number of players collected. +// +#define COLLECT_ONLY_LIVING_PLAYERS true +#define APPEND_PLAYERS true +template < typename T > +int CollectPlayers( CUtlVector< T * > *playerVector, int team = TEAM_ANY, bool isAlive = false, bool shouldAppend = false ) +{ + if ( !shouldAppend ) + { + playerVector->RemoveAll(); + } + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = UTIL_PlayerByIndex( i ); + + if ( player == NULL ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( !player->IsPlayer() ) + continue; + + if ( !player->IsConnected() ) + continue; + + if ( team != TEAM_ANY && player->GetTeamNumber() != team ) + continue; + + if ( isAlive && !player->IsAlive() ) + continue; + + playerVector->AddToTail( assert_cast< T * >( player ) ); + } + + return playerVector->Count(); +} + +template < typename T > +int CollectHumanPlayers( CUtlVector< T * > *playerVector, int team = TEAM_ANY, bool isAlive = false, bool shouldAppend = false ) +{ + if ( !shouldAppend ) + { + playerVector->RemoveAll(); + } + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = UTIL_PlayerByIndex( i ); + + if ( player == NULL ) + continue; + + if ( FNullEnt( player->edict() ) ) + continue; + + if ( !player->IsPlayer() ) + continue; + + if ( player->IsBot() ) + continue; + + if ( !player->IsConnected() ) + continue; + + if ( team != TEAM_ANY && player->GetTeamNumber() != team ) + continue; + + if ( isAlive && !player->IsAlive() ) + continue; + + playerVector->AddToTail( assert_cast< T * >( player ) ); + } + + return playerVector->Count(); +} + +enum +{ + VEHICLE_ANALOG_BIAS_NONE = 0, + VEHICLE_ANALOG_BIAS_FORWARD, + VEHICLE_ANALOG_BIAS_REVERSE, +}; + +#endif // PLAYER_H diff --git a/sp/src/game/server/player_command.cpp b/sp/src/game/server/player_command.cpp new file mode 100644 index 00000000..d5a6a1f0 --- /dev/null +++ b/sp/src/game/server/player_command.cpp @@ -0,0 +1,486 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "usercmd.h" +#include "igamemovement.h" +#include "mathlib/mathlib.h" +#include "client.h" +#include "player_command.h" +#include "movehelper_server.h" +#include "iservervehicle.h" +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef MAPBASE +// This turned out to be causing major issues with VPhysics collision. +// It's deactivated until a fix is found. +// See prediction.cpp as well. +//#define PLAYER_COMMAND_FIX 1 +#endif + +extern IGameMovement *g_pGameMovement; +extern CMoveData *g_pMoveData; // This is a global because it is subclassed by each game. +extern ConVar sv_noclipduringpause; + +ConVar sv_maxusrcmdprocessticks_warning( "sv_maxusrcmdprocessticks_warning", "-1", FCVAR_NONE, "Print a warning when user commands get dropped due to insufficient usrcmd ticks allocated, number of seconds to throttle, negative disabled" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPlayerMove::CPlayerMove( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: We're about to run this usercmd for the specified player. We can set up groupinfo and masking here, etc. +// This is the time to examine the usercmd for anything extra. This call happens even if think does not. +// Input : *player - +// *cmd - +//----------------------------------------------------------------------------- +void CPlayerMove::StartCommand( CBasePlayer *player, CUserCmd *cmd ) +{ + VPROF( "CPlayerMove::StartCommand" ); + +#if !defined( NO_ENTITY_PREDICTION ) + CPredictableId::ResetInstanceCounters(); +#endif + + player->m_pCurrentCommand = cmd; + CBaseEntity::SetPredictionRandomSeed( cmd ); + CBaseEntity::SetPredictionPlayer( player ); + +#if defined (HL2_DLL) + // pull out backchannel data and move this out + + // Let's not bother with IK Ground Contact Info in MP games -- the system needs to be re-worked, every client sends down the same info for each entity, so how would it determine which to use? + if ( 1 == gpGlobals->maxClients ) + { + int i; + for (i = 0; i < cmd->entitygroundcontact.Count(); i++) + { + int entindex = cmd->entitygroundcontact[i].entindex; + CBaseEntity *pEntity = CBaseEntity::Instance( engine->PEntityOfEntIndex( entindex) ); + if (pEntity) + { + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + if (pAnimating) + { + pAnimating->SetIKGroundContactInfo( cmd->entitygroundcontact[i].minheight, cmd->entitygroundcontact[i].maxheight ); + } + } + } + } + + +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: We've finished running a user's command +// Input : *player - +//----------------------------------------------------------------------------- +void CPlayerMove::FinishCommand( CBasePlayer *player ) +{ + VPROF( "CPlayerMove::FinishCommand" ); + + player->m_pCurrentCommand = NULL; + CBaseEntity::SetPredictionRandomSeed( NULL ); + CBaseEntity::SetPredictionPlayer( NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: Checks if the player is standing on a moving entity and adjusts velocity and +// basevelocity appropriately +// Input : *player - +// frametime - +//----------------------------------------------------------------------------- +void CPlayerMove::CheckMovingGround( CBasePlayer *player, double frametime ) +{ + VPROF( "CPlayerMove::CheckMovingGround()" ); + + CBaseEntity *groundentity; + + if ( player->GetFlags() & FL_ONGROUND ) + { + groundentity = player->GetGroundEntity(); + if ( groundentity && ( groundentity->GetFlags() & FL_CONVEYOR) ) + { + Vector vecNewVelocity; + groundentity->GetGroundVelocityToApply( vecNewVelocity ); + if ( player->GetFlags() & FL_BASEVELOCITY ) + { + vecNewVelocity += player->GetBaseVelocity(); + } + player->SetBaseVelocity( vecNewVelocity ); + player->AddFlag( FL_BASEVELOCITY ); + } + } + + if ( !( player->GetFlags() & FL_BASEVELOCITY ) ) + { + // Apply momentum (add in half of the previous frame of velocity first) + player->ApplyAbsVelocityImpulse( (1.0 + ( frametime * 0.5 )) * player->GetBaseVelocity() ); + player->SetBaseVelocity( vec3_origin ); + } + + player->RemoveFlag( FL_BASEVELOCITY ); +} + +//----------------------------------------------------------------------------- +// Purpose: Prepares for running movement +// Input : *player - +// *ucmd - +// *pHelper - +// *move - +// time - +//----------------------------------------------------------------------------- +void CPlayerMove::SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) +{ + VPROF( "CPlayerMove::SetupMove" ); + + // Allow sound, etc. to be created by movement code + move->m_bFirstRunOfFunctions = true; + move->m_bGameCodeMovedPlayer = false; + if ( player->GetPreviouslyPredictedOrigin() != player->GetAbsOrigin() ) + { + move->m_bGameCodeMovedPlayer = true; + } + + // Prepare the usercmd fields + move->m_nImpulseCommand = ucmd->impulse; + move->m_vecViewAngles = ucmd->viewangles; + + CBaseEntity *pMoveParent = player->GetMoveParent(); + if (!pMoveParent) + { + move->m_vecAbsViewAngles = move->m_vecViewAngles; + } + else + { + matrix3x4_t viewToParent, viewToWorld; + AngleMatrix( move->m_vecViewAngles, viewToParent ); + ConcatTransforms( pMoveParent->EntityToWorldTransform(), viewToParent, viewToWorld ); + MatrixAngles( viewToWorld, move->m_vecAbsViewAngles ); + } + + move->m_nButtons = ucmd->buttons; + + // Ingore buttons for movement if at controls + if ( player->GetFlags() & FL_ATCONTROLS ) + { + move->m_flForwardMove = 0; + move->m_flSideMove = 0; + move->m_flUpMove = 0; + } + else + { + move->m_flForwardMove = ucmd->forwardmove; + move->m_flSideMove = ucmd->sidemove; + move->m_flUpMove = ucmd->upmove; + } + + // Prepare remaining fields + move->m_flClientMaxSpeed = player->m_flMaxspeed; + move->m_nOldButtons = player->m_Local.m_nOldButtons; + move->m_vecAngles = player->pl.v_angle; + + move->m_vecVelocity = player->GetAbsVelocity(); + + move->m_nPlayerHandle = player; + + move->SetAbsOrigin( player->GetAbsOrigin() ); + + // Copy constraint information + if ( player->m_hConstraintEntity.Get() ) + move->m_vecConstraintCenter = player->m_hConstraintEntity.Get()->GetAbsOrigin(); + else + move->m_vecConstraintCenter = player->m_vecConstraintCenter; + move->m_flConstraintRadius = player->m_flConstraintRadius; + move->m_flConstraintWidth = player->m_flConstraintWidth; + move->m_flConstraintSpeedFactor = player->m_flConstraintSpeedFactor; +} + +//----------------------------------------------------------------------------- +// Purpose: Finishes running movement +// Input : *player - +// *move - +// *ucmd - +// time - +//----------------------------------------------------------------------------- +void CPlayerMove::FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ) +{ + VPROF( "CPlayerMove::FinishMove" ); + + // NOTE: Don't copy this. the movement code modifies its local copy but is not expecting to be authoritative + //player->m_flMaxspeed = move->m_flClientMaxSpeed; + player->SetAbsOrigin( move->GetAbsOrigin() ); + player->SetAbsVelocity( move->m_vecVelocity ); + player->SetPreviouslyPredictedOrigin( move->GetAbsOrigin() ); + + player->m_Local.m_nOldButtons = move->m_nButtons; + + // Convert final pitch to body pitch + float pitch = move->m_vecAngles[ PITCH ]; + if ( pitch > 180.0f ) + { + pitch -= 360.0f; + } + pitch = clamp( pitch, -90.f, 90.f ); + + move->m_vecAngles[ PITCH ] = pitch; + + player->SetBodyPitch( pitch ); + + player->SetLocalAngles( move->m_vecAngles ); + + // The class had better not have changed during the move!! + if ( player->m_hConstraintEntity ) + Assert( move->m_vecConstraintCenter == player->m_hConstraintEntity.Get()->GetAbsOrigin() ); + else + Assert( move->m_vecConstraintCenter == player->m_vecConstraintCenter ); + Assert( move->m_flConstraintRadius == player->m_flConstraintRadius ); + Assert( move->m_flConstraintWidth == player->m_flConstraintWidth ); + Assert( move->m_flConstraintSpeedFactor == player->m_flConstraintSpeedFactor ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called before player thinks +// Input : *player - +// thinktime - +//----------------------------------------------------------------------------- +void CPlayerMove::RunPreThink( CBasePlayer *player ) +{ + VPROF( "CPlayerMove::RunPreThink" ); + + // Run think functions on the player + VPROF_SCOPE_BEGIN( "player->PhysicsRunThink()" ); + if ( !player->PhysicsRunThink() ) + return; + VPROF_SCOPE_END(); + + VPROF_SCOPE_BEGIN( "g_pGameRules->PlayerThink( player )" ); + // Called every frame to let game rules do any specific think logic for the player + g_pGameRules->PlayerThink( player ); + VPROF_SCOPE_END(); + + VPROF_SCOPE_BEGIN( "player->PreThink()" ); + player->PreThink(); + VPROF_SCOPE_END(); +} + +//----------------------------------------------------------------------------- +// Purpose: Runs the PLAYER's thinking code if time. There is some play in the exact time the think +// function will be called, because it is called before any movement is done +// in a frame. Not used for pushmove objects, because they must be exact. +// Returns false if the entity removed itself. +// Input : *ent - +// frametime - +// clienttimebase - +// Output : void CPlayerMove::RunThink +//----------------------------------------------------------------------------- +void CPlayerMove::RunThink (CBasePlayer *player, double frametime ) +{ + VPROF( "CPlayerMove::RunThink" ); + int thinktick = player->GetNextThinkTick(); + + if ( thinktick <= 0 || thinktick > player->m_nTickBase ) + return; + + //gpGlobals->curtime = thinktime; + player->SetNextThink( TICK_NEVER_THINK ); + + // Think + player->Think(); +} + +//----------------------------------------------------------------------------- +// Purpose: Called after player movement +// Input : *player - +// thinktime - +// frametime - +//----------------------------------------------------------------------------- +void CPlayerMove::RunPostThink( CBasePlayer *player ) +{ + VPROF( "CPlayerMove::RunPostThink" ); + + // Run post-think + player->PostThink(); +} + +void CommentarySystem_PePlayerRunCommand( CBasePlayer *player, CUserCmd *ucmd ); + +//----------------------------------------------------------------------------- +// Purpose: Runs movement commands for the player +// Input : *player - +// *ucmd - +// *moveHelper - +// Output : void CPlayerMove::RunCommand +//----------------------------------------------------------------------------- +void CPlayerMove::RunCommand ( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *moveHelper ) +{ + const float playerCurTime = player->m_nTickBase * TICK_INTERVAL; + const float playerFrameTime = player->m_bGamePaused ? 0 : TICK_INTERVAL; + const float flTimeAllowedForProcessing = player->ConsumeMovementTimeForUserCmdProcessing( playerFrameTime ); + if ( !player->IsBot() && ( flTimeAllowedForProcessing < playerFrameTime ) ) + { + // Make sure that the activity in command is erased because player cheated or dropped too many packets + double dblWarningFrequencyThrottle = sv_maxusrcmdprocessticks_warning.GetFloat(); + if ( dblWarningFrequencyThrottle >= 0 ) + { + static double s_dblLastWarningTime = 0; + double dblTimeNow = Plat_FloatTime(); + if ( !s_dblLastWarningTime || ( dblTimeNow - s_dblLastWarningTime >= dblWarningFrequencyThrottle ) ) + { + s_dblLastWarningTime = dblTimeNow; + Warning( "sv_maxusrcmdprocessticks_warning at server tick %u: Ignored client %s usrcmd (%.6f < %.6f)!\n", gpGlobals->tickcount, player->GetPlayerName(), flTimeAllowedForProcessing, playerFrameTime ); + } + } + return; // Don't process this command + } + + StartCommand( player, ucmd ); + + // Set globals appropriately + gpGlobals->curtime = playerCurTime; + gpGlobals->frametime = playerFrameTime; + + // Prevent hacked clients from sending us invalid view angles to try to get leaf server code to crash + if ( !ucmd->viewangles.IsValid() || !IsEntityQAngleReasonable(ucmd->viewangles) ) + { + ucmd->viewangles = vec3_angle; + } + + // Add and subtract buttons we're forcing on the player + ucmd->buttons |= player->m_afButtonForced; + ucmd->buttons &= ~player->m_afButtonDisabled; + + if ( player->m_bGamePaused ) + { + // If no clipping and cheats enabled and noclipduring game enabled, then leave + // forwardmove and angles stuff in usercmd + if ( player->GetMoveType() == MOVETYPE_NOCLIP && + sv_cheats->GetBool() && + sv_noclipduringpause.GetBool() ) + { + gpGlobals->frametime = TICK_INTERVAL; + } + } + + /* + // TODO: We can check whether the player is sending more commands than elapsed real time + cmdtimeremaining -= ucmd->msec; + if ( cmdtimeremaining < 0 ) + { + // return; + } + */ + + g_pGameMovement->StartTrackPredictionErrors( player ); + + CommentarySystem_PePlayerRunCommand( player, ucmd ); + + // Do weapon selection + if ( ucmd->weaponselect != 0 ) + { + CBaseCombatWeapon *weapon = dynamic_cast< CBaseCombatWeapon * >( CBaseEntity::Instance( ucmd->weaponselect ) ); + if ( weapon ) + { + VPROF( "player->SelectItem()" ); + player->SelectItem( weapon->GetName(), ucmd->weaponsubtype ); + } + } + + IServerVehicle *pVehicle = player->GetVehicle(); + + // Latch in impulse. + if ( ucmd->impulse ) + { + // Discard impulse commands unless the vehicle allows them. + // FIXME: UsingStandardWeapons seems like a bad filter for this. The flashlight is an impulse command, for example. + if ( !pVehicle || player->UsingStandardWeaponsInVehicle() ) + { + player->m_nImpulse = ucmd->impulse; + } + } + + // Update player input button states + VPROF_SCOPE_BEGIN( "player->UpdateButtonState" ); + player->UpdateButtonState( ucmd->buttons ); + VPROF_SCOPE_END(); + + CheckMovingGround( player, TICK_INTERVAL ); + + g_pMoveData->m_vecOldAngles = player->pl.v_angle; + + // Copy from command to player unless game .dll has set angle using fixangle + if ( player->pl.fixangle == FIXANGLE_NONE ) + { + player->pl.v_angle = ucmd->viewangles; + } + else if( player->pl.fixangle == FIXANGLE_RELATIVE ) + { + player->pl.v_angle = ucmd->viewangles + player->pl.anglechange; + } + +#ifdef PLAYER_COMMAND_FIX + // Let server invoke any needed impact functions + VPROF_SCOPE_BEGIN( "moveHelper->ProcessImpacts" ); + moveHelper->ProcessImpacts(); + VPROF_SCOPE_END(); +#endif + + // Call standard client pre-think + RunPreThink( player ); + + // Call Think if one is set + RunThink( player, TICK_INTERVAL ); + + // Setup input. + SetupMove( player, ucmd, moveHelper, g_pMoveData ); + + // Let the game do the movement. + if ( !pVehicle ) + { + VPROF( "g_pGameMovement->ProcessMovement()" ); + Assert( g_pGameMovement ); + g_pGameMovement->ProcessMovement( player, g_pMoveData ); + } + else + { + VPROF( "pVehicle->ProcessMovement()" ); + pVehicle->ProcessMovement( player, g_pMoveData ); + } + +#ifdef PLAYER_COMMAND_FIX + RunPostThink( player ); +#endif + + // Copy output + FinishMove( player, ucmd, g_pMoveData ); + +#ifndef PLAYER_COMMAND_FIX + // Let server invoke any needed impact functions + VPROF_SCOPE_BEGIN( "moveHelper->ProcessImpacts" ); + moveHelper->ProcessImpacts(); + VPROF_SCOPE_END(); + + RunPostThink( player ); +#endif + + g_pGameMovement->FinishTrackPredictionErrors( player ); + + FinishCommand( player ); + + // Let time pass + if ( gpGlobals->frametime > 0 ) + { + player->m_nTickBase++; + } +} diff --git a/sp/src/game/server/player_command.h b/sp/src/game/server/player_command.h new file mode 100644 index 00000000..574fc8dd --- /dev/null +++ b/sp/src/game/server/player_command.h @@ -0,0 +1,64 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef PLAYER_COMMAND_H +#define PLAYER_COMMAND_H +#pragma once + + +#include "edict.h" +#include "usercmd.h" + + +class IMoveHelper; +class CMoveData; +class CBasePlayer; + +//----------------------------------------------------------------------------- +// Purpose: Server side player movement +//----------------------------------------------------------------------------- +class CPlayerMove +{ +public: + DECLARE_CLASS_NOBASE( CPlayerMove ); + + // Construction/destruction + CPlayerMove( void ); + virtual ~CPlayerMove( void ) {} + + // Public interfaces: + // Run a movement command from the player + void RunCommand ( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *moveHelper ); + +protected: + // Prepare for running movement + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ); + + // Finish movement + virtual void FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ); + + // Called before and after any movement processing + virtual void StartCommand( CBasePlayer *player, CUserCmd *cmd ); + void FinishCommand( CBasePlayer *player ); + + // Helper to determine if the user is standing on ground + void CheckMovingGround( CBasePlayer *player, double frametime ); + + // Helpers to call pre and post think for player, and to call think if a think function is set + void RunPreThink( CBasePlayer *player ); + void RunThink (CBasePlayer *ent, double frametime ); + void RunPostThink( CBasePlayer *player ); +}; + + +//----------------------------------------------------------------------------- +// Singleton accessor +//----------------------------------------------------------------------------- +CPlayerMove *PlayerMove(); + + +#endif // PLAYER_COMMAND_H diff --git a/sp/src/game/server/player_lagcompensation.cpp b/sp/src/game/server/player_lagcompensation.cpp new file mode 100644 index 00000000..08503652 --- /dev/null +++ b/sp/src/game/server/player_lagcompensation.cpp @@ -0,0 +1,833 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "usercmd.h" +#include "igamesystem.h" +#include "ilagcompensationmanager.h" +#include "inetchannelinfo.h" +#include "utllinkedlist.h" +#include "BaseAnimatingOverlay.h" +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define LC_NONE 0 +#define LC_ALIVE (1<<0) + +#define LC_ORIGIN_CHANGED (1<<8) +#define LC_ANGLES_CHANGED (1<<9) +#define LC_SIZE_CHANGED (1<<10) +#define LC_ANIMATION_CHANGED (1<<11) + +static ConVar sv_lagcompensation_teleport_dist( "sv_lagcompensation_teleport_dist", "64", FCVAR_DEVELOPMENTONLY | FCVAR_CHEAT, "How far a player got moved by game code before we can't lag compensate their position back" ); +#define LAG_COMPENSATION_EPS_SQR ( 0.1f * 0.1f ) +// Allow 4 units of error ( about 1 / 8 bbox width ) +#define LAG_COMPENSATION_ERROR_EPS_SQR ( 4.0f * 4.0f ) + +ConVar sv_unlag( "sv_unlag", "1", FCVAR_DEVELOPMENTONLY, "Enables player lag compensation" ); +ConVar sv_maxunlag( "sv_maxunlag", "1.0", FCVAR_DEVELOPMENTONLY, "Maximum lag compensation in seconds", true, 0.0f, true, 1.0f ); +ConVar sv_lagflushbonecache( "sv_lagflushbonecache", "1", FCVAR_DEVELOPMENTONLY, "Flushes entity bone cache on lag compensation" ); +ConVar sv_showlagcompensation( "sv_showlagcompensation", "0", FCVAR_CHEAT, "Show lag compensated hitboxes whenever a player is lag compensated." ); + +ConVar sv_unlag_fixstuck( "sv_unlag_fixstuck", "0", FCVAR_DEVELOPMENTONLY, "Disallow backtracking a player for lag compensation if it will cause them to become stuck" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +#define MAX_LAYER_RECORDS (CBaseAnimatingOverlay::MAX_OVERLAYS) + +struct LayerRecord +{ + int m_sequence; + float m_cycle; + float m_weight; + int m_order; + + LayerRecord() + { + m_sequence = 0; + m_cycle = 0; + m_weight = 0; + m_order = 0; + } + + LayerRecord( const LayerRecord& src ) + { + m_sequence = src.m_sequence; + m_cycle = src.m_cycle; + m_weight = src.m_weight; + m_order = src.m_order; + } +}; + +struct LagRecord +{ +public: + LagRecord() + { + m_fFlags = 0; + m_vecOrigin.Init(); + m_vecAngles.Init(); + m_vecMinsPreScaled.Init(); + m_vecMaxsPreScaled.Init(); + m_flSimulationTime = -1; + m_masterSequence = 0; + m_masterCycle = 0; + } + + LagRecord( const LagRecord& src ) + { + m_fFlags = src.m_fFlags; + m_vecOrigin = src.m_vecOrigin; + m_vecAngles = src.m_vecAngles; + m_vecMinsPreScaled = src.m_vecMinsPreScaled; + m_vecMaxsPreScaled = src.m_vecMaxsPreScaled; + m_flSimulationTime = src.m_flSimulationTime; + for( int layerIndex = 0; layerIndex < MAX_LAYER_RECORDS; ++layerIndex ) + { + m_layerRecords[layerIndex] = src.m_layerRecords[layerIndex]; + } + m_masterSequence = src.m_masterSequence; + m_masterCycle = src.m_masterCycle; + } + + // Did player die this frame + int m_fFlags; + + // Player position, orientation and bbox + Vector m_vecOrigin; + QAngle m_vecAngles; + Vector m_vecMinsPreScaled; + Vector m_vecMaxsPreScaled; + + float m_flSimulationTime; + + // Player animation details, so we can get the legs in the right spot. + LayerRecord m_layerRecords[MAX_LAYER_RECORDS]; + int m_masterSequence; + float m_masterCycle; +}; + + +// +// Try to take the player from his current origin to vWantedPos. +// If it can't get there, leave the player where he is. +// + +ConVar sv_unlag_debug( "sv_unlag_debug", "0", FCVAR_GAMEDLL | FCVAR_DEVELOPMENTONLY ); + +float g_flFractionScale = 0.95; +static void RestorePlayerTo( CBasePlayer *pPlayer, const Vector &vWantedPos ) +{ + // Try to move to the wanted position from our current position. + trace_t tr; + VPROF_BUDGET( "RestorePlayerTo", "CLagCompensationManager" ); + UTIL_TraceEntity( pPlayer, vWantedPos, vWantedPos, MASK_PLAYERSOLID, pPlayer, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + if ( tr.startsolid || tr.allsolid ) + { + if ( sv_unlag_debug.GetBool() ) + { + DevMsg( "RestorePlayerTo() could not restore player position for client \"%s\" ( %.1f %.1f %.1f )\n", + pPlayer->GetPlayerName(), vWantedPos.x, vWantedPos.y, vWantedPos.z ); + } + + UTIL_TraceEntity( pPlayer, pPlayer->GetLocalOrigin(), vWantedPos, MASK_PLAYERSOLID, pPlayer, COLLISION_GROUP_PLAYER_MOVEMENT, &tr ); + if ( tr.startsolid || tr.allsolid ) + { + // In this case, the guy got stuck back wherever we lag compensated him to. Nasty. + + if ( sv_unlag_debug.GetBool() ) + DevMsg( " restore failed entirely\n" ); + } + else + { + // We can get to a valid place, but not all the way back to where we were. + Vector vPos; + VectorLerp( pPlayer->GetLocalOrigin(), vWantedPos, tr.fraction * g_flFractionScale, vPos ); + UTIL_SetOrigin( pPlayer, vPos, true ); + + if ( sv_unlag_debug.GetBool() ) + DevMsg( " restore got most of the way\n" ); + } + } + else + { + // Cool, the player can go back to whence he came. + UTIL_SetOrigin( pPlayer, tr.endpos, true ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CLagCompensationManager : public CAutoGameSystemPerFrame, public ILagCompensationManager +{ +public: + CLagCompensationManager( char const *name ) : CAutoGameSystemPerFrame( name ), m_flTeleportDistanceSqr( 64 *64 ) + { + } + + // IServerSystem stuff + virtual void Shutdown() + { + ClearHistory(); + } + + virtual void LevelShutdownPostEntity() + { + ClearHistory(); + } + + // called after entities think + virtual void FrameUpdatePostEntityThink(); + + // ILagCompensationManager stuff + + // Called during player movement to set up/restore after lag compensation + void StartLagCompensation( CBasePlayer *player, CUserCmd *cmd ); + void FinishLagCompensation( CBasePlayer *player ); + +private: + void BacktrackPlayer( CBasePlayer *player, float flTargetTime ); + + void ClearHistory() + { + for ( int i=0; i m_PlayerTrack[ MAX_PLAYERS ]; + + // Scratchpad for determining what needs to be restored + CBitVec m_RestorePlayer; + bool m_bNeedToRestore; + + LagRecord m_RestoreData[ MAX_PLAYERS ]; // player data before we moved him back + LagRecord m_ChangeData[ MAX_PLAYERS ]; // player data where we moved him back + + CBasePlayer *m_pCurrentPlayer; // The player we are doing lag compensation for + + float m_flTeleportDistanceSqr; +}; + +static CLagCompensationManager g_LagCompensationManager( "CLagCompensationManager" ); +ILagCompensationManager *lagcompensation = &g_LagCompensationManager; + + +//----------------------------------------------------------------------------- +// Purpose: Called once per frame after all entities have had a chance to think +//----------------------------------------------------------------------------- +void CLagCompensationManager::FrameUpdatePostEntityThink() +{ + if ( (gpGlobals->maxClients <= 1) || !sv_unlag.GetBool() ) + { + ClearHistory(); + return; + } + + m_flTeleportDistanceSqr = sv_lagcompensation_teleport_dist.GetFloat() * sv_lagcompensation_teleport_dist.GetFloat(); + + VPROF_BUDGET( "FrameUpdatePostEntityThink", "CLagCompensationManager" ); + + // remove all records before that time: + int flDeadtime = gpGlobals->curtime - sv_maxunlag.GetFloat(); + + // Iterate all active players + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + CUtlFixedLinkedList< LagRecord > *track = &m_PlayerTrack[i-1]; + + if ( !pPlayer ) + { + if ( track->Count() > 0 ) + { + track->RemoveAll(); + } + + continue; + } + + Assert( track->Count() < 1000 ); // insanity check + + // remove tail records that are too old + int tailIndex = track->Tail(); + while ( track->IsValidIndex( tailIndex ) ) + { + LagRecord &tail = track->Element( tailIndex ); + + // if tail is within limits, stop + if ( tail.m_flSimulationTime >= flDeadtime ) + break; + + // remove tail, get new tail + track->Remove( tailIndex ); + tailIndex = track->Tail(); + } + + // check if head has same simulation time + if ( track->Count() > 0 ) + { + LagRecord &head = track->Element( track->Head() ); + + // check if player changed simulation time since last time updated + if ( head.m_flSimulationTime >= pPlayer->GetSimulationTime() ) + continue; // don't add new entry for same or older time + } + + // add new record to player track + LagRecord &record = track->Element( track->AddToHead() ); + + record.m_fFlags = 0; + if ( pPlayer->IsAlive() ) + { + record.m_fFlags |= LC_ALIVE; + } + + record.m_flSimulationTime = pPlayer->GetSimulationTime(); + record.m_vecAngles = pPlayer->GetLocalAngles(); + record.m_vecOrigin = pPlayer->GetLocalOrigin(); + record.m_vecMinsPreScaled = pPlayer->CollisionProp()->OBBMinsPreScaled(); + record.m_vecMaxsPreScaled = pPlayer->CollisionProp()->OBBMaxsPreScaled(); + + int layerCount = pPlayer->GetNumAnimOverlays(); + for( int layerIndex = 0; layerIndex < layerCount; ++layerIndex ) + { + CAnimationLayer *currentLayer = pPlayer->GetAnimOverlay(layerIndex); + if( currentLayer ) + { + record.m_layerRecords[layerIndex].m_cycle = currentLayer->m_flCycle; + record.m_layerRecords[layerIndex].m_order = currentLayer->m_nOrder; + record.m_layerRecords[layerIndex].m_sequence = currentLayer->m_nSequence; + record.m_layerRecords[layerIndex].m_weight = currentLayer->m_flWeight; + } + } + record.m_masterSequence = pPlayer->GetSequence(); + record.m_masterCycle = pPlayer->GetCycle(); + } + + //Clear the current player. + m_pCurrentPlayer = NULL; +} + +// Called during player movement to set up/restore after lag compensation +void CLagCompensationManager::StartLagCompensation( CBasePlayer *player, CUserCmd *cmd ) +{ + //DONT LAG COMP AGAIN THIS FRAME IF THERES ALREADY ONE IN PROGRESS + //IF YOU'RE HITTING THIS THEN IT MEANS THERES A CODE BUG + if ( m_pCurrentPlayer ) + { + Assert( m_pCurrentPlayer == NULL ); + Warning( "Trying to start a new lag compensation session while one is already active!\n" ); + return; + } + + // Assume no players need to be restored + m_RestorePlayer.ClearAll(); + m_bNeedToRestore = false; + + m_pCurrentPlayer = player; + + if ( !player->m_bLagCompensation // Player not wanting lag compensation + || (gpGlobals->maxClients <= 1) // no lag compensation in single player + || !sv_unlag.GetBool() // disabled by server admin + || player->IsBot() // not for bots + || player->IsObserver() // not for spectators + ) + return; + + // NOTE: Put this here so that it won't show up in single player mode. + VPROF_BUDGET( "StartLagCompensation", VPROF_BUDGETGROUP_OTHER_NETWORKING ); + Q_memset( m_RestoreData, 0, sizeof( m_RestoreData ) ); + Q_memset( m_ChangeData, 0, sizeof( m_ChangeData ) ); + + // Get true latency + + // correct is the amout of time we have to correct game time + float correct = 0.0f; + + INetChannelInfo *nci = engine->GetPlayerNetInfo( player->entindex() ); + + if ( nci ) + { + // add network latency + correct+= nci->GetLatency( FLOW_OUTGOING ); + } + + // calc number of view interpolation ticks - 1 + int lerpTicks = TIME_TO_TICKS( player->m_fLerpTime ); + + // add view interpolation latency see C_BaseEntity::GetInterpolationAmount() + correct += TICKS_TO_TIME( lerpTicks ); + + // check bouns [0,sv_maxunlag] + correct = clamp( correct, 0.0f, sv_maxunlag.GetFloat() ); + + // correct tick send by player + int targettick = cmd->tick_count - lerpTicks; + + // calc difference between tick send by player and our latency based tick + float deltaTime = correct - TICKS_TO_TIME(gpGlobals->tickcount - targettick); + + if ( fabs( deltaTime ) > 0.2f ) + { + // difference between cmd time and latency is too big > 200ms, use time correction based on latency + // DevMsg("StartLagCompensation: delta too big (%.3f)\n", deltaTime ); + targettick = gpGlobals->tickcount - TIME_TO_TICKS( correct ); + } + + // Iterate all active players + const CBitVec *pEntityTransmitBits = engine->GetEntityTransmitBitsForClient( player->entindex() - 1 ); + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( !pPlayer ) + { + continue; + } + + // Don't lag compensate yourself you loser... + if ( player == pPlayer ) + { + continue; + } + + // Custom checks for if things should lag compensate (based on things like what team the player is on). + if ( !player->WantsLagCompensationOnEntity( pPlayer, cmd, pEntityTransmitBits ) ) + continue; + + // Move other player back in time + BacktrackPlayer( pPlayer, TICKS_TO_TIME( targettick ) ); + } +} + +void CLagCompensationManager::BacktrackPlayer( CBasePlayer *pPlayer, float flTargetTime ) +{ + Vector org; + Vector minsPreScaled; + Vector maxsPreScaled; + QAngle ang; + + VPROF_BUDGET( "BacktrackPlayer", "CLagCompensationManager" ); + int pl_index = pPlayer->entindex() - 1; + + // get track history of this player + CUtlFixedLinkedList< LagRecord > *track = &m_PlayerTrack[ pl_index ]; + + // check if we have at leat one entry + if ( track->Count() <= 0 ) + return; + + int curr = track->Head(); + + LagRecord *prevRecord = NULL; + LagRecord *record = NULL; + + Vector prevOrg = pPlayer->GetLocalOrigin(); + + // Walk context looking for any invalidating event + while( track->IsValidIndex(curr) ) + { + // remember last record + prevRecord = record; + + // get next record + record = &track->Element( curr ); + + if ( !(record->m_fFlags & LC_ALIVE) ) + { + // player most be alive, lost track + return; + } + + Vector delta = record->m_vecOrigin - prevOrg; + if ( delta.Length2DSqr() > m_flTeleportDistanceSqr ) + { + // lost track, too much difference + return; + } + + // did we find a context smaller than target time ? + if ( record->m_flSimulationTime <= flTargetTime ) + break; // hurra, stop + + prevOrg = record->m_vecOrigin; + + // go one step back + curr = track->Next( curr ); + } + + Assert( record ); + + if ( !record ) + { + if ( sv_unlag_debug.GetBool() ) + { + DevMsg( "No valid positions in history for BacktrackPlayer client ( %s )\n", pPlayer->GetPlayerName() ); + } + + return; // that should never happen + } + + float frac = 0.0f; + if ( prevRecord && + (record->m_flSimulationTime < flTargetTime) && + (record->m_flSimulationTime < prevRecord->m_flSimulationTime) ) + { + // we didn't find the exact time but have a valid previous record + // so interpolate between these two records; + + Assert( prevRecord->m_flSimulationTime > record->m_flSimulationTime ); + Assert( flTargetTime < prevRecord->m_flSimulationTime ); + + // calc fraction between both records + frac = ( flTargetTime - record->m_flSimulationTime ) / + ( prevRecord->m_flSimulationTime - record->m_flSimulationTime ); + + Assert( frac > 0 && frac < 1 ); // should never extrapolate + + ang = Lerp( frac, record->m_vecAngles, prevRecord->m_vecAngles ); + org = Lerp( frac, record->m_vecOrigin, prevRecord->m_vecOrigin ); + minsPreScaled = Lerp( frac, record->m_vecMinsPreScaled, prevRecord->m_vecMinsPreScaled ); + maxsPreScaled = Lerp( frac, record->m_vecMaxsPreScaled, prevRecord->m_vecMaxsPreScaled ); + } + else + { + // we found the exact record or no other record to interpolate with + // just copy these values since they are the best we have + org = record->m_vecOrigin; + ang = record->m_vecAngles; + minsPreScaled = record->m_vecMinsPreScaled; + maxsPreScaled = record->m_vecMaxsPreScaled; + } + + // See if this is still a valid position for us to teleport to + if ( sv_unlag_fixstuck.GetBool() ) + { + // Try to move to the wanted position from our current position. + trace_t tr; + UTIL_TraceEntity( pPlayer, org, org, MASK_PLAYERSOLID, &tr ); + if ( tr.startsolid || tr.allsolid ) + { + if ( sv_unlag_debug.GetBool() ) + DevMsg( "WARNING: BackupPlayer trying to back player into a bad position - client %s\n", pPlayer->GetPlayerName() ); + + CBasePlayer *pHitPlayer = dynamic_cast( tr.m_pEnt ); + + // don't lag compensate the current player + if ( pHitPlayer && ( pHitPlayer != m_pCurrentPlayer ) ) + { + // If we haven't backtracked this player, do it now + // this deliberately ignores WantsLagCompensationOnEntity. + if ( !m_RestorePlayer.Get( pHitPlayer->entindex() - 1 ) ) + { + // prevent recursion - save a copy of m_RestorePlayer, + // pretend that this player is off-limits + int pl_index = pPlayer->entindex() - 1; + + // Temp turn this flag on + m_RestorePlayer.Set( pl_index ); + + BacktrackPlayer( pHitPlayer, flTargetTime ); + + // Remove the temp flag + m_RestorePlayer.Clear( pl_index ); + } + } + + // now trace us back as far as we can go + UTIL_TraceEntity( pPlayer, pPlayer->GetLocalOrigin(), org, MASK_PLAYERSOLID, &tr ); + + if ( tr.startsolid || tr.allsolid ) + { + // Our starting position is bogus + + if ( sv_unlag_debug.GetBool() ) + DevMsg( "Backtrack failed completely, bad starting position\n" ); + } + else + { + // We can get to a valid place, but not all the way to the target + Vector vPos; + VectorLerp( pPlayer->GetLocalOrigin(), org, tr.fraction * g_flFractionScale, vPos ); + + // This is as close as we're going to get + org = vPos; + + if ( sv_unlag_debug.GetBool() ) + DevMsg( "Backtrack got most of the way\n" ); + } + } + } + + // See if this represents a change for the player + int flags = 0; + LagRecord *restore = &m_RestoreData[ pl_index ]; + LagRecord *change = &m_ChangeData[ pl_index ]; + + QAngle angdiff = pPlayer->GetLocalAngles() - ang; + Vector orgdiff = pPlayer->GetLocalOrigin() - org; + + // Always remember the pristine simulation time in case we need to restore it. + restore->m_flSimulationTime = pPlayer->GetSimulationTime(); + + if ( angdiff.LengthSqr() > LAG_COMPENSATION_EPS_SQR ) + { + flags |= LC_ANGLES_CHANGED; + restore->m_vecAngles = pPlayer->GetLocalAngles(); + pPlayer->SetLocalAngles( ang ); + change->m_vecAngles = ang; + } + + // Use absolute equality here + if ( minsPreScaled != pPlayer->CollisionProp()->OBBMinsPreScaled() || maxsPreScaled != pPlayer->CollisionProp()->OBBMaxsPreScaled() ) + { + flags |= LC_SIZE_CHANGED; + + restore->m_vecMinsPreScaled = pPlayer->CollisionProp()->OBBMinsPreScaled(); + restore->m_vecMaxsPreScaled = pPlayer->CollisionProp()->OBBMaxsPreScaled(); + + pPlayer->SetSize( minsPreScaled, maxsPreScaled ); + + change->m_vecMinsPreScaled = minsPreScaled; + change->m_vecMaxsPreScaled = maxsPreScaled; + } + + // Note, do origin at end since it causes a relink into the k/d tree + if ( orgdiff.LengthSqr() > LAG_COMPENSATION_EPS_SQR ) + { + flags |= LC_ORIGIN_CHANGED; + restore->m_vecOrigin = pPlayer->GetLocalOrigin(); + pPlayer->SetLocalOrigin( org ); + change->m_vecOrigin = org; + } + + // Sorry for the loss of the optimization for the case of people + // standing still, but you breathe even on the server. + // This is quicker than actually comparing all bazillion floats. + flags |= LC_ANIMATION_CHANGED; + restore->m_masterSequence = pPlayer->GetSequence(); + restore->m_masterCycle = pPlayer->GetCycle(); + + bool interpolationAllowed = false; + if( prevRecord && (record->m_masterSequence == prevRecord->m_masterSequence) ) + { + // If the master state changes, all layers will be invalid too, so don't interp (ya know, interp barely ever happens anyway) + interpolationAllowed = true; + } + + //////////////////////// + // First do the master settings + bool interpolatedMasters = false; + if( frac > 0.0f && interpolationAllowed ) + { + interpolatedMasters = true; + pPlayer->SetSequence( Lerp( frac, record->m_masterSequence, prevRecord->m_masterSequence ) ); + pPlayer->SetCycle( Lerp( frac, record->m_masterCycle, prevRecord->m_masterCycle ) ); + + if( record->m_masterCycle > prevRecord->m_masterCycle ) + { + // the older record is higher in frame than the newer, it must have wrapped around from 1 back to 0 + // add one to the newer so it is lerping from .9 to 1.1 instead of .9 to .1, for example. + float newCycle = Lerp( frac, record->m_masterCycle, prevRecord->m_masterCycle + 1 ); + pPlayer->SetCycle(newCycle < 1 ? newCycle : newCycle - 1 );// and make sure .9 to 1.2 does not end up 1.05 + } + else + { + pPlayer->SetCycle( Lerp( frac, record->m_masterCycle, prevRecord->m_masterCycle ) ); + } + } + if( !interpolatedMasters ) + { + pPlayer->SetSequence(record->m_masterSequence); + pPlayer->SetCycle(record->m_masterCycle); + } + + //////////////////////// + // Now do all the layers + int layerCount = pPlayer->GetNumAnimOverlays(); + for( int layerIndex = 0; layerIndex < layerCount; ++layerIndex ) + { + CAnimationLayer *currentLayer = pPlayer->GetAnimOverlay(layerIndex); + if( currentLayer ) + { + restore->m_layerRecords[layerIndex].m_cycle = currentLayer->m_flCycle; + restore->m_layerRecords[layerIndex].m_order = currentLayer->m_nOrder; + restore->m_layerRecords[layerIndex].m_sequence = currentLayer->m_nSequence; + restore->m_layerRecords[layerIndex].m_weight = currentLayer->m_flWeight; + + bool interpolated = false; + if( (frac > 0.0f) && interpolationAllowed ) + { + LayerRecord &recordsLayerRecord = record->m_layerRecords[layerIndex]; + LayerRecord &prevRecordsLayerRecord = prevRecord->m_layerRecords[layerIndex]; + if( (recordsLayerRecord.m_order == prevRecordsLayerRecord.m_order) + && (recordsLayerRecord.m_sequence == prevRecordsLayerRecord.m_sequence) + ) + { + // We can't interpolate across a sequence or order change + interpolated = true; + if( recordsLayerRecord.m_cycle > prevRecordsLayerRecord.m_cycle ) + { + // the older record is higher in frame than the newer, it must have wrapped around from 1 back to 0 + // add one to the newer so it is lerping from .9 to 1.1 instead of .9 to .1, for example. + float newCycle = Lerp( frac, recordsLayerRecord.m_cycle, prevRecordsLayerRecord.m_cycle + 1 ); + currentLayer->m_flCycle = newCycle < 1 ? newCycle : newCycle - 1;// and make sure .9 to 1.2 does not end up 1.05 + } + else + { + currentLayer->m_flCycle = Lerp( frac, recordsLayerRecord.m_cycle, prevRecordsLayerRecord.m_cycle ); + } + currentLayer->m_nOrder = recordsLayerRecord.m_order; + currentLayer->m_nSequence = recordsLayerRecord.m_sequence; + currentLayer->m_flWeight = Lerp( frac, recordsLayerRecord.m_weight, prevRecordsLayerRecord.m_weight ); + } + } + if( !interpolated ) + { + //Either no interp, or interp failed. Just use record. + currentLayer->m_flCycle = record->m_layerRecords[layerIndex].m_cycle; + currentLayer->m_nOrder = record->m_layerRecords[layerIndex].m_order; + currentLayer->m_nSequence = record->m_layerRecords[layerIndex].m_sequence; + currentLayer->m_flWeight = record->m_layerRecords[layerIndex].m_weight; + } + } + } + + if ( !flags ) + return; // we didn't change anything + + if ( sv_lagflushbonecache.GetBool() ) + pPlayer->InvalidateBoneCache(); + + /*char text[256]; Q_snprintf( text, sizeof(text), "time %.2f", flTargetTime ); + pPlayer->DrawServerHitboxes( 10 ); + NDebugOverlay::Text( org, text, false, 10 ); + NDebugOverlay::EntityBounds( pPlayer, 255, 0, 0, 32, 10 ); */ + + m_RestorePlayer.Set( pl_index ); //remember that we changed this player + m_bNeedToRestore = true; // we changed at least one player + restore->m_fFlags = flags; // we need to restore these flags + change->m_fFlags = flags; // we have changed these flags + + if( sv_showlagcompensation.GetInt() == 1 ) + { + pPlayer->DrawServerHitboxes(4, true); + } +} + + +void CLagCompensationManager::FinishLagCompensation( CBasePlayer *player ) +{ + VPROF_BUDGET_FLAGS( "FinishLagCompensation", VPROF_BUDGETGROUP_OTHER_NETWORKING, BUDGETFLAG_CLIENT|BUDGETFLAG_SERVER ); + + m_pCurrentPlayer = NULL; + + if ( !m_bNeedToRestore ) + return; // no player was changed at all + + // Iterate all active players + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + int pl_index = i - 1; + + if ( !m_RestorePlayer.Get( pl_index ) ) + { + // player wasn't changed by lag compensation + continue; + } + + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( !pPlayer ) + { + continue; + } + + LagRecord *restore = &m_RestoreData[ pl_index ]; + LagRecord *change = &m_ChangeData[ pl_index ]; + + bool restoreSimulationTime = false; + + if ( restore->m_fFlags & LC_SIZE_CHANGED ) + { + restoreSimulationTime = true; + + // see if simulation made any changes, if no, then do the restore, otherwise, + // leave new values in + if ( pPlayer->CollisionProp()->OBBMinsPreScaled() == change->m_vecMinsPreScaled && + pPlayer->CollisionProp()->OBBMaxsPreScaled() == change->m_vecMaxsPreScaled ) + { + // Restore it + pPlayer->SetSize( restore->m_vecMinsPreScaled, restore->m_vecMaxsPreScaled ); + } +#ifdef STAGING_ONLY + else + { + Warning( "Should we really not restore the size?\n" ); + } +#endif + } + + if ( restore->m_fFlags & LC_ANGLES_CHANGED ) + { + restoreSimulationTime = true; + + if ( pPlayer->GetLocalAngles() == change->m_vecAngles ) + { + pPlayer->SetLocalAngles( restore->m_vecAngles ); + } + } + + if ( restore->m_fFlags & LC_ORIGIN_CHANGED ) + { + restoreSimulationTime = true; + + // Okay, let's see if we can do something reasonable with the change + Vector delta = pPlayer->GetLocalOrigin() - change->m_vecOrigin; + + // If it moved really far, just leave the player in the new spot!!! + if ( delta.Length2DSqr() < m_flTeleportDistanceSqr ) + { + RestorePlayerTo( pPlayer, restore->m_vecOrigin + delta ); + } + } + + if( restore->m_fFlags & LC_ANIMATION_CHANGED ) + { + restoreSimulationTime = true; + + pPlayer->SetSequence(restore->m_masterSequence); + pPlayer->SetCycle(restore->m_masterCycle); + + int layerCount = pPlayer->GetNumAnimOverlays(); + for( int layerIndex = 0; layerIndex < layerCount; ++layerIndex ) + { + CAnimationLayer *currentLayer = pPlayer->GetAnimOverlay(layerIndex); + if( currentLayer ) + { + currentLayer->m_flCycle = restore->m_layerRecords[layerIndex].m_cycle; + currentLayer->m_nOrder = restore->m_layerRecords[layerIndex].m_order; + currentLayer->m_nSequence = restore->m_layerRecords[layerIndex].m_sequence; + currentLayer->m_flWeight = restore->m_layerRecords[layerIndex].m_weight; + } + } + } + + if ( restoreSimulationTime ) + { + pPlayer->SetSimulationTime( restore->m_flSimulationTime ); + } + } +} + + diff --git a/sp/src/game/server/player_pickup.cpp b/sp/src/game/server/player_pickup.cpp new file mode 100644 index 00000000..c0892f1a --- /dev/null +++ b/sp/src/game/server/player_pickup.cpp @@ -0,0 +1,175 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "player_pickup.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// player pickup utility routine +void Pickup_ForcePlayerToDropThisObject( CBaseEntity *pTarget ) +{ + if ( pTarget == NULL ) + return; + + IPhysicsObject *pPhysics = pTarget->VPhysicsGetObject(); + + if ( pPhysics == NULL ) + return; + + if ( pPhysics->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + pPlayer->ForceDropOfCarriedPhysObjects( pTarget ); + } +} + + +void Pickup_OnPhysGunDrop( CBaseEntity *pDroppedObject, CBasePlayer *pPlayer, PhysGunDrop_t Reason ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pDroppedObject); + if ( pPickup ) + { + pPickup->OnPhysGunDrop( pPlayer, Reason ); + } +} + + +void Pickup_OnPhysGunPickup( CBaseEntity *pPickedUpObject, CBasePlayer *pPlayer, PhysGunPickup_t reason ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pPickedUpObject); + if ( pPickup ) + { + pPickup->OnPhysGunPickup( pPlayer, reason ); + } + + // send phys gun pickup item event, but only in single player + if ( !g_pGameRules->IsMultiplayer() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "physgun_pickup" ); + if ( event ) + { + event->SetInt( "entindex", pPickedUpObject->entindex() ); + gameeventmanager->FireEvent( event ); + } + } +} + +bool Pickup_OnAttemptPhysGunPickup( CBaseEntity *pPickedUpObject, CBasePlayer *pPlayer, PhysGunPickup_t reason ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pPickedUpObject); + if ( pPickup ) + { + return pPickup->OnAttemptPhysGunPickup( pPlayer, reason ); + } + return true; +} + +CBaseEntity *Pickup_OnFailedPhysGunPickup( CBaseEntity *pPickedUpObject, Vector vPhysgunPos ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pPickedUpObject); + if ( pPickup ) + { + return pPickup->OnFailedPhysGunPickup( vPhysgunPos ); + } + + return NULL; +} + +bool Pickup_GetPreferredCarryAngles( CBaseEntity *pObject, CBasePlayer *pPlayer, matrix3x4_t &localToWorld, QAngle &outputAnglesWorldSpace ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pObject); + if ( pPickup ) + { + if ( pPickup->HasPreferredCarryAnglesForPlayer( pPlayer ) ) + { + outputAnglesWorldSpace = TransformAnglesToWorldSpace( pPickup->PreferredCarryAngles(), localToWorld ); + return true; + } + } + return false; +} + +bool Pickup_ForcePhysGunOpen( CBaseEntity *pObject, CBasePlayer *pPlayer ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pObject); + if ( pPickup ) + { + return pPickup->ForcePhysgunOpen( pPlayer ); + } + return false; +} + +AngularImpulse Pickup_PhysGunLaunchAngularImpulse( CBaseEntity *pObject, PhysGunForce_t reason ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pObject); + if ( pPickup != NULL && pPickup->ShouldPuntUseLaunchForces( reason ) ) + { + return pPickup->PhysGunLaunchAngularImpulse(); + } + return RandomAngularImpulse( -600, 600 ); +} + +Vector Pickup_DefaultPhysGunLaunchVelocity( const Vector &vecForward, float flMass ) +{ +#ifdef HL2_DLL + // Calculate the velocity based on physcannon rules + float flForceMax = physcannon_maxforce.GetFloat(); + float flForce = flForceMax; + + float mass = flMass; + if ( mass > 100 ) + { + mass = MIN( mass, 1000 ); + float flForceMin = physcannon_minforce.GetFloat(); + flForce = SimpleSplineRemapValClamped( mass, 100, 600, flForceMax, flForceMin ); + } + + return ( vecForward * flForce ); +#endif + + // Do the simple calculation + return ( vecForward * flMass ); +} + +Vector Pickup_PhysGunLaunchVelocity( CBaseEntity *pObject, const Vector &vecForward, PhysGunForce_t reason ) +{ + // The object must be valid + if ( pObject == NULL ) + { + Assert( 0 ); + return vec3_origin; + } + + // Shouldn't ever get here with a non-vphysics object. + IPhysicsObject *pPhysicsObject = pObject->VPhysicsGetObject(); + if ( pPhysicsObject == NULL ) + { + Assert( 0 ); + return vec3_origin; + } + + // Call the pickup entity's callback + IPlayerPickupVPhysics *pPickup = dynamic_cast(pObject); + if ( pPickup != NULL && pPickup->ShouldPuntUseLaunchForces( reason ) ) + return pPickup->PhysGunLaunchVelocity( vecForward, pPhysicsObject->GetMass() ); + + // Do our default behavior + return Pickup_DefaultPhysGunLaunchVelocity( vecForward, pPhysicsObject->GetMass() ); +} + +bool Pickup_ShouldPuntUseLaunchForces( CBaseEntity *pObject, PhysGunForce_t reason ) +{ + IPlayerPickupVPhysics *pPickup = dynamic_cast(pObject); + if ( pPickup ) + { + return pPickup->ShouldPuntUseLaunchForces( reason ); + } + return false; +} + diff --git a/sp/src/game/server/player_pickup.h b/sp/src/game/server/player_pickup.h new file mode 100644 index 00000000..1b6d252a --- /dev/null +++ b/sp/src/game/server/player_pickup.h @@ -0,0 +1,93 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: APIs for player pickup of physics objects +// +//=============================================================================// + +#ifndef PLAYER_PICKUP_H +#define PLAYER_PICKUP_H +#ifdef _WIN32 +#pragma once +#endif + +#ifdef HL2_DLL +// Needed for launch velocity +extern ConVar physcannon_minforce; +extern ConVar physcannon_maxforce; +#endif + +// Reasons behind a pickup +enum PhysGunPickup_t +{ + PICKED_UP_BY_CANNON, + PUNTED_BY_CANNON, + PICKED_UP_BY_PLAYER, // Picked up by +USE, not physgun. +}; + +// Reasons behind a drop +enum PhysGunDrop_t +{ + DROPPED_BY_PLAYER, + THROWN_BY_PLAYER, + DROPPED_BY_CANNON, + LAUNCHED_BY_CANNON, +}; + +enum PhysGunForce_t +{ + PHYSGUN_FORCE_DROPPED, // Dropped by +USE + PHYSGUN_FORCE_THROWN, // Thrown from +USE + PHYSGUN_FORCE_PUNTED, // Punted by cannon + PHYSGUN_FORCE_LAUNCHED, // Launched by cannon +}; + +void PlayerPickupObject( CBasePlayer *pPlayer, CBaseEntity *pObject ); +void Pickup_ForcePlayerToDropThisObject( CBaseEntity *pTarget ); + +void Pickup_OnPhysGunDrop( CBaseEntity *pDroppedObject, CBasePlayer *pPlayer, PhysGunDrop_t reason ); +void Pickup_OnPhysGunPickup( CBaseEntity *pPickedUpObject, CBasePlayer *pPlayer, PhysGunPickup_t reason = PICKED_UP_BY_CANNON ); +bool Pickup_OnAttemptPhysGunPickup( CBaseEntity *pPickedUpObject, CBasePlayer *pPlayer, PhysGunPickup_t reason = PICKED_UP_BY_CANNON ); +bool Pickup_GetPreferredCarryAngles( CBaseEntity *pObject, CBasePlayer *pPlayer, matrix3x4_t &localToWorld, QAngle &outputAnglesWorldSpace ); +bool Pickup_ForcePhysGunOpen( CBaseEntity *pObject, CBasePlayer *pPlayer ); +bool Pickup_ShouldPuntUseLaunchForces( CBaseEntity *pObject, PhysGunForce_t reason ); +AngularImpulse Pickup_PhysGunLaunchAngularImpulse( CBaseEntity *pObject, PhysGunForce_t reason ); +Vector Pickup_DefaultPhysGunLaunchVelocity( const Vector &vecForward, float flMass ); +Vector Pickup_PhysGunLaunchVelocity( CBaseEntity *pObject, const Vector &vecForward, PhysGunForce_t reason ); + +CBaseEntity *Pickup_OnFailedPhysGunPickup( CBaseEntity *pPickedUpObject, Vector vPhysgunPos ); + +abstract_class IPlayerPickupVPhysics +{ +public: + // Callbacks for the physgun/cannon picking up an entity + virtual bool OnAttemptPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason = PICKED_UP_BY_CANNON ) = 0; + virtual CBaseEntity *OnFailedPhysGunPickup( Vector vPhysgunPos ) = 0; + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason = PICKED_UP_BY_CANNON ) = 0; + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) = 0; + virtual bool HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer = NULL ) = 0; + virtual QAngle PreferredCarryAngles( void ) = 0; + virtual bool ForcePhysgunOpen( CBasePlayer *pPlayer ) = 0; + virtual AngularImpulse PhysGunLaunchAngularImpulse() = 0; + virtual bool ShouldPuntUseLaunchForces( PhysGunForce_t reason ) = 0; + virtual Vector PhysGunLaunchVelocity( const Vector &vecForward, float flMass ) = 0; +}; + +class CDefaultPlayerPickupVPhysics : public IPlayerPickupVPhysics +{ +public: + virtual bool OnAttemptPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason = PICKED_UP_BY_CANNON ) { return true; } + virtual CBaseEntity *OnFailedPhysGunPickup( Vector vPhysgunPos ) { return NULL; } + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason = PICKED_UP_BY_CANNON ) {} + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ) {} + virtual bool HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ) { return false; } + virtual QAngle PreferredCarryAngles( void ) { return vec3_angle; } + virtual bool ForcePhysgunOpen( CBasePlayer *pPlayer ) { return false; } + virtual AngularImpulse PhysGunLaunchAngularImpulse() { return RandomAngularImpulse( -600, 600 ); } + virtual bool ShouldPuntUseLaunchForces( PhysGunForce_t reason ) { return false; } + virtual Vector PhysGunLaunchVelocity( const Vector &vecForward, float flMass ) + { + return Pickup_DefaultPhysGunLaunchVelocity( vecForward, flMass ); + } +}; + +#endif // PLAYER_PICKUP_H diff --git a/sp/src/game/server/player_resource.cpp b/sp/src/game/server/player_resource.cpp new file mode 100644 index 00000000..e4829cde --- /dev/null +++ b/sp/src/game/server/player_resource.cpp @@ -0,0 +1,130 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity that propagates general data needed by clients for every player. +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "player.h" +#include "player_resource.h" +#include + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Datatable +IMPLEMENT_SERVERCLASS_ST_NOBASE(CPlayerResource, DT_PlayerResource) +// SendPropArray( SendPropString( SENDINFO(m_szName[0]) ), SENDARRAYINFO(m_szName) ), + SendPropArray3( SENDINFO_ARRAY3(m_iPing), SendPropInt( SENDINFO_ARRAY(m_iPing), 10, SPROP_UNSIGNED ) ), +// SendPropArray( SendPropInt( SENDINFO_ARRAY(m_iPacketloss), 7, SPROP_UNSIGNED ), m_iPacketloss ), + SendPropArray3( SENDINFO_ARRAY3(m_iScore), SendPropInt( SENDINFO_ARRAY(m_iScore), 12 ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iDeaths), SendPropInt( SENDINFO_ARRAY(m_iDeaths), 12 ) ), + SendPropArray3( SENDINFO_ARRAY3(m_bConnected), SendPropInt( SENDINFO_ARRAY(m_bConnected), 1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iTeam), SendPropInt( SENDINFO_ARRAY(m_iTeam), 4 ) ), + SendPropArray3( SENDINFO_ARRAY3(m_bAlive), SendPropInt( SENDINFO_ARRAY(m_bAlive), 1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iHealth), SendPropInt( SENDINFO_ARRAY(m_iHealth), -1, SPROP_VARINT | SPROP_UNSIGNED ) ), +END_SEND_TABLE() + +BEGIN_DATADESC( CPlayerResource ) + + // DEFINE_ARRAY( m_iPing, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_ARRAY( m_iPacketloss, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_ARRAY( m_iScore, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_ARRAY( m_iDeaths, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_ARRAY( m_bConnected, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_FIELD( m_flNextPingUpdate, FIELD_FLOAT ), + // DEFINE_ARRAY( m_iTeam, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_ARRAY( m_bAlive, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_ARRAY( m_iHealth, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_FIELD( m_nUpdateCounter, FIELD_INTEGER ), + + // Function Pointers + DEFINE_FUNCTION( ResourceThink ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( player_manager, CPlayerResource ); + +CPlayerResource *g_pPlayerResource; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPlayerResource::Spawn( void ) +{ + for ( int i=0; i < MAX_PLAYERS+1; i++ ) + { + m_iPing.Set( i, 0 ); + m_iScore.Set( i, 0 ); + m_iDeaths.Set( i, 0 ); + m_bConnected.Set( i, 0 ); + m_iTeam.Set( i, 0 ); + m_bAlive.Set( i, 0 ); + } + + SetThink( &CPlayerResource::ResourceThink ); + SetNextThink( gpGlobals->curtime ); + m_nUpdateCounter = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: The Player resource is always transmitted to clients +//----------------------------------------------------------------------------- +int CPlayerResource::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: Wrapper for the virtual GrabPlayerData Think function +//----------------------------------------------------------------------------- +void CPlayerResource::ResourceThink( void ) +{ + m_nUpdateCounter++; + + UpdatePlayerData(); + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPlayerResource::UpdatePlayerData( void ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = (CBasePlayer*)UTIL_PlayerByIndex( i ); + + if ( pPlayer && pPlayer->IsConnected() ) + { + m_iScore.Set( i, pPlayer->FragCount() ); + m_iDeaths.Set( i, pPlayer->DeathCount() ); + m_bConnected.Set( i, 1 ); + m_iTeam.Set( i, pPlayer->GetTeamNumber() ); + m_bAlive.Set( i, pPlayer->IsAlive()?1:0 ); + m_iHealth.Set(i, MAX( 0, pPlayer->GetHealth() ) ); + + // Don't update ping / packetloss everytime + + if ( !(m_nUpdateCounter%20) ) + { + // update ping all 20 think ticks = (20*0.1=2seconds) + int ping, packetloss; + UTIL_GetPlayerConnectionInfo( i, ping, packetloss ); + + // calc avg for scoreboard so it's not so jittery + ping = 0.8f * m_iPing.Get(i) + 0.2f * ping; + + + m_iPing.Set( i, ping ); + // m_iPacketloss.Set( i, packetloss ); + } + } + else + { + m_bConnected.Set( i, 0 ); + } + } +} diff --git a/sp/src/game/server/player_resource.h b/sp/src/game/server/player_resource.h new file mode 100644 index 00000000..298eda04 --- /dev/null +++ b/sp/src/game/server/player_resource.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity that propagates general data needed by clients for every player. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef PLAYER_RESOURCE_H +#define PLAYER_RESOURCE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "shareddefs.h" + +class CPlayerResource : public CBaseEntity +{ + DECLARE_CLASS( CPlayerResource, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + virtual void Spawn( void ); + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_DONT_SAVE; } + virtual void ResourceThink( void ); + virtual void UpdatePlayerData( void ); + virtual int UpdateTransmitState(void); + +protected: + // Data for each player that's propagated to all clients + // Stored in individual arrays so they can be sent down via datatables + CNetworkArray( int, m_iPing, MAX_PLAYERS+1 ); + CNetworkArray( int, m_iScore, MAX_PLAYERS+1 ); + CNetworkArray( int, m_iDeaths, MAX_PLAYERS+1 ); + CNetworkArray( int, m_bConnected, MAX_PLAYERS+1 ); + CNetworkArray( int, m_iTeam, MAX_PLAYERS+1 ); + CNetworkArray( int, m_bAlive, MAX_PLAYERS+1 ); + CNetworkArray( int, m_iHealth, MAX_PLAYERS+1 ); + + int m_nUpdateCounter; +}; + +extern CPlayerResource *g_pPlayerResource; + +#endif // PLAYER_RESOURCE_H diff --git a/sp/src/game/server/playerinfomanager.cpp b/sp/src/game/server/playerinfomanager.cpp new file mode 100644 index 00000000..69917511 --- /dev/null +++ b/sp/src/game/server/playerinfomanager.cpp @@ -0,0 +1,113 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: implementation of player info manager +// +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "playerinfomanager.h" +#include "edict.h" + +#if defined( TF_DLL ) +#include "tf_shareddefs.h" +#elif defined( CSTRIKE_DLL ) +#include "weapon_csbase.h" +#elif defined( DOD_DLL ) +#include "weapon_dodbase.h" +#elif defined( SDK_DLL ) +#include "weapon_sdkbase.h" +#endif + +extern CGlobalVars *gpGlobals; +static CPlayerInfoManager s_PlayerInfoManager; +static CPluginBotManager s_BotManager; + +namespace +{ + + // + // Old version support + // + + //Tony; pulled out version 1 and 2 support for orange box, we're starting fresh now with v3 player and v2 of the bot interface. +} + +IPlayerInfo *CPlayerInfoManager::GetPlayerInfo( edict_t *pEdict ) +{ + CBasePlayer *pPlayer = ( ( CBasePlayer * )CBaseEntity::Instance( pEdict )); + if ( pPlayer ) + return pPlayer->GetPlayerInfo(); + else + return NULL; +} +IPlayerInfo *CPlayerInfoManager::GetPlayerInfo( int index ) +{ + return GetPlayerInfo( engine->PEntityOfEntIndex( index ) ); +} + +// Games implementing advanced bot support should override this. +int CPlayerInfoManager::AliasToWeaponId(const char *weaponName) +{ + //Tony; TF doesn't support this. Should it? +#if defined ( CSTRIKE_DLL ) || defined ( DOD_DLL ) || defined ( SDK_DLL ) + return AliasToWeaponID(weaponName); +#endif + return -1; +} + +// Games implementing advanced bot support should override this. +const char *CPlayerInfoManager::WeaponIdToAlias(int weaponId) +{ +#if defined( TF_DLL ) + return WeaponIdToAlias(weaponId); +#elif defined ( CSTRIKE_DLL ) || defined ( DOD_DLL ) || defined ( SDK_DLL ) + return WeaponIDToAlias(weaponId); +#endif + return "MOD_DIDNT_IMPLEMENT_ME"; +} +CGlobalVars *CPlayerInfoManager::GetGlobalVars() +{ + return gpGlobals; +} + + + +IBotController *CPluginBotManager::GetBotController( edict_t *pEdict ) +{ + CBasePlayer *pPlayer = ( ( CBasePlayer * )CBaseEntity::Instance( pEdict )); + if ( pPlayer && pPlayer->IsBot() ) + { + return pPlayer->GetBotController(); + } + else + { + return NULL; + } +} + +edict_t *CPluginBotManager::CreateBot( const char *botname ) +{ + edict_t *pEdict = engine->CreateFakeClient( botname ); + if (!pEdict) + { + Msg( "Failed to create Bot.\n"); + return NULL; + } + + // Allocate a player entity for the bot, and call spawn + CBasePlayer *pPlayer = ((CBasePlayer*)CBaseEntity::Instance( pEdict )); + + pPlayer->ClearFlags(); + pPlayer->AddFlag( FL_CLIENT | FL_FAKECLIENT ); + pPlayer->ChangeTeam( TEAM_UNASSIGNED ); + pPlayer->AddEFlags( EFL_PLUGIN_BASED_BOT ); // Mark it as a plugin based bot + pPlayer->RemoveAllItems( true ); + pPlayer->Spawn(); + + return pEdict; +} + +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CPlayerInfoManager, IPlayerInfoManager, INTERFACEVERSION_PLAYERINFOMANAGER, s_PlayerInfoManager); +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CPluginBotManager, IBotManager, INTERFACEVERSION_PLAYERBOTMANAGER, s_BotManager); + diff --git a/sp/src/game/server/playerinfomanager.h b/sp/src/game/server/playerinfomanager.h new file mode 100644 index 00000000..2b110562 --- /dev/null +++ b/sp/src/game/server/playerinfomanager.h @@ -0,0 +1,38 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: implementation of player info manager +// +//=============================================================================// +#ifndef PLAYERINFOMANAGER_H +#define PLAYERINFOMANAGER_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "game/server/iplayerinfo.h" + +//----------------------------------------------------------------------------- +// Purpose: interface for plugins to get player info +//----------------------------------------------------------------------------- +class CPlayerInfoManager: public IPlayerInfoManager +{ +public: + virtual IPlayerInfo *GetPlayerInfo( edict_t *pEdict ); + virtual IPlayerInfo *GetPlayerInfo( int index ); + virtual CGlobalVars *GetGlobalVars(); + // accessor to hook into aliastoweaponid + virtual int AliasToWeaponId(const char *weaponName); + // accessor to hook into weaponidtoalias + virtual const char *WeaponIdToAlias(int weaponId); + +}; + +class CPluginBotManager: public IBotManager +{ +public: + virtual IBotController *GetBotController( edict_t *pEdict ); + virtual edict_t *CreateBot( const char *botname ); +}; + +#endif \ No newline at end of file diff --git a/sp/src/game/server/playerlocaldata.cpp b/sp/src/game/server/playerlocaldata.cpp new file mode 100644 index 00000000..3b9aea08 --- /dev/null +++ b/sp/src/game/server/playerlocaldata.cpp @@ -0,0 +1,303 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "playerlocaldata.h" +#include "player.h" +#include "mathlib/mathlib.h" +#include "entitylist.h" +#include "SkyCamera.h" +#include "playernet_vars.h" +#include "fogcontroller.h" +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//============================================================================= + +BEGIN_SEND_TABLE_NOBASE( CPlayerLocalData, DT_Local ) + + SendPropArray3 (SENDINFO_ARRAY3(m_chAreaBits), SendPropInt(SENDINFO_ARRAY(m_chAreaBits), 8, SPROP_UNSIGNED)), + SendPropArray3 (SENDINFO_ARRAY3(m_chAreaPortalBits), SendPropInt(SENDINFO_ARRAY(m_chAreaPortalBits), 8, SPROP_UNSIGNED)), + + SendPropInt (SENDINFO(m_iHideHUD), HIDEHUD_BITCOUNT, SPROP_UNSIGNED), + SendPropFloat (SENDINFO(m_flFOVRate), 0, SPROP_NOSCALE ), + SendPropInt (SENDINFO(m_bDucked), 1, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_bDucking), 1, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_bInDuckJump), 1, SPROP_UNSIGNED ), + SendPropFloat (SENDINFO(m_flDucktime), 12, SPROP_ROUNDDOWN|SPROP_CHANGES_OFTEN, 0.0f, 2048.0f ), + SendPropFloat (SENDINFO(m_flDuckJumpTime), 12, SPROP_ROUNDDOWN, 0.0f, 2048.0f ), + SendPropFloat (SENDINFO(m_flJumpTime), 12, SPROP_ROUNDDOWN, 0.0f, 2048.0f ), +#if PREDICTION_ERROR_CHECK_LEVEL > 1 + SendPropFloat (SENDINFO(m_flFallVelocity), 32, SPROP_NOSCALE ), + + SendPropFloat ( SENDINFO_VECTORELEM(m_vecPunchAngle, 0), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecPunchAngle, 1), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecPunchAngle, 2), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + + SendPropFloat ( SENDINFO_VECTORELEM(m_vecPunchAngleVel, 0), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecPunchAngleVel, 1), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + SendPropFloat ( SENDINFO_VECTORELEM(m_vecPunchAngleVel, 2), 32, SPROP_NOSCALE|SPROP_CHANGES_OFTEN ), + +#else + SendPropFloat (SENDINFO(m_flFallVelocity), 17, SPROP_CHANGES_OFTEN, -4096.0f, 4096.0f ), + SendPropVector (SENDINFO(m_vecPunchAngle), -1, SPROP_COORD|SPROP_CHANGES_OFTEN), + SendPropVector (SENDINFO(m_vecPunchAngleVel), -1, SPROP_COORD), +#endif + SendPropInt (SENDINFO(m_bDrawViewmodel), 1, SPROP_UNSIGNED ), + SendPropInt (SENDINFO(m_bWearingSuit), 1, SPROP_UNSIGNED ), + SendPropBool (SENDINFO(m_bPoisoned)), + + SendPropFloat (SENDINFO(m_flStepSize), 16, SPROP_ROUNDUP, 0.0f, 128.0f ), + SendPropInt (SENDINFO(m_bAllowAutoMovement),1, SPROP_UNSIGNED ), + + // 3d skybox data + SendPropInt(SENDINFO_STRUCTELEM(m_skybox3d.scale), 12), + SendPropVector (SENDINFO_STRUCTELEM(m_skybox3d.origin), -1, SPROP_COORD), +#ifdef MAPBASE + SendPropVector (SENDINFO_STRUCTELEM(m_skybox3d.angles), -1, SPROP_COORD), + SendPropEHandle (SENDINFO_STRUCTELEM(m_skybox3d.skycamera)), + SendPropInt (SENDINFO_STRUCTELEM(m_skybox3d.skycolor), 32, (SPROP_COORD|SPROP_UNSIGNED), SendProxy_Color32ToInt), +#endif + SendPropInt (SENDINFO_STRUCTELEM(m_skybox3d.area), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_STRUCTELEM( m_skybox3d.fog.enable ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_STRUCTELEM( m_skybox3d.fog.blend ), 1, SPROP_UNSIGNED ), + SendPropVector( SENDINFO_STRUCTELEM(m_skybox3d.fog.dirPrimary), -1, SPROP_COORD), + SendPropInt( SENDINFO_STRUCTELEM( m_skybox3d.fog.colorPrimary ), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_STRUCTELEM( m_skybox3d.fog.colorSecondary ), 32, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO_STRUCTELEM( m_skybox3d.fog.start ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_skybox3d.fog.end ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_STRUCTELEM( m_skybox3d.fog.maxdensity ), 0, SPROP_NOSCALE ), +#ifdef MAPBASE + SendPropFloat( SENDINFO_STRUCTELEM( m_skybox3d.fog.farz ), 0, SPROP_NOSCALE ), +#endif + + SendPropEHandle( SENDINFO_STRUCTELEM( m_PlayerFog.m_hCtrl ) ), + + // audio data + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 0 ), -1, SPROP_COORD), + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 1 ), -1, SPROP_COORD), + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 2 ), -1, SPROP_COORD), + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 3 ), -1, SPROP_COORD), + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 4 ), -1, SPROP_COORD), + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 5 ), -1, SPROP_COORD), + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 6 ), -1, SPROP_COORD), + SendPropVector( SENDINFO_STRUCTARRAYELEM( m_audio.localSound, 7 ), -1, SPROP_COORD), + SendPropInt( SENDINFO_STRUCTELEM( m_audio.soundscapeIndex ), 17, 0 ), + SendPropInt( SENDINFO_STRUCTELEM( m_audio.localBits ), NUM_AUDIO_LOCAL_SOUNDS, SPROP_UNSIGNED ), + SendPropEHandle( SENDINFO_STRUCTELEM( m_audio.ent ) ), + + //Tony; tonemap stuff! -- TODO! Optimize this with bit sizes from env_tonemap_controller. + SendPropFloat ( SENDINFO_STRUCTELEM( m_TonemapParams.m_flTonemapScale ), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO_STRUCTELEM( m_TonemapParams.m_flTonemapRate ), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO_STRUCTELEM( m_TonemapParams.m_flBloomScale ), 0, SPROP_NOSCALE ), + + SendPropFloat ( SENDINFO_STRUCTELEM( m_TonemapParams.m_flAutoExposureMin ), 0, SPROP_NOSCALE ), + SendPropFloat ( SENDINFO_STRUCTELEM( m_TonemapParams.m_flAutoExposureMax ), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + +BEGIN_SIMPLE_DATADESC( fogplayerparams_t ) + DEFINE_FIELD( m_hCtrl, FIELD_EHANDLE ), + DEFINE_FIELD( m_flTransitionTime, FIELD_FLOAT ), + DEFINE_FIELD( m_OldColor, FIELD_COLOR32 ), + DEFINE_FIELD( m_flOldStart, FIELD_FLOAT ), + DEFINE_FIELD( m_flOldEnd, FIELD_FLOAT ), + DEFINE_FIELD( m_NewColor, FIELD_COLOR32 ), + DEFINE_FIELD( m_flNewStart, FIELD_FLOAT ), + DEFINE_FIELD( m_flNewEnd, FIELD_FLOAT ), +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( fogparams_t ) + + DEFINE_FIELD( enable, FIELD_BOOLEAN ), + DEFINE_FIELD( blend, FIELD_BOOLEAN ), + DEFINE_FIELD( dirPrimary, FIELD_VECTOR ), + DEFINE_FIELD( colorPrimary, FIELD_COLOR32 ), + DEFINE_FIELD( colorSecondary, FIELD_COLOR32 ), + DEFINE_FIELD( start, FIELD_FLOAT ), + DEFINE_FIELD( end, FIELD_FLOAT ), + DEFINE_FIELD( farz, FIELD_FLOAT ), + DEFINE_FIELD( maxdensity, FIELD_FLOAT ), + DEFINE_FIELD( colorPrimaryLerpTo, FIELD_COLOR32 ), + DEFINE_FIELD( colorSecondaryLerpTo, FIELD_COLOR32 ), + DEFINE_FIELD( startLerpTo, FIELD_FLOAT ), + DEFINE_FIELD( endLerpTo, FIELD_FLOAT ), + DEFINE_FIELD( lerptime, FIELD_TIME ), + DEFINE_FIELD( duration, FIELD_FLOAT ), +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( sky3dparams_t ) + + DEFINE_FIELD( scale, FIELD_INTEGER ), + DEFINE_FIELD( origin, FIELD_VECTOR ), +#ifdef MAPBASE + DEFINE_FIELD( angles, FIELD_VECTOR ), + DEFINE_FIELD( skycamera, FIELD_EHANDLE ), + DEFINE_FIELD( skycolor, FIELD_COLOR32 ), +#endif + DEFINE_FIELD( area, FIELD_INTEGER ), + DEFINE_EMBEDDED( fog ), + +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( audioparams_t ) + + DEFINE_AUTO_ARRAY( localSound, FIELD_VECTOR ), + DEFINE_FIELD( soundscapeIndex, FIELD_INTEGER ), + DEFINE_FIELD( localBits, FIELD_INTEGER ), + DEFINE_FIELD( ent, FIELD_EHANDLE ), + +END_DATADESC() + +//Tony; tonepam params!! +BEGIN_SIMPLE_DATADESC ( tonemap_params_t ) + DEFINE_FIELD( m_flTonemapScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flTonemapRate, FIELD_FLOAT ), + DEFINE_FIELD( m_flBloomScale, FIELD_FLOAT ), + DEFINE_FIELD( m_flAutoExposureMin, FIELD_FLOAT ), + DEFINE_FIELD( m_flAutoExposureMax, FIELD_FLOAT ), +END_DATADESC() + + +BEGIN_SIMPLE_DATADESC( CPlayerLocalData ) + DEFINE_AUTO_ARRAY( m_chAreaBits, FIELD_CHARACTER ), + DEFINE_AUTO_ARRAY( m_chAreaPortalBits, FIELD_CHARACTER ), + DEFINE_FIELD( m_iHideHUD, FIELD_INTEGER ), + DEFINE_FIELD( m_flFOVRate, FIELD_FLOAT ), + DEFINE_FIELD( m_vecOverViewpoint, FIELD_VECTOR ), + DEFINE_FIELD( m_bDucked, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bDucking, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bInDuckJump, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flDucktime, FIELD_TIME ), + DEFINE_FIELD( m_flDuckJumpTime, FIELD_TIME ), + DEFINE_FIELD( m_flJumpTime, FIELD_TIME ), + DEFINE_FIELD( m_nStepside, FIELD_INTEGER ), + DEFINE_FIELD( m_flFallVelocity, FIELD_FLOAT ), + DEFINE_FIELD( m_nOldButtons, FIELD_INTEGER ), + DEFINE_FIELD( m_vecPunchAngle, FIELD_VECTOR ), + DEFINE_FIELD( m_vecPunchAngleVel, FIELD_VECTOR ), + DEFINE_FIELD( m_bDrawViewmodel, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bWearingSuit, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bPoisoned, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flStepSize, FIELD_FLOAT ), + DEFINE_FIELD( m_bAllowAutoMovement, FIELD_BOOLEAN ), + DEFINE_EMBEDDED( m_skybox3d ), + DEFINE_EMBEDDED( m_PlayerFog ), + DEFINE_EMBEDDED( m_fog ), + DEFINE_EMBEDDED( m_audio ), + + //Tony; added + DEFINE_EMBEDDED( m_TonemapParams ), + + // "Why don't we save this field, grandpa?" + // + // "You see Billy, trigger_vphysics_motion uses vPhysics to touch the player, + // so if the trigger is Disabled via an input while the player is inside it, + // the trigger won't get its EndTouch until the player moves. Since touchlinks + // aren't saved and restored, if the we save before EndTouch is called, it + // will never be called after we load." + //DEFINE_FIELD( m_bSlowMovement, FIELD_BOOLEAN ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Constructor. +//----------------------------------------------------------------------------- +CPlayerLocalData::CPlayerLocalData() +{ +#ifdef _DEBUG + m_vecOverViewpoint.Init(); + m_vecPunchAngle.Init(); +#endif + m_audio.soundscapeIndex = 0; + m_audio.localBits = 0; + m_audio.ent.Set( NULL ); + m_pOldSkyCamera = NULL; + m_bDrawViewmodel = true; +} + + +void CPlayerLocalData::UpdateAreaBits( CBasePlayer *pl, unsigned char chAreaPortalBits[MAX_AREA_PORTAL_STATE_BYTES] ) +{ + Vector origin = pl->EyePosition(); + + unsigned char tempBits[32]; + COMPILE_TIME_ASSERT( sizeof( tempBits ) >= sizeof( ((CPlayerLocalData*)0)->m_chAreaBits ) ); + + int i; + int area = engine->GetArea( origin ); + engine->GetAreaBits( area, tempBits, sizeof( tempBits ) ); + for ( i=0; i < m_chAreaBits.Count(); i++ ) + { + if ( tempBits[i] != m_chAreaBits[ i ] ) + { + m_chAreaBits.Set( i, tempBits[i] ); + } + } + + for ( i=0; i < MAX_AREA_PORTAL_STATE_BYTES; i++ ) + { + if ( chAreaPortalBits[i] != m_chAreaPortalBits[i] ) + m_chAreaPortalBits.Set( i, chAreaPortalBits[i] ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Fills in CClientData values for local player just before sending over wire +// Input : player - +//----------------------------------------------------------------------------- + +void ClientData_Update( CBasePlayer *pl ) +{ + // HACKHACK: for 3d skybox + // UNDONE: Support multiple sky cameras? + CSkyCamera *pSkyCamera = GetCurrentSkyCamera(); +#ifdef MAPBASE + // Needs null protection now that the sky can go from valid to null + if ( !pSkyCamera ) + { + pl->m_Local.m_skybox3d.area = 255; + } + else if ( pSkyCamera != pl->m_Local.m_pOldSkyCamera ) + { + pl->m_Local.m_pOldSkyCamera = pSkyCamera; + pl->m_Local.m_skybox3d.CopyFrom(pSkyCamera->m_skyboxData); + } +#else + if ( pSkyCamera != pl->m_Local.m_pOldSkyCamera ) + { + pl->m_Local.m_pOldSkyCamera = pSkyCamera; + pl->m_Local.m_skybox3d.CopyFrom(pSkyCamera->m_skyboxData); + } + else if ( !pSkyCamera ) + { + pl->m_Local.m_skybox3d.area = 255; + } +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void UpdateAllClientData( void ) +{ + VPROF( "UpdateAllClientData" ); + int i; + CBasePlayer *pl; + + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + pl = ( CBasePlayer * )UTIL_PlayerByIndex( i ); + if ( !pl ) + continue; + + ClientData_Update( pl ); + } +} + diff --git a/sp/src/game/server/playerlocaldata.h b/sp/src/game/server/playerlocaldata.h new file mode 100644 index 00000000..587dbd1a --- /dev/null +++ b/sp/src/game/server/playerlocaldata.h @@ -0,0 +1,96 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef PLAYERLOCALDATA_H +#define PLAYERLOCALDATA_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "playernet_vars.h" +#include "networkvar.h" +#include "fogcontroller.h" +#include "postprocesscontroller.h" + +//----------------------------------------------------------------------------- +// Purpose: Player specific data ( sent only to local player, too ) +//----------------------------------------------------------------------------- +class CPlayerLocalData +{ +public: + // Save/restore + DECLARE_SIMPLE_DATADESC(); + // Prediction data copying + DECLARE_CLASS_NOBASE( CPlayerLocalData ); + DECLARE_EMBEDDED_NETWORKVAR(); + + CPlayerLocalData(); + + void UpdateAreaBits( CBasePlayer *pl, unsigned char chAreaPortalBits[MAX_AREA_PORTAL_STATE_BYTES] ); + + +public: + + CNetworkArray( unsigned char, m_chAreaBits, MAX_AREA_STATE_BYTES ); // Which areas are potentially visible to the client? + CNetworkArray( unsigned char, m_chAreaPortalBits, MAX_AREA_PORTAL_STATE_BYTES ); // Which area portals are open? + + CNetworkVar( int, m_iHideHUD ); // bitfields containing sections of the HUD to hide + CNetworkVar( float, m_flFOVRate ); // rate at which the FOV changes (defaults to 0) + + Vector m_vecOverViewpoint; // Viewpoint overriding the real player's viewpoint + + // Fully ducked + CNetworkVar( bool, m_bDucked ); + // In process of ducking + CNetworkVar( bool, m_bDucking ); + // In process of duck-jumping + CNetworkVar( bool, m_bInDuckJump ); + // During ducking process, amount of time before full duc + CNetworkVar( float, m_flDucktime ); + CNetworkVar( float, m_flDuckJumpTime ); + // Jump time, time to auto unduck (since we auto crouch jump now). + CNetworkVar( float, m_flJumpTime ); + // Step sound side flip/flip + int m_nStepside;; + // Velocity at time when we hit ground + CNetworkVar( float, m_flFallVelocity ); + // Previous button state + int m_nOldButtons; + class CSkyCamera *m_pOldSkyCamera; + // Base velocity that was passed in to server physics so + // client can predict conveyors correctly. Server zeroes it, so we need to store here, too. + // auto-decaying view angle adjustment + CNetworkQAngle( m_vecPunchAngle ); + CNetworkQAngle( m_vecPunchAngleVel ); + // Draw view model for the player + CNetworkVar( bool, m_bDrawViewmodel ); + + // Is the player wearing the HEV suit + CNetworkVar( bool, m_bWearingSuit ); + CNetworkVar( bool, m_bPoisoned ); + CNetworkVar( float, m_flStepSize ); + CNetworkVar( bool, m_bAllowAutoMovement ); + + // 3d skybox + CNetworkVarEmbedded( sky3dparams_t, m_skybox3d ); + // world fog + CNetworkVarEmbedded( fogplayerparams_t, m_PlayerFog ); + fogparams_t m_fog; + // audio environment + CNetworkVarEmbedded( audioparams_t, m_audio ); + + //Tony; added so tonemap controller can work in multiplayer with inputs. + CNetworkVarEmbedded( tonemap_params_t, m_TonemapParams ); + + CNetworkVar( bool, m_bSlowMovement ); +}; + +EXTERN_SEND_TABLE(DT_Local); + + +#endif // PLAYERLOCALDATA_H diff --git a/sp/src/game/server/plugin_check.cpp b/sp/src/game/server/plugin_check.cpp new file mode 100644 index 00000000..833f5efc --- /dev/null +++ b/sp/src/game/server/plugin_check.cpp @@ -0,0 +1,30 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// This class allows the game dll to control what functions 3rd party plugins can +// call on clients. +// +//=============================================================================// + +#include "cbase.h" +#include "eiface.h" + +//----------------------------------------------------------------------------- +// Purpose: An implementation +//----------------------------------------------------------------------------- +class CPluginHelpersCheck : public IPluginHelpersCheck +{ +public: + virtual bool CreateMessage( const char *plugin, edict_t *pEntity, DIALOG_TYPE type, KeyValues *data ); +}; + +CPluginHelpersCheck s_PluginCheck; +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CPluginHelpersCheck, IPluginHelpersCheck, INTERFACEVERSION_PLUGINHELPERSCHECK, s_PluginCheck); + +bool CPluginHelpersCheck::CreateMessage( const char *plugin, edict_t *pEntity, DIALOG_TYPE type, KeyValues *data ) +{ + // return false here to disallow a plugin from running this command on this client + return true; +} + diff --git a/sp/src/game/server/point_camera.cpp b/sp/src/game/server/point_camera.cpp new file mode 100644 index 00000000..626bc61b --- /dev/null +++ b/sp/src/game/server/point_camera.cpp @@ -0,0 +1,486 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "igamesystem.h" +#include "point_camera.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define CAM_THINK_INTERVAL 0.05 + +// Spawnflags +#define SF_CAMERA_START_OFF 0x01 + +// UNDONE: Share properly with the client code!!! +#define POINT_CAMERA_MSG_SETACTIVE 1 + +CEntityClassList g_PointCameraList; +template <> CPointCamera *CEntityClassList::m_pClassList = NULL; + +CPointCamera* GetPointCameraList() +{ + return g_PointCameraList.m_pClassList; +} + +// These are already built into CBaseEntity +// DEFINE_KEYFIELD( m_iName, FIELD_STRING, "targetname" ), +// DEFINE_KEYFIELD( m_iParent, FIELD_STRING, "parentname" ), +// DEFINE_KEYFIELD( m_target, FIELD_STRING, "target" ), + +LINK_ENTITY_TO_CLASS( point_camera, CPointCamera ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPointCamera::~CPointCamera() +{ + g_PointCameraList.Remove( this ); +} + +CPointCamera::CPointCamera() +{ + // Set these to opposites so that it'll be sent the first time around. + m_bActive = false; + m_bIsOn = false; + + m_bFogEnable = false; + +#ifdef MAPBASE + // Equivalent to SKYBOX_2DSKYBOX_VISIBLE, the original sky setting + m_iSkyMode = 2; + + m_iszRenderTarget = AllocPooledString( "_rt_Camera" ); +#endif + + g_PointCameraList.Insert( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCamera::Spawn( void ) +{ + BaseClass::Spawn(); + + if ( m_spawnflags & SF_CAMERA_START_OFF ) + { + m_bIsOn = false; + } + else + { + m_bIsOn = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Override ShouldTransmit since we want to be sent even though we don't have a model, etc. +// All that matters is if we are in the pvs. +//----------------------------------------------------------------------------- +int CPointCamera::UpdateTransmitState() +{ + if ( m_bActive ) + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + else + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCamera::SetActive( bool bActive ) +{ + // If the mapmaker's told the camera it's off, it enforces inactive state + if ( !m_bIsOn ) + { + bActive = false; + } + + if ( m_bActive != bActive ) + { + m_bActive = bActive; + DispatchUpdateTransmitState(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCamera::InputChangeFOV( inputdata_t &inputdata ) +{ + // Parse the keyvalue data + char parseString[255]; + + Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); + + // Get FOV + char *pszParam = strtok(parseString," "); + if(pszParam) + { + m_TargetFOV = atof( pszParam ); + } + else + { + // Assume no change + m_TargetFOV = m_FOV; + } + + // Get Time + float flChangeTime; + pszParam = strtok(NULL," "); + if(pszParam) + { + flChangeTime = atof( pszParam ); + } + else + { + // Assume 1 second. + flChangeTime = 1.0; + } + + m_DegreesPerSecond = ( m_TargetFOV - m_FOV ) / flChangeTime; + + SetThink( &CPointCamera::ChangeFOVThink ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCamera::ChangeFOVThink( void ) +{ + SetNextThink( gpGlobals->curtime + CAM_THINK_INTERVAL ); + + float newFOV = m_FOV; + + newFOV += m_DegreesPerSecond * CAM_THINK_INTERVAL; + + if( m_DegreesPerSecond < 0 ) + { + if( newFOV <= m_TargetFOV ) + { + newFOV = m_TargetFOV; + SetThink( NULL ); + } + } + else + { + if( newFOV >= m_TargetFOV ) + { + newFOV = m_TargetFOV; + SetThink( NULL ); + } + } + + m_FOV = newFOV; +} + +//----------------------------------------------------------------------------- +// Purpose: Turn this camera on, and turn all other cameras off +//----------------------------------------------------------------------------- +void CPointCamera::InputSetOnAndTurnOthersOff( inputdata_t &inputdata ) +{ + CBaseEntity *pEntity = NULL; + while ((pEntity = gEntList.FindEntityByClassname( pEntity, "point_camera" )) != NULL) + { + CPointCamera *pCamera = (CPointCamera*)pEntity; + +#ifdef MAPBASE + // Do not turn off cameras which use different render targets + if (pCamera->m_iszRenderTarget.Get() != m_iszRenderTarget.Get()) + continue; +#endif + + pCamera->InputSetOff( inputdata ); + } + + // Now turn myself on + InputSetOn( inputdata ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCamera::InputSetOn( inputdata_t &inputdata ) +{ + m_bIsOn = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCamera::InputSetOff( inputdata_t &inputdata ) +{ + m_bIsOn = false; + SetActive( false ); +} + +BEGIN_DATADESC( CPointCamera ) + + // Save/restore Keyvalue fields + DEFINE_KEYFIELD( m_FOV, FIELD_FLOAT, "FOV" ), + DEFINE_KEYFIELD( m_Resolution, FIELD_FLOAT, "resolution" ), + DEFINE_KEYFIELD( m_bFogEnable, FIELD_BOOLEAN, "fogEnable" ), + DEFINE_KEYFIELD( m_FogColor, FIELD_COLOR32, "fogColor" ), + DEFINE_KEYFIELD( m_flFogStart, FIELD_FLOAT, "fogStart" ), + DEFINE_KEYFIELD( m_flFogEnd, FIELD_FLOAT, "fogEnd" ), + DEFINE_KEYFIELD( m_flFogMaxDensity, FIELD_FLOAT, "fogMaxDensity" ), + DEFINE_KEYFIELD( m_bUseScreenAspectRatio, FIELD_BOOLEAN, "UseScreenAspectRatio" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iSkyMode, FIELD_INTEGER, "SkyMode" ), + DEFINE_KEYFIELD( m_iszRenderTarget, FIELD_STRING, "RenderTarget" ), +#endif + DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bIsOn, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_TargetFOV, FIELD_FLOAT ), + DEFINE_FIELD( m_DegreesPerSecond, FIELD_FLOAT ), + // This is re-set up in the constructor + //DEFINE_FIELD( m_pNext, FIELD_CLASSPTR ), + + DEFINE_FUNCTION( ChangeFOVThink ), + + // Input + DEFINE_INPUTFUNC( FIELD_STRING, "ChangeFOV", InputChangeFOV ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetOnAndTurnOthersOff", InputSetOnAndTurnOthersOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetOn", InputSetOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetOff", InputSetOff ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetSkyMode", InputSetSkyMode ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetRenderTarget", InputSetRenderTarget ), +#endif + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPointCamera, DT_PointCamera ) + SendPropFloat( SENDINFO( m_FOV ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_Resolution ), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_bFogEnable ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_STRUCTELEM( m_FogColor ), 32, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO( m_flFogStart ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_flFogEnd ), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO( m_flFogMaxDensity ), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_bActive ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_bUseScreenAspectRatio ), 1, SPROP_UNSIGNED ), +#ifdef MAPBASE + SendPropInt( SENDINFO( m_iSkyMode ) ), + SendPropStringT( SENDINFO( m_iszRenderTarget ) ), +#endif +END_SEND_TABLE() + +#ifdef MAPBASE + +//============================================================================= +// Orthographic point_camera +//============================================================================= + +BEGIN_DATADESC( CPointCameraOrtho ) + + DEFINE_KEYFIELD( m_bOrtho, FIELD_BOOLEAN, "IsOrtho" ), + DEFINE_ARRAY( m_OrthoDimensions, FIELD_FLOAT, CPointCameraOrtho::NUM_ORTHO_DIMENSIONS ), + + DEFINE_ARRAY( m_TargetOrtho, FIELD_FLOAT, CPointCameraOrtho::NUM_ORTHO_DIMENSIONS ), + DEFINE_FIELD( m_TargetOrthoDPS, FIELD_FLOAT ), + + DEFINE_FUNCTION( ChangeOrthoThink ), + + // Input + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetOrthoEnabled", InputSetOrthoEnabled ), + DEFINE_INPUTFUNC( FIELD_STRING, "ScaleOrtho", InputScaleOrtho ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetOrthoTop", InputSetOrthoTop ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetOrthoBottom", InputSetOrthoBottom ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetOrthoLeft", InputSetOrthoLeft ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetOrthoRight", InputSetOrthoRight ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPointCameraOrtho, DT_PointCameraOrtho ) + SendPropInt( SENDINFO( m_bOrtho ), 1, SPROP_UNSIGNED ), + SendPropArray( SendPropFloat(SENDINFO_ARRAY(m_OrthoDimensions), CPointCameraOrtho::NUM_ORTHO_DIMENSIONS, SPROP_NOSCALE ), m_OrthoDimensions ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( point_camera_ortho, CPointCameraOrtho ); + +CPointCameraOrtho::~CPointCameraOrtho() +{ +} + +CPointCameraOrtho::CPointCameraOrtho() +{ +} + +void CPointCameraOrtho::Spawn( void ) +{ + BaseClass::Spawn(); + + // If 0, get the FOV + if (m_OrthoDimensions[ORTHO_TOP] == 0.0f) + m_OrthoDimensions.Set( ORTHO_TOP, GetFOV() ); + + // If 0, get the negative top ortho + if (m_OrthoDimensions[ORTHO_BOTTOM] == 0.0f) + m_OrthoDimensions.Set( ORTHO_BOTTOM, -m_OrthoDimensions[ORTHO_TOP] ); + + // If 0, get the top ortho + if (m_OrthoDimensions[ORTHO_LEFT] == 0.0f) + m_OrthoDimensions.Set( ORTHO_LEFT, m_OrthoDimensions[ORTHO_TOP] ); + + // If 0, get the negative left ortho + if (m_OrthoDimensions[ORTHO_RIGHT] == 0.0f) + m_OrthoDimensions.Set( ORTHO_RIGHT, -m_OrthoDimensions[ORTHO_LEFT] ); +} + +bool CPointCameraOrtho::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( strncmp( szKeyName, "Ortho", 5 ) == 0 ) + { + int iOrtho = atoi(szKeyName + 5); + m_OrthoDimensions.Set( iOrtho, atof( szValue ) ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +bool CPointCameraOrtho::GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ) +{ + if ( strncmp( szKeyName, "Ortho", 5 ) ) + { + int iOrtho = atoi(szKeyName + 5); + Q_snprintf( szValue, iMaxLen, "%f", m_OrthoDimensions[iOrtho] ); + } + else + return BaseClass::GetKeyValue( szKeyName, szValue, iMaxLen ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCameraOrtho::ChangeOrtho( int iType, const char *szChange ) +{ + // Parse the keyvalue data + char parseString[255]; + Q_strncpy( parseString, szChange, sizeof( parseString ) ); + + // Get Ortho + char *pszParam = strtok( parseString, " " ); + if (pszParam) + { + m_TargetOrtho[iType] = atof( pszParam ); + } + else + { + // Assume no change + m_TargetOrtho[iType] = m_OrthoDimensions[iType]; + } + + // Get Time + float flChangeTime; + pszParam = strtok( NULL, " " ); + if (pszParam) + { + flChangeTime = atof( pszParam ); + } + else + { + // Assume 1 second + flChangeTime = 1.0; + } + + m_TargetOrthoDPS = ( m_TargetOrtho[iType] - m_OrthoDimensions[iType] ) / flChangeTime; + + SetThink( &CPointCameraOrtho::ChangeOrthoThink ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCameraOrtho::InputScaleOrtho( inputdata_t &inputdata ) +{ + // Parse the keyvalue data + char parseString[255]; + Q_strncpy( parseString, inputdata.value.String(), sizeof( parseString ) ); + + // Get Scale + float flScale = 1.0f; + char *pszParam = strtok( parseString, " " ); + if (pszParam) + { + flScale = atof( pszParam ); + } + + // Get Time + float flChangeTime = 1.0f; + pszParam = strtok( NULL, " " ); + if (pszParam) + { + flChangeTime = atof( pszParam ); + } + + int iLargest = 0; + for (int i = 0; i < NUM_ORTHO_DIMENSIONS; i++) + { + m_TargetOrtho[i] = flScale * m_OrthoDimensions[i]; + + if (m_TargetOrtho[iLargest] <= m_TargetOrtho[i]) + iLargest = i; + } + + m_TargetOrthoDPS = (m_TargetOrtho[iLargest] - m_OrthoDimensions[iLargest]) / flChangeTime; + + SetThink( &CPointCameraOrtho::ChangeOrthoThink ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointCameraOrtho::ChangeOrthoThink( void ) +{ + SetNextThink( gpGlobals->curtime + CAM_THINK_INTERVAL ); + + int iChanging = 0; + for (int i = 0; i < NUM_ORTHO_DIMENSIONS; i++) + { + float newDim = m_OrthoDimensions[i]; + if (newDim == m_TargetOrtho[i]) + continue; + + newDim += m_TargetOrthoDPS * CAM_THINK_INTERVAL; + + if (m_TargetOrthoDPS < 0) + { + if (newDim <= m_TargetOrtho[i]) + { + newDim = m_TargetOrtho[i]; + } + } + else + { + if (newDim >= m_TargetOrtho[i]) + { + newDim = m_TargetOrtho[i]; + } + } + + m_OrthoDimensions.Set(i, newDim); + } + + if (iChanging == 0) + SetThink( NULL ); +} +#endif diff --git a/sp/src/game/server/point_camera.h b/sp/src/game/server/point_camera.h new file mode 100644 index 00000000..c669ab82 --- /dev/null +++ b/sp/src/game/server/point_camera.h @@ -0,0 +1,120 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef CAMERA_H +#define CAMERA_H +#ifdef _WIN32 +#pragma once +#endif + +#include "cbase.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPointCamera : public CBaseEntity +{ +public: + DECLARE_CLASS( CPointCamera, CBaseEntity ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + CPointCamera(); + ~CPointCamera(); + + void Spawn( void ); + + // Tell the client that this camera needs to be rendered + void SetActive( bool bActive ); + int UpdateTransmitState(void); + + void ChangeFOVThink( void ); + + void InputChangeFOV( inputdata_t &inputdata ); + void InputSetOnAndTurnOthersOff( inputdata_t &inputdata ); + void InputSetOn( inputdata_t &inputdata ); + void InputSetOff( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetSkyMode( inputdata_t &inputdata ) { m_iSkyMode = inputdata.value.Int(); } + void InputSetRenderTarget( inputdata_t &inputdata ) { m_iszRenderTarget = inputdata.value.StringID(); } + + float GetFOV() const { return m_FOV; } +#endif + +private: + float m_TargetFOV; + float m_DegreesPerSecond; + + CNetworkVar( float, m_FOV ); + CNetworkVar( float, m_Resolution ); + CNetworkVar( bool, m_bFogEnable ); + CNetworkColor32( m_FogColor ); + CNetworkVar( float, m_flFogStart ); + CNetworkVar( float, m_flFogEnd ); + CNetworkVar( float, m_flFogMaxDensity ); + CNetworkVar( bool, m_bActive ); + CNetworkVar( bool, m_bUseScreenAspectRatio ); +#ifdef MAPBASE + CNetworkVar( int, m_iSkyMode ); + CNetworkVar( string_t, m_iszRenderTarget ); +#endif + + // Allows the mapmaker to control whether a camera is active or not + bool m_bIsOn; + +public: + CPointCamera *m_pNext; +}; + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPointCameraOrtho : public CPointCamera +{ +public: + DECLARE_CLASS( CPointCameraOrtho, CPointCamera ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + CPointCameraOrtho(); + ~CPointCameraOrtho(); + + enum + { + ORTHO_TOP, + ORTHO_BOTTOM, + ORTHO_LEFT, + ORTHO_RIGHT, + + NUM_ORTHO_DIMENSIONS + }; + + void Spawn( void ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + bool GetKeyValue( const char *szKeyName, char *szValue, int iMaxLen ); + + void ChangeOrtho( int iType, const char *szChange ); + void ChangeOrthoThink( void ); + + void InputSetOrthoEnabled( inputdata_t &inputdata ) { m_bOrtho = inputdata.value.Bool(); } + void InputScaleOrtho( inputdata_t &inputdata ); + void InputSetOrthoTop( inputdata_t &inputdata ) { ChangeOrtho(ORTHO_TOP, inputdata.value.String()); } + void InputSetOrthoBottom( inputdata_t &inputdata ) { ChangeOrtho( ORTHO_BOTTOM, inputdata.value.String() ); } + void InputSetOrthoLeft( inputdata_t &inputdata ) { ChangeOrtho( ORTHO_LEFT, inputdata.value.String() ); } + void InputSetOrthoRight( inputdata_t &inputdata ) { ChangeOrtho( ORTHO_RIGHT, inputdata.value.String() ); } + +private: + float m_TargetOrtho[NUM_ORTHO_DIMENSIONS]; + float m_TargetOrthoDPS; + + CNetworkVar( bool, m_bOrtho ); + CNetworkArray( float, m_OrthoDimensions, NUM_ORTHO_DIMENSIONS ); +}; +#endif + +CPointCamera *GetPointCameraList(); +#endif // CAMERA_H diff --git a/sp/src/game/server/point_devshot_camera.cpp b/sp/src/game/server/point_devshot_camera.cpp new file mode 100644 index 00000000..4d476664 --- /dev/null +++ b/sp/src/game/server/point_devshot_camera.cpp @@ -0,0 +1,254 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: A camera entity that's used by the -makedevshots system to take +// dev screenshots everytime the map is checked into source control. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "tier0/icommandline.h" +#include "igamesystem.h" +#include "filesystem.h" +#include + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +int g_iDevShotCameraCount = 0; +#define DEVSHOT_INITIAL_WAIT 5 // Time after the level spawn before the first devshot camera takes it's shot +#define DEVSHOT_INTERVAL 5 // Time between each devshot camera taking it's shot + +//----------------------------------------------------------------------------- +// Purpose: A camera entity that's used by the -makedevshots system to take +// dev screenshots everytime the map is checked into source control. +//----------------------------------------------------------------------------- +class CPointDevShotCamera : public CBaseEntity +{ + DECLARE_CLASS( CPointDevShotCamera, CBaseEntity ); +public: + DECLARE_DATADESC(); + + void Spawn( void ); + void DevShotThink_Setup( void ); + void DevShotThink_TakeShot( void ); + void DevShotThink_PostShot( void ); + + // Always transmit to clients so they know where to move the view to + virtual int UpdateTransmitState(); + +private: + string_t m_iszCameraName; + int m_iFOV; +}; + +BEGIN_DATADESC( CPointDevShotCamera ) + DEFINE_FUNCTION( DevShotThink_Setup ), + DEFINE_FUNCTION( DevShotThink_TakeShot ), + DEFINE_FUNCTION( DevShotThink_PostShot ), + + DEFINE_KEYFIELD( m_iszCameraName, FIELD_STRING, "cameraname" ), + DEFINE_KEYFIELD( m_iFOV, FIELD_INTEGER, "FOV" ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( point_devshot_camera, CPointDevShotCamera ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointDevShotCamera::Spawn( void ) +{ + BaseClass::Spawn(); + + // Remove this entity immediately if we're not making devshots + if ( !CommandLine()->FindParm("-makedevshots") ) + { + UTIL_Remove( this ); + return; + } + + // Take a screenshot when it's my turn + SetThink( &CPointDevShotCamera::DevShotThink_Setup ); + SetNextThink( gpGlobals->curtime + DEVSHOT_INITIAL_WAIT + (g_iDevShotCameraCount * DEVSHOT_INTERVAL) ); + + g_iDevShotCameraCount++; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPointDevShotCamera::UpdateTransmitState() +{ + // always transmit if currently used by a monitor + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointDevShotCamera::DevShotThink_Setup( void ) +{ + // Move the player to the devshot camera + CBasePlayer *pPlayer = UTIL_GetLocalPlayerOrListenServerHost(); + if ( !pPlayer ) + return; + + // Hide stuff + engine->ClientCommand( pPlayer->edict(), "developer 0" ); + engine->ClientCommand( pPlayer->edict(), "cl_drawhud 0" ); + engine->ClientCommand( pPlayer->edict(), "sv_cheats 1" ); + engine->ClientCommand( pPlayer->edict(), "god" ); + engine->ClientCommand( pPlayer->edict(), "notarget" ); + + pPlayer->AddSolidFlags( FSOLID_NOT_SOLID ); + pPlayer->EnableControl(FALSE); + pPlayer->SetViewEntity( this ); + pPlayer->SetFOV( this, m_iFOV ); + + // Hide the player's viewmodel + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->AddEffects( EF_NODRAW ); + } + + DispatchUpdateTransmitState(); + + // Now take the shot next frame + SetThink( &CPointDevShotCamera::DevShotThink_TakeShot ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointDevShotCamera::DevShotThink_TakeShot( void ) +{ + // Take the screenshot + CBasePlayer *pPlayer = UTIL_GetLocalPlayerOrListenServerHost(); + if ( !pPlayer ) + return; + + engine->ClientCommand( pPlayer->edict(), "devshots_screenshot \"%s\"", STRING(m_iszCameraName) ); + + // Now take the shot next frame + SetThink( &CPointDevShotCamera::DevShotThink_PostShot ); + SetNextThink( gpGlobals->curtime + (DEVSHOT_INTERVAL - 1) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointDevShotCamera::DevShotThink_PostShot( void ) +{ + // Take the screenshot + CBasePlayer *pPlayer = UTIL_GetLocalPlayerOrListenServerHost(); + if ( !pPlayer ) + return; + + pPlayer->SetFOV( this, 0 ); + + // If all cameras have taken their shots, move to the next map + g_iDevShotCameraCount--; + if ( !g_iDevShotCameraCount ) + { + engine->ClientCommand( pPlayer->edict(), "devshots_nextmap" ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Game system to detect maps without cameras in them, and move on +//----------------------------------------------------------------------------- +class CDevShotSystem : public CAutoGameSystemPerFrame +{ +public: + + CDevShotSystem( char const *name ) : CAutoGameSystemPerFrame( name ) + { + } + + virtual void LevelInitPreEntity() + { + m_bIssuedNextMapCommand = false; + g_iDevShotCameraCount = 0; + m_bParsedMapFile = false; + } + + virtual void SafeRemoveIfDesired( void ) + { + // If we're not making devshots, remove this system immediately + if ( !CommandLine()->FindParm("-makedevshots") ) + { + Remove( this ); + return; + } + } + + virtual void FrameUpdatePostEntityThink( void ) + { + // Wait until we're all spawned in + if ( gpGlobals->curtime < 5 ) + return; + + if ( m_bIssuedNextMapCommand ) + return; + + if ( !m_bParsedMapFile ) + { + m_bParsedMapFile = true; + + // See if we've got a camera file to import cameras from + char szFullName[512]; + Q_snprintf(szFullName,sizeof(szFullName), "maps/%s.txt", STRING( gpGlobals->mapname )); + KeyValues *pkvMapCameras = new KeyValues( "MapCameras" ); + if ( pkvMapCameras->LoadFromFile( filesystem, szFullName, "MOD" ) ) + { + Warning( "Devshots: Loading point_devshot_camera positions from %s. \n", szFullName ); + + // Get each camera, and add it to our list + KeyValues *pkvCamera = pkvMapCameras->GetFirstSubKey(); + while ( pkvCamera ) + { + // Get camera name + const char *pCameraName = pkvCamera->GetName(); + + // Make a camera, and move it to the position specified + CPointDevShotCamera *pCamera = (CPointDevShotCamera*)CreateEntityByName( "point_devshot_camera" ); + Assert( pCamera ); + pCamera->KeyValue( "cameraname", pCameraName ); + pCamera->KeyValue( "origin", pkvCamera->GetString( "origin", "0 0 0" ) ); + pCamera->KeyValue( "angles", pkvCamera->GetString( "angles", "0 0 0" ) ); + pCamera->KeyValue( "FOV", pkvCamera->GetString( "FOV", "75" ) ); + DispatchSpawn( pCamera ); + pCamera->Activate(); + + // Move to next camera + pkvCamera = pkvCamera->GetNextKey(); + } + } + +#ifdef MAPBASE // VDC Memory Leak Fixes + pkvMapCameras->deleteThis(); +#endif + + if ( !g_iDevShotCameraCount ) + { + Warning( "Devshots: No point_devshot_camera in %s. Moving to next map.\n", STRING( gpGlobals->mapname ) ); + + CBasePlayer *pPlayer = UTIL_GetLocalPlayerOrListenServerHost(); + if ( pPlayer ) + { + engine->ClientCommand( pPlayer->edict(), "devshots_nextmap" ); + m_bIssuedNextMapCommand = true; + return; + } + } + } + } + +private: + bool m_bIssuedNextMapCommand; + bool m_bParsedMapFile; +}; + +CDevShotSystem DevShotSystem( "CDevShotSystem" ); diff --git a/sp/src/game/server/point_entity_finder.cpp b/sp/src/game/server/point_entity_finder.cpp new file mode 100644 index 00000000..d26b71f4 --- /dev/null +++ b/sp/src/game/server/point_entity_finder.cpp @@ -0,0 +1,207 @@ +//----------------------------------------------------------------------------- +// class CPointEntityFinder +// +// Purpose: Finds an entity using a specified heuristic and outputs it as !caller +// with the OnFoundEntity output. +//----------------------------------------------------------------------------- + +#include "cbase.h" +#include "filters.h" + +// NOTE: This has to be the last file included! +#include "tier0/memdbgon.h" + + +enum EntFinderMethod_t +{ + ENT_FIND_METHOD_NEAREST = 0, + ENT_FIND_METHOD_FARTHEST, + ENT_FIND_METHOD_RANDOM, +}; + +class CPointEntityFinder : public CBaseEntity +{ + void Activate( void ); + + DECLARE_CLASS( CPointEntityFinder, CBaseEntity ); + +private: + + EHANDLE m_hEntity; + string_t m_iFilterName; + CHandle m_hFilter; + string_t m_iRefName; + EHANDLE m_hReference; + + EntFinderMethod_t m_FindMethod; + + void FindEntity( void ); + void FindByDistance( void ); + void FindByRandom( void ); + + // Input handlers + void InputFindEntity( inputdata_t &inputdata ); + + // Output handlers + COutputEvent m_OnFoundEntity; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( point_entity_finder, CPointEntityFinder ); + +BEGIN_DATADESC( CPointEntityFinder ) + + DEFINE_KEYFIELD( m_FindMethod, FIELD_INTEGER, "method" ), + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + DEFINE_FIELD( m_hFilter, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_iRefName, FIELD_STRING, "referencename" ), + DEFINE_FIELD( m_hReference, FIELD_EHANDLE ), + + DEFINE_OUTPUT( m_OnFoundEntity, "OnFoundEntity" ), + + //--------------------------------- + + DEFINE_INPUTFUNC( FIELD_VOID, "FindEntity", InputFindEntity ), + +END_DATADESC() + + +void CPointEntityFinder::Activate( void ) +{ + // Get the filter, if it exists. + if (m_iFilterName != NULL_STRING) + { + m_hFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iFilterName )); + } + + BaseClass::Activate(); +} + + +void CPointEntityFinder::FindEntity( void ) +{ + // Get the reference entity, if it exists. + if (m_iRefName != NULL_STRING) + { + m_hReference = gEntList.FindEntityByName( NULL, m_iRefName ); + } + + switch ( m_FindMethod ) + { + + case ( ENT_FIND_METHOD_NEAREST ): + FindByDistance(); + break; + case ( ENT_FIND_METHOD_FARTHEST ): + FindByDistance(); + break; + case ( ENT_FIND_METHOD_RANDOM ): + FindByRandom(); + break; + } +} + +void CPointEntityFinder::FindByDistance( void ) +{ + m_hEntity = NULL; + CBaseFilter *pFilter = m_hFilter.Get(); + +// go through each entity and determine whether it's closer or farther from the current entity. Pick according to Method selected. + + float flBestDist = 0; + CBaseEntity *pEntity = gEntList.FirstEnt(); + while ( pEntity ) + { + if ( FStrEq( STRING( pEntity->m_iClassname ), "worldspawn" ) + || FStrEq( STRING( pEntity->m_iClassname ), "soundent" ) + || FStrEq( STRING( pEntity->m_iClassname ), "player_manager" ) + || FStrEq( STRING( pEntity->m_iClassname ), "bodyque" ) + || FStrEq( STRING( pEntity->m_iClassname ), "ai_network" ) + || pEntity == this + || ( pFilter && !( pFilter->PassesFilter( this, pEntity ) ) ) ) + { + pEntity = gEntList.NextEnt( pEntity ); + continue; + } + + // if we have a reference entity, use that, otherwise, check against 'this' + Vector vecStart; + if ( m_hReference ) + { + vecStart = m_hReference->GetAbsOrigin(); + } + else + { + vecStart = GetAbsOrigin(); + } + + // init m_hEntity with a valid entity. + if (m_hEntity == NULL ) + { + m_hEntity = pEntity; + flBestDist = ( pEntity->GetAbsOrigin() - vecStart ).LengthSqr(); + } + + float flNewDist = ( pEntity->GetAbsOrigin() - vecStart ).LengthSqr(); + + switch ( m_FindMethod ) + { + + case ( ENT_FIND_METHOD_NEAREST ): + if ( flNewDist < flBestDist ) + { + m_hEntity = pEntity; + flBestDist = flNewDist; + } + break; + + case ( ENT_FIND_METHOD_FARTHEST ): + if ( flNewDist > flBestDist ) + { + m_hEntity = pEntity; + flBestDist = flNewDist; + } + break; + + default: + Assert( false ); + break; + } + + pEntity = gEntList.NextEnt( pEntity ); + } +} + +void CPointEntityFinder::FindByRandom( void ) +{ + // TODO: optimize the case where there is no filter + m_hEntity = NULL; + CBaseFilter *pFilter = m_hFilter.Get(); + CUtlVector ValidEnts; + + CBaseEntity *pEntity = gEntList.FirstEnt(); + do // note all valid entities. + { + if ( pFilter && pFilter->PassesFilter( this, pEntity ) ) + { + ValidEnts.AddToTail( pEntity ); + } + + pEntity = gEntList.NextEnt( pEntity ); + + } while ( pEntity ); + + // pick one at random + if ( ValidEnts.Count() != 0 ) + { + m_hEntity = ValidEnts[ RandomInt( 0, ValidEnts.Count() - 1 )]; + } +} + +void CPointEntityFinder::InputFindEntity( inputdata_t &inputdata ) +{ + FindEntity(); + + m_OnFoundEntity.FireOutput( inputdata.pActivator, m_hEntity ); +} \ No newline at end of file diff --git a/sp/src/game/server/point_playermoveconstraint.cpp b/sp/src/game/server/point_playermoveconstraint.cpp new file mode 100644 index 00000000..df78487e --- /dev/null +++ b/sp/src/game/server/point_playermoveconstraint.cpp @@ -0,0 +1,161 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: An entity that can be used to constrain the player's movement around it +// +//=============================================================================// + +#include "cbase.h" +#include "saverestore_utlvector.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_TELEPORT_TO_SPAWN_POS 0x00000001 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPointPlayerMoveConstraint : public CBaseEntity +{ + DECLARE_CLASS( CPointPlayerMoveConstraint, CBaseEntity ); +public: + DECLARE_DATADESC(); + + int UpdateTransmitState( void ); + void Activate( void ); + void ConstraintThink( void ); + + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + +private: + float m_flRadius; + float m_flConstraintWidth; + float m_flSpeedFactor; + float m_flRadiusSquared; + CUtlVector m_hConstrainedPlayers; + COutputEvent m_OnConstraintBroken; +}; + +LINK_ENTITY_TO_CLASS( point_playermoveconstraint, CPointPlayerMoveConstraint ); + +BEGIN_DATADESC( CPointPlayerMoveConstraint ) + + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + DEFINE_KEYFIELD( m_flConstraintWidth, FIELD_FLOAT, "width" ), + DEFINE_KEYFIELD( m_flSpeedFactor, FIELD_FLOAT, "speedfactor" ), + // DEFINE_FIELD( m_flRadiusSquared, FIELD_FLOAT ), // Don't Save + DEFINE_UTLVECTOR( m_hConstrainedPlayers, FIELD_EHANDLE ), + + DEFINE_THINKFUNC( ConstraintThink ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + + DEFINE_OUTPUT( m_OnConstraintBroken, "OnConstraintBroken" ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPointPlayerMoveConstraint::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointPlayerMoveConstraint::Activate( void ) +{ + BaseClass::Activate(); + + m_flRadiusSquared = (m_flRadius * m_flRadius); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointPlayerMoveConstraint::InputTurnOn( inputdata_t &inputdata ) +{ + // Find all players within our radius and constraint them + float flRadius = m_flRadius; + // If we're in singleplayer, blow the radius a bunch + if ( gpGlobals->maxClients == 1 ) + { + flRadius = MAX_COORD_RANGE; + } + CBaseEntity *pEntity = NULL; + while ( (pEntity = gEntList.FindEntityByClassnameWithin( pEntity, "player", GetLocalOrigin(), flRadius)) != NULL ) + { + CBasePlayer *pPlayer = ToBasePlayer( pEntity ); + Assert( pPlayer ); + + // Only add him if he's not already constrained + if ( m_hConstrainedPlayers.Find( pPlayer ) == m_hConstrainedPlayers.InvalidIndex() ) + { + m_hConstrainedPlayers.AddToTail( pPlayer ); + + pPlayer->ActivateMovementConstraint( this, GetAbsOrigin(), m_flRadius, m_flConstraintWidth, m_flSpeedFactor ); + } + } + + // Only think if we found any + if ( m_hConstrainedPlayers.Count() ) + { + SetThink( &CPointPlayerMoveConstraint::ConstraintThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + +//------------------------------------------------------------------------------ +// Purpose: Release all players we've constrained +//------------------------------------------------------------------------------ +void CPointPlayerMoveConstraint::InputTurnOff( inputdata_t &inputdata ) +{ + int iCount = m_hConstrainedPlayers.Count(); + for ( int i = 0; i < iCount; i++ ) + { + CBasePlayer *pPlayer = ToBasePlayer( m_hConstrainedPlayers[i] ); + if ( pPlayer ) + { + pPlayer->DeactivateMovementConstraint(); + } + } + + m_hConstrainedPlayers.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: Check to see if any of our constrained players have broken the constraint +//----------------------------------------------------------------------------- +void CPointPlayerMoveConstraint::ConstraintThink( void ) +{ + int iCount = m_hConstrainedPlayers.Count(); + + // Count backwards, because we might drop them if they've broken the constraint + for ( int i = (iCount-1); i >= 0; i-- ) + { + CBasePlayer *pPlayer = ToBasePlayer( m_hConstrainedPlayers[i] ); + if ( pPlayer ) + { + float flDistanceSqr = (pPlayer->GetAbsOrigin() - GetAbsOrigin()).LengthSqr(); + if ( flDistanceSqr > m_flRadiusSquared ) + { + // Break the constraint to this player + pPlayer->DeactivateMovementConstraint(); + m_hConstrainedPlayers.Remove(i); + + // Fire the broken output + m_OnConstraintBroken.FireOutput( this, pPlayer ); + } + } + } + + // Only keep thinking if we any left + if ( m_hConstrainedPlayers.Count() ) + { + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} diff --git a/sp/src/game/server/point_spotlight.cpp b/sp/src/game/server/point_spotlight.cpp new file mode 100644 index 00000000..d5901714 --- /dev/null +++ b/sp/src/game/server/point_spotlight.cpp @@ -0,0 +1,567 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "cbase.h" +#include "beam_shared.h" +#include "spotlightend.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Spawnflags +#define SF_SPOTLIGHT_START_LIGHT_ON 0x1 +#define SF_SPOTLIGHT_NO_DYNAMIC_LIGHT 0x2 + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPointSpotlight : public CPointEntity +{ + DECLARE_CLASS( CPointSpotlight, CPointEntity ); +public: + DECLARE_DATADESC(); + + CPointSpotlight(); +#ifdef MAPBASE + ~CPointSpotlight(); +#endif + + void Precache(void); + void Spawn(void); + virtual void Activate(); + + virtual void OnEntityEvent( EntityEvent_t event, void *pEventData ); + +private: + int UpdateTransmitState(); + void SpotlightThink(void); + void SpotlightUpdate(void); + Vector SpotlightCurrentPos(void); + void SpotlightCreate(void); + void SpotlightDestroy(void); + + // ------------------------------ + // Inputs + // ------------------------------ + void InputLightOn( inputdata_t &inputdata ); + void InputLightOff( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputLightToggle( inputdata_t &inputdata ) { m_bSpotlightOn ? InputLightOff(inputdata) : InputLightOn(inputdata); } +#endif + + // Creates the efficient spotlight + void CreateEfficientSpotlight(); + + // Computes render info for a spotlight + void ComputeRenderInfo(); + +private: + bool m_bSpotlightOn; + bool m_bEfficientSpotlight; + Vector m_vSpotlightTargetPos; + Vector m_vSpotlightCurrentPos; + Vector m_vSpotlightDir; + int m_nHaloSprite; + CHandle m_hSpotlight; + CHandle m_hSpotlightTarget; + + float m_flSpotlightMaxLength; + float m_flSpotlightCurLength; + float m_flSpotlightGoalWidth; + float m_flHDRColorScale; + int m_nMinDXLevel; + +#ifdef MAPBASE + float m_flHaloScale; + string_t m_iszHaloMaterial; + string_t m_iszSpotlightMaterial; +#endif + +public: + COutputEvent m_OnOn, m_OnOff; ///< output fires when turned on, off +}; + +BEGIN_DATADESC( CPointSpotlight ) + DEFINE_FIELD( m_flSpotlightCurLength, FIELD_FLOAT ), + + DEFINE_FIELD( m_bSpotlightOn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bEfficientSpotlight, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vSpotlightTargetPos, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vSpotlightCurrentPos, FIELD_POSITION_VECTOR ), + + // Robin: Don't Save, recreated after restore/transition + //DEFINE_FIELD( m_hSpotlight, FIELD_EHANDLE ), + //DEFINE_FIELD( m_hSpotlightTarget, FIELD_EHANDLE ), + + DEFINE_FIELD( m_vSpotlightDir, FIELD_VECTOR ), + DEFINE_FIELD( m_nHaloSprite, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_flSpotlightMaxLength,FIELD_FLOAT, "SpotlightLength"), + DEFINE_KEYFIELD( m_flSpotlightGoalWidth,FIELD_FLOAT, "SpotlightWidth"), + DEFINE_KEYFIELD( m_flHDRColorScale, FIELD_FLOAT, "HDRColorScale" ), + DEFINE_KEYFIELD( m_nMinDXLevel, FIELD_INTEGER, "mindxlevel" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_flHaloScale, FIELD_FLOAT, "HaloScale" ), + DEFINE_KEYFIELD( m_iszHaloMaterial, FIELD_STRING, "HaloMaterial" ), + DEFINE_KEYFIELD( m_iszSpotlightMaterial, FIELD_STRING, "SpotlightMaterial" ), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "LightOn", InputLightOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "LightOff", InputLightOff ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "LightToggle", InputLightToggle ), +#endif + DEFINE_OUTPUT( m_OnOn, "OnLightOn" ), + DEFINE_OUTPUT( m_OnOff, "OnLightOff" ), + + DEFINE_THINKFUNC( SpotlightThink ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS(point_spotlight, CPointSpotlight); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPointSpotlight::CPointSpotlight() +{ +#ifdef _DEBUG + m_vSpotlightTargetPos.Init(); + m_vSpotlightCurrentPos.Init(); + m_vSpotlightDir.Init(); +#endif + m_flHDRColorScale = 1.0f; + m_nMinDXLevel = 0; +#ifdef MAPBASE + m_flHaloScale = 60.0f; +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPointSpotlight::~CPointSpotlight() +{ + SpotlightDestroy(); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointSpotlight::Precache(void) +{ + BaseClass::Precache(); + + // Sprites. +#ifdef MAPBASE + if (m_iszHaloMaterial == NULL_STRING) + { + m_iszHaloMaterial = AllocPooledString( "sprites/light_glow03.vmt" ); + } + + if (m_iszSpotlightMaterial == NULL_STRING) + { + m_iszSpotlightMaterial = AllocPooledString( "sprites/glow_test02.vmt" ); + } + + m_nHaloSprite = PrecacheModel( STRING( m_iszHaloMaterial ) ); + PrecacheModel( STRING( m_iszSpotlightMaterial ) ); +#else + m_nHaloSprite = PrecacheModel("sprites/light_glow03.vmt"); + PrecacheModel( "sprites/glow_test02.vmt" ); +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointSpotlight::Spawn(void) +{ + Precache(); + + UTIL_SetSize( this,vec3_origin,vec3_origin ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); + m_bEfficientSpotlight = true; + + // Check for user error + if (m_flSpotlightMaxLength <= 0) + { + DevMsg("%s (%s) has an invalid spotlight length <= 0, setting to 500\n", GetClassname(), GetDebugName() ); + m_flSpotlightMaxLength = 500; + } + if (m_flSpotlightGoalWidth <= 0) + { + DevMsg("%s (%s) has an invalid spotlight width <= 0, setting to 10\n", GetClassname(), GetDebugName() ); + m_flSpotlightGoalWidth = 10; + } + + if (m_flSpotlightGoalWidth > MAX_BEAM_WIDTH ) + { + DevMsg("%s (%s) has an invalid spotlight width %.1f (max %.1f).\n", GetClassname(), GetDebugName(), m_flSpotlightGoalWidth, MAX_BEAM_WIDTH ); + m_flSpotlightGoalWidth = MAX_BEAM_WIDTH; + } + + // ------------------------------------ + // Init all class vars + // ------------------------------------ + m_vSpotlightTargetPos = vec3_origin; + m_vSpotlightCurrentPos = vec3_origin; + m_hSpotlight = NULL; + m_hSpotlightTarget = NULL; + m_vSpotlightDir = vec3_origin; + m_flSpotlightCurLength = m_flSpotlightMaxLength; + + m_bSpotlightOn = HasSpawnFlags( SF_SPOTLIGHT_START_LIGHT_ON ); + + SetThink( &CPointSpotlight::SpotlightThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +//----------------------------------------------------------------------------- +// Computes render info for a spotlight +//----------------------------------------------------------------------------- +void CPointSpotlight::ComputeRenderInfo() +{ + // Fade out spotlight end if past max length. + if ( m_flSpotlightCurLength > 2*m_flSpotlightMaxLength ) + { + m_hSpotlightTarget->SetRenderColorA( 0 ); + m_hSpotlight->SetFadeLength( m_flSpotlightMaxLength ); + } + else if ( m_flSpotlightCurLength > m_flSpotlightMaxLength ) + { + m_hSpotlightTarget->SetRenderColorA( (1-((m_flSpotlightCurLength-m_flSpotlightMaxLength)/m_flSpotlightMaxLength)) ); + m_hSpotlight->SetFadeLength( m_flSpotlightMaxLength ); + } + else + { + m_hSpotlightTarget->SetRenderColorA( 1.0 ); + m_hSpotlight->SetFadeLength( m_flSpotlightCurLength ); + } + + // Adjust end width to keep beam width constant + float flNewWidth = m_flSpotlightGoalWidth * (m_flSpotlightCurLength / m_flSpotlightMaxLength); + flNewWidth = clamp(flNewWidth, 0.f, MAX_BEAM_WIDTH ); + m_hSpotlight->SetEndWidth(flNewWidth); + + // Adjust width of light on the end. + if ( FBitSet (m_spawnflags, SF_SPOTLIGHT_NO_DYNAMIC_LIGHT) ) + { + m_hSpotlightTarget->m_flLightScale = 0.0; + } + else + { + // <> - magic number 1.8 depends on sprite size + m_hSpotlightTarget->m_flLightScale = 1.8*flNewWidth; + } +} + + +//----------------------------------------------------------------------------- +// Creates the efficient spotlight +//----------------------------------------------------------------------------- +void CPointSpotlight::CreateEfficientSpotlight() +{ + if ( m_hSpotlightTarget.Get() != NULL ) + return; + + SpotlightCreate(); + m_vSpotlightCurrentPos = SpotlightCurrentPos(); + m_hSpotlightTarget->SetAbsOrigin( m_vSpotlightCurrentPos ); + m_hSpotlightTarget->m_vSpotlightOrg = GetAbsOrigin(); + VectorSubtract( m_hSpotlightTarget->GetAbsOrigin(), m_hSpotlightTarget->m_vSpotlightOrg, m_hSpotlightTarget->m_vSpotlightDir ); + m_flSpotlightCurLength = VectorNormalize( m_hSpotlightTarget->m_vSpotlightDir ); + m_hSpotlightTarget->SetMoveType( MOVETYPE_NONE ); + ComputeRenderInfo(); + + m_OnOn.FireOutput( this, this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointSpotlight::Activate(void) +{ + BaseClass::Activate(); + + if ( GetMoveParent() ) + { + m_bEfficientSpotlight = false; + } + + if ( m_bEfficientSpotlight ) + { + if ( m_bSpotlightOn ) + { + CreateEfficientSpotlight(); + } + + // Don't think + SetThink( NULL ); + } +} + + +//------------------------------------------------------------------------------------- +// Optimization to deal with spotlights +//------------------------------------------------------------------------------------- +void CPointSpotlight::OnEntityEvent( EntityEvent_t event, void *pEventData ) +{ + if ( event == ENTITY_EVENT_PARENT_CHANGED ) + { + if ( GetMoveParent() ) + { + m_bEfficientSpotlight = false; + if ( m_hSpotlightTarget ) + { + m_hSpotlightTarget->SetMoveType( MOVETYPE_FLY ); + } + SetThink( &CPointSpotlight::SpotlightThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } + } + + BaseClass::OnEntityEvent( event, pEventData ); +} + + +//------------------------------------------------------------------------------------- +// Purpose : Send even though we don't have a model so spotlight gets proper position +// Input : +// Output : +//------------------------------------------------------------------------------------- +int CPointSpotlight::UpdateTransmitState() +{ + if ( m_bEfficientSpotlight ) + return SetTransmitState( FL_EDICT_DONTSEND ); + + return SetTransmitState( FL_EDICT_PVSCHECK ); +} + +//----------------------------------------------------------------------------- +// Purpose: Plays the engine sound. +//----------------------------------------------------------------------------- +void CPointSpotlight::SpotlightThink( void ) +{ + if ( GetMoveParent() ) + { + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); + } + else + { + SetNextThink( gpGlobals->curtime + 0.1f ); + } + + SpotlightUpdate(); +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CPointSpotlight::SpotlightCreate(void) +{ + if ( m_hSpotlightTarget.Get() != NULL ) + return; + + AngleVectors( GetAbsAngles(), &m_vSpotlightDir ); + + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + m_vSpotlightDir * m_flSpotlightMaxLength, MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr); + + m_hSpotlightTarget = (CSpotlightEnd*)CreateEntityByName( "spotlight_end" ); + m_hSpotlightTarget->Spawn(); + m_hSpotlightTarget->SetAbsOrigin( tr.endpos ); + m_hSpotlightTarget->SetOwnerEntity( this ); + m_hSpotlightTarget->m_clrRender = m_clrRender; + m_hSpotlightTarget->m_Radius = m_flSpotlightMaxLength; + + if ( FBitSet (m_spawnflags, SF_SPOTLIGHT_NO_DYNAMIC_LIGHT) ) + { + m_hSpotlightTarget->m_flLightScale = 0.0; + } + + //m_hSpotlight = CBeam::BeamCreate( "sprites/spotlight.vmt", m_flSpotlightGoalWidth ); +#ifdef MAPBASE + m_hSpotlight = CBeam::BeamCreate( STRING(m_iszSpotlightMaterial), m_flSpotlightGoalWidth ); +#else + m_hSpotlight = CBeam::BeamCreate( "sprites/glow_test02.vmt", m_flSpotlightGoalWidth ); +#endif + // Set the temporary spawnflag on the beam so it doesn't save (we'll recreate it on restore) + m_hSpotlight->SetHDRColorScale( m_flHDRColorScale ); + m_hSpotlight->AddSpawnFlags( SF_BEAM_TEMPORARY ); + m_hSpotlight->SetColor( m_clrRender->r, m_clrRender->g, m_clrRender->b ); + m_hSpotlight->SetHaloTexture(m_nHaloSprite); +#ifdef MAPBASE + m_hSpotlight->SetHaloScale(m_flHaloScale); +#else + m_hSpotlight->SetHaloScale(60); +#endif + m_hSpotlight->SetEndWidth(m_flSpotlightGoalWidth); + m_hSpotlight->SetBeamFlags( (FBEAM_SHADEOUT|FBEAM_NOTILE) ); + m_hSpotlight->SetBrightness( 64 ); + m_hSpotlight->SetNoise( 0 ); + m_hSpotlight->SetMinDXLevel( m_nMinDXLevel ); + + if ( m_bEfficientSpotlight ) + { + m_hSpotlight->PointsInit( GetAbsOrigin(), m_hSpotlightTarget->GetAbsOrigin() ); + } + else + { + m_hSpotlight->EntsInit( this, m_hSpotlightTarget ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +Vector CPointSpotlight::SpotlightCurrentPos(void) +{ + AngleVectors( GetAbsAngles(), &m_vSpotlightDir ); + + // Get beam end point. Only collide with solid objects, not npcs + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + (m_vSpotlightDir * 2 * m_flSpotlightMaxLength), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + return tr.endpos; +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CPointSpotlight::SpotlightDestroy(void) +{ + if ( m_hSpotlight ) + { + m_OnOff.FireOutput( this, this ); + + UTIL_Remove(m_hSpotlight); + UTIL_Remove(m_hSpotlightTarget); + } +} + +//------------------------------------------------------------------------------ +// Purpose : Update the direction and position of my spotlight +// Input : +// Output : +//------------------------------------------------------------------------------ +void CPointSpotlight::SpotlightUpdate(void) +{ + // --------------------------------------------------- + // If I don't have a spotlight attempt to create one + // --------------------------------------------------- + if ( !m_hSpotlight ) + { + if ( m_bSpotlightOn ) + { + // Make the spotlight + SpotlightCreate(); + } + else + { + return; + } + } + else if ( !m_bSpotlightOn ) + { + SpotlightDestroy(); + return; + } + + if ( !m_hSpotlightTarget ) + { + DevWarning( "**Attempting to update point_spotlight but target ent is NULL\n" ); + SpotlightDestroy(); + SpotlightCreate(); + if ( !m_hSpotlightTarget ) + return; + } + + m_vSpotlightCurrentPos = SpotlightCurrentPos(); + + // Update spotlight target velocity + Vector vTargetDir; + VectorSubtract( m_vSpotlightCurrentPos, m_hSpotlightTarget->GetAbsOrigin(), vTargetDir ); + float vTargetDist = vTargetDir.Length(); + + // If we haven't moved at all, don't recompute + if ( vTargetDist < 1 ) + { + m_hSpotlightTarget->SetAbsVelocity( vec3_origin ); + return; + } + + Vector vecNewVelocity = vTargetDir; + VectorNormalize(vecNewVelocity); + vecNewVelocity *= (10 * vTargetDist); + + // If a large move is requested, just jump to final spot as we probably hit a discontinuity + if (vecNewVelocity.Length() > 200) + { + VectorNormalize(vecNewVelocity); + vecNewVelocity *= 200; + VectorNormalize(vTargetDir); + m_hSpotlightTarget->SetAbsOrigin( m_vSpotlightCurrentPos ); + } + m_hSpotlightTarget->SetAbsVelocity( vecNewVelocity ); + m_hSpotlightTarget->m_vSpotlightOrg = GetAbsOrigin(); + + // Avoid sudden change in where beam fades out when cross disconinuities + VectorSubtract( m_hSpotlightTarget->GetAbsOrigin(), m_hSpotlightTarget->m_vSpotlightOrg, m_hSpotlightTarget->m_vSpotlightDir ); + float flBeamLength = VectorNormalize( m_hSpotlightTarget->m_vSpotlightDir ); + m_flSpotlightCurLength = (0.60*m_flSpotlightCurLength) + (0.4*flBeamLength); + + ComputeRenderInfo(); + + //NDebugOverlay::Cross3D(GetAbsOrigin(),Vector(-5,-5,-5),Vector(5,5,5),0,255,0,true,0.1); + //NDebugOverlay::Cross3D(m_vSpotlightCurrentPos,Vector(-5,-5,-5),Vector(5,5,5),0,255,0,true,0.1); + //NDebugOverlay::Cross3D(m_vSpotlightTargetPos,Vector(-5,-5,-5),Vector(5,5,5),255,0,0,true,0.1); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointSpotlight::InputLightOn( inputdata_t &inputdata ) +{ + if ( !m_bSpotlightOn ) + { + m_bSpotlightOn = true; + if ( m_bEfficientSpotlight ) + { + CreateEfficientSpotlight(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointSpotlight::InputLightOff( inputdata_t &inputdata ) +{ + if ( m_bSpotlightOn ) + { + m_bSpotlightOn = false; + if ( m_bEfficientSpotlight ) + { + SpotlightDestroy(); + } + } +} diff --git a/sp/src/game/server/point_template.cpp b/sp/src/game/server/point_template.cpp new file mode 100644 index 00000000..3cb654ca --- /dev/null +++ b/sp/src/game/server/point_template.cpp @@ -0,0 +1,614 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Point entity used to create templates out of other entities or groups of entities +// +//=============================================================================// + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "TemplateEntities.h" +#include "point_template.h" +#include "saverestore_utlvector.h" +#include "mapentities.h" +#include "tier0/icommandline.h" +#include "mapentities_shared.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_POINTTEMPLATE_DONTREMOVETEMPLATEENTITIES 0x0001 + +// Level designers can suppress the uniquification of the spawned entity +// names with a spawnflag, provided they guarantee that only one instance +// of the entities will ever be spawned at a time. +#define SF_POINTTEMPLATE_PRESERVE_NAMES 0x0002 + + +LINK_ENTITY_TO_CLASS(point_template, CPointTemplate); + +BEGIN_SIMPLE_DATADESC( template_t ) + DEFINE_FIELD( iTemplateIndex, FIELD_INTEGER ), + DEFINE_FIELD( matEntityToTemplate, FIELD_VMATRIX ), +END_DATADESC() + +BEGIN_DATADESC( CPointTemplate ) + // Keys + + // Silence, Classcheck! + // DEFINE_ARRAY( m_iszTemplateEntityNames, FIELD_STRING, MAX_NUM_TEMPLATES ), + + DEFINE_KEYFIELD( m_iszTemplateEntityNames[0], FIELD_STRING, "Template01"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[1], FIELD_STRING, "Template02"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[2], FIELD_STRING, "Template03"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[3], FIELD_STRING, "Template04"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[4], FIELD_STRING, "Template05"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[5], FIELD_STRING, "Template06"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[6], FIELD_STRING, "Template07"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[7], FIELD_STRING, "Template08"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[8], FIELD_STRING, "Template09"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[9], FIELD_STRING, "Template10"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[10], FIELD_STRING, "Template11"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[11], FIELD_STRING, "Template12"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[12], FIELD_STRING, "Template13"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[13], FIELD_STRING, "Template14"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[14], FIELD_STRING, "Template15"), + DEFINE_KEYFIELD( m_iszTemplateEntityNames[15], FIELD_STRING, "Template16"), + DEFINE_UTLVECTOR( m_hTemplateEntities, FIELD_CLASSPTR ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bFixupExpanded, FIELD_BOOLEAN, "FixupMode" ), +#endif + + DEFINE_UTLVECTOR( m_hTemplates, FIELD_EMBEDDED ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "ForceSpawn", InputForceSpawn ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "ForceSpawnRandomTemplate", InputForceSpawnRandomTemplate ), +#endif + + // Outputs + DEFINE_OUTPUT( m_pOutputOnSpawned, "OnEntitySpawned" ), +#ifdef MAPBASE + DEFINE_OUTPUT( m_pOutputOutEntity, "OutSpawnedEntity" ), +#endif + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: A simple system to help precache point_template entities ... ywb +//----------------------------------------------------------------------------- +class CPointTemplatePrecacher : public CAutoGameSystem +{ +public: + + CPointTemplatePrecacher( char const *name ) : CAutoGameSystem( name ) + { + } + + void AddToPrecache( CBaseEntity *ent ) + { + m_Ents.AddToTail( EHANDLE( ent ) ); + } + + virtual void LevelInitPreEntity() + { + m_Ents.RemoveAll(); + } + + virtual void Shutdown() + { + m_Ents.RemoveAll(); + } + + void Precache() + { + int c = m_Ents.Count(); + for ( int i = 0 ; i < c; ++i ) + { + CPointTemplate *ent = m_Ents[ i ].Get(); + if ( ent ) + { + ent->PerformPrecache(); + } + } + + m_Ents.RemoveAll(); + } +private: + + CUtlVector< CHandle< CPointTemplate > > m_Ents; +}; + +CPointTemplatePrecacher g_PointTemplatePrecacher( "CPointTemplatePrecacher" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void PrecachePointTemplates() +{ + g_PointTemplatePrecacher.Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointTemplate::Spawn( void ) +{ + Precache(); + ScriptInstallPreSpawnHook(); + ValidateScriptScope(); +} + +void CPointTemplate::Precache() +{ + // We can't call precache right when we instance the template, we need to defer it until after all map entities have + // been loaded, so add this template to a list which is cleared after map entity parsing is completed. + g_PointTemplatePrecacher.AddToPrecache( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Level designers can suppress the uniquification of the spawned entity +// names with a spawnflag, provided they guarantee that only one instance +// of the entities will ever be spawned at a time. +//----------------------------------------------------------------------------- +bool CPointTemplate::AllowNameFixup() +{ + return !HasSpawnFlags( SF_POINTTEMPLATE_PRESERVE_NAMES ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called at the start of template initialization for this point_template. +// Find all the entities referenced by this point_template, which will +// then be turned into templates by the map-parsing code. +//----------------------------------------------------------------------------- +void CPointTemplate::StartBuildingTemplates( void ) +{ + // Build our list of template entities + for ( int i = 0; i < MAX_NUM_TEMPLATES; i++ ) + { + if ( m_iszTemplateEntityNames[i] != NULL_STRING ) + { + CBaseEntity *pEntity = NULL; + int iOldNum = m_hTemplateEntities.Count(); + // Add all the entities with the matching targetname + while ( (pEntity = gEntList.FindEntityByName( pEntity, STRING(m_iszTemplateEntityNames[i]) )) != NULL ) + { + m_hTemplateEntities.AddToTail( pEntity ); + } + + // Useful mapmaker warning + if ( iOldNum == m_hTemplateEntities.Count() ) + { + Warning( "Couldn't find any entities named %s, which point_template %s is specifying.\n", STRING(m_iszTemplateEntityNames[i]), STRING(GetEntityName()) ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called at the end of template initialization for this point_template. +// All of our referenced entities have now been destroyed. +//----------------------------------------------------------------------------- +void CPointTemplate::FinishBuildingTemplates( void ) +{ + // Our template entities are now gone, deleted by the server post turning them into templates. + m_hTemplateEntities.Purge(); + + // Now tell the template system to hook up all the Entity I/O connections within our set of templates. + Templates_ReconnectIOForGroup( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointTemplate::AddTemplate( CBaseEntity *pEntity, const char *pszMapData, int nLen ) +{ + // Add it to the template list + int iIndex = Templates_Add( pEntity, pszMapData, nLen ); + if ( iIndex == -1 ) + { + Warning( "point_template %s failed to add template.\n", STRING(GetEntityName()) ); + return; + } + + template_t newTemplate; + newTemplate.iTemplateIndex = iIndex; + + // Store the entity's origin & angles in a matrix in the template's local space + VMatrix matTemplateToWorld, matWorldToTemplate, matEntityToWorld, matEntityToTemplate; + matTemplateToWorld.SetupMatrixOrgAngles( GetAbsOrigin(), GetAbsAngles() ); + matTemplateToWorld.InverseTR( matWorldToTemplate ); + matEntityToWorld.SetupMatrixOrgAngles( pEntity->GetAbsOrigin(), pEntity->GetAbsAngles() ); + MatrixMultiply( matWorldToTemplate, matEntityToWorld, matEntityToTemplate ); + + newTemplate.matEntityToTemplate = matEntityToTemplate; + m_hTemplates.AddToTail( newTemplate ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPointTemplate::ShouldRemoveTemplateEntities( void ) +{ + return ( !(m_spawnflags & SF_POINTTEMPLATE_DONTREMOVETEMPLATEENTITIES) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPointTemplate::GetNumTemplates( void ) +{ + return m_hTemplates.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPointTemplate::GetTemplateIndexForTemplate( int iTemplate ) +{ + Assert( iTemplate < m_hTemplates.Count() ); + return m_hTemplates[iTemplate].iTemplateIndex; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPointTemplate::GetNumTemplateEntities( void ) +{ + return m_hTemplateEntities.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CPointTemplate::GetTemplateEntity( int iTemplateNumber ) +{ + Assert( iTemplateNumber < m_hTemplateEntities.Count() ); + + return m_hTemplateEntities[iTemplateNumber]; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointTemplate::PerformPrecache() +{ + // Go through all our templated map data and precache all the entities in it + int iTemplates = m_hTemplates.Count(); + if ( !iTemplates ) + { + Msg("Precache called on a point_template that has no templates: %s\n", STRING(GetEntityName()) ); + return; + } + + // Tell the template system we're about to start a new template + Templates_StartUniqueInstance(); + + //HierarchicalSpawn_t *pSpawnList = (HierarchicalSpawn_t*)stackalloc( iTemplates * sizeof(HierarchicalSpawn_t) ); + + int i; + for ( i = 0; i < iTemplates; i++ ) + { + //CBaseEntity *pEntity = NULL; + char *pMapData; + int iTemplateIndex = m_hTemplates[i].iTemplateIndex; + + // Some templates have Entity I/O connecting the entities within the template. + // Unique versions of these templates need to be created whenever they're instanced. + int nStringSize; + if ( AllowNameFixup() && Templates_IndexRequiresEntityIOFixup( iTemplateIndex ) ) + { + // This template requires instancing. + // Create a new mapdata block and ask the template system to fill it in with + // a unique version (with fixed Entity I/O connections). + pMapData = Templates_GetEntityIOFixedMapData( iTemplateIndex ); + + } + else + { + // Use the unmodified mapdata + pMapData = (char*)STRING( Templates_FindByIndex( iTemplateIndex ) ); + } + + nStringSize = Templates_GetStringSize( iTemplateIndex ); + + // Create the entity from the mapdata + MapEntity_PrecacheEntity( pMapData, nStringSize ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn the entities I contain +// Input : &vecOrigin - +// &vecAngles - +// pEntities - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPointTemplate::CreateInstance( const Vector &vecOrigin, const QAngle &vecAngles, CUtlVector *pEntities ) +{ + // Go through all our templated map data and spawn all the entities in it + int iTemplates = m_hTemplates.Count(); + if ( !iTemplates ) + { + Msg("CreateInstance called on a point_template that has no templates: %s\n", STRING(GetEntityName()) ); + return false; + } + + // Tell the template system we're about to start a new template + Templates_StartUniqueInstance(); + + HierarchicalSpawn_t *pSpawnList = (HierarchicalSpawn_t*)stackalloc( iTemplates * sizeof(HierarchicalSpawn_t) ); + + int i; + for ( i = 0; i < iTemplates; i++ ) + { + CBaseEntity *pEntity = NULL; + char *pMapData; + int iTemplateIndex = m_hTemplates[i].iTemplateIndex; + + // Some templates have Entity I/O connecting the entities within the template. + // Unique versions of these templates need to be created whenever they're instanced. + if ( AllowNameFixup() && ( Templates_IndexRequiresEntityIOFixup( iTemplateIndex ) || m_ScriptScope.IsInitialized() ) ) + { + // This template requires instancing. + // Create a new mapdata block and ask the template system to fill it in with + // a unique version (with fixed Entity I/O connections). + pMapData = Templates_GetEntityIOFixedMapData( iTemplateIndex ); + } + else + { + // Use the unmodified mapdata + pMapData = (char*)STRING( Templates_FindByIndex( iTemplateIndex ) ); + } + + // Create the entity from the mapdata + MapEntity_ParseEntity( pEntity, pMapData, NULL ); + if ( pEntity == NULL ) + { + Msg("Failed to initialize templated entity with mapdata: %s\n", pMapData ); + return false; + } + + // Get a matrix that'll convert from world to the new local space + VMatrix matNewTemplateToWorld, matStoredLocalToWorld; + matNewTemplateToWorld.SetupMatrixOrgAngles( vecOrigin, vecAngles ); + MatrixMultiply( matNewTemplateToWorld, m_hTemplates[i].matEntityToTemplate, matStoredLocalToWorld ); + + // Get the world origin & angles from the stored local coordinates + Vector vecNewOrigin; + QAngle vecNewAngles; + vecNewOrigin = matStoredLocalToWorld.GetTranslation(); + MatrixToAngles( matStoredLocalToWorld, vecNewAngles ); + + // Set its origin & angles + pEntity->SetAbsOrigin( vecNewOrigin ); + pEntity->SetAbsAngles( vecNewAngles ); + + if (ScriptPreInstanceSpawn(&m_ScriptScope, pEntity, Templates_FindByIndex(iTemplateIndex))) + { + pSpawnList[i].m_pEntity = pEntity; + } + else + { + pSpawnList[i].m_pEntity = NULL; + UTIL_RemoveImmediate(pEntity); + } + pSpawnList[i].m_nDepth = 0; + pSpawnList[i].m_pDeferredParent = NULL; + } + + SpawnHierarchicalList( iTemplates, pSpawnList, true ); + + for ( i = 0; i < iTemplates; ++i ) + { + if ( pSpawnList[i].m_pEntity ) + { + pEntities->AddToTail( pSpawnList[i].m_pEntity ); + } + } + + return true; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Spawn one of the entities I contain +// Input : &vecOrigin - +// &vecAngles - +// pEntities - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPointTemplate::CreateSpecificInstance( int iTemplate, const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity **pOutEntity ) +{ + // Go through all our templated map data and spawn all the entities in it + int iTemplates = m_hTemplates.Count(); + if ( !iTemplates ) + { + Msg("CreateInstance called on a point_template that has no templates: %s\n", STRING(GetEntityName()) ); + return false; + } + + // Tell the template system we're about to start a new template + Templates_StartUniqueInstance(); + + CBaseEntity *pEntity = NULL; + char *pMapData; + int iTemplateIndex = m_hTemplates[iTemplate].iTemplateIndex; + + // Some templates have Entity I/O connecting the entities within the template. + // Unique versions of these templates need to be created whenever they're instanced. + if ( AllowNameFixup() && ( Templates_IndexRequiresEntityIOFixup( iTemplateIndex ) || m_ScriptScope.IsInitialized() ) ) + { + // This template requires instancing. + // Create a new mapdata block and ask the template system to fill it in with + // a unique version (with fixed Entity I/O connections). + pMapData = Templates_GetEntityIOFixedMapData( iTemplateIndex ); + } + else + { + // Use the unmodified mapdata + pMapData = (char*)STRING( Templates_FindByIndex( iTemplateIndex ) ); + } + + // Create the entity from the mapdata + MapEntity_ParseEntity( pEntity, pMapData, NULL ); + if ( pEntity == NULL ) + { + Msg("Failed to initialize templated entity with mapdata: %s\n", pMapData ); + return false; + } + + // Get a matrix that'll convert from world to the new local space + VMatrix matNewTemplateToWorld, matStoredLocalToWorld; + matNewTemplateToWorld.SetupMatrixOrgAngles( vecOrigin, vecAngles ); + MatrixMultiply( matNewTemplateToWorld, m_hTemplates[iTemplate].matEntityToTemplate, matStoredLocalToWorld ); + + // Get the world origin & angles from the stored local coordinates + Vector vecNewOrigin; + QAngle vecNewAngles; + vecNewOrigin = matStoredLocalToWorld.GetTranslation(); + MatrixToAngles( matStoredLocalToWorld, vecNewAngles ); + + // Set its origin & angles + pEntity->SetAbsOrigin( vecNewOrigin ); + pEntity->SetAbsAngles( vecNewAngles ); + + // Spawn it + DispatchSpawn( pEntity ); + + if (pOutEntity) + { + *pOutEntity = pEntity; + } + + return true; +} +#endif + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CPointTemplate::CreationComplete( const CUtlVector &entities ) +{ + if ( !entities.Count() ) + return; + + ScriptPostSpawn( &m_ScriptScope, (CBaseEntity **)entities.Base(), entities.Count() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPointTemplate::InputForceSpawn( inputdata_t &inputdata ) +{ + // Spawn our template + CUtlVector hNewEntities; + if ( !CreateInstance( GetAbsOrigin(), GetAbsAngles(), &hNewEntities ) ) + return; + + // Fire our output + m_pOutputOnSpawned.FireOutput( this, this ); + +#ifdef MAPBASE + for ( int i = 0; i < hNewEntities.Count(); i++ ) + { + m_pOutputOutEntity.Set(hNewEntities[i], hNewEntities[i], this); + } +#endif +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Randomly spawns one of our templates. +// This is copied from CreateInstance(). +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPointTemplate::InputForceSpawnRandomTemplate( inputdata_t &inputdata ) +{ + // Spawn our template + CBaseEntity *pEntity = NULL; + if ( !CreateSpecificInstance( RandomInt(0, GetNumTemplates() - 1), GetAbsOrigin(), GetAbsAngles(), &pEntity ) ) + return; + + // Fire our output + m_pOutputOnSpawned.FireOutput( this, this ); + m_pOutputOutEntity.Set(pEntity, pEntity, this); +} +#endif + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void ScriptInstallPreSpawnHook() +{ +#ifdef MAPBASE_VSCRIPT + if ( !g_pScriptVM ) + return; +#endif + +#ifdef IS_WINDOWS_PC + if ( !g_pScriptVM->ValueExists( "__ExecutePreSpawn " ) ) + { + //g_pScriptVM->Run( g_Script_spawn_helper ); + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: This function is called after a spawner creates its child entity +// but before the keyvalues are injected. This gives us an +// opportunity to change any keyvalues before the entity is +// configured and spawned. In this case, we see if there is a VScript +// that wants to change anything about this entity. +//----------------------------------------------------------------------------- +bool ScriptPreInstanceSpawn( CScriptScope *pScriptScope, CBaseEntity *pChild, string_t iszKeyValueData ) +{ + if ( !pScriptScope->IsInitialized() ) + return true; + + ScriptVariant_t result; + if ( pScriptScope->Call( "__ExecutePreSpawn", &result, ToHScript( pChild ) ) != SCRIPT_DONE ) + return true; + + if ( ( result.m_type == FIELD_BOOLEAN && !result.m_bool ) || ( result.m_type == FIELD_INTEGER && !result.m_int ) ) + return false; + + return true; + +} + +void ScriptPostSpawn( CScriptScope *pScriptScope, CBaseEntity **ppEntities, int nEntities ) +{ + if ( !pScriptScope->IsInitialized() ) + return; + + HSCRIPT hPostSpawnFunc = pScriptScope->LookupFunction( "PostSpawn" ); + + if ( !hPostSpawnFunc ) + return; + + ScriptVariant_t varEntityMakerResultTable; + if ( !g_pScriptVM->GetValue( *pScriptScope, "__EntityMakerResult", &varEntityMakerResultTable ) ) + return; + + if ( varEntityMakerResultTable.m_type != FIELD_HSCRIPT ) + return; + + HSCRIPT hEntityMakerResultTable = varEntityMakerResultTable.m_hScript; + char szEntName[256]; + for ( int i = 0; i < nEntities; i++ ) + { + V_strncpy( szEntName, ppEntities[i]->GetEntityNameAsCStr(), ARRAYSIZE(szEntName) ); + char *pAmpersand = V_strrchr( szEntName, '&' ); + if ( pAmpersand ) + *pAmpersand = 0; + g_pScriptVM->SetValue( hEntityMakerResultTable, szEntName, ToHScript( ppEntities[i] ) ); + } + pScriptScope->Call( hPostSpawnFunc, NULL, hEntityMakerResultTable ); + pScriptScope->Call( "__FinishSpawn" ); + g_pScriptVM->ReleaseValue( varEntityMakerResultTable ); + g_pScriptVM->ReleaseFunction( hPostSpawnFunc ); +} diff --git a/sp/src/game/server/point_template.h b/sp/src/game/server/point_template.h new file mode 100644 index 00000000..283a4b62 --- /dev/null +++ b/sp/src/game/server/point_template.h @@ -0,0 +1,95 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Point entity used to create templates out of other entities or groups of entities +// +//=============================================================================// + +#ifndef POINT_TEMPLATE_H +#define POINT_TEMPLATE_H +#ifdef _WIN32 +#pragma once +#endif + +#define MAX_NUM_TEMPLATES 16 + +struct template_t +{ + int iTemplateIndex; + VMatrix matEntityToTemplate; + + DECLARE_SIMPLE_DATADESC(); +}; + +void ScriptInstallPreSpawnHook(); +bool ScriptPreInstanceSpawn( CScriptScope *pScriptScope, CBaseEntity *pChild, string_t iszKeyValueData ); +void ScriptPostSpawn( CScriptScope *pScriptScope, CBaseEntity **ppEntities, int nEntities ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPointTemplate : public CLogicalEntity +{ + DECLARE_CLASS( CPointTemplate, CLogicalEntity ); +public: + DECLARE_DATADESC(); + + virtual void Spawn( void ); + virtual void Precache(); + + // Template initialization + void StartBuildingTemplates( void ); + void FinishBuildingTemplates( void ); + + // Template Entity accessors + int GetNumTemplateEntities( void ); + CBaseEntity *GetTemplateEntity( int iTemplateNumber ); + void AddTemplate( CBaseEntity *pEntity, const char *pszMapData, int nLen ); + bool ShouldRemoveTemplateEntities( void ); + bool AllowNameFixup(); +#ifdef MAPBASE + bool NameFixupExpanded() { return m_bFixupExpanded; } +#endif + + // Templates accessors + int GetNumTemplates( void ); + int GetTemplateIndexForTemplate( int iTemplate ); + + // Template instancing + bool CreateInstance( const Vector &vecOrigin, const QAngle &vecAngles, CUtlVector *pEntities ); +#ifdef MAPBASE + bool CreateSpecificInstance( int iTemplate, const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity **pOutEntity ); +#endif + void CreationComplete(const CUtlVector& entities); + + // Inputs + void InputForceSpawn( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputForceSpawnRandomTemplate( inputdata_t &inputdata ); +#endif + + virtual void PerformPrecache(); + +private: + string_t m_iszTemplateEntityNames[MAX_NUM_TEMPLATES]; + + // List of map entities this template targets. Built inside our Spawn(). + // It's only valid between Spawn() & Activate(), because the map entity parsing + // code removes all the entities in it once it finishes turning them into templates. + CUtlVector< CBaseEntity * > m_hTemplateEntities; + +#ifdef MAPBASE + // Allows name fixup to target all instances of a name in a keyvalue, including output parameters. + // TODO: Support for multiple fixup modes? + bool m_bFixupExpanded; +#endif + + // List of templates, generated from our template entities. + CUtlVector< template_t > m_hTemplates; + + COutputEvent m_pOutputOnSpawned; +#ifdef MAPBASE + COutputEHANDLE m_pOutputOutEntity; +#endif +}; + +#endif // POINT_TEMPLATE_H diff --git a/sp/src/game/server/pointanglesensor.cpp b/sp/src/game/server/pointanglesensor.cpp new file mode 100644 index 00000000..d625ca9c --- /dev/null +++ b/sp/src/game/server/pointanglesensor.cpp @@ -0,0 +1,562 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Used to fire events based on the orientation of a given entity. +// +// Looks at its target's angles every frame and fires an output if its +// target's forward vector points at a specified lookat entity for more +// than a specified length of time. +// +// It also fires an output whenever the target's angles change. +// +//=============================================================================// + +#include "cbase.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "mathlib/mathlib.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_USE_TARGET_FACING (1<<0) // Use the target entity's direction instead of position + +class CPointAngleSensor : public CPointEntity +{ + DECLARE_CLASS(CPointAngleSensor, CPointEntity); +public: + + bool KeyValue(const char *szKeyName, const char *szValue); + void Activate(void); + void Spawn(void); + void Think(void); + + int DrawDebugTextOverlays(void); + +protected: + + void Enable(); + void Disable(); + + // Input handlers + void InputEnable(inputdata_t &inputdata); + void InputDisable(inputdata_t &inputdata); + void InputToggle(inputdata_t &inputdata); + void InputTest(inputdata_t &inputdata); + void InputSetTargetEntity(inputdata_t &inputdata); + + bool IsFacingWithinTolerance(CBaseEntity *pEntity, CBaseEntity *pTarget, float flTolerance, float *pflDot = NULL); + + bool m_bDisabled; // When disabled, we do not think or fire outputs. + string_t m_nLookAtName; // Name of the entity that the target must point at to fire the OnTrue output. + + EHANDLE m_hTargetEntity; // Entity whose angles are being monitored. + EHANDLE m_hLookAtEntity; // Entity that the target must look at to fire the OnTrue output. + + float m_flDuration; // Time in seconds for which the entity must point at the target. + float m_flDotTolerance; // Degrees of error allowed to satisfy the condition, expressed as a dot product. + float m_flFacingTime; // The time at which the target entity pointed at the lookat entity. + bool m_bFired; // Latches the output so it only fires once per true. + + // Outputs + COutputEvent m_OnFacingLookat; // Fired when the target points at the lookat entity. + COutputEvent m_OnNotFacingLookat; // Fired in response to a Test input if the target is not looking at the lookat entity. + COutputVector m_TargetDir; + COutputFloat m_FacingPercentage; // Normalize value representing how close the entity is to facing directly at the target + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS(point_anglesensor, CPointAngleSensor); + + +BEGIN_DATADESC(CPointAngleSensor) + + // Keys + DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled"), + DEFINE_KEYFIELD(m_nLookAtName, FIELD_STRING, "lookatname"), + DEFINE_FIELD(m_hTargetEntity, FIELD_EHANDLE), + DEFINE_FIELD(m_hLookAtEntity, FIELD_EHANDLE), + DEFINE_KEYFIELD(m_flDuration, FIELD_FLOAT, "duration"), + DEFINE_FIELD(m_flDotTolerance, FIELD_FLOAT), + DEFINE_FIELD(m_flFacingTime, FIELD_TIME), + DEFINE_FIELD(m_bFired, FIELD_BOOLEAN), + + // Outputs + DEFINE_OUTPUT(m_OnFacingLookat, "OnFacingLookat"), + DEFINE_OUTPUT(m_OnNotFacingLookat, "OnNotFacingLookat"), + DEFINE_OUTPUT(m_TargetDir, "TargetDir"), + DEFINE_OUTPUT(m_FacingPercentage, "FacingPercentage"), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable), + DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + DEFINE_INPUTFUNC(FIELD_VOID, "Test", InputTest), + DEFINE_INPUTFUNC(FIELD_STRING, "SetTargetEntity", InputSetTargetEntity), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Handles keyvalues that require special processing. +// Output : Returns true if handled, false if not. +//----------------------------------------------------------------------------- +bool CPointAngleSensor::KeyValue(const char *szKeyName, const char *szValue) +{ + if (FStrEq(szKeyName, "tolerance")) + { + float flTolerance = atof(szValue); + m_flDotTolerance = cos(DEG2RAD(flTolerance)); + } + else + { + return(BaseClass::KeyValue(szKeyName, szValue)); + } + + return(true); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning after parsing keyvalues. +//----------------------------------------------------------------------------- +void CPointAngleSensor::Spawn(void) +{ + BaseClass::Spawn(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called after all entities have spawned on new map or savegame load. +//----------------------------------------------------------------------------- +void CPointAngleSensor::Activate(void) +{ + BaseClass::Activate(); + + if (!m_hTargetEntity) + { + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target ); + } + + if (!m_hLookAtEntity && (m_nLookAtName != NULL_STRING)) + { + m_hLookAtEntity = gEntList.FindEntityByName( NULL, m_nLookAtName ); + if (!m_hLookAtEntity) + { + DevMsg(1, "Angle sensor '%s' could not find look at entity '%s'.\n", GetDebugName(), STRING(m_nLookAtName)); + } + } + + // It's okay to not have a look at entity, it just means we measure and output the angles + // of the target entity without testing them against the look at entity. + if (!m_bDisabled && m_hTargetEntity) + { + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Determines if one entity is facing within a given tolerance of another +// Input : pEntity - +// pTarget - +// flTolerance - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPointAngleSensor::IsFacingWithinTolerance(CBaseEntity *pEntity, CBaseEntity *pTarget, float flTolerance, float *pflDot) +{ + if (pflDot) + { + *pflDot = 0; + } + + if ((pEntity == NULL) || (pTarget == NULL)) + { + return(false); + } + + Vector forward; + pEntity->GetVectors(&forward, NULL, NULL); + + Vector dir; + // Use either our position relative to the target, or the target's raw facing + if ( HasSpawnFlags( SF_USE_TARGET_FACING ) ) + { + pTarget->GetVectors(&dir, NULL, NULL); + } + else + { + dir = pTarget->GetAbsOrigin() - pEntity->GetAbsOrigin(); + VectorNormalize(dir); + } + + // + // Larger dot product corresponds to a smaller angle. + // + float flDot = dir.Dot(forward); + if (pflDot) + { + *pflDot = flDot; + } + + if (flDot >= m_flDotTolerance) + { + return(true); + } + + return(false); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called every frame. +//----------------------------------------------------------------------------- +void CPointAngleSensor::Think(void) +{ + if (m_hTargetEntity != NULL) + { + Vector forward; + m_hTargetEntity->GetVectors(&forward, NULL, NULL); + m_TargetDir.Set(forward, this, this); + + if (m_hLookAtEntity != NULL) + { + // + // Check to see if the measure entity's forward vector has been within + // given tolerance of the target entity for the given period of time. + // + float flDot; + if (IsFacingWithinTolerance(m_hTargetEntity, m_hLookAtEntity, m_flDotTolerance, &flDot )) + { + if (!m_bFired) + { + if (!m_flFacingTime) + { + m_flFacingTime = gpGlobals->curtime; + } + + if (gpGlobals->curtime >= m_flFacingTime + m_flDuration) + { + m_OnFacingLookat.FireOutput(this, this); + m_bFired = true; + } + } + } + else + { + // Reset the fired state + if ( m_bFired ) + { + m_bFired = false; + } + + // Always reset the time when we've lost our facing + m_flFacingTime = 0; + } + + // Output the angle range we're in + float flPerc = RemapValClamped( flDot, 1.0f, m_flDotTolerance, 1.0f, 0.0f ); + m_FacingPercentage.Set( flPerc, this, this ); + } + + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for forcing an instantaneous test of the condition. +//----------------------------------------------------------------------------- +void CPointAngleSensor::InputTest(inputdata_t &inputdata) +{ + if (IsFacingWithinTolerance(m_hTargetEntity, m_hLookAtEntity, m_flDotTolerance)) + { + m_OnFacingLookat.FireOutput(inputdata.pActivator, this); + } + else + { + m_OnNotFacingLookat.FireOutput(inputdata.pActivator, this); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointAngleSensor::InputSetTargetEntity(inputdata_t &inputdata) +{ + if ((inputdata.value.String() == NULL) || (inputdata.value.StringID() == NULL_STRING) || (inputdata.value.String()[0] == '\0')) + { + m_target = NULL_STRING; + m_hTargetEntity = NULL; + SetNextThink( TICK_NEVER_THINK ); + } + else + { + m_target = AllocPooledString(inputdata.value.String()); + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target, NULL, inputdata.pActivator, inputdata.pCaller ); + if (!m_bDisabled && m_hTargetEntity) + { + SetNextThink( gpGlobals->curtime ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointAngleSensor::InputEnable(inputdata_t &inputdata) +{ + Enable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointAngleSensor::InputDisable(inputdata_t &inputdata) +{ + Disable(); +} + + +//----------------------------------------------------------------------------- +// Purpose: I like separators between my functions. +//----------------------------------------------------------------------------- +void CPointAngleSensor::InputToggle(inputdata_t &inputdata) +{ + if (m_bDisabled) + { + Enable(); + } + else + { + Disable(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointAngleSensor::Enable() +{ + m_bDisabled = false; + if (m_hTargetEntity) + { + SetNextThink(gpGlobals->curtime); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointAngleSensor::Disable() +{ + m_bDisabled = true; + SetNextThink(TICK_NEVER_THINK); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPointAngleSensor::DrawDebugTextOverlays(void) +{ + int nOffset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + float flDot; + bool bFacing = IsFacingWithinTolerance(m_hTargetEntity, m_hLookAtEntity, m_flDotTolerance, &flDot); + + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr), "delta ang (dot) : %.2f (%f)", RAD2DEG(acos(flDot)), flDot); + EntityText( nOffset, tempstr, 0); + nOffset++; + + Q_snprintf(tempstr, sizeof(tempstr), "tolerance ang (dot): %.2f (%f)", RAD2DEG(acos(m_flDotTolerance)), m_flDotTolerance); + EntityText( nOffset, tempstr, 0); + nOffset++; + + Q_snprintf(tempstr, sizeof(tempstr), "facing: %s", bFacing ? "yes" : "no"); + EntityText( nOffset, tempstr, 0); + nOffset++; + } + + return nOffset; +} + +// ==================================================================== +// Proximity sensor +// ==================================================================== + +#define SF_PROXIMITY_TEST_AGAINST_AXIS (1<<0) + +class CPointProximitySensor : public CPointEntity +{ + DECLARE_CLASS( CPointProximitySensor, CPointEntity ); + +public: + + virtual void Activate( void ); + +protected: + + void Think( void ); + void Enable( void ); + void Disable( void ); + + // Input handlers + void InputEnable(inputdata_t &inputdata); + void InputDisable(inputdata_t &inputdata); + void InputToggle(inputdata_t &inputdata); + void InputSetTargetEntity(inputdata_t &inputdata); + +private: + + bool m_bDisabled; // When disabled, we do not think or fire outputs. + EHANDLE m_hTargetEntity; // Entity whose angles are being monitored. + + COutputFloat m_Distance; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( point_proximity_sensor, CPointProximitySensor ); + +BEGIN_DATADESC( CPointProximitySensor ) + + // Keys + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE ), + + // Outputs + DEFINE_OUTPUT( m_Distance, "Distance"), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable), + DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + DEFINE_INPUTFUNC(FIELD_STRING, "SetTargetEntity", InputSetTargetEntity), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Called after all entities have spawned on new map or savegame load. +//----------------------------------------------------------------------------- +void CPointProximitySensor::Activate( void ) +{ + BaseClass::Activate(); + + if ( m_hTargetEntity == NULL ) + { + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target ); + } + + if ( m_bDisabled == false && m_hTargetEntity != NULL ) + { + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointProximitySensor::InputSetTargetEntity(inputdata_t &inputdata) +{ + if ((inputdata.value.String() == NULL) || (inputdata.value.StringID() == NULL_STRING) || (inputdata.value.String()[0] == '\0')) + { + m_target = NULL_STRING; + m_hTargetEntity = NULL; + SetNextThink( TICK_NEVER_THINK ); + } + else + { + m_target = AllocPooledString(inputdata.value.String()); + m_hTargetEntity = gEntList.FindEntityByName( NULL, m_target, NULL, inputdata.pActivator, inputdata.pCaller ); + if (!m_bDisabled && m_hTargetEntity) + { + SetNextThink( gpGlobals->curtime ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointProximitySensor::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointProximitySensor::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointProximitySensor::InputToggle( inputdata_t &inputdata ) +{ + if ( m_bDisabled ) + { + Enable(); + } + else + { + Disable(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointProximitySensor::Enable( void ) +{ + m_bDisabled = false; + if ( m_hTargetEntity ) + { + SetNextThink( gpGlobals->curtime ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointProximitySensor::Disable( void ) +{ + m_bDisabled = true; + SetNextThink( TICK_NEVER_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called every frame +//----------------------------------------------------------------------------- +void CPointProximitySensor::Think( void ) +{ + if ( m_hTargetEntity != NULL ) + { + Vector vecTestDir = ( m_hTargetEntity->GetAbsOrigin() - GetAbsOrigin() ); + float flDist = VectorNormalize( vecTestDir ); + + // If we're only interested in the distance along a vector, modify the length the accomodate that + if ( HasSpawnFlags( SF_PROXIMITY_TEST_AGAINST_AXIS ) ) + { + Vector vecDir; + GetVectors( &vecDir, NULL, NULL ); + + float flDot = DotProduct( vecTestDir, vecDir ); + flDist *= fabs( flDot ); + } + + m_Distance.Set( flDist, this, this ); + SetNextThink( gpGlobals->curtime ); + } +} diff --git a/sp/src/game/server/pointhurt.cpp b/sp/src/game/server/pointhurt.cpp new file mode 100644 index 00000000..ce103e86 --- /dev/null +++ b/sp/src/game/server/pointhurt.cpp @@ -0,0 +1,204 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements hurting point entity +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "entitylist.h" +#include "gamerules.h" +#include "basecombatcharacter.h" +#include "ammodef.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +const int SF_PHURT_START_ON = 1; + +class CPointHurt : public CPointEntity +{ + DECLARE_CLASS( CPointHurt, CPointEntity ); + +public: + void Spawn( void ); + void Precache( void ); + void HurtThink( void ); + + // Input handlers + void InputTurnOn(inputdata_t &inputdata); + void InputTurnOff(inputdata_t &inputdata); + void InputToggle(inputdata_t &inputdata); + void InputHurt(inputdata_t &inputdata); + +#ifdef MAPBASE + bool KeyValue( const char *szKeyName, const char *szValue ); +#endif + + DECLARE_DATADESC(); + + int m_nDamage; + int m_bitsDamageType; + float m_flRadius; + float m_flDelay; + string_t m_strTarget; + EHANDLE m_pActivator; +}; + +BEGIN_DATADESC( CPointHurt ) + + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "DamageRadius" ), + DEFINE_KEYFIELD( m_nDamage, FIELD_INTEGER, "Damage" ), + DEFINE_KEYFIELD( m_flDelay, FIELD_FLOAT, "DamageDelay" ), + DEFINE_KEYFIELD( m_bitsDamageType, FIELD_INTEGER, "DamageType" ), + DEFINE_KEYFIELD( m_strTarget, FIELD_STRING, "DamageTarget" ), + + // Function Pointers + DEFINE_FUNCTION( HurtThink ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Hurt", InputHurt ), + + DEFINE_FIELD( m_pActivator, FIELD_EHANDLE ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( point_hurt, CPointHurt ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointHurt::Spawn(void) +{ + SetThink( NULL ); + SetUse( NULL ); + + m_pActivator = NULL; + + if ( HasSpawnFlags( SF_PHURT_START_ON ) ) + { + SetThink( &CPointHurt::HurtThink ); + } + + SetNextThink( gpGlobals->curtime + 0.1f ); + + if ( m_flRadius <= 0.0f ) + { + m_flRadius = 128.0f; + } + + if ( m_nDamage <= 0 ) + { + m_nDamage = 2; + } + + if ( m_flDelay <= 0 ) + { + m_flDelay = 0.1f; + } + + Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointHurt::Precache( void ) +{ + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPointHurt::HurtThink( void ) +{ + if ( m_strTarget != NULL_STRING ) + { + CBaseEntity *pEnt = NULL; + + CTakeDamageInfo info( this, m_pActivator, m_nDamage, m_bitsDamageType ); + while ( ( pEnt = gEntList.FindEntityByName( pEnt, m_strTarget, NULL, m_pActivator ) ) != NULL ) + { + GuessDamageForce( &info, (pEnt->GetAbsOrigin() - GetAbsOrigin()), pEnt->GetAbsOrigin() ); + pEnt->TakeDamage( info ); + } + } + else + { + RadiusDamage( CTakeDamageInfo( this, this, m_nDamage, m_bitsDamageType ), GetAbsOrigin(), m_flRadius, CLASS_NONE, NULL ); + } + + SetNextThink( gpGlobals->curtime + m_flDelay ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for turning on the point hurt. +//----------------------------------------------------------------------------- +void CPointHurt::InputTurnOn( inputdata_t &data ) +{ + SetThink( &CPointHurt::HurtThink ); + + SetNextThink( gpGlobals->curtime + 0.1f ); + + m_pActivator = data.pActivator; +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for turning off the point hurt. +//----------------------------------------------------------------------------- +void CPointHurt::InputTurnOff( inputdata_t &data ) +{ + SetThink( NULL ); + + m_pActivator = data.pActivator; +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for toggling the on/off state of the point hurt. +//----------------------------------------------------------------------------- +void CPointHurt::InputToggle( inputdata_t &data ) +{ + m_pActivator = data.pActivator; + + if ( m_pfnThink == (void (CBaseEntity::*)())&CPointHurt::HurtThink ) + { + SetThink( NULL ); + } + else + { + SetThink( &CPointHurt::HurtThink ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for instantaneously hurting whatever is near us. +//----------------------------------------------------------------------------- +void CPointHurt::InputHurt( inputdata_t &data ) +{ + m_pActivator = data.pActivator; + + HurtThink(); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPointHurt::KeyValue( const char *szKeyName, const char *szValue ) +{ + // Additional OR flags + if (FStrEq( szKeyName, "damageor" ) || FStrEq( szKeyName, "damagepresets" )) + { + m_bitsDamageType |= atoi(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} +#endif + diff --git a/sp/src/game/server/pointteleport.cpp b/sp/src/game/server/pointteleport.cpp new file mode 100644 index 00000000..e79f6fc3 --- /dev/null +++ b/sp/src/game/server/pointteleport.cpp @@ -0,0 +1,234 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Teleports a named entity to a given position and restores +// it's physics state +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + + +#include "in_buttons.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_TELEPORT_TO_SPAWN_POS 0x00000001 +#define SF_TELEPORT_INTO_DUCK 0x00000002 ///< episodic only: player should be ducked after this teleport + +class CPointTeleport : public CBaseEntity +{ + DECLARE_CLASS( CPointTeleport, CBaseEntity ); +public: + void Activate( void ); + +#ifdef MAPBASE + void TeleportEntity( CBaseEntity *pTarget, const Vector &vecPosition, const QAngle &angAngles ); +#endif + + void InputTeleport( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputTeleportEntity( inputdata_t &inputdata ); + void InputTeleportToCurrentPos( inputdata_t &inputdata ); +#endif + +private: + + bool EntityMayTeleport( CBaseEntity *pTarget ); + + Vector m_vSaveOrigin; + QAngle m_vSaveAngles; + + DECLARE_DATADESC(); +}; + + +LINK_ENTITY_TO_CLASS( point_teleport, CPointTeleport ); + + +BEGIN_DATADESC( CPointTeleport ) + + DEFINE_FIELD( m_vSaveOrigin, FIELD_VECTOR ), + DEFINE_FIELD( m_vSaveAngles, FIELD_VECTOR ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Teleport", InputTeleport ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_EHANDLE, "TeleportEntity", InputTeleportEntity ), + DEFINE_INPUTFUNC( FIELD_VOID, "TeleportToCurrentPos", InputTeleportToCurrentPos ), +#endif + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pTarget - +// Output : Returns true if the entity may be teleported +//----------------------------------------------------------------------------- +bool CPointTeleport::EntityMayTeleport( CBaseEntity *pTarget ) +{ + if ( pTarget->GetMoveParent() != NULL ) + { + // Passengers in a vehicle are allowed to teleport; their behavior handles it + CBaseCombatCharacter *pBCC = pTarget->MyCombatCharacterPointer(); + if ( pBCC == NULL || ( pBCC != NULL && pBCC->IsInAVehicle() == false ) ) + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointTeleport::Activate( void ) +{ + // Start with our origin point + m_vSaveOrigin = GetAbsOrigin(); + m_vSaveAngles = GetAbsAngles(); + + // Save off the spawn position of the target if instructed to do so + if ( m_spawnflags & SF_TELEPORT_TO_SPAWN_POS ) + { + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_target ); + if ( pTarget ) + { + // If teleport object is in a movement hierarchy, remove it first + if ( EntityMayTeleport( pTarget ) ) + { + // Save the points + m_vSaveOrigin = pTarget->GetAbsOrigin(); + m_vSaveAngles = pTarget->GetAbsAngles(); + } + else + { + Warning("ERROR: (%s) can't teleport object (%s) as it has a parent (%s)!\n",GetDebugName(),pTarget->GetDebugName(),pTarget->GetMoveParent()->GetDebugName()); + BaseClass::Activate(); + return; + } + } + else + { + Warning("ERROR: (%s) target '%s' not found. Deleting.\n", GetDebugName(), STRING(m_target)); + UTIL_Remove( this ); + return; + } + } + + BaseClass::Activate(); +} + +#ifdef MAPBASE +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointTeleport::TeleportEntity( CBaseEntity *pTarget, const Vector &vecPosition, const QAngle &angAngles ) +{ + // in episodic, we have a special spawn flag that forces Gordon into a duck +#ifdef HL2_EPISODIC + if ( (m_spawnflags & SF_TELEPORT_INTO_DUCK) && pTarget->IsPlayer() ) + { + CBasePlayer *pPlayer = ToBasePlayer( pTarget ); + if ( pPlayer != NULL ) + { + pPlayer->m_nButtons |= IN_DUCK; + pPlayer->AddFlag( FL_DUCKING ); + pPlayer->m_Local.m_bDucked = true; + pPlayer->m_Local.m_bDucking = true; + pPlayer->m_Local.m_flDucktime = 0.0f; + pPlayer->SetViewOffset( VEC_DUCK_VIEW_SCALED( pPlayer ) ); + pPlayer->SetCollisionBounds( VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX ); + } + } +#endif + + pTarget->Teleport( &vecPosition, &angAngles, NULL ); +} +#endif + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointTeleport::InputTeleport( inputdata_t &inputdata ) +{ + // Attempt to find the entity in question + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_target, this, inputdata.pActivator, inputdata.pCaller ); + if ( pTarget == NULL ) + return; + + // If teleport object is in a movement hierarchy, remove it first + if ( EntityMayTeleport( pTarget ) == false ) + { + Warning("ERROR: (%s) can't teleport object (%s) as it has a parent (%s)!\n",GetDebugName(),pTarget->GetDebugName(),pTarget->GetMoveParent()->GetDebugName()); + return; + } + +#ifdef MAPBASE + TeleportEntity( pTarget, m_vSaveOrigin, m_vSaveAngles ); +#else + // in episodic, we have a special spawn flag that forces Gordon into a duck +#ifdef HL2_EPISODIC + if ( (m_spawnflags & SF_TELEPORT_INTO_DUCK) && pTarget->IsPlayer() ) + { + CBasePlayer *pPlayer = ToBasePlayer( pTarget ); + if ( pPlayer != NULL ) + { + pPlayer->m_nButtons |= IN_DUCK; + pPlayer->AddFlag( FL_DUCKING ); + pPlayer->m_Local.m_bDucked = true; + pPlayer->m_Local.m_bDucking = true; + pPlayer->m_Local.m_flDucktime = 0.0f; + pPlayer->SetViewOffset( VEC_DUCK_VIEW_SCALED( pPlayer ) ); + pPlayer->SetCollisionBounds( VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX ); + } + } +#endif + + pTarget->Teleport( &m_vSaveOrigin, &m_vSaveAngles, NULL ); +#endif +} + +#ifdef MAPBASE +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointTeleport::InputTeleportEntity( inputdata_t &inputdata ) +{ + if ( !inputdata.value.Entity() ) + { + Warning( "%s unable to find entity from TeleportEntity\n", GetDebugName() ); + return; + } + + // If teleport object is in a movement hierarchy, remove it first + if ( EntityMayTeleport( inputdata.value.Entity() ) == false ) + { + Warning("ERROR: (%s) can't teleport object (%s) as it has a parent (%s)!\n",GetDebugName(),inputdata.value.Entity()->GetDebugName(),inputdata.value.Entity()->GetMoveParent()->GetDebugName()); + return; + } + + TeleportEntity( inputdata.value.Entity(), m_vSaveOrigin, m_vSaveAngles ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPointTeleport::InputTeleportToCurrentPos( inputdata_t &inputdata ) +{ + // Attempt to find the entity in question + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_target, this, inputdata.pActivator, inputdata.pCaller ); + if ( pTarget == NULL ) + return; + + // If teleport object is in a movement hierarchy, remove it first + if ( EntityMayTeleport( pTarget ) == false ) + { + Warning("ERROR: (%s) can't teleport object (%s) as it has a parent (%s)!\n",GetDebugName(),pTarget->GetDebugName(),pTarget->GetMoveParent()->GetDebugName()); + return; + } + + TeleportEntity( pTarget, GetAbsOrigin(), GetAbsAngles() ); +} +#endif + diff --git a/sp/src/game/server/postprocesscontroller.cpp b/sp/src/game/server/postprocesscontroller.cpp new file mode 100644 index 00000000..3e3fd6b5 --- /dev/null +++ b/sp/src/game/server/postprocesscontroller.cpp @@ -0,0 +1,207 @@ +//========= Copyright © 1996-2007, Valve Corporation, All rights reserved. ========== +// +// An entity that allows level designer control over the post-processing parameters. +// +//=================================================================================== + +#include "cbase.h" +#include "postprocesscontroller.h" +#include "entityinput.h" +#include "entityoutput.h" +#include "eventqueue.h" +#include "player.h" +#include "world.h" +#include "ndebugoverlay.h" +#include "triggers.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CPostProcessSystem s_PostProcessSystem( "PostProcessSystem" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPostProcessSystem *PostProcessSystem() +{ + return &s_PostProcessSystem; +} + + +LINK_ENTITY_TO_CLASS( postprocess_controller, CPostProcessController ); + +BEGIN_DATADESC( CPostProcessController ) + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_FADE_TIME ], FIELD_FLOAT, "fadetime" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_LOCAL_CONTRAST_STRENGTH ], FIELD_FLOAT, "localcontraststrength" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_LOCAL_CONTRAST_EDGE_STRENGTH ], FIELD_FLOAT, "localcontrastedgestrength" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_VIGNETTE_START ], FIELD_TIME, "vignettestart" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_VIGNETTE_END ], FIELD_TIME, "vignetteend" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_VIGNETTE_BLUR_STRENGTH ], FIELD_FLOAT, "vignetteblurstrength" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_FADE_TO_BLACK_STRENGTH ], FIELD_FLOAT, "fadetoblackstrength" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_DEPTH_BLUR_FOCAL_DISTANCE ], FIELD_FLOAT, "depthblurfocaldistance" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_DEPTH_BLUR_STRENGTH ], FIELD_FLOAT, "depthblurstrength" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_SCREEN_BLUR_STRENGTH ], FIELD_FLOAT, "screenblurstrength" ), + DEFINE_KEYFIELD( m_flPostProcessParameters[ PPPN_FILM_GRAIN_STRENGTH ], FIELD_FLOAT, "filmgrainstrength" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFadeTime", InputSetFadeTime ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetLocalContrastStrength", InputSetLocalContrastStrength ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetLocalContrastEdgeStrength", InputSetLocalContrastEdgeStrength ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetVignetteStart", InputSetVignetteStart ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetVignetteEnd", InputSetVignetteEnd ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetVignetteBlurStrength", InputSetVignetteBlurStrength ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFadeToBlackStrength", InputSetFadeToBlackStrength ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDepthBlurFocalDistance", InputSetDepthBlurFocalDistance), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDepthBlurStrength", InputSetDepthBlurStrength ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetScreenBlurStrength", InputSetScreenBlurStrength ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFilmGrainStrength", InputSetFilmGrainStrength ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPostProcessController, DT_PostProcessController ) + SendPropArray3( SENDINFO_ARRAY3( m_flPostProcessParameters ), SendPropFloat( SENDINFO_ARRAY( m_flPostProcessParameters ) ) ), + SendPropBool( SENDINFO(m_bMaster) ), +END_SEND_TABLE() + + +CPostProcessController::CPostProcessController() +{ + m_bMaster = false; +} + +CPostProcessController::~CPostProcessController() +{ +} + +void CPostProcessController::Spawn() +{ + BaseClass::Spawn(); + + m_bMaster = IsMaster(); +} + +int CPostProcessController::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +void CPostProcessController::InputSetFadeTime( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_FADE_TIME, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetLocalContrastStrength( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_LOCAL_CONTRAST_STRENGTH, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetLocalContrastEdgeStrength( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_LOCAL_CONTRAST_EDGE_STRENGTH, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetVignetteStart( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_VIGNETTE_START, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetVignetteEnd( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_VIGNETTE_END, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetVignetteBlurStrength( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_VIGNETTE_BLUR_STRENGTH, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetFadeToBlackStrength( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_FADE_TO_BLACK_STRENGTH, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetDepthBlurFocalDistance( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_DEPTH_BLUR_FOCAL_DISTANCE, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetDepthBlurStrength( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_DEPTH_BLUR_STRENGTH, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetScreenBlurStrength( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_SCREEN_BLUR_STRENGTH, inputdata.value.Float() ); +} + +void CPostProcessController::InputSetFilmGrainStrength( inputdata_t &inputdata ) +{ + m_flPostProcessParameters.Set( PPPN_FILM_GRAIN_STRENGTH, inputdata.value.Float() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Clear out the PostProcess controller. +//----------------------------------------------------------------------------- +void CPostProcessSystem::LevelInitPreEntity() +{ + m_hMasterController = nullptr; + ListenForGameEvent( "round_start" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Find the master controller. If no controller is +// set as Master, use the first controller found. +//----------------------------------------------------------------------------- +void CPostProcessSystem::InitMasterController() +{ + CPostProcessController *pPostProcessController = nullptr; + + do + { + pPostProcessController = dynamic_cast( gEntList.FindEntityByClassname( pPostProcessController, "postprocess_controller" ) ); + if ( pPostProcessController ) + { + if ( m_hMasterController.Get() == nullptr ) + { + m_hMasterController = pPostProcessController; + } + else + { + if ( pPostProcessController->IsMaster() ) + { + m_hMasterController = pPostProcessController; + } + } + } + } while ( pPostProcessController ); +} + +//----------------------------------------------------------------------------- +// Purpose: On a multiplayer map restart, re-find the master controller. +//----------------------------------------------------------------------------- +void CPostProcessSystem::FireGameEvent( IGameEvent *pEvent ) +{ + InitMasterController(); +} + +//----------------------------------------------------------------------------- +// Purpose: On level load find the master PostProcess controller. If no controller is +// set as Master, use the first PostProcess controller found. +//----------------------------------------------------------------------------- +void CPostProcessSystem::LevelInitPostEntity() +{ + InitMasterController(); + + // HACK: Singleplayer games don't get a call to CBasePlayer::Spawn on level transitions. + // CBasePlayer::Activate is called before this is called so that's too soon to set up the PostProcess controller. + // We don't have a hook similar to Activate that happens after LevelInitPostEntity + // is called, or we could just do this in the player itself. + if ( gpGlobals->maxClients == 1 ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer && ( pPlayer->m_hPostProcessCtrl.Get() == nullptr ) ) + { + pPlayer->InitPostProcessController(); + } + } +} diff --git a/sp/src/game/server/postprocesscontroller.h b/sp/src/game/server/postprocesscontroller.h new file mode 100644 index 00000000..5608394a --- /dev/null +++ b/sp/src/game/server/postprocesscontroller.h @@ -0,0 +1,80 @@ +#pragma once + +#include "GameEventListener.h" +#include "postprocess_shared.h" + +// Spawn Flags +#define SF_POSTPROCESS_MASTER 0x0001 + +//============================================================================= +// Class Postprocess Controller: +//============================================================================= +class CPostProcessController : public CBaseEntity +{ +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + DECLARE_CLASS( CPostProcessController, CBaseEntity ); + + CPostProcessController(); + virtual ~CPostProcessController(); + + virtual int UpdateTransmitState(); + + // Input handlers + void InputSetFadeTime(inputdata_t &data); + void InputSetLocalContrastStrength(inputdata_t &data); + void InputSetLocalContrastEdgeStrength(inputdata_t &data); + void InputSetVignetteStart(inputdata_t &data); + void InputSetVignetteEnd(inputdata_t &data); + void InputSetVignetteBlurStrength(inputdata_t &data); + void InputSetFadeToBlackStrength(inputdata_t &data); + void InputSetDepthBlurFocalDistance(inputdata_t &data); + void InputSetDepthBlurStrength(inputdata_t &data); + void InputSetScreenBlurStrength(inputdata_t &data); + void InputSetFilmGrainStrength(inputdata_t &data); + + void InputTurnOn(inputdata_t &data); + void InputTurnOff(inputdata_t &data); + + void Spawn(); + + bool IsMaster() const { return HasSpawnFlags( SF_FOG_MASTER ); } + +public: + CNetworkArray( float, m_flPostProcessParameters, POST_PROCESS_PARAMETER_COUNT ); + + CNetworkVar( bool, m_bMaster ); +}; + +//============================================================================= +// +// Postprocess Controller System. +// +class CPostProcessSystem : public CAutoGameSystem, public CGameEventListener +{ +public: + + // Creation/Init. + CPostProcessSystem( char const *name ) : CAutoGameSystem( name ) + { + m_hMasterController = nullptr; + } + + ~CPostProcessSystem() + { + m_hMasterController = nullptr; + } + + virtual void LevelInitPreEntity(); + virtual void LevelInitPostEntity(); + virtual void FireGameEvent( IGameEvent *pEvent ); + CPostProcessController *GetMasterPostProcessController() { return m_hMasterController; } + +private: + + void InitMasterController(); + CHandle< CPostProcessController > m_hMasterController; +}; + +CPostProcessSystem *PostProcessSystem(); diff --git a/sp/src/game/server/props.cpp b/sp/src/game/server/props.cpp new file mode 100644 index 00000000..9263628b --- /dev/null +++ b/sp/src/game/server/props.cpp @@ -0,0 +1,6992 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: static_prop - don't move, don't animate, don't do anything. +// physics_prop - move, take damage, but don't animate +// +//===========================================================================// + + +#include "cbase.h" +#include "BasePropDoor.h" +#include "ai_basenpc.h" +#include "npcevent.h" +#include "engine/IEngineSound.h" +#include "locksounds.h" +#include "filters.h" +#include "physics.h" +#include "vphysics_interface.h" +#include "entityoutput.h" +#include "vcollide_parse.h" +#include "studio.h" +#include "explode.h" +#include "utlrbtree.h" +#include "tier1/strtools.h" +#include "physics_impact_damage.h" +#include "KeyValues.h" +#include "filesystem.h" +#include "scriptevent.h" +#include "entityblocker.h" +#include "soundent.h" +#include "EntityFlame.h" +#include "game.h" +#include "physics_prop_ragdoll.h" +#include "decals.h" +#include "hierarchy.h" +#include "shareddefs.h" +#include "physobj.h" +#include "physics_npc_solver.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "datacache/imdlcache.h" +#include "doors.h" +#include "physics_collisionevent.h" +#include "gamestats.h" +#include "vehicle_base.h" +#ifdef MAPBASE +#include "mapbase/GlobalStrings.h" +#include "collisionutils.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define DOOR_HARDWARE_GROUP 1 + +// Any barrel farther away than this is ignited rather than exploded. +#define PROP_EXPLOSION_IGNITE_RADIUS 32.0f + +// How many times to remind the player that supply crates can be broken +// (displayed when the supply crate is picked up) +#define NUM_SUPPLY_CRATE_HUD_HINTS 3 + +extern CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ); + + +ConVar g_debug_doors( "g_debug_doors", "0" ); +ConVar breakable_disable_gib_limit( "breakable_disable_gib_limit", "0" ); +ConVar breakable_multiplayer( "breakable_multiplayer", "1" ); + +// AI Interaction for being hit by a physics object +int g_interactionHitByPlayerThrownPhysObj = 0; +int g_interactionPlayerPuntedHeavyObject = 0; + +int g_ActiveGibCount = 0; +ConVar prop_active_gib_limit( "prop_active_gib_limit", "999999" ); +ConVar prop_active_gib_max_fade_time( "prop_active_gib_max_fade_time", "999999" ); +#define ACTIVE_GIB_LIMIT prop_active_gib_limit.GetInt() +#define ACTIVE_GIB_FADE prop_active_gib_max_fade_time.GetInt() + + +// Damage type modifiers for breakable objects. +ConVar func_breakdmg_bullet( "func_breakdmg_bullet", "0.5" ); +ConVar func_breakdmg_club( "func_breakdmg_club", "1.5" ); +ConVar func_breakdmg_explosive( "func_breakdmg_explosive", "1.25" ); + +ConVar sv_turbophysics( "sv_turbophysics", "0", FCVAR_REPLICATED, "Turns on turbo physics" ); + +#ifdef MAPBASE +ConVar mapbase_prop_consistency_noremove("mapbase_prop_consistency_noremove", "1", FCVAR_NONE, "Prevents the removal of props when their classes do not match up with their models' propdata."); +#endif + +#ifdef HL2_EPISODIC +#ifdef MAPBASE + #define PROP_FLARE_LIFETIME GetFlareLifetime() + float GetEnvFlareLifetime( CBaseEntity *pEntity ); +#else + #define PROP_FLARE_LIFETIME 30.0f +#endif + #define PROP_FLARE_IGNITE_SUBSTRACT 5.0f + CBaseEntity *CreateFlare( Vector vOrigin, QAngle Angles, CBaseEntity *pOwner, float flDuration ); + void KillFlare( CBaseEntity *pOwnerEntity, CBaseEntity *pEntity, float flKillTime ); +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Breakable objects take different levels of damage based upon the damage type. +// This isn't contained by CBaseProp, because func_breakables use it as well. +//----------------------------------------------------------------------------- +float GetBreakableDamage( const CTakeDamageInfo &inputInfo, IBreakableWithPropData *pProp ) +{ + float flDamage = inputInfo.GetDamage(); + int iDmgType = inputInfo.GetDamageType(); + + // Bullet damage? + if ( iDmgType & DMG_BULLET ) + { + // Buckshot does double damage to breakables + if ( iDmgType & DMG_BUCKSHOT ) + { + if ( pProp ) + { + flDamage *= (pProp->GetDmgModBullet() * 2); + } + else + { + // Bullets do little damage to breakables + flDamage *= (func_breakdmg_bullet.GetFloat() * 2); + } + } + else + { + if ( pProp ) + { + flDamage *= pProp->GetDmgModBullet(); + } + else + { + // Bullets do little damage to breakables + flDamage *= func_breakdmg_bullet.GetFloat(); + } + } + } + + // Club damage? + if ( iDmgType & DMG_CLUB ) + { + if ( pProp ) + { + flDamage *= pProp->GetDmgModClub(); + } + else + { + // Club does extra damage + flDamage *= func_breakdmg_club.GetFloat(); + } + } + + // Explosive damage? + if ( iDmgType & DMG_BLAST ) + { + if ( pProp ) + { + flDamage *= pProp->GetDmgModExplosive(); + } + else + { + // Explosions do extra damage + flDamage *= func_breakdmg_explosive.GetFloat(); + } + } + + if ( (iDmgType & DMG_SLASH) && (iDmgType & DMG_CRUSH) ) + { + // Cut by a Ravenholm propeller trap + flDamage *= 10.0f; + } + + // Poison & other timebased damage types do no damage + if ( g_pGameRules->Damage_IsTimeBased( iDmgType ) ) + { + flDamage = 0; + } + + return flDamage; +} + +//============================================================================================================= +// BASE PROP +//============================================================================================================= +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseProp::Spawn( void ) +{ + char *szModel = (char *)STRING( GetModelName() ); + if (!szModel || !*szModel) + { + Warning( "prop at %.0f %.0f %0.f missing modelname\n", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + UTIL_Remove( this ); + return; + } + + PrecacheModel( szModel ); + Precache(); + SetModel( szModel ); + + // Load this prop's data from the propdata file + int iResult = ParsePropData(); + if ( !OverridePropdata() ) + { +#ifdef MAPBASE + if (mapbase_prop_consistency_noremove.GetBool()) + { + switch (iResult) + { + case PARSE_FAILED_BAD_DATA: + Warning("%s at %.0f %.0f %0.f uses model %s, which has an invalid prop_data type. Not deleted due to mapbase_prop_consistency_noremove.\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, szModel); + break; + case PARSE_FAILED_NO_DATA: + { + if ( FClassnameIs( this, "prop_physics" ) ) + { + Warning("%s at %.0f %.0f %0.f uses model %s, which has no propdata which means it should be used on a prop_static. Not deleted due to mapbase_prop_consistency_noremove.\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, szModel); + } + } break; + case PARSE_SUCCEEDED: + { + if (!IsPropPhysics()) + { + Warning( "%s at %.0f %.0f %0.f uses model %s, which has propdata which means that it should be used on a prop_physics. Not deleted due to mapbase_prop_consistency_noremove.\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, szModel ); + } + } + } + } + else + { + // No comment. + #define DevWarning Warning +#endif + if ( iResult == PARSE_FAILED_BAD_DATA ) + { + DevWarning( "%s at %.0f %.0f %0.f uses model %s, which has an invalid prop_data type. DELETED.\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, szModel ); + UTIL_Remove( this ); + return; + } + else if ( iResult == PARSE_FAILED_NO_DATA ) + { + // If we don't have data, but we're a prop_physics, fail + if ( FClassnameIs( this, "prop_physics" ) ) + { + DevWarning( "%s at %.0f %.0f %0.f uses model %s, which has no propdata which means it must be used on a prop_static. DELETED.\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, szModel ); + UTIL_Remove( this ); + return; + } + } + else if ( iResult == PARSE_SUCCEEDED ) + { + // If we have data, and we're not a physics prop, fail +#ifdef MAPBASE + if ( !IsPropPhysics() ) +#else + if ( !dynamic_cast(this) ) +#endif + { + DevWarning( "%s at %.0f %.0f %0.f uses model %s, which has propdata which means that it be used on a prop_physics. DELETED.\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z, szModel ); + UTIL_Remove( this ); + return; + } + } +#ifdef MAPBASE + #undef DevWarning + } +#endif + } + + SetMoveType( MOVETYPE_PUSH ); + m_takedamage = DAMAGE_NO; + SetNextThink( TICK_NEVER_THINK ); + + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0; + SetCycle( 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseProp::Precache( void ) +{ + if ( GetModelName() == NULL_STRING ) + { + Msg( "%s at (%.3f, %.3f, %.3f) has no model name!\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + SetModelName( AllocPooledString( "models/error.mdl" ) ); + } + + PrecacheModel( STRING( GetModelName() ) ); + + PrecacheScriptSound( "Metal.SawbladeStick" ); + PrecacheScriptSound( "PropaneTank.Burst" ); + +#ifdef HL2_EPISODIC + UTIL_PrecacheOther( "env_flare" ); +#endif + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseProp::Activate( void ) +{ + BaseClass::Activate(); + + // Make sure mapmakers haven't used the wrong prop type. + if ( m_takedamage == DAMAGE_NO && m_iHealth != 0 ) + { + Warning("%s has a health specified in model '%s'. Use prop_physics or prop_dynamic instead.\n", GetClassname(), STRING(GetModelName()) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handles keyvalues from the BSP. Called before spawning. +//----------------------------------------------------------------------------- +bool CBaseProp::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq(szKeyName, "health") ) + { + // Only override props are allowed to override health. +#ifdef MAPBASE + if ( OverridePropdata() && !FStrEq(szValue, "-1") ) +#else + if ( FClassnameIs( this, "prop_physics_override" ) || FClassnameIs( this, "prop_dynamic_override" ) ) +#endif + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Calculate whether this prop should block LOS or not +//----------------------------------------------------------------------------- +void CBaseProp::CalculateBlockLOS( void ) +{ + // We block LOS if: + // - One of our dimensions is >40 + // - Our other 2 dimensions are >30 + // By default, entities block LOS, so we only need to detect non-blockage + bool bFoundLarge = false; + Vector vecSize = CollisionProp()->OBBMaxs() - CollisionProp()->OBBMins(); + for ( int i = 0; i < 3; i++ ) + { + if ( vecSize[i] > 40 ) + { + bFoundLarge = true; + } + if ( vecSize[i] > 30 ) + continue; + + // Dimension smaller than 30. + SetBlocksLOS( false ); + return; + } + + if ( !bFoundLarge ) + { + // No dimension larger than 40 + SetBlocksLOS( false ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Parse this prop's data from the model, if it has a keyvalues section. +// Returns true only if this prop is using a model that has a prop_data section that's invalid. +//----------------------------------------------------------------------------- +int CBaseProp::ParsePropData( void ) +{ + KeyValues *modelKeyValues = new KeyValues(""); + if ( !modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) + { + modelKeyValues->deleteThis(); + return PARSE_FAILED_NO_DATA; + } + + // Do we have a props section? + KeyValues *pkvPropData = modelKeyValues->FindKey("prop_data"); + if ( !pkvPropData ) + { + modelKeyValues->deleteThis(); + return PARSE_FAILED_NO_DATA; + } + + int iResult = g_PropDataSystem.ParsePropFromKV( this, pkvPropData, modelKeyValues ); + modelKeyValues->deleteThis(); + return iResult; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseProp::DrawDebugGeometryOverlays( void ) +{ + BaseClass::DrawDebugGeometryOverlays(); + + if ( m_debugOverlays & OVERLAY_PROP_DEBUG ) + { + if ( m_takedamage == DAMAGE_NO ) + { + NDebugOverlay::EntityBounds(this, 255, 0, 0, 0, 0 ); + } + else if ( m_takedamage == DAMAGE_EVENTS_ONLY ) + { + NDebugOverlay::EntityBounds(this, 255, 255, 255, 0, 0 ); + } + else + { + // Remap health to green brightness + float flG = RemapVal( m_iHealth, 0, 100, 64, 255 ); + flG = clamp( flG, 0.f, 255.f ); + NDebugOverlay::EntityBounds(this, 0, flG, 0, 0, 0 ); + } + } +} + + +class CEnableMotionFixup : public CBaseEntity +{ + DECLARE_CLASS( CEnableMotionFixup, CBaseEntity ); +}; + +LINK_ENTITY_TO_CLASS( point_enable_motion_fixup, CEnableMotionFixup ); + +static const char *s_pFadeScaleThink = "FadeScaleThink"; +static const char *s_pPropAnimateThink = "PropAnimateThink"; + +void CBreakableProp::SetEnableMotionPosition( const Vector &position, const QAngle &angles ) +{ + ClearEnableMotionPosition(); + CBaseEntity *pFixup = CBaseEntity::Create( "point_enable_motion_fixup", position, angles, this ); + if ( pFixup ) + { + pFixup->SetParent( this ); + } +} + +CBaseEntity *CBreakableProp::FindEnableMotionFixup() +{ + CUtlVector list; + GetAllChildren( this, list ); + for ( int i = list.Count()-1; i >= 0; --i ) + { + if ( FClassnameIs( list[i], "point_enable_motion_fixup" ) ) + return list[i]; + } + + return NULL; +} + +bool CBreakableProp::GetEnableMotionPosition( Vector *pPosition, QAngle *pAngles ) +{ + CBaseEntity *pFixup = FindEnableMotionFixup(); + if ( !pFixup ) + return false; + *pPosition = pFixup->GetAbsOrigin(); + *pAngles = pFixup->GetAbsAngles(); + return true; +} + +void CBreakableProp::ClearEnableMotionPosition() +{ + CBaseEntity *pFixup = FindEnableMotionFixup(); + if ( pFixup ) + { + UnlinkFromParent( pFixup ); + UTIL_Remove( pFixup ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBreakableProp::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bool bCalledByLevelDesigner ) +{ + if( IsOnFire() ) + return; + + if( !HasInteraction( PROPINTER_FIRE_FLAMMABLE ) ) + return; + + BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner ); + + if ( g_pGameRules->ShouldBurningPropsEmitLight() ) + { + GetEffectEntity()->AddEffects( EF_DIMLIGHT ); + } + + // Frighten AIs, just in case this is an exploding thing. + CSoundEnt::InsertSound( SOUND_DANGER, GetAbsOrigin(), 128.0f, 1.0f, this, SOUNDENT_CHANNEL_REPEATED_DANGER ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CBreakableProp::HandleFirstCollisionInteractions( int index, gamevcollisionevent_t *pEvent ) +{ + if ( pEvent->pEntities[ !index ]->IsWorld() ) + { + if ( HasInteraction( PROPINTER_PHYSGUN_WORLD_STICK ) ) + { + HandleInteractionStick( index, pEvent ); + } + } + + if( HasInteraction( PROPINTER_PHYSGUN_FIRST_BREAK ) ) + { + // Looks like it's best to break by having the object damage itself. + CTakeDamageInfo info; + + info.SetDamage( m_iHealth ); + info.SetAttacker( this ); + info.SetInflictor( this ); + info.SetDamageType( DMG_GENERIC ); + + Vector vecPosition; + Vector vecVelocity; + + VPhysicsGetObject()->GetVelocity( &vecVelocity, NULL ); + VPhysicsGetObject()->GetPosition( &vecPosition, NULL ); + + info.SetDamageForce( vecVelocity ); + info.SetDamagePosition( vecPosition ); + + TakeDamage( info ); + return; + } + + if( HasInteraction( PROPINTER_PHYSGUN_FIRST_PAINT ) ) + { + IPhysicsObject *pObj = VPhysicsGetObject(); + + Vector vecPos; + pObj->GetPosition( &vecPos, NULL ); + + Vector vecVelocity = pEvent->preVelocity[0]; + VectorNormalize(vecVelocity); + + trace_t tr; + UTIL_TraceLine( vecPos, vecPos + (vecVelocity * 64), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.m_pEnt ) + { +#ifdef HL2_DLL + // Don't paintsplat friendlies + int iClassify = tr.m_pEnt->Classify(); + if ( iClassify != CLASS_PLAYER_ALLY_VITAL && iClassify != CLASS_PLAYER_ALLY && + iClassify != CLASS_CITIZEN_PASSIVE && iClassify != CLASS_CITIZEN_REBEL ) +#endif + { + switch( entindex() % 3 ) + { + case 0: + UTIL_DecalTrace( &tr, "PaintSplatBlue" ); + break; + + case 1: + UTIL_DecalTrace( &tr, "PaintSplatGreen" ); + break; + + case 2: + UTIL_DecalTrace( &tr, "PaintSplatPink" ); + break; + } + } + } + } + + if ( HasInteraction( PROPINTER_PHYSGUN_NOTIFY_CHILDREN ) ) + { + CUtlVector children; + GetAllChildren( this, children ); + for (int i = 0; i < children.Count(); i++ ) + { + CBaseEntity *pent = children.Element( i ); + + IParentPropInteraction *pPropInter = dynamic_cast( pent ); + if ( pPropInter ) + { + pPropInter->OnParentCollisionInteraction( COLLISIONINTER_PARENT_FIRST_IMPACT, index, pEvent ); + } + } + } +} + + +void CBreakableProp::CheckRemoveRagdolls() +{ + if ( HasSpawnFlags( SF_PHYSPROP_HAS_ATTACHED_RAGDOLLS ) ) + { + DetachAttachedRagdollsForEntity( this ); + RemoveSpawnFlags( SF_PHYSPROP_HAS_ATTACHED_RAGDOLLS ); + } +} +//----------------------------------------------------------------------------- +// Purpose: Handle special physgun interactions +// Input : index - +// *pEvent - +//----------------------------------------------------------------------------- +void CPhysicsProp::HandleAnyCollisionInteractions( int index, gamevcollisionevent_t *pEvent ) +{ + // If we're supposed to impale, and we've hit an NPC, impale it + if ( HasInteraction( PROPINTER_PHYSGUN_FIRST_IMPALE ) ) + { + Vector vel = pEvent->preVelocity[index]; + + Vector forward; + QAngle angImpaleForward; + if ( GetPropDataAngles( "impale_forward", angImpaleForward ) ) + { + Vector vecImpaleForward; + AngleVectors( angImpaleForward, &vecImpaleForward ); + VectorRotate( vecImpaleForward, EntityToWorldTransform(), forward ); + } + else + { + GetVectors( &forward, NULL, NULL ); + } + + float speed = DotProduct( forward, vel ); + if ( speed < 1000.0f ) + { + // not going to stick, so remove any ragdolls we've got + CheckRemoveRagdolls(); + return; + } + CBaseEntity *pHitEntity = pEvent->pEntities[!index]; + if ( pHitEntity->IsWorld() ) + { + Vector normal; + float sign = index ? -1.0f : 1.0f; + pEvent->pInternalData->GetSurfaceNormal( normal ); + float dot = DotProduct( forward, normal ); + if ( (sign*dot) < DOT_45DEGREE ) + return; + // Impale sticks to the wall if we hit end on + HandleInteractionStick( index, pEvent ); + } + else if ( pHitEntity->MyNPCPointer() ) + { + CAI_BaseNPC *pNPC = pHitEntity->MyNPCPointer(); + IPhysicsObject *pObj = VPhysicsGetObject(); + + // do not impale NPCs if the impaler is friendly + CBasePlayer *pAttacker = HasPhysicsAttacker( 25.0f ); + if (pAttacker && pNPC->IRelationType( pAttacker ) == D_LI) + { + return; + } + + Vector vecPos; + pObj->GetPosition( &vecPos, NULL ); + + // Find the bone for the hitbox we hit + trace_t tr; + UTIL_TraceLine( vecPos, vecPos + pEvent->preVelocity[index] * 1.5, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + Vector vecImpalePos = tr.endpos; + int iBone = -1; + if ( tr.hitbox ) + { + Vector vecBonePos; + QAngle vecBoneAngles; + iBone = pNPC->GetHitboxBone( tr.hitbox ); + pNPC->GetBonePosition( iBone, vecBonePos, vecBoneAngles ); + + Teleport( &vecBonePos, NULL, NULL ); + vecImpalePos = vecBonePos; + } + + // Kill the NPC and make an attached ragdoll + pEvent->pInternalData->GetContactPoint( vecImpalePos ); + CBaseEntity *pRagdoll = CreateServerRagdollAttached( pNPC, vec3_origin, -1, COLLISION_GROUP_INTERACTIVE_DEBRIS, pObj, this, 0, vecImpalePos, iBone, vec3_origin ); + if ( pRagdoll ) + { + Vector vecVelocity = pEvent->preVelocity[index] * pObj->GetMass(); + PhysCallbackImpulse( pObj, vecVelocity, vec3_origin ); + UTIL_Remove( pNPC ); + AddSpawnFlags( SF_PHYSPROP_HAS_ATTACHED_RAGDOLLS ); + } + } + } +} + + +void CBreakableProp::StickAtPosition( const Vector &stickPosition, const Vector &savePosition, const QAngle &saveAngles ) +{ + if ( !VPhysicsGetObject()->IsMotionEnabled() ) + return; + + EmitSound("Metal.SawbladeStick"); + Teleport( &stickPosition, NULL, NULL ); + SetEnableMotionPosition( savePosition, saveAngles ); // this uses hierarchy, so it must be set after teleport + + VPhysicsGetObject()->EnableMotion( false ); + AddSpawnFlags( SF_PHYSPROP_ENABLE_ON_PHYSCANNON ); + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// *pEvent - +//----------------------------------------------------------------------------- +void CBreakableProp::HandleInteractionStick( int index, gamevcollisionevent_t *pEvent ) +{ + Vector vecDir = pEvent->preVelocity[ index ]; + float speed = VectorNormalize( vecDir ); + + // Make sure the object is travelling fast enough to stick. + if( speed > 1000.0f ) + { + Vector position; + QAngle angles; + VPhysicsGetObject()->GetPosition( &position, &angles ); + + Vector vecNormal; + pEvent->pInternalData->GetSurfaceNormal( vecNormal ); + + // we want the normal that points away from this object + if ( index == 1 ) + { + vecNormal *= -1.0f; + } + float flDot = DotProduct( vecDir, vecNormal ); + + // Make sure the object isn't hitting the world at too sharp an angle. + if( flDot > 0.3 ) + { + // Finally, inhibit sticking in metal, grates, sky, or anything else that doesn't make a sound. + const surfacedata_t *psurf = physprops->GetSurfaceData( pEvent->surfaceProps[!index] ); + + if (psurf->game.material != CHAR_TEX_METAL && psurf->game.material != CHAR_TEX_GRATE && psurf->game.material != 'X' ) + { + Vector savePosition = position; + + Vector vecEmbed = pEvent->preVelocity[ index ]; + VectorNormalize( vecEmbed ); + vecEmbed *= 8; + + position += vecEmbed; + g_PostSimulationQueue.QueueCall( this, &CBreakableProp::StickAtPosition, position, savePosition, angles ); + } + } + } +} + +#ifdef MAPBASE +extern int g_interactionBarnacleVictimBite; +extern ConVar npc_barnacle_ignite; +//----------------------------------------------------------------------------- +// Purpose: Uses the new CBaseEntity interaction implementation +// Input : The type of interaction, extra info pointer, and who started it +// Output : true - if sub-class has a response for the interaction +// false - if sub-class has no response +//----------------------------------------------------------------------------- +bool CBreakableProp::HandleInteraction( int interactionType, void *data, CBaseCombatCharacter* sourceEnt ) +{ +#ifdef HL2_EPISODIC + // Allows flares to ignite barnacles. + if ( interactionType == g_interactionBarnacleVictimBite ) + { + if ( npc_barnacle_ignite.GetBool() && sourceEnt->IsOnFire() == false ) + { + sourceEnt->Ignite( 25.0f ); + KillFlare( this, m_hFlareEnt, PROP_FLARE_IGNITE_SUBSTRACT ); + IGameEvent *event = gameeventmanager->CreateEvent( "flare_ignite_npc" ); + if ( event ) + { + event->SetInt( "entindex", sourceEnt->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + + return true; + } +#endif + + return BaseClass::HandleInteraction(interactionType, data, sourceEnt); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Turn on prop debugging mode +//----------------------------------------------------------------------------- +void CC_Prop_Debug( void ) +{ + // Toggle the prop debug bit on all props + for ( CBaseEntity *pEntity = gEntList.FirstEnt(); pEntity != NULL; pEntity = gEntList.NextEnt(pEntity) ) + { + CBaseProp *pProp = dynamic_cast(pEntity); + if ( pProp ) + { + if ( pProp->m_debugOverlays & OVERLAY_PROP_DEBUG ) + { + pProp->m_debugOverlays &= ~OVERLAY_PROP_DEBUG; + } + else + { + pProp->m_debugOverlays |= OVERLAY_PROP_DEBUG; + } + } + } +} +static ConCommand prop_debug("prop_debug", CC_Prop_Debug, "Toggle prop debug mode. If on, props will show colorcoded bounding boxes. Red means ignore all damage. White means respond physically to damage but never break. Green maps health in the range of 100 down to 1.", FCVAR_CHEAT); + +//============================================================================================================= +// BREAKABLE PROPS +//============================================================================================================= +IMPLEMENT_SERVERCLASS_ST(CBreakableProp, DT_BreakableProp) +END_SEND_TABLE() + +BEGIN_DATADESC( CBreakableProp ) + + DEFINE_KEYFIELD( m_explodeDamage, FIELD_FLOAT, "ExplodeDamage"), + DEFINE_KEYFIELD( m_explodeRadius, FIELD_FLOAT, "ExplodeRadius"), + DEFINE_KEYFIELD( m_iMinHealthDmg, FIELD_INTEGER, "minhealthdmg" ), + DEFINE_FIELD( m_createTick, FIELD_INTEGER ), + DEFINE_FIELD( m_hBreaker, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_PerformanceMode, FIELD_INTEGER, "PerformanceMode" ), + + DEFINE_KEYFIELD( m_iszBreakModelMessage, FIELD_STRING, "BreakModelMessage" ), + + DEFINE_FIELD( m_flDmgModBullet, FIELD_FLOAT ), + DEFINE_FIELD( m_flDmgModClub, FIELD_FLOAT ), + DEFINE_FIELD( m_flDmgModExplosive, FIELD_FLOAT ), + DEFINE_FIELD( m_iszPhysicsDamageTableName, FIELD_STRING ), + DEFINE_FIELD( m_iszBreakableModel, FIELD_STRING ), + DEFINE_FIELD( m_iBreakableSkin, FIELD_INTEGER ), + DEFINE_FIELD( m_iBreakableCount, FIELD_INTEGER ), + DEFINE_FIELD( m_iMaxBreakableSize, FIELD_INTEGER ), + DEFINE_FIELD( m_iszBasePropData, FIELD_STRING ), + DEFINE_FIELD( m_iInteractions, FIELD_INTEGER ), + DEFINE_FIELD( m_iNumBreakableChunks, FIELD_INTEGER ), + DEFINE_FIELD( m_nPhysgunState, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_iszPuntSound, FIELD_STRING, "puntsound" ), + + DEFINE_KEYFIELD( m_flPressureDelay, FIELD_FLOAT, "PressureDelay" ), + DEFINE_FIELD( m_preferredCarryAngles, FIELD_VECTOR ), +#ifdef MAPBASE + DEFINE_FIELD( m_bUsesCustomCarryAngles, FIELD_BOOLEAN ), +#endif + DEFINE_FIELD( m_flDefaultFadeScale, FIELD_FLOAT ), + DEFINE_FIELD( m_bUsePuntSound, FIELD_BOOLEAN ), + // DEFINE_FIELD( m_mpBreakMode, mp_break_t ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Break", InputBreak ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "AddHealth", InputAddHealth ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveHealth", InputRemoveHealth ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetInteraction", InputSetInteraction ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveInteraction", InputRemoveInteraction ), +#endif + DEFINE_INPUT( m_impactEnergyScale, FIELD_FLOAT, "physdamagescale" ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnablePhyscannonPickup", InputEnablePhyscannonPickup ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisablePhyscannonPickup", InputDisablePhyscannonPickup ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnablePuntSound", InputEnablePuntSound ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisablePuntSound", InputDisablePuntSound ), + + // Outputs + DEFINE_OUTPUT( m_OnBreak, "OnBreak" ), + DEFINE_OUTPUT( m_OnHealthChanged, "OnHealthChanged" ), + DEFINE_OUTPUT( m_OnTakeDamage, "OnTakeDamage" ), + DEFINE_OUTPUT( m_OnPhysCannonDetach, "OnPhysCannonDetach" ), + DEFINE_OUTPUT( m_OnPhysCannonAnimatePreStarted, "OnPhysCannonAnimatePreStarted" ), + DEFINE_OUTPUT( m_OnPhysCannonAnimatePullStarted, "OnPhysCannonAnimatePullStarted" ), + DEFINE_OUTPUT( m_OnPhysCannonAnimatePostStarted, "OnPhysCannonAnimatePostStarted" ), + DEFINE_OUTPUT( m_OnPhysCannonPullAnimFinished, "OnPhysCannonPullAnimFinished" ), + + // Function Pointers + DEFINE_THINKFUNC( BreakThink ), + DEFINE_THINKFUNC( AnimateThink ), + DEFINE_THINKFUNC( RampToDefaultFadeScale ), + DEFINE_ENTITYFUNC( BreakablePropTouch ), + + // Physics Influence + DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), + + DEFINE_FIELD( m_bOriginalBlockLOS, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bBlockLOSSetByPropData, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bIsWalkableSetByPropData, FIELD_BOOLEAN ), + + // Damage + DEFINE_FIELD( m_hLastAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_hFlareEnt, FIELD_EHANDLE ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Constructor: +//----------------------------------------------------------------------------- +CBreakableProp::CBreakableProp() +{ + m_fadeMinDist = -1; + m_fadeMaxDist = 0; + m_flFadeScale = 1; + m_flDefaultFadeScale = 1; + m_mpBreakMode = MULTIPLAYER_BREAK_DEFAULT; + + // This defaults to on. Most times mapmakers won't specify a punt sound to play. + m_bUsePuntSound = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBreakableProp::Spawn() +{ + // Starts out as the default fade scale value + m_flDefaultFadeScale = m_flFadeScale; + + // Initialize damage modifiers. Must be done before baseclass spawn. + m_flDmgModBullet = 1.0; + m_flDmgModClub = 1.0; + m_flDmgModExplosive = 1.0; + + //jmd: I am guessing that the call to Spawn will set any flags that should be set anyway; this + //clears flags we don't want (specifically the FL_ONFIRE for explosive barrels in HL2MP)] +#ifdef HL2MP + ClearFlags(); +#endif + + BaseClass::Spawn(); + + if ( IsMarkedForDeletion() ) + return; + + CStudioHdr *pStudioHdr = GetModelPtr( ); + if ( pStudioHdr->flags() & STUDIOHDR_FLAGS_NO_FORCED_FADE ) + { + DisableAutoFade(); + } + else + { + m_flFadeScale = m_flDefaultFadeScale; + } + + // If we have no custom breakable chunks, see if we're breaking into generic ones + if ( !m_iNumBreakableChunks ) + { + IBreakableWithPropData *pBreakableInterface = assert_cast(this); + if ( pBreakableInterface->GetBreakableModel() != NULL_STRING && pBreakableInterface->GetBreakableCount() ) + { + m_iNumBreakableChunks = pBreakableInterface->GetBreakableCount(); + } + } + + // Setup takedamage based upon the health we parsed earlier, and our interactions + if ( ( m_iHealth == 0 ) || + ( !m_iNumBreakableChunks && + !HasInteraction( PROPINTER_PHYSGUN_BREAK_EXPLODE ) && + !HasInteraction( PROPINTER_PHYSGUN_FIRST_BREAK ) && + !HasInteraction( PROPINTER_FIRE_FLAMMABLE ) && + !HasInteraction( PROPINTER_FIRE_IGNITE_HALFHEALTH ) && + !HasInteraction( PROPINTER_FIRE_EXPLOSIVE_RESIST ) ) ) + { + m_iHealth = 0; + m_takedamage = DAMAGE_EVENTS_ONLY; + } + else + { + m_takedamage = DAMAGE_YES; + + if( g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE ) + { + if ( HasInteraction( PROPINTER_PHYSGUN_BREAK_EXPLODE ) || + HasInteraction( PROPINTER_FIRE_IGNITE_HALFHEALTH ) ) + { + // Exploding barrels, exploding gas cans + AddFlag( FL_AIMTARGET ); + } + } + } + + m_iMaxHealth = ( m_iHealth > 0 ) ? m_iHealth : 1; + + m_createTick = gpGlobals->tickcount; + if ( m_impactEnergyScale == 0 ) + { + m_impactEnergyScale = 0.1f; + } + +#ifdef MAPBASE + if (!m_bUsesCustomCarryAngles) +#endif + m_preferredCarryAngles = QAngle( -5, 0, 0 ); + + // The presence of this activity causes us to have to detach it before it can be grabbed. + if ( SelectWeightedSequence( ACT_PHYSCANNON_ANIMATE ) != ACTIVITY_NOT_AVAILABLE ) + { + m_nPhysgunState = PHYSGUN_ANIMATE_ON_PULL; + } + else if ( SelectWeightedSequence( ACT_PHYSCANNON_DETACH ) != ACTIVITY_NOT_AVAILABLE ) + { + m_nPhysgunState = PHYSGUN_MUST_BE_DETACHED; + } + else + { + m_nPhysgunState = PHYSGUN_CAN_BE_GRABBED; + } + + m_hLastAttacker = NULL; + + m_hBreaker = NULL; + + SetTouch( &CBreakableProp::BreakablePropTouch ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Handles keyvalues from the BSP. Called before spawning. +//----------------------------------------------------------------------------- +bool CBreakableProp::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( OverridePropdata() ) + { + if ( FStrEq(szKeyName, "InitialInteractions") ) + { + // Only override props are allowed to override interactions. + if (strchr(szValue, ' ')) + { + // How many interactions could there possibly be? + char szInteractions[64]; + Q_strncpy(szInteractions, szValue, sizeof(szInteractions)); + + char *token = strtok(szInteractions, " ,"); + while (token) + { + SetInteraction((propdata_interactions_t)atoi(token)); + token = strtok(token, " ,"); + } + } + else + SetInteraction((propdata_interactions_t)atoi(szValue)); + } + else if ( FStrEq(szKeyName, "preferredcarryangles") ) + { + // Only detect as custom if it's non-zero + if (!FStrEq( szValue, "0" )) + { + QAngle angCarryAngles; + UTIL_StringToVector( angCarryAngles.Base(), szValue ); + + m_preferredCarryAngles = angCarryAngles; + m_bUsesCustomCarryAngles = true; + } + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} +#endif + + +//----------------------------------------------------------------------------- +// Disable auto fading under dx7 or when level fades are specified +//----------------------------------------------------------------------------- +void CBreakableProp::DisableAutoFade() +{ + m_flFadeScale = 0; + m_flDefaultFadeScale = 0; +} + + +//----------------------------------------------------------------------------- +// Copy fade from another breakable. +//----------------------------------------------------------------------------- +void CBreakableProp::CopyFadeFrom( CBreakableProp *pSource ) +{ + m_flDefaultFadeScale = pSource->m_flDefaultFadeScale; + m_flFadeScale = pSource->m_flFadeScale; + if ( m_flFadeScale != m_flDefaultFadeScale ) + { + float flNextThink = pSource->GetNextThink( s_pFadeScaleThink ); + if ( flNextThink < gpGlobals->curtime + TICK_INTERVAL ) + { + flNextThink = gpGlobals->curtime + TICK_INTERVAL; + } + + SetContextThink( &CBreakableProp::RampToDefaultFadeScale, flNextThink, s_pFadeScaleThink ); + } +} + + +//----------------------------------------------------------------------------- +// Make physcannonable, or not +//----------------------------------------------------------------------------- +void CBreakableProp::InputEnablePhyscannonPickup( inputdata_t &inputdata ) +{ + RemoveEFlags( EFL_NO_PHYSCANNON_INTERACTION ); +} + +void CBreakableProp::InputDisablePhyscannonPickup( inputdata_t &inputdata ) +{ + AddEFlags( EFL_NO_PHYSCANNON_INTERACTION ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pOther - +//----------------------------------------------------------------------------- +void CBreakableProp::BreakablePropTouch( CBaseEntity *pOther ) +{ + if ( HasSpawnFlags( SF_PHYSPROP_TOUCH ) ) + { + // can be broken when run into + float flDamage = pOther->GetSmoothedVelocity().Length() * 0.01; + + if ( flDamage >= m_iHealth ) + { + // Make sure we can take damage + m_takedamage = DAMAGE_YES; + OnTakeDamage( CTakeDamageInfo( pOther, pOther, flDamage, DMG_CRUSH ) ); + + // do a little damage to player if we broke glass or computer + CTakeDamageInfo info( pOther, pOther, flDamage/4, DMG_SLASH ); + CalculateMeleeDamageForce( &info, (pOther->GetAbsOrigin() - GetAbsOrigin()), GetAbsOrigin() ); + pOther->TakeDamage( info ); + } + } + + if ( HasSpawnFlags( SF_PHYSPROP_PRESSURE ) && pOther->GetGroundEntity() == this ) + { + // can be broken when stood upon + // play creaking sound here. + // DamageSound(); + + m_hBreaker = pOther; + + if ( m_pfnThink != (void (CBaseEntity::*)())&CBreakableProp::BreakThink ) + { + SetThink( &CBreakableProp::BreakThink ); + //SetTouch( NULL ); + + // Add optional delay + SetNextThink( gpGlobals->curtime + m_flPressureDelay ); + } + } + +#ifdef HL2_EPISODIC + if ( m_hFlareEnt ) + { + CAI_BaseNPC *pNPC = pOther->MyNPCPointer(); + + if ( pNPC && pNPC->AllowedToIgnite() && pNPC->IsOnFire() == false ) + { + pNPC->Ignite( 25.0f ); + KillFlare( this, m_hFlareEnt, PROP_FLARE_IGNITE_SUBSTRACT ); + IGameEvent *event = gameeventmanager->CreateEvent( "flare_ignite_npc" ); + if ( event ) + { + event->SetInt( "entindex", pNPC->entindex() ); + gameeventmanager->FireEvent( event ); + } + } + } +#endif +} + +//----------------------------------------------------------------------------- +// UNDONE: Time stamp the object's creation so that an explosion or something doesn't break the parent object +// and then break the children who spawn afterward ? +// Explosions should use entities in box before they start to do damage. Make sure nothing traverses the list +// in a way that would hose this. +int CBreakableProp::OnTakeDamage( const CTakeDamageInfo &inputInfo ) +{ + CTakeDamageInfo info = inputInfo; + + // If attacker can't do at least the min required damage to us, don't take any damage from them + if ( info.GetDamage() < m_iMinHealthDmg ) + return 0; + + if (!PassesDamageFilter( info )) + { + return 1; + } + + if( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() ) + { + m_hLastAttacker.Set( info.GetAttacker() ); + } + + float flPropDamage = GetBreakableDamage( info, assert_cast(this) ); + info.SetDamage( flPropDamage ); + + // UNDONE: Do this? +#if 0 + // Make a shard noise each time func breakable is hit. + // Don't play shard noise if being burned. + // Don't play shard noise if cbreakable actually died. + if ( ( bitsDamageType & DMG_BURN ) == false ) + { + DamageSound(); + } +#endif + + // don't take damage on the same frame you were created + // (avoids a set of explosions progressively vaporizing a compound breakable) + if ( m_createTick == (unsigned int)gpGlobals->tickcount ) + { + int saveFlags = m_takedamage; + m_takedamage = DAMAGE_EVENTS_ONLY; + int ret = BaseClass::OnTakeDamage( info ); + m_takedamage = saveFlags; + + return ret; + } + + // Ignore fire damage from other flames if I'm already on fire. + // (i.e., only let the flames attached to me damage me) + if( IsOnFire() && (inputInfo.GetDamageType() & DMG_BURN) && !(inputInfo.GetDamageType() & DMG_DIRECT) ) + { + return 0; + } + + bool bDeadly = info.GetDamage() >= m_iHealth; + + if( bDeadly && (info.GetDamageType() & DMG_BLAST) && HasInteraction( PROPINTER_FIRE_EXPLOSIVE_RESIST ) && info.GetInflictor() ) + { + // This explosion would kill me, but I have a special interaction with explosions. + + float flDist = ( WorldSpaceCenter() - info.GetInflictor()->WorldSpaceCenter() ).Length(); + + // I'm going to burn for a bit instead of exploding right now. + float flBurnTime; + if( flDist >= PROP_EXPLOSION_IGNITE_RADIUS ) + { + // I'm far from the blast. Ignite and burn for several seconds. + const float MAX_BLAST_DIST = 256.0f; + + // Just clamp distance. + if( flDist > MAX_BLAST_DIST ) + flDist = MAX_BLAST_DIST; + + float flFactor; + flFactor = flDist / MAX_BLAST_DIST; + const float MAX_BURN_TIME = 5.0f; + flBurnTime = MAX( 0.5, MAX_BURN_TIME * flFactor ); + flBurnTime += random->RandomFloat( 0, 0.5 ); + } + else + { + // Very near the explosion. explode almost immediately. + flBurnTime = random->RandomFloat( 0.1, 0.2 ); + } + + // Change my health so that I burn for flBurnTime seconds. + float flIdealHealth = MIN( m_iHealth, FLAME_DIRECT_DAMAGE_PER_SEC * flBurnTime ); + float flIdealDamage = m_iHealth - flIdealHealth; + + // Scale the damage to do ideal damage. + info.ScaleDamage( flIdealDamage / info.GetDamage() ); + + // Re-evaluate the deadly + bDeadly = info.GetDamage() >= m_iHealth; + } + + if( !bDeadly && (info.GetDamageType() & DMG_BLAST) ) + { + Ignite( random->RandomFloat( 10, 15 ), false ); + } + else if( !bDeadly && (info.GetDamageType() & DMG_BURN) ) + { + // Ignite if burned, and flammable (the Ignite() function takes care of all of this). + Ignite( random->RandomFloat( 10, 15 ), false ); + } + else if( !bDeadly && (info.GetDamageType() & DMG_BULLET) ) + { + if( HasInteraction( PROPINTER_FIRE_IGNITE_HALFHEALTH ) ) + { + if( (m_iHealth - info.GetDamage()) <= m_iMaxHealth / 2 && !IsOnFire() ) + { + // Bump back up to full health so it burns longer. Magically getting health back isn't + // a big problem because if this item takes damage again whilst burning, it will break. + m_iHealth = m_iMaxHealth; + Ignite( random->RandomFloat( 10, 15 ), false ); + } + else if( IsOnFire() ) + { + // Explode right now! + info.ScaleDamage( m_iHealth / info.GetDamage() ); + } + } + } + + int ret = BaseClass::OnTakeDamage( info ); + + // Output the new health as a percentage of max health [0..1] + float flRatio = clamp( (float)m_iHealth / (float)m_iMaxHealth, 0.f, 1.f ); + m_OnHealthChanged.Set( flRatio, info.GetAttacker(), this ); + m_OnTakeDamage.FireOutput( info.GetAttacker(), this ); + + return ret; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBreakableProp::Event_Killed( const CTakeDamageInfo &info ) +{ + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if ( pPhysics && !pPhysics->IsMoveable() ) + { + pPhysics->EnableMotion( true ); + VPhysicsTakeDamage( info ); + } + Break( info.GetInflictor(), info ); + BaseClass::Event_Killed( info ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for breaking the breakable immediately. +//----------------------------------------------------------------------------- +void CBreakableProp::InputBreak( inputdata_t &inputdata ) +{ + CTakeDamageInfo info; + info.SetAttacker( this ); + Break( inputdata.pActivator, info ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for adding to the breakable's health. +// Input : Integer health points to add. +//----------------------------------------------------------------------------- +void CBreakableProp::InputAddHealth( inputdata_t &inputdata ) +{ + UpdateHealth( m_iHealth + inputdata.value.Int(), inputdata.pActivator ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for removing health from the breakable. +// Input : Integer health points to remove. +//----------------------------------------------------------------------------- +void CBreakableProp::InputRemoveHealth( inputdata_t &inputdata ) +{ + UpdateHealth( m_iHealth - inputdata.value.Int(), inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the breakable's health. +//----------------------------------------------------------------------------- +void CBreakableProp::InputSetHealth( inputdata_t &inputdata ) +{ + UpdateHealth( inputdata.value.Int(), inputdata.pActivator ); +} + + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting interactions. +//----------------------------------------------------------------------------- +void CBreakableProp::InputSetInteraction( inputdata_t &inputdata ) +{ + SetInteraction( (propdata_interactions_t)inputdata.value.Int() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler for Adding interactions. +//----------------------------------------------------------------------------- +void CBreakableProp::InputRemoveInteraction( inputdata_t &inputdata ) +{ + RemoveInteraction( (propdata_interactions_t)inputdata.value.Int() ); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Choke point for changes to breakable health. Ensures outputs are fired. +// Input : iNewHealth - +// pActivator - +// Output : Returns true if the breakable survived, false if it died (broke). +//----------------------------------------------------------------------------- +bool CBreakableProp::UpdateHealth( int iNewHealth, CBaseEntity *pActivator ) +{ + if ( iNewHealth != m_iHealth ) + { + m_iHealth = iNewHealth; + + if ( m_iMaxHealth == 0 ) + { + Assert( false ); + m_iMaxHealth = 1; + } + + // Output the new health as a percentage of max health [0..1] + float flRatio = clamp( (float)m_iHealth / (float)m_iMaxHealth, 0.f, 1.f ); + m_OnHealthChanged.Set( flRatio, pActivator, this ); + + if ( m_iHealth <= 0 ) + { + CTakeDamageInfo info; + info.SetAttacker( this ); + Break( pActivator, info ); + + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Advance a ripped-off-animation frame +//----------------------------------------------------------------------------- +bool CBreakableProp::OnAttemptPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + if ( m_nPhysgunState == PHYSGUN_CAN_BE_GRABBED ) + return true; + if ( m_nPhysgunState == PHYSGUN_ANIMATE_FINISHED ) + return false; + + if ( m_nPhysgunState == PHYSGUN_MUST_BE_DETACHED ) + { + // A punt advances + ResetSequence( SelectWeightedSequence( ACT_PHYSCANNON_DETACH ) ); + SetPlaybackRate( 0.0f ); + ResetClientsideFrame(); + m_nPhysgunState = PHYSGUN_IS_DETACHING; + return false; + } + + if ( m_nPhysgunState == PHYSGUN_ANIMATE_ON_PULL ) + { + // Animation-requiring detachments ignore punts + if ( reason == PUNTED_BY_CANNON ) + return false; + + // Do we have a pre sequence? + int iSequence = SelectWeightedSequence( ACT_PHYSCANNON_ANIMATE_PRE ); + if ( iSequence != ACTIVITY_NOT_AVAILABLE ) + { + m_nPhysgunState = PHYSGUN_ANIMATE_IS_PRE_ANIMATING; + SetContextThink( &CBreakableProp::AnimateThink, gpGlobals->curtime + 0.1, s_pPropAnimateThink ); + + m_OnPhysCannonAnimatePreStarted.FireOutput( NULL,this ); + } + else + { + // Go straight to the animate sequence + iSequence = SelectWeightedSequence( ACT_PHYSCANNON_ANIMATE ); + m_nPhysgunState = PHYSGUN_ANIMATE_IS_ANIMATING; + + m_OnPhysCannonAnimatePullStarted.FireOutput( NULL,this ); + } + + ResetSequence( iSequence ); + SetPlaybackRate( 1.0f ); + ResetClientsideFrame(); + } + + // If we're running PRE or POST ANIMATE sequences, wait for them to be done + if ( m_nPhysgunState == PHYSGUN_ANIMATE_IS_PRE_ANIMATING || + m_nPhysgunState == PHYSGUN_ANIMATE_IS_POST_ANIMATING ) + return false; + + if ( m_nPhysgunState == PHYSGUN_ANIMATE_IS_ANIMATING ) + { + // Animation-requiring detachments ignore punts + if ( reason == PUNTED_BY_CANNON ) + return false; + + StudioFrameAdvanceManual( gpGlobals->frametime ); + DispatchAnimEvents( this ); + + if ( IsActivityFinished() ) + { + int iSequence = SelectWeightedSequence( ACT_PHYSCANNON_ANIMATE_POST ); + if ( iSequence != ACTIVITY_NOT_AVAILABLE ) + { + m_nPhysgunState = PHYSGUN_ANIMATE_IS_POST_ANIMATING; + SetContextThink( &CBreakableProp::AnimateThink, gpGlobals->curtime + 0.1, s_pPropAnimateThink ); + ResetSequence( iSequence ); + SetPlaybackRate( 1.0f ); + ResetClientsideFrame(); + + m_OnPhysCannonAnimatePostStarted.FireOutput( NULL,this ); + } + else + { + m_nPhysgunState = PHYSGUN_ANIMATE_FINISHED; + m_OnPhysCannonPullAnimFinished.FireOutput( NULL,this ); + } + } + } + else + { + // Here, we're grabbing it. If we try to punt it, advance frames by quite a bit. + StudioFrameAdvanceManual( (reason == PICKED_UP_BY_CANNON) ? gpGlobals->frametime : 0.5f ); + ResetClientsideFrame(); + DispatchAnimEvents( this ); + + if ( IsActivityFinished() ) + { + // We're done, reset the playback rate. + SetPlaybackRate( 1.0f ); + m_nPhysgunState = PHYSGUN_CAN_BE_GRABBED; + m_OnPhysCannonDetach.FireOutput( NULL,this ); + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Physics Attacker +//----------------------------------------------------------------------------- +void CBreakableProp::AnimateThink( void ) +{ + if ( m_nPhysgunState == PHYSGUN_ANIMATE_IS_PRE_ANIMATING || m_nPhysgunState == PHYSGUN_ANIMATE_IS_POST_ANIMATING ) + { + StudioFrameAdvanceManual( 0.1 ); + DispatchAnimEvents( this ); + SetNextThink( gpGlobals->curtime + 0.1, s_pPropAnimateThink ); + + if ( IsActivityFinished() ) + { + if ( m_nPhysgunState == PHYSGUN_ANIMATE_IS_PRE_ANIMATING ) + { + // Start the animate sequence + m_nPhysgunState = PHYSGUN_ANIMATE_IS_ANIMATING; + + ResetSequence( SelectWeightedSequence( ACT_PHYSCANNON_ANIMATE ) ); + SetPlaybackRate( 1.0f ); + ResetClientsideFrame(); + + m_OnPhysCannonAnimatePullStarted.FireOutput( NULL,this ); + } + else + { + m_nPhysgunState = PHYSGUN_ANIMATE_FINISHED; + m_OnPhysCannonPullAnimFinished.FireOutput( NULL,this ); + } + + SetContextThink( NULL, 0, s_pPropAnimateThink ); + } + } +} + +//----------------------------------------------------------------------------- +// Physics Attacker +//----------------------------------------------------------------------------- +void CBreakableProp::SetPhysicsAttacker( CBasePlayer *pEntity, float flTime ) +{ + m_hPhysicsAttacker = pEntity; + m_flLastPhysicsInfluenceTime = flTime; +} + + +//----------------------------------------------------------------------------- +// Prevents fade scale from happening +//----------------------------------------------------------------------------- +void CBreakableProp::ForceFadeScaleToAlwaysVisible() +{ + m_flFadeScale = 0.0f; + SetContextThink( NULL, gpGlobals->curtime, s_pFadeScaleThink ); +} + + +void CBreakableProp::RampToDefaultFadeScale() +{ + m_flFadeScale += m_flDefaultFadeScale * TICK_INTERVAL / 2.0f; + if ( m_flFadeScale >= m_flDefaultFadeScale ) + { + m_flFadeScale = m_flDefaultFadeScale; + SetContextThink( NULL, gpGlobals->curtime, s_pFadeScaleThink ); + } + else + { + SetContextThink( &CBreakableProp::RampToDefaultFadeScale, gpGlobals->curtime + TICK_INTERVAL, s_pFadeScaleThink ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Keep track of physgun influence +//----------------------------------------------------------------------------- +void CBreakableProp::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + // Make sure held objects are always visible + if ( reason == PICKED_UP_BY_CANNON ) + { + ForceFadeScaleToAlwaysVisible(); + } + else + { + SetContextThink( &CBreakableProp::RampToDefaultFadeScale, gpGlobals->curtime + 2.0f, s_pFadeScaleThink ); + } + + if( reason == PUNTED_BY_CANNON ) + { + PlayPuntSound(); + } + + if ( IsX360() ) + { + if( reason != PUNTED_BY_CANNON && (pPhysGunUser->m_nNumCrateHudHints < NUM_SUPPLY_CRATE_HUD_HINTS) ) + { + if( FClassnameIs( this, "item_item_crate") ) + { + pPhysGunUser->m_nNumCrateHudHints++; + UTIL_HudHintText( pPhysGunUser, "#Valve_Hint_Hold_ItemCrate" ); + } + } + } + + SetPhysicsAttacker( pPhysGunUser, gpGlobals->curtime ); + + // Store original BlockLOS, and disable BlockLOS + m_bOriginalBlockLOS = BlocksLOS(); + SetBlocksLOS( false ); + +#ifdef HL2_EPISODIC + if ( HasInteraction( PROPINTER_PHYSGUN_CREATE_FLARE ) ) + { + CreateFlare( PROP_FLARE_LIFETIME ); + } +#endif +} + + +#ifdef HL2_EPISODIC +//----------------------------------------------------------------------------- +// Purpose: Create a flare at the attachment point +//----------------------------------------------------------------------------- +void CBreakableProp::CreateFlare( float flLifetime ) +{ + // Create the flare + CBaseEntity *pFlare = ::CreateFlare( GetAbsOrigin(), GetAbsAngles(), this, flLifetime ); + if ( pFlare ) + { + int iAttachment = LookupAttachment( "fuse" ); + + Vector vOrigin; +#ifdef MAPBASE + if (!GetAttachment( iAttachment, vOrigin )) + vOrigin = GetLocalOrigin(); +#else + GetAttachment( iAttachment, vOrigin ); +#endif + + pFlare->SetMoveType( MOVETYPE_NONE ); + pFlare->SetSolid( SOLID_NONE ); + pFlare->SetRenderMode( kRenderTransAlpha ); + pFlare->SetRenderColorA( 1 ); + pFlare->SetLocalOrigin( vOrigin ); + pFlare->SetParent( this, iAttachment ); + RemoveInteraction( PROPINTER_PHYSGUN_CREATE_FLARE ); + m_hFlareEnt = pFlare; + + SetThink( &CBreakable::SUB_FadeOut ); + SetNextThink( gpGlobals->curtime + flLifetime + 5.0f ); + + m_nSkin = 1; + + AddEntityToDarknessCheck( pFlare ); + + AddEffects( EF_NOSHADOW ); + } +} +#endif // HL2_EPISODIC + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBreakableProp::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + SetContextThink( &CBreakableProp::RampToDefaultFadeScale, gpGlobals->curtime + 2.0f, s_pFadeScaleThink ); + + SetPhysicsAttacker( pPhysGunUser, gpGlobals->curtime ); + + if( (int)Reason == (int)PUNTED_BY_CANNON ) + { + PlayPuntSound(); + } + + // Restore original BlockLOS + SetBlocksLOS( m_bOriginalBlockLOS ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +AngularImpulse CBreakableProp::PhysGunLaunchAngularImpulse() +{ + if( HasInteraction( PROPINTER_PHYSGUN_LAUNCH_SPIN_NONE ) || HasInteraction( PROPINTER_PHYSGUN_LAUNCH_SPIN_Z ) ) + { + // Don't add in random angular impulse if this object is supposed to spin in a specific way. + AngularImpulse ang( 0, 0, 0 ); + return ang; + } + + return CDefaultPlayerPickupVPhysics::PhysGunLaunchAngularImpulse(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CBasePlayer *CBreakableProp::HasPhysicsAttacker( float dt ) +{ + if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) + { + return m_hPhysicsAttacker; + } + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBreakableProp::BreakThink( void ) +{ + CTakeDamageInfo info; + info.SetAttacker( this ); + Break( m_hBreaker, info ); +} + +//----------------------------------------------------------------------------- +// Purpose: Play the sound (if any) that I'm supposed to play when punted. +//----------------------------------------------------------------------------- +void CBreakableProp::PlayPuntSound() +{ + if( !m_bUsePuntSound ) + return; + + if( m_iszPuntSound == NULL_STRING ) + return; + + EmitSound( STRING(m_iszPuntSound) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBreakableProp::Precache() +{ + m_iNumBreakableChunks = PropBreakablePrecacheAll( GetModelName() ); + + if( m_iszPuntSound != NULL_STRING ) + { + PrecacheScriptSound( STRING(m_iszPuntSound) ); + } + + BaseClass::Precache(); +} + +// Get the root physics object from which all broken pieces will +// derive their positions and velocities +IPhysicsObject *CBreakableProp::GetRootPhysicsObjectForBreak() +{ + return VPhysicsGetObject(); +} + +void CBreakableProp::Break( CBaseEntity *pBreaker, const CTakeDamageInfo &info ) +{ + const char *pModelName = STRING( GetModelName() ); + if ( pModelName && Q_stristr( pModelName, "crate" ) ) + { + bool bSmashed = false; + if ( pBreaker && pBreaker->IsPlayer() ) + { + bSmashed = true; + } + else if ( m_hPhysicsAttacker.Get() && m_hPhysicsAttacker->IsPlayer() ) + { + bSmashed = true; + } + else if ( pBreaker && dynamic_cast< CPropVehicleDriveable * >( pBreaker ) ) + { + CPropVehicleDriveable *veh = static_cast< CPropVehicleDriveable * >( pBreaker ); + CBaseEntity *driver = veh->GetDriver(); + if ( driver && driver->IsPlayer() ) + { + bSmashed = true; + } + } + if ( bSmashed ) + { + gamestats->Event_CrateSmashed(); + } + } + + IGameEvent * event = gameeventmanager->CreateEvent( "break_prop" ); + + if ( event ) + { + if ( pBreaker && pBreaker->IsPlayer() ) + { + event->SetInt( "userid", ToBasePlayer( pBreaker )->GetUserID() ); + } + else + { + event->SetInt( "userid", 0 ); + } + event->SetInt( "entindex", entindex() ); + gameeventmanager->FireEvent( event ); + } + + m_takedamage = DAMAGE_NO; + m_OnBreak.FireOutput( pBreaker, this ); + + Vector velocity; + AngularImpulse angVelocity; + IPhysicsObject *pPhysics = GetRootPhysicsObjectForBreak(); + + Vector origin; + QAngle angles; + AddSolidFlags( FSOLID_NOT_SOLID ); + if ( pPhysics ) + { + pPhysics->GetVelocity( &velocity, &angVelocity ); + pPhysics->GetPosition( &origin, &angles ); + pPhysics->RecheckCollisionFilter(); + } + else + { + velocity = GetAbsVelocity(); + QAngleToAngularImpulse( GetLocalAngularVelocity(), angVelocity ); + origin = GetAbsOrigin(); + angles = GetAbsAngles(); + } + + PhysBreakSound( this, VPhysicsGetObject(), GetAbsOrigin() ); + + bool bExploded = false; + + CBaseEntity *pAttacker = info.GetAttacker(); + if ( m_hLastAttacker ) + { + // Pass along the person who made this explosive breakable explode. + // This way the player allies can get immunity from barrels exploded by the player. + pAttacker = m_hLastAttacker; + } + else if( m_hPhysicsAttacker ) + { + // If I have a physics attacker and was influenced in the last 2 seconds, + // Make the attacker my physics attacker. This helps protect citizens from dying + // in the explosion of a physics object that was thrown by the player's physgun + // and exploded on impact. + if( gpGlobals->curtime - m_flLastPhysicsInfluenceTime <= 2.0f ) + { + pAttacker = m_hPhysicsAttacker; + } + } + + if ( m_explodeDamage > 0 || m_explodeRadius > 0 ) + { + if( HasInteraction( PROPINTER_PHYSGUN_BREAK_EXPLODE ) ) + { + ExplosionCreate( WorldSpaceCenter(), angles, pAttacker, m_explodeDamage, m_explodeRadius, + SF_ENVEXPLOSION_NOSPARKS | SF_ENVEXPLOSION_NODLIGHTS | SF_ENVEXPLOSION_NOSMOKE | SF_ENVEXPLOSION_SURFACEONLY | SF_ENVEXPLOSION_NOSOUND, + 0.0f, this ); + EmitSound("PropaneTank.Burst"); + } + else + { + float flScale = GetModelScale(); + ExplosionCreate( WorldSpaceCenter(), angles, pAttacker, m_explodeDamage * flScale, m_explodeRadius * flScale, + SF_ENVEXPLOSION_NOSPARKS | SF_ENVEXPLOSION_NODLIGHTS | SF_ENVEXPLOSION_NOSMOKE | SF_ENVEXPLOSION_SURFACEONLY, + 0.0f, this ); + } + + bExploded = true; + } + + // Allow derived classes to emit special things + OnBreak( velocity, angVelocity, pBreaker ); + + breakablepropparams_t params( origin, angles, velocity, angVelocity ); + params.impactEnergyScale = m_impactEnergyScale; + params.defCollisionGroup = GetCollisionGroup(); + if ( params.defCollisionGroup == COLLISION_GROUP_NONE ) + { + // don't automatically make anything COLLISION_GROUP_NONE or it will + // collide with debris being ejected by breaking + params.defCollisionGroup = COLLISION_GROUP_INTERACTIVE; + } + params.defBurstScale = 100; + + if ( m_iszBreakModelMessage != NULL_STRING ) + { + CPVSFilter filter( GetAbsOrigin() ); + UserMessageBegin( filter, STRING( m_iszBreakModelMessage ) ); + WRITE_SHORT( GetModelIndex() ); + WRITE_VEC3COORD( GetAbsOrigin() ); + WRITE_ANGLES( GetAbsAngles() ); + MessageEnd(); + +#ifndef HL2MP + UTIL_Remove( this ); +#endif + return; + } + + // in multiplayer spawn break models as clientside temp ents + if ( gpGlobals->maxClients > 1 && breakable_multiplayer.GetBool() ) + { + CPASFilter filter( WorldSpaceCenter() ); + + Vector velocity; velocity.Init(); + + if ( pPhysics ) + pPhysics->GetVelocity( &velocity, NULL ); + + switch ( GetMultiplayerBreakMode() ) + { + case MULTIPLAYER_BREAK_DEFAULT: // default is to break client-side + case MULTIPLAYER_BREAK_CLIENTSIDE: + te->PhysicsProp( filter, -1, GetModelIndex(), m_nSkin, GetAbsOrigin(), GetAbsAngles(), velocity, true, GetEffects() ); + break; + case MULTIPLAYER_BREAK_SERVERSIDE: // server-side break + if ( m_PerformanceMode != PM_NO_GIBS || breakable_disable_gib_limit.GetBool() ) + { + PropBreakableCreateAll( GetModelIndex(), pPhysics, params, this, -1, ( m_PerformanceMode == PM_FULL_GIBS ), false ); + } + break; + case MULTIPLAYER_BREAK_BOTH: // pieces break from both dlls + te->PhysicsProp( filter, -1, GetModelIndex(), m_nSkin, GetAbsOrigin(), GetAbsAngles(), velocity, true, GetEffects() ); + if ( m_PerformanceMode != PM_NO_GIBS || breakable_disable_gib_limit.GetBool() ) + { + PropBreakableCreateAll( GetModelIndex(), pPhysics, params, this, -1, ( m_PerformanceMode == PM_FULL_GIBS ), false ); + } + break; + } + } + // no damage/damage force? set a burst of 100 for some movement + else if ( m_PerformanceMode != PM_NO_GIBS || breakable_disable_gib_limit.GetBool() ) + { + PropBreakableCreateAll( GetModelIndex(), pPhysics, params, this, -1, ( m_PerformanceMode == PM_FULL_GIBS ) ); + } + + if( HasInteraction( PROPINTER_PHYSGUN_BREAK_EXPLODE ) ) + { + if ( bExploded == false ) + { + ExplosionCreate( origin, angles, pAttacker, 1, m_explodeRadius, + SF_ENVEXPLOSION_NOSPARKS | SF_ENVEXPLOSION_NODLIGHTS | SF_ENVEXPLOSION_NOSMOKE, 0.0f, this ); + } + + // Find and ignite all NPC's within the radius + CBaseEntity *pEntity = NULL; + for ( CEntitySphereQuery sphere( origin, m_explodeRadius ); ( pEntity = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) + { + if( pEntity && pEntity->MyCombatCharacterPointer() ) + { + // Check damage filters so we don't ignite friendlies + if ( pEntity->PassesDamageFilter( info ) ) + { + pEntity->MyCombatCharacterPointer()->Ignite( 30 ); + } + } + } + } + +#ifndef HL2MP + UTIL_Remove( this ); +#endif +} + + +//============================================================================================================= +// DYNAMIC PROPS +//============================================================================================================= +LINK_ENTITY_TO_CLASS( dynamic_prop, CDynamicProp ); +LINK_ENTITY_TO_CLASS( prop_dynamic, CDynamicProp ); +LINK_ENTITY_TO_CLASS( prop_dynamic_override, CDynamicProp ); + +BEGIN_DATADESC( CDynamicProp ) + + // Fields + DEFINE_KEYFIELD( m_iszDefaultAnim, FIELD_STRING, "DefaultAnim"), + DEFINE_FIELD( m_iGoalSequence, FIELD_INTEGER ), + DEFINE_FIELD( m_iTransitionDirection, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_bRandomAnimator, FIELD_BOOLEAN, "RandomAnimation"), + DEFINE_FIELD( m_flNextRandAnim, FIELD_TIME ), + DEFINE_KEYFIELD( m_flMinRandAnimTime, FIELD_FLOAT, "MinAnimTime"), + DEFINE_KEYFIELD( m_flMaxRandAnimTime, FIELD_FLOAT, "MaxAnimTime"), + DEFINE_KEYFIELD( m_bStartDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_KEYFIELD( m_bDisableBoneFollowers, FIELD_BOOLEAN, "DisableBoneFollowers" ), + DEFINE_FIELD( m_bUseHitboxesForRenderBox, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nPendingSequence, FIELD_SHORT ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetAnimation", InputSetAnimation ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetDefaultAnimation", InputSetDefaultAnimation ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableCollision", InputEnableCollision ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableCollision", InputDisableCollision ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetPlaybackRate", InputSetPlaybackRate ), + + // Outputs + DEFINE_OUTPUT( m_pOutputAnimBegun, "OnAnimationBegun" ), + DEFINE_OUTPUT( m_pOutputAnimOver, "OnAnimationDone" ), + + // Function Pointers + DEFINE_THINKFUNC( AnimThink ), + + DEFINE_EMBEDDED( m_BoneFollowerManager ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST(CDynamicProp, DT_DynamicProp) + SendPropBool( SENDINFO( m_bUseHitboxesForRenderBox ) ), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CDynamicProp::CDynamicProp() +{ + m_nPendingSequence = -1; + if ( g_pGameRules->IsMultiplayer() ) + { + UseClientSideAnimation(); + } + m_iGoalSequence = -1; +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CDynamicProp::Spawn( ) +{ + // Condense classname's to one, except for "prop_dynamic_override" + if ( FClassnameIs( this, "dynamic_prop" ) ) + { + SetClassname( "prop_dynamic" ); + } + + // If the prop is not-solid, the bounding box needs to be + // OBB to correctly surround the prop as it rotates. + // Check the classname so we don't mess with doors & other derived classes. + if ( GetSolid() == SOLID_NONE && FClassnameIs( this, "prop_dynamic" ) ) + { + SetSolid( SOLID_OBB ); + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + BaseClass::Spawn(); + + if ( IsMarkedForDeletion() ) + return; + + // Now condense all classnames to one + if ( FClassnameIs( this, "dynamic_prop" ) || FClassnameIs( this, "prop_dynamic_override" ) ) + { + SetClassname("prop_dynamic"); + } + + AddFlag( FL_STATICPROP ); + + if ( m_bRandomAnimator || ( m_iszDefaultAnim != NULL_STRING ) ) + { + RemoveFlag( FL_STATICPROP ); + + if ( m_bRandomAnimator ) + { + SetThink( &CDynamicProp::AnimThink ); + m_flNextRandAnim = gpGlobals->curtime + random->RandomFloat( m_flMinRandAnimTime, m_flMaxRandAnimTime ); + SetNextThink( gpGlobals->curtime + m_flNextRandAnim + 0.1 ); + } + else + { + PropSetAnim( STRING( m_iszDefaultAnim ) ); + } + } + + CreateVPhysics(); + + BoneFollowerHierarchyChanged(); + + if( m_bStartDisabled ) + { + AddEffects( EF_NODRAW ); + } + + if ( !PropDataOverrodeBlockLOS() ) + { + CalculateBlockLOS(); + } + + m_bUseHitboxesForRenderBox = HasSpawnFlags( SF_DYNAMICPROP_USEHITBOX_FOR_RENDERBOX ); + + if ( HasSpawnFlags( SF_DYNAMICPROP_DISABLE_COLLISION ) ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + //m_debugOverlays |= OVERLAY_ABSBOX_BIT; + +#ifdef TF_DLL + const char *pszModelName = modelinfo->GetModelName( GetModel() ); + if ( pszModelName && pszModelName[0] ) + { + if ( FStrEq( pszModelName, "models/bots/boss_bot/carrier_parts.mdl" ) ) + { + SetModelIndexOverride( VISION_MODE_NONE, modelinfo->GetModelIndex( pszModelName ) ); + SetModelIndexOverride( VISION_MODE_ROME, modelinfo->GetModelIndex( "models/bots/tw2/boss_bot/twcarrier_addon.mdl" ) ); + } + } +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDynamicProp::OnRestore( void ) +{ + BaseClass::OnRestore(); + + BoneFollowerHierarchyChanged(); +} + +void CDynamicProp::SetParent( CBaseEntity *pNewParent, int iAttachment ) +{ + BaseClass::SetParent(pNewParent, iAttachment); + BoneFollowerHierarchyChanged(); +} + +// Call this when creating bone followers or changing hierarchy to make sure the bone followers get updated when hierarchy changes +void CDynamicProp::BoneFollowerHierarchyChanged() +{ + // If we have bone followers and we're parented to something, we need to constantly update our bone followers + if ( m_BoneFollowerManager.GetNumBoneFollowers() && GetParent() ) + { + WatchPositionChanges(this, this); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CDynamicProp::OverridePropdata( void ) +{ + return ( FClassnameIs(this, "prop_dynamic_override" ) ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +bool CDynamicProp::CreateVPhysics( void ) +{ + if ( GetSolid() == SOLID_NONE || ((GetSolidFlags() & FSOLID_NOT_SOLID) && HasSpawnFlags(SF_DYNAMICPROP_NO_VPHYSICS))) + return true; + + if ( !m_bDisableBoneFollowers ) + { + CreateBoneFollowers(); + } + + if ( m_BoneFollowerManager.GetNumBoneFollowers() ) + { + if ( GetSolidFlags() & FSOLID_NOT_SOLID ) + { + // Already non-solid? Must need bone followers for some other reason + // like needing to attach constraints to this object + for ( int i = 0; i < m_BoneFollowerManager.GetNumBoneFollowers(); i++ ) + { + CBaseEntity *pFollower = m_BoneFollowerManager.GetBoneFollower(i)->hFollower; + if ( pFollower ) + { + pFollower->AddSolidFlags(FSOLID_NOT_SOLID); + } + } + + } + // If our collision is through bone followers, we want to be non-solid + AddSolidFlags( FSOLID_NOT_SOLID ); + // add these for the client, FSOLID_NOT_SOLID should keep it out of the testCollision code + // except in the case of TraceEntity() which the client does for impact effects + AddSolidFlags( FSOLID_CUSTOMRAYTEST | FSOLID_CUSTOMBOXTEST ); + return true; + } + else + { + VPhysicsInitStatic(); + } + return true; +} + +void CDynamicProp::CreateBoneFollowers() +{ + // already created bone followers? Don't do so again. + if ( m_BoneFollowerManager.GetNumBoneFollowers() ) + return; + + KeyValues *modelKeyValues = new KeyValues(""); + if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) + { + // Do we have a bone follower section? + KeyValues *pkvBoneFollowers = modelKeyValues->FindKey("bone_followers"); + if ( pkvBoneFollowers ) + { + // Loop through the list and create the bone followers + KeyValues *pBone = pkvBoneFollowers->GetFirstSubKey(); + while ( pBone ) + { + // Add it to the list + const char *pBoneName = pBone->GetString(); + m_BoneFollowerManager.AddBoneFollower( this, pBoneName ); + + pBone = pBone->GetNextKey(); + } + } + + modelKeyValues->deleteThis(); + } + + // if we got here, we don't have a bone follower section, but if we have a ragdoll + // go ahead and create default bone followers for it + if ( m_BoneFollowerManager.GetNumBoneFollowers() == 0 ) + { + vcollide_t *pCollide = modelinfo->GetVCollide( GetModelIndex() ); + if ( pCollide && pCollide->solidCount > 1 ) + { + CreateBoneFollowersFromRagdoll(this, &m_BoneFollowerManager, pCollide); + } + } +} + + +bool CDynamicProp::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ + if ( IsSolidFlagSet(FSOLID_NOT_SOLID) ) + { + // if this entity is marked non-solid and custom test it must have bone followers + if ( IsSolidFlagSet( FSOLID_CUSTOMBOXTEST ) && IsSolidFlagSet( FSOLID_CUSTOMRAYTEST )) + { + for ( int i = 0; i < m_BoneFollowerManager.GetNumBoneFollowers(); i++ ) + { + CBaseEntity *pEntity = m_BoneFollowerManager.GetBoneFollower(i)->hFollower; + if ( pEntity && pEntity->TestCollision(ray, mask, trace) ) + return true; + } + } + } + return false; +} + + +IPhysicsObject *CDynamicProp::GetRootPhysicsObjectForBreak() +{ + if ( m_BoneFollowerManager.GetNumBoneFollowers() ) + { + physfollower_t *pFollower = m_BoneFollowerManager.GetBoneFollower(0); + CBaseEntity *pFollowerEntity = pFollower->hFollower; + if ( pFollowerEntity ) + { + return pFollowerEntity->VPhysicsGetObject(); + } + } + + return BaseClass::GetRootPhysicsObjectForBreak(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDynamicProp::UpdateOnRemove( void ) +{ + m_BoneFollowerManager.DestroyBoneFollowers(); + BaseClass::UpdateOnRemove(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CDynamicProp::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case SCRIPT_EVENT_FIRE_INPUT: + { + variant_t emptyVariant; + this->AcceptInput( pEvent->options, this, this, emptyVariant, 0 ); + return; + } + + case SCRIPT_EVENT_SOUND: + { + EmitSound( pEvent->options ); + break; + } + + default: + { + break; + } + } + + BaseClass::HandleAnimEvent( pEvent ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDynamicProp::NotifyPositionChanged( CBaseEntity *pEntity ) +{ + Assert(pEntity==this); + m_BoneFollowerManager.UpdateBoneFollowers(this); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CDynamicProp::AnimThink( void ) +{ + if ( m_nPendingSequence != -1 ) + { + FinishSetSequence( m_nPendingSequence ); + m_nPendingSequence = -1; + } + + if ( m_bRandomAnimator && m_flNextRandAnim < gpGlobals->curtime ) + { + ResetSequence( SelectWeightedSequence( ACT_IDLE ) ); + ResetClientsideFrame(); + + // Fire output + m_pOutputAnimBegun.FireOutput( NULL,this ); + + m_flNextRandAnim = gpGlobals->curtime + random->RandomFloat( m_flMinRandAnimTime, m_flMaxRandAnimTime ); + } + + if ( ((m_iTransitionDirection > 0 && GetCycle() >= 0.999f) || (m_iTransitionDirection < 0 && GetCycle() <= 0.0f)) && !SequenceLoops() ) + { + Assert( m_iGoalSequence >= 0 ); + if (GetSequence() != m_iGoalSequence) + { + PropSetSequence( m_iGoalSequence ); + } + else + { + // Fire output + m_pOutputAnimOver.FireOutput(NULL,this); + + // If I'm a random animator, think again when it's time to change sequence + if ( m_bRandomAnimator ) + { + SetNextThink( gpGlobals->curtime + m_flNextRandAnim + 0.1 ); + } + else + { + if (m_iszDefaultAnim != NULL_STRING) + { + PropSetAnim( STRING( m_iszDefaultAnim ) ); + } + } + } + } + else + { + SetNextThink( gpGlobals->curtime + 0.1f ); + } + + StudioFrameAdvance(); + DispatchAnimEvents(this); + m_BoneFollowerManager.UpdateBoneFollowers(this); +} + + +//------------------------------------------------------------------------------ +// Purpose: Sets an animation by sequence name or activity name. +//------------------------------------------------------------------------------ +void CDynamicProp::PropSetAnim( const char *szAnim ) +{ + if ( !szAnim ) + return; + + int nSequence = LookupSequence( szAnim ); + + // Set to the desired anim, or default anim if the desired is not present + if ( nSequence > ACTIVITY_NOT_AVAILABLE ) + { + PropSetSequence( nSequence ); + + // Fire output + m_pOutputAnimBegun.FireOutput( NULL,this ); + } + else + { + // Not available try to get default anim + Warning( "Dynamic prop %s: no sequence named:%s\n", GetDebugName(), szAnim ); + SetSequence( 0 ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CDynamicProp::InputSetAnimation( inputdata_t &inputdata ) +{ + PropSetAnim( inputdata.value.String() ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CDynamicProp::InputSetDefaultAnimation( inputdata_t &inputdata ) +{ + m_iszDefaultAnim = inputdata.value.StringID(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CDynamicProp::InputSetPlaybackRate( inputdata_t &inputdata ) +{ + SetPlaybackRate( inputdata.value.Float() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Helper in case we have to async load the sequence +// Input : nSequence - +//----------------------------------------------------------------------------- +void CDynamicProp::FinishSetSequence( int nSequence ) +{ + // Msg("%.2f CDynamicProp::FinishSetSequence( %d )\n", gpGlobals->curtime, nSequence ); + SetCycle( 0 ); + m_flAnimTime = gpGlobals->curtime; + ResetSequence( nSequence ); + ResetClientsideFrame(); + RemoveFlag( FL_STATICPROP ); + SetPlaybackRate( m_iTransitionDirection > 0 ? 1.0f : -1.0f ); + SetCycle( m_iTransitionDirection > 0 ? 0.0f : 0.999f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the sequence and starts thinking. +// Input : nSequence - +//----------------------------------------------------------------------------- +void CDynamicProp::PropSetSequence( int nSequence ) +{ + m_iGoalSequence = nSequence; + + // Msg("%.2f CDynamicProp::PropSetSequence( %d (%d:%.1f:%.3f)\n", gpGlobals->curtime, nSequence, GetSequence(), GetPlaybackRate(), GetCycle() ); + + int nNextSequence; + float nextCycle; + float flInterval = 0.1f; + + if (GotoSequence( GetSequence(), GetCycle(), GetPlaybackRate(), m_iGoalSequence, nNextSequence, nextCycle, m_iTransitionDirection )) + { + FinishSetSequence( nNextSequence ); + } + + SetThink( &CDynamicProp::AnimThink ); + if ( GetNextThink() <= gpGlobals->curtime ) + SetNextThink( gpGlobals->curtime + flInterval ); +} + + +// NOTE: To avoid risk, currently these do nothing about collisions, only visually on/off +void CDynamicProp::InputTurnOn( inputdata_t &inputdata ) +{ + RemoveEffects( EF_NODRAW ); +} + +void CDynamicProp::InputTurnOff( inputdata_t &inputdata ) +{ + AddEffects( EF_NODRAW ); +} + +void CDynamicProp::InputDisableCollision( inputdata_t &inputdata ) +{ + AddSolidFlags( FSOLID_NOT_SOLID ); +} + +void CDynamicProp::InputEnableCollision( inputdata_t &inputdata ) +{ + RemoveSolidFlags( FSOLID_NOT_SOLID ); +} + +//----------------------------------------------------------------------------- +// Purpose: Ornamental prop that follows a studio +//----------------------------------------------------------------------------- +class COrnamentProp : public CDynamicProp +{ + DECLARE_CLASS( COrnamentProp, CDynamicProp ); +public: + DECLARE_DATADESC(); + + void Spawn(); + void Activate(); + void AttachTo( const char *pAttachEntity, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL ); + void DetachFromOwner(); + + // Input handlers + void InputSetAttached( inputdata_t &inputdata ); + void InputDetach( inputdata_t &inputdata ); + +private: + string_t m_initialOwner; +}; + +LINK_ENTITY_TO_CLASS( prop_dynamic_ornament, COrnamentProp ); + +BEGIN_DATADESC( COrnamentProp ) + + DEFINE_KEYFIELD( m_initialOwner, FIELD_STRING, "InitialOwner" ), + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "SetAttached", InputSetAttached ), + DEFINE_INPUTFUNC( FIELD_VOID, "Detach", InputDetach ), + +END_DATADESC() + +void COrnamentProp::Spawn() +{ + BaseClass::Spawn(); + DetachFromOwner(); +} + +void COrnamentProp::DetachFromOwner() +{ + SetOwnerEntity( NULL ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); + AddEffects( EF_NODRAW ); +} + +void COrnamentProp::Activate() +{ + BaseClass::Activate(); + + if ( m_initialOwner != NULL_STRING ) + { + AttachTo( STRING(m_initialOwner) ); + } +} + +void COrnamentProp::InputSetAttached( inputdata_t &inputdata ) +{ + AttachTo( inputdata.value.String(), inputdata.pActivator, inputdata.pCaller ); +} + +void COrnamentProp::AttachTo( const char *pAttachName, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + // find and notify the new parent + CBaseEntity *pAttach = gEntList.FindEntityByName( NULL, pAttachName, NULL, pActivator, pCaller ); + if ( pAttach ) + { + RemoveEffects( EF_NODRAW ); + FollowEntity( pAttach ); + } +} + +void COrnamentProp::InputDetach( inputdata_t &inputdata ) +{ + DetachFromOwner(); +} + +#ifdef MAPBASE +#define SF_INTERACTABLE_USE_INTERACTS 512 // Allows +USE interaction. +#define SF_INTERACTABLE_TOUCH_INTERACTS 1024 // Allows touch interaction. +#define SF_INTERACTABLE_IGNORE_COMMANDS_WHEN_LOCKED 2048 // Completely ignores player commands when locked. +#define SF_INTERACTABLE_RADIUS_USE 4096 // Uses radius +USE + +//----------------------------------------------------------------------------- +// Purpose: Button prop for +USEable dynamic props +//----------------------------------------------------------------------------- +class CInteractableProp : public CDynamicProp +{ + DECLARE_CLASS( CInteractableProp, CDynamicProp ); +public: + DECLARE_DATADESC(); + + void Spawn(); + void Precache(); + //void Activate(); + + int ObjectCaps() + { + int caps = BaseClass::ObjectCaps(); + + if (HasSpawnFlags(SF_INTERACTABLE_USE_INTERACTS) && (!HasSpawnFlags( SF_INTERACTABLE_IGNORE_COMMANDS_WHEN_LOCKED ) || !m_bLocked)) + { + caps |= FCAP_IMPULSE_USE; + + if (HasSpawnFlags(SF_INTERACTABLE_RADIUS_USE)) + caps |= FCAP_USE_IN_RADIUS; + } + + return caps; + }; + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void InteractablePropTouch( CBaseEntity *pOther ); + + void SetPushSequence(int iSequence); + void PushThink(); + + // Input handlers + void InputLock( inputdata_t &inputdata ); + void InputUnlock( inputdata_t &inputdata ); + void InputPress( inputdata_t &inputdata ); + + void InputEnableUseInteraction( inputdata_t &inputdata ) { AddSpawnFlags(SF_INTERACTABLE_USE_INTERACTS); } + void InputDisableUseInteraction( inputdata_t &inputdata ) { RemoveSpawnFlags(SF_INTERACTABLE_USE_INTERACTS); } + void InputEnableTouchInteraction( inputdata_t &inputdata ) { AddSpawnFlags( SF_INTERACTABLE_TOUCH_INTERACTS ); } + void InputDisableTouchInteraction( inputdata_t &inputdata ) { RemoveSpawnFlags( SF_INTERACTABLE_TOUCH_INTERACTS ); } + void InputStartIgnoringCommandsWhenLocked( inputdata_t &inputdata ) { AddSpawnFlags( SF_INTERACTABLE_IGNORE_COMMANDS_WHEN_LOCKED ); } + void InputStopIgnoringCommandsWhenLocked( inputdata_t &inputdata ) { RemoveSpawnFlags( SF_INTERACTABLE_IGNORE_COMMANDS_WHEN_LOCKED ); } + void InputEnableRadiusInteract( inputdata_t &inputdata ) { AddSpawnFlags( SF_INTERACTABLE_RADIUS_USE ); } + void InputDisableRadiusInteract( inputdata_t &inputdata ) { RemoveSpawnFlags( SF_INTERACTABLE_RADIUS_USE ); } + + COutputEvent m_OnPressed; + COutputEvent m_OnLockedUse; + COutputEvent m_OnIn; + COutputEvent m_OnOut; + + bool m_bLocked; + + float m_flCooldown; + +private: + float m_flCooldownTime; + + int m_iCurSequence = INTERACTSEQ_NONE; // Currently in a sequence + enum + { + INTERACTSEQ_NONE = -1, + INTERACTSEQ_IN, + INTERACTSEQ_OUT, + INTERACTSEQ_LOCKED, + }; + + string_t m_iszPressedSound; + string_t m_iszLockedSound; + + string_t m_iszInSequence; + string_t m_iszOutSequence; + string_t m_iszLockedSequence; + + Vector m_vecUseMins; + Vector m_vecUseMaxs; +}; + +LINK_ENTITY_TO_CLASS( prop_interactable, CInteractableProp ); + +BEGIN_DATADESC( CInteractableProp ) + + DEFINE_KEYFIELD( m_bLocked, FIELD_BOOLEAN, "Locked" ), + DEFINE_INPUT( m_flCooldown, FIELD_FLOAT, "SetCooldown" ), + DEFINE_FIELD( m_flCooldownTime, FIELD_TIME ), + DEFINE_FIELD( m_iCurSequence, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_iszPressedSound, FIELD_STRING, "PressedSound" ), + DEFINE_KEYFIELD( m_iszLockedSound, FIELD_STRING, "LockedSound" ), + DEFINE_KEYFIELD( m_iszInSequence, FIELD_STRING, "InSequence" ), + DEFINE_KEYFIELD( m_iszOutSequence, FIELD_STRING, "OutSequence" ), + DEFINE_KEYFIELD( m_iszLockedSequence, FIELD_STRING, "LockedSequence" ), + + DEFINE_KEYFIELD( m_vecUseMins, FIELD_VECTOR, "use_mins" ), + DEFINE_KEYFIELD( m_vecUseMaxs, FIELD_VECTOR, "use_maxs" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Lock", InputLock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Unlock", InputUnlock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Press", InputPress ), + + DEFINE_INPUTFUNC( FIELD_VOID, "EnableUseInteraction", InputEnableUseInteraction ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableUseInteraction", InputDisableUseInteraction ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableTouchInteraction", InputEnableTouchInteraction ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableTouchInteraction", InputDisableTouchInteraction ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartIgnoringCommandsWhenLocked", InputStartIgnoringCommandsWhenLocked ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopIgnoringCommandsWhenLocked", InputStopIgnoringCommandsWhenLocked ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableRadiusInteract", InputEnableRadiusInteract ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableRadiusInteract", InputDisableRadiusInteract ), + + // Outputs + DEFINE_OUTPUT( m_OnPressed, "OnPressed" ), + DEFINE_OUTPUT( m_OnLockedUse, "OnLockedUse" ), + DEFINE_OUTPUT( m_OnIn, "OnIn" ), + DEFINE_OUTPUT( m_OnOut, "OnOut" ), + + DEFINE_THINKFUNC( PushThink ), + DEFINE_ENTITYFUNC( InteractablePropTouch ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInteractableProp::Spawn( void ) +{ + BaseClass::Spawn(); + + SetTouch( &CInteractableProp::InteractablePropTouch ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInteractableProp::Precache( void ) +{ + BaseClass::Precache(); + + if (m_iszPressedSound != NULL_STRING) + PrecacheScriptSound( STRING(m_iszPressedSound) ); + if (m_iszLockedSound != NULL_STRING) + PrecacheScriptSound( STRING(m_iszLockedSound) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActivator - +// *pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CInteractableProp::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if (m_flCooldownTime > gpGlobals->curtime) + return; + + // If we're using +USE mins/maxs, make sure this is being +USE'd from the right place + if (m_vecUseMins.LengthSqr() != 0.0f && m_vecUseMaxs.LengthSqr() != 0.0f) + { + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if (pPlayer) + { + Vector forward; + pPlayer->EyeVectors( &forward, NULL, NULL ); + + // This might be a little convoluted and/or seem needlessly expensive, but I couldn't figure out any better way to do this. + // TOOD: Can we calculate a box in local space instead of world space? + Vector vecWorldMins, vecWorldMaxs; + RotateAABB( EntityToWorldTransform(), m_vecUseMins, m_vecUseMaxs, vecWorldMins, vecWorldMaxs ); + TransformAABB( EntityToWorldTransform(), vecWorldMins, vecWorldMaxs, vecWorldMins, vecWorldMaxs ); + if (!IsBoxIntersectingRay( vecWorldMins, vecWorldMaxs, pPlayer->EyePosition(), forward * 1024 )) + { + // Reject this +USE if it's not in our box + DevMsg("Outside of +USE box\n"); + return; + } + } + } + + int nSequence = -1; + + if (m_bLocked) + { + m_OnLockedUse.FireOutput( pActivator, this ); + EmitSound(STRING(m_iszLockedSound)); + nSequence = LookupSequence( STRING( m_iszLockedSequence ) ); + m_iCurSequence = INTERACTSEQ_LOCKED; + } + else + { + m_OnPressed.FireOutput( pActivator, this ); + EmitSound(STRING(m_iszPressedSound)); + nSequence = LookupSequence( STRING( m_iszInSequence ) ); + m_iCurSequence = INTERACTSEQ_IN; + } + + if ( nSequence > ACTIVITY_NOT_AVAILABLE ) + { + SetPushSequence(nSequence); + + // We still fire our inherited animation outputs + m_pOutputAnimBegun.FireOutput( pActivator, this ); + } + + m_flCooldownTime = gpGlobals->curtime + m_flCooldown; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pOther - +//----------------------------------------------------------------------------- +void CInteractableProp::InteractablePropTouch( CBaseEntity *pOther ) +{ + // Do base touch function first + BreakablePropTouch( pOther ); + + if ( HasSpawnFlags(SF_INTERACTABLE_TOUCH_INTERACTS) && (!HasSpawnFlags(SF_INTERACTABLE_IGNORE_COMMANDS_WHEN_LOCKED) || !m_bLocked) && pOther->IsPlayer() ) + { + Use( pOther, pOther, USE_ON, 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInteractableProp::InputLock( inputdata_t &inputdata ) +{ + m_bLocked = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInteractableProp::InputUnlock( inputdata_t &inputdata ) +{ + m_bLocked = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInteractableProp::InputPress( inputdata_t &inputdata ) +{ + Use( inputdata.pActivator, inputdata.pCaller, USE_ON, 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInteractableProp::SetPushSequence( int iSequence ) +{ + m_iGoalSequence = iSequence; + + int nNextSequence; + float nextCycle; + float flInterval = 0.1f; + + if (GotoSequence( GetSequence(), GetCycle(), GetPlaybackRate(), m_iGoalSequence, nNextSequence, nextCycle, m_iTransitionDirection )) + { + FinishSetSequence( nNextSequence ); + } + + SetThink( &CInteractableProp::PushThink ); + if ( GetNextThink() <= gpGlobals->curtime ) + SetNextThink( gpGlobals->curtime + flInterval ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInteractableProp::PushThink() +{ + if ( m_nPendingSequence != -1 ) + { + FinishSetSequence( m_nPendingSequence ); + m_nPendingSequence = -1; + } + + SetNextThink( gpGlobals->curtime + 0.1f ); + + if ( ((m_iTransitionDirection > 0 && GetCycle() >= 0.999f) || (m_iTransitionDirection < 0 && GetCycle() <= 0.0f)) && !SequenceLoops() ) + { + if (!SequenceLoops()) + { + // We still fire our inherited animation outputs + m_pOutputAnimOver.FireOutput(NULL, this); + } + + if (m_iCurSequence == INTERACTSEQ_OUT) + { + m_OnOut.FireOutput( NULL, this ); + + m_iCurSequence = INTERACTSEQ_NONE; + } + else + { + m_OnIn.FireOutput( NULL, this ); + } + } + + StudioFrameAdvance(); + DispatchAnimEvents(this); + m_BoneFollowerManager.UpdateBoneFollowers(this); + + if (m_flCooldownTime < gpGlobals->curtime) + { + if (m_iCurSequence == INTERACTSEQ_IN) + { + int nSequence = LookupSequence( STRING(m_iszOutSequence) ); + if ( m_iszOutSequence != NULL_STRING && nSequence > ACTIVITY_NOT_AVAILABLE ) + { + m_iCurSequence = INTERACTSEQ_OUT; + SetPushSequence(nSequence); + + // We still fire our inherited animation outputs + m_pOutputAnimBegun.FireOutput( NULL, this ); + } + else + { + m_iCurSequence = INTERACTSEQ_NONE; + } + } + + if (m_iCurSequence == INTERACTSEQ_NONE) + { + if (m_iszDefaultAnim != NULL_STRING) + { + PropSetAnim( STRING( m_iszDefaultAnim ) ); + } + + SetNextThink( TICK_NEVER_THINK ); + } + } +} +#endif + + +//============================================================================= +// PHYSICS PROPS +//============================================================================= +LINK_ENTITY_TO_CLASS( physics_prop, CPhysicsProp ); +LINK_ENTITY_TO_CLASS( prop_physics, CPhysicsProp ); +LINK_ENTITY_TO_CLASS( prop_physics_override, CPhysicsProp ); + +BEGIN_DATADESC( CPhysicsProp ) + + DEFINE_INPUTFUNC( FIELD_VOID, "EnableMotion", InputEnableMotion ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableMotion", InputDisableMotion ), + DEFINE_INPUTFUNC( FIELD_VOID, "Wake", InputWake ), + DEFINE_INPUTFUNC( FIELD_VOID, "Sleep", InputSleep ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableFloating", InputDisableFloating ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetDebris", InputSetDebris ), +#endif + + DEFINE_FIELD( m_bAwake, FIELD_BOOLEAN ), + + DEFINE_KEYFIELD( m_massScale, FIELD_FLOAT, "massscale" ), + DEFINE_KEYFIELD( m_inertiaScale, FIELD_FLOAT, "inertiascale" ), + DEFINE_KEYFIELD( m_damageType, FIELD_INTEGER, "Damagetype" ), + DEFINE_KEYFIELD( m_iszOverrideScript, FIELD_STRING, "overridescript" ), + + DEFINE_KEYFIELD( m_damageToEnableMotion, FIELD_INTEGER, "damagetoenablemotion" ), + DEFINE_KEYFIELD( m_flForceToEnableMotion, FIELD_FLOAT, "forcetoenablemotion" ), + DEFINE_OUTPUT( m_OnAwakened, "OnAwakened" ), + DEFINE_OUTPUT( m_MotionEnabled, "OnMotionEnabled" ), + DEFINE_OUTPUT( m_OnPhysGunPickup, "OnPhysGunPickup" ), + DEFINE_OUTPUT( m_OnPhysGunOnlyPickup, "OnPhysGunOnlyPickup" ), + DEFINE_OUTPUT( m_OnPhysGunPunt, "OnPhysGunPunt" ), + DEFINE_OUTPUT( m_OnPhysGunDrop, "OnPhysGunDrop" ), + DEFINE_OUTPUT( m_OnPlayerUse, "OnPlayerUse" ), + DEFINE_OUTPUT( m_OnPlayerPickup, "OnPlayerPickup" ), + DEFINE_OUTPUT( m_OnOutOfWorld, "OnOutOfWorld" ), + + DEFINE_FIELD( m_bThrownByPlayer, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bFirstCollisionAfterLaunch, FIELD_BOOLEAN ), + + DEFINE_THINKFUNC( ClearFlagsThink ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPhysicsProp, DT_PhysicsProp ) + SendPropBool( SENDINFO( m_bAwake ) ), +END_SEND_TABLE() + +// external function to tell if this entity is a gib physics prop +bool PropIsGib( CBaseEntity *pEntity ) +{ + if ( FClassnameIs(pEntity, "prop_physics") ) + { + CPhysicsProp *pProp = static_cast(pEntity); + return pProp->IsGib(); + } + return false; +} + +CPhysicsProp::~CPhysicsProp() +{ + if (HasSpawnFlags(SF_PHYSPROP_IS_GIB)) + { + g_ActiveGibCount--; + } +} + +bool CPhysicsProp::IsGib() +{ + return (m_spawnflags & SF_PHYSPROP_IS_GIB) ? true : false; +} + +//----------------------------------------------------------------------------- +// Purpose: Create a physics object for this prop +//----------------------------------------------------------------------------- +void CPhysicsProp::Spawn( ) +{ + if (HasSpawnFlags(SF_PHYSPROP_IS_GIB)) + { + g_ActiveGibCount++; + } + + // Condense classname's to one, except for "prop_physics_override" + if ( FClassnameIs( this, "physics_prop" ) ) + { +#ifdef MAPBASE + m_iClassname = gm_isz_class_PropPhysics; +#else + SetClassname( "prop_physics" ); +#endif + } + + BaseClass::Spawn(); + + if ( IsMarkedForDeletion() ) + return; + + // Now condense all classnames to one +#ifdef MAPBASE + if ( EntIsClass( this, gm_isz_class_PropPhysicsOverride ) ) + { + m_iClassname = gm_isz_class_PropPhysics; + } +#else + if ( FClassnameIs( this, "prop_physics_override") ) + { + SetClassname( "prop_physics" ); + } +#endif + + if ( HasSpawnFlags( SF_PHYSPROP_DEBRIS ) || HasInteraction( PROPINTER_PHYSGUN_CREATE_FLARE ) ) + { + SetCollisionGroup( HasSpawnFlags( SF_PHYSPROP_FORCE_TOUCH_TRIGGERS ) ? COLLISION_GROUP_DEBRIS_TRIGGER : COLLISION_GROUP_DEBRIS ); + } + + if ( HasSpawnFlags( SF_PHYSPROP_NO_ROTORWASH_PUSH ) ) + { + AddEFlags( EFL_NO_ROTORWASH_PUSH ); + } + + CreateVPhysics(); + + if ( !PropDataOverrodeBlockLOS() ) + { + CalculateBlockLOS(); + } + + //Episode 1 change: + //Hi, since we're trying to ship this game we'll just go ahead and make all these doors not fade out instead of changing all the levels. + if ( Q_strcmp( STRING( GetModelName() ), "models/props_c17/door01_left.mdl" ) == 0 ) + { + SetFadeDistance( -1, 0 ); + DisableAutoFade(); + } + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsProp::Precache( void ) +{ + if ( GetModelName() == NULL_STRING ) + { + Msg( "%s at (%.3f, %.3f, %.3f) has no model name!\n", GetClassname(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + } + else + { + PrecacheModel( STRING( GetModelName() ) ); + BaseClass::Precache(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPhysicsProp::CreateVPhysics() +{ + // Create the object in the physics system + bool asleep = HasSpawnFlags( SF_PHYSPROP_START_ASLEEP ) ? true : false; + + solid_t tmpSolid; + PhysModelParseSolid( tmpSolid, this, GetModelIndex() ); + + if ( m_massScale > 0 ) + { + tmpSolid.params.mass *= m_massScale; + } + + if ( m_inertiaScale > 0 ) + { + tmpSolid.params.inertia *= m_inertiaScale; + if ( tmpSolid.params.inertia < 0.5 ) + tmpSolid.params.inertia = 0.5; + } + + PhysGetMassCenterOverride( this, modelinfo->GetVCollide( GetModelIndex() ), tmpSolid ); + if ( HasSpawnFlags(SF_PHYSPROP_NO_COLLISIONS) ) + { + tmpSolid.params.enableCollisions = false; + } + PhysSolidOverride( tmpSolid, m_iszOverrideScript ); + + IPhysicsObject *pPhysicsObject = VPhysicsInitNormal( SOLID_VPHYSICS, 0, asleep, &tmpSolid ); + + if ( !pPhysicsObject ) + { + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + Warning("ERROR!: Can't create physics object for %s\n", STRING( GetModelName() ) ); + } + else + { + if ( m_damageType == 1 ) + { + PhysSetGameFlags( pPhysicsObject, FVPHYSICS_DMG_SLICE ); + } + if ( HasSpawnFlags( SF_PHYSPROP_MOTIONDISABLED ) || m_damageToEnableMotion > 0 || m_flForceToEnableMotion > 0 ) + { + pPhysicsObject->EnableMotion( false ); + } + } + + // fix up any noncompliant blades. + if( HasInteraction( PROPINTER_PHYSGUN_LAUNCH_SPIN_Z ) ) + { + if( !(VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_DMG_SLICE) ) + { + PhysSetGameFlags( pPhysicsObject, FVPHYSICS_DMG_SLICE ); + +#if 0 + if( g_pDeveloper->GetInt() ) + { + // Highlight them in developer mode. + m_debugOverlays |= (OVERLAY_TEXT_BIT|OVERLAY_BBOX_BIT); + } +#endif + } + } + + if( HasInteraction( PROPINTER_PHYSGUN_DAMAGE_NONE ) ) + { + PhysSetGameFlags( pPhysicsObject, FVPHYSICS_NO_IMPACT_DMG ); + } + + if ( HasSpawnFlags(SF_PHYSPROP_PREVENT_PICKUP) ) + { + PhysSetGameFlags(pPhysicsObject, FVPHYSICS_NO_PLAYER_PICKUP); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPhysicsProp::CanBePickedUpByPhyscannon( void ) +{ + if ( HasSpawnFlags( SF_PHYSPROP_PREVENT_PICKUP ) ) + return false; + + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject && pPhysicsObject->IsMoveable() == false ) + { + if ( HasSpawnFlags( SF_PHYSPROP_ENABLE_ON_PHYSCANNON ) == false ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPhysicsProp::OverridePropdata( void ) +{ +#ifdef MAPBASE + return EntIsClass(this, gm_isz_class_PropPhysicsOverride); +#else + return ( FClassnameIs(this, "prop_physics_override" ) ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler to start the physics prop simulating. +//----------------------------------------------------------------------------- +void CPhysicsProp::InputWake( inputdata_t &inputdata ) +{ + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->Wake(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler to stop the physics prop simulating. +//----------------------------------------------------------------------------- +void CPhysicsProp::InputSleep( inputdata_t &inputdata ) +{ + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->Sleep(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Enable physics motion and collision response (on by default) +//----------------------------------------------------------------------------- +void CPhysicsProp::InputEnableMotion( inputdata_t &inputdata ) +{ + EnableMotion(); +} + +//----------------------------------------------------------------------------- +// Purpose: Disable any physics motion or collision response +//----------------------------------------------------------------------------- +void CPhysicsProp::InputDisableMotion( inputdata_t &inputdata ) +{ + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->EnableMotion( false ); + } +} + +// Turn off floating simulation (and cost) +void CPhysicsProp::InputDisableFloating( inputdata_t &inputdata ) +{ + PhysEnableFloating( VPhysicsGetObject(), false ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Adds or removes the debris spawnflag. +//----------------------------------------------------------------------------- +void CPhysicsProp::InputSetDebris( inputdata_t &inputdata ) +{ + if (inputdata.value.Bool()) + { + AddSpawnFlags(SF_PHYSPROP_DEBRIS); + SetCollisionGroup(HasSpawnFlags(SF_PHYSPROP_FORCE_TOUCH_TRIGGERS) ? COLLISION_GROUP_DEBRIS_TRIGGER : COLLISION_GROUP_DEBRIS); + } + else + { + RemoveSpawnFlags(SF_PHYSPROP_DEBRIS); + SetCollisionGroup(COLLISION_GROUP_INTERACTIVE); // Is this the default collision group? + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsProp::EnableMotion( void ) +{ + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject ) + { + Vector pos; + QAngle angles; + + if ( GetEnableMotionPosition( &pos, &angles ) ) + { + ClearEnableMotionPosition(); + //pPhysicsObject->SetPosition( pos, angles, true ); + Teleport( &pos, &angles, NULL ); + } + + pPhysicsObject->EnableMotion( true ); + pPhysicsObject->Wake(); + + m_MotionEnabled.FireOutput( this, this, 0 ); + } + CheckRemoveRagdolls(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsProp::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + BaseClass::OnPhysGunPickup( pPhysGunUser, reason ); + + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + if ( pPhysicsObject && !pPhysicsObject->IsMoveable() ) + { + if ( !HasSpawnFlags( SF_PHYSPROP_ENABLE_ON_PHYSCANNON ) ) + return; + + EnableMotion(); + + if( HasInteraction( PROPINTER_PHYSGUN_WORLD_STICK ) ) + { + SetCollisionGroup( COLLISION_GROUP_INTERACTIVE_DEBRIS ); + } + } + + m_OnPhysGunPickup.FireOutput( pPhysGunUser, this ); + + if( reason == PICKED_UP_BY_CANNON ) + { + m_OnPhysGunOnlyPickup.FireOutput( pPhysGunUser, this ); + } + + if ( reason == PUNTED_BY_CANNON ) + { + m_OnPhysGunPunt.FireOutput( pPhysGunUser, this ); + } + + if ( reason == PICKED_UP_BY_CANNON || reason == PICKED_UP_BY_PLAYER ) + { + m_OnPlayerPickup.FireOutput( pPhysGunUser, this ); + } + + CheckRemoveRagdolls(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsProp::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + BaseClass::OnPhysGunDrop( pPhysGunUser, Reason ); + + if ( Reason == LAUNCHED_BY_CANNON ) + { + if ( HasInteraction( PROPINTER_PHYSGUN_LAUNCH_SPIN_Z ) ) + { + AngularImpulse angVel( 0, 0, 5000.0 ); + VPhysicsGetObject()->AddVelocity( NULL, &angVel ); + + // no angular drag on this object anymore + float angDrag = 0.0f; + VPhysicsGetObject()->SetDragCoefficient( NULL, &angDrag ); + } + + PhysSetGameFlags( VPhysicsGetObject(), FVPHYSICS_WAS_THROWN ); + m_bFirstCollisionAfterLaunch = true; + } + else if ( Reason == THROWN_BY_PLAYER ) + { + // Remember the player threw us for NPC response purposes + m_bThrownByPlayer = true; + } + + m_OnPhysGunDrop.FireOutput( pPhysGunUser, this ); + + if ( HasInteraction( PROPINTER_PHYSGUN_NOTIFY_CHILDREN ) ) + { + CUtlVector children; + GetAllChildren( this, children ); + for (int i = 0; i < children.Count(); i++ ) + { + CBaseEntity *pent = children.Element( i ); + + IParentPropInteraction *pPropInter = dynamic_cast( pent ); + if ( pPropInter ) + { + pPropInter->OnParentPhysGunDrop( pPhysGunUser, Reason ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Get the specified key's angles for this prop from the QC's physgun_interactions +//----------------------------------------------------------------------------- +bool CPhysicsProp::GetPropDataAngles( const char *pKeyName, QAngle &vecAngles ) +{ + KeyValues *modelKeyValues = new KeyValues(""); + if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) + { + KeyValues *pkvPropData = modelKeyValues->FindKey( "physgun_interactions" ); + if ( pkvPropData ) + { + char const *pszBase = pkvPropData->GetString( pKeyName ); + if ( pszBase && pszBase[0] ) + { + UTIL_StringToVector( vecAngles.Base(), pszBase ); + modelKeyValues->deleteThis(); + return true; + } + } + } + + modelKeyValues->deleteThis(); + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CPhysicsProp::GetCarryDistanceOffset( void ) +{ + KeyValues *modelKeyValues = new KeyValues(""); + if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) + { + KeyValues *pkvPropData = modelKeyValues->FindKey( "physgun_interactions" ); + if ( pkvPropData ) + { + float flDistance = pkvPropData->GetFloat( "carry_distance_offset", 0 ); + modelKeyValues->deleteThis(); + return flDistance; + } + } + + modelKeyValues->deleteThis(); + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPhysicsProp::ObjectCaps() +{ + int caps = BaseClass::ObjectCaps() | FCAP_WCEDIT_POSITION; + + if ( HasSpawnFlags( SF_PHYSPROP_ENABLE_PICKUP_OUTPUT ) ) + { + caps |= FCAP_IMPULSE_USE; + } + else if ( CBasePlayer::CanPickupObject( this, 35, 128 ) ) + { + caps |= FCAP_IMPULSE_USE; + + if( hl2_episodic.GetBool() && HasInteraction( PROPINTER_PHYSGUN_CREATE_FLARE ) ) + { + caps |= FCAP_USE_IN_RADIUS; + } + } + + if( HasSpawnFlags( SF_PHYSPROP_RADIUS_PICKUP ) ) + { + caps |= FCAP_USE_IN_RADIUS; + } + + return caps; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActivator - +// *pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CPhysicsProp::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( pPlayer ) + { + if ( HasSpawnFlags( SF_PHYSPROP_ENABLE_PICKUP_OUTPUT ) ) + { + m_OnPlayerUse.FireOutput( this, this ); + } + + pPlayer->PickupObject( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPhysics - +//----------------------------------------------------------------------------- +void CPhysicsProp::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + BaseClass::VPhysicsUpdate( pPhysics ); + m_bAwake = !pPhysics->IsAsleep(); + NetworkStateChanged(); + if ( HasSpawnFlags( SF_PHYSPROP_START_ASLEEP ) ) + { + if ( m_bAwake ) + { + m_OnAwakened.FireOutput(this, this); + RemoveSpawnFlags( SF_PHYSPROP_START_ASLEEP ); + } + } + + // If we're asleep, clear the player thrown flag + if ( m_bThrownByPlayer && !m_bAwake ) + { + m_bThrownByPlayer = false; + } + + if ( !IsInWorld() ) + { + m_OnOutOfWorld.FireOutput( this, this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsProp::ClearFlagsThink( void ) +{ + // collision may have destroyed the physics object, recheck + if ( VPhysicsGetObject() ) + { + PhysClearGameFlags( VPhysicsGetObject(), FVPHYSICS_WAS_THROWN ); + } + SetContextThink( NULL, 0, "PROP_CLEARFLAGS" ); +} + + +//----------------------------------------------------------------------------- +// Compute impulse to apply to the enabled entity. +//----------------------------------------------------------------------------- +void CPhysicsProp::ComputeEnablingImpulse( int index, gamevcollisionevent_t *pEvent ) +{ + // Surface speed of the object that hit us = v + w x r + // NOTE: w is specified in local space + Vector vecContactPoint, vecLocalContactPoint; + pEvent->pInternalData->GetContactPoint( vecContactPoint ); + + // Compute the angular component of velocity + IPhysicsObject *pImpactObject = pEvent->pObjects[!index]; + pImpactObject->WorldToLocal( &vecLocalContactPoint, vecContactPoint ); + vecLocalContactPoint -= pImpactObject->GetMassCenterLocalSpace(); + + Vector vecLocalContactVelocity, vecContactVelocity; + + AngularImpulse vecAngularVelocity = pEvent->preAngularVelocity[!index]; + vecAngularVelocity *= M_PI / 180.0f; + CrossProduct( vecAngularVelocity, vecLocalContactPoint, vecLocalContactVelocity ); + pImpactObject->LocalToWorldVector( &vecContactVelocity, vecLocalContactVelocity ); + + // Add in the center-of-mass velocity + vecContactVelocity += pEvent->preVelocity[!index]; + + // Compute the force + torque to apply + vecContactVelocity *= pImpactObject->GetMass(); + + Vector vecForce; + AngularImpulse vecTorque; + pEvent->pObjects[index]->CalculateForceOffset( vecContactVelocity, vecContactPoint, &vecForce, &vecTorque ); + + PhysCallbackImpulse( pEvent->pObjects[index], vecForce, vecTorque ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPhysicsProp::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + BaseClass::VPhysicsCollision( index, pEvent ); + + IPhysicsObject *pPhysObj = pEvent->pObjects[!index]; + + if ( m_flForceToEnableMotion ) + { + CBaseEntity *pOther = static_cast(pPhysObj->GetGameData()); + + // Don't allow the player to bump an object active if we've requested not to + if ( ( pOther && pOther->IsPlayer() && HasSpawnFlags( SF_PHYSPROP_PREVENT_PLAYER_TOUCH_ENABLE ) ) == false ) + { + // Large enough to enable motion? + float flForce = pEvent->collisionSpeed * pPhysObj->GetMass(); + + if ( flForce >= m_flForceToEnableMotion ) + { + ComputeEnablingImpulse( index, pEvent ); + EnableMotion(); + m_flForceToEnableMotion = 0; + } + } + } + + if( m_bFirstCollisionAfterLaunch ) + { + HandleFirstCollisionInteractions( index, pEvent ); + } + + if ( HasPhysicsAttacker( 2.0f ) ) + { + HandleAnyCollisionInteractions( index, pEvent ); + } + + if ( !HasSpawnFlags( SF_PHYSPROP_DONT_TAKE_PHYSICS_DAMAGE ) ) + { + int damageType = 0; + + IBreakableWithPropData *pBreakableInterface = assert_cast(this); + float damage = CalculateDefaultPhysicsDamage( index, pEvent, m_impactEnergyScale, true, damageType, pBreakableInterface->GetPhysicsDamageTable() ); + if ( damage > 0 ) + { + // Take extra damage after we're punted by the physcannon + if ( m_bFirstCollisionAfterLaunch && !m_bThrownByPlayer ) + { + damage *= 10; + } + + CBaseEntity *pHitEntity = pEvent->pEntities[!index]; + if ( !pHitEntity ) + { + // hit world + pHitEntity = GetContainingEntity( INDEXENT(0) ); + } + Vector damagePos; + pEvent->pInternalData->GetContactPoint( damagePos ); + Vector damageForce = pEvent->postVelocity[index] * pEvent->pObjects[index]->GetMass(); + if ( damageForce == vec3_origin ) + { + // This can happen if this entity is motion disabled, and can't move. + // Use the velocity of the entity that hit us instead. + damageForce = pEvent->postVelocity[!index] * pEvent->pObjects[!index]->GetMass(); + } + + // FIXME: this doesn't pass in who is responsible if some other entity "caused" this collision + PhysCallbackDamage( this, CTakeDamageInfo( pHitEntity, pHitEntity, damageForce, damagePos, damage, damageType ), *pEvent, index ); + } + } + + if ( m_bThrownByPlayer || m_bFirstCollisionAfterLaunch ) + { + // If we were thrown by a player, and we've hit an NPC, let the NPC know + CBaseEntity *pHitEntity = pEvent->pEntities[!index]; + if ( pHitEntity && pHitEntity->MyNPCPointer() ) + { + pHitEntity->MyNPCPointer()->DispatchInteraction( g_interactionHitByPlayerThrownPhysObj, this, NULL ); + m_bThrownByPlayer = false; + } + } + + if ( m_bFirstCollisionAfterLaunch ) + { + m_bFirstCollisionAfterLaunch = false; + + // Setup the think function to remove the flags + RegisterThinkContext( "PROP_CLEARFLAGS" ); + SetContextThink( &CPhysicsProp::ClearFlagsThink, gpGlobals->curtime, "PROP_CLEARFLAGS" ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPhysicsProp::OnTakeDamage( const CTakeDamageInfo &info ) +{ + // note: if motion is disabled, OnTakeDamage can't apply physics force + int ret = BaseClass::OnTakeDamage( info ); + + if( IsOnFire() ) + { + if( (info.GetDamageType() & DMG_BURN) && (info.GetDamageType() & DMG_DIRECT) ) + { + // Burning! scare things in my path if I'm moving. + Vector vel; + + if( VPhysicsGetObject() ) + { + VPhysicsGetObject()->GetVelocity( &vel, NULL ); + + int dangerRadius = 256; // generous radius to begin with + + if( hl2_episodic.GetBool() ) + { + // In Episodic, burning items (such as destroyed APCs) are making very large + // danger sounds which frighten NPCs. This danger sound was designed to frighten + // NPCs away from burning objects that are about to explode (barrels, etc). + // So if this item has no more health (ie, has died but hasn't exploded), + // make a smaller danger sound, just to keep NPCs away from the flames. + // I suspect this problem didn't appear in HL2 simply because we didn't have + // NPCs in such close proximity to destroyed NPCs. (sjb) + if( GetHealth() < 1 ) + { + // This item has no health, but still exists. That means that it may keep + // burning, but isn't likely to explode, so don't frighten over such a large radius. + dangerRadius = 120; + } + } + + trace_t tr; + UTIL_TraceLine( WorldSpaceCenter(), WorldSpaceCenter() + vel, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + CSoundEnt::InsertSound( SOUND_DANGER, tr.endpos, dangerRadius, 1.0, this, SOUNDENT_CHANNEL_REPEATED_DANGER ); + } + } + } + + // If we have a force to enable motion, and we're still disabled, check to see if this should enable us + if ( m_flForceToEnableMotion ) + { + // Large enough to enable motion? + float flForce = info.GetDamageForce().Length(); + if ( flForce >= m_flForceToEnableMotion ) + { + EnableMotion(); + m_flForceToEnableMotion = 0; + } + } + + // Check our health against the threshold: + if( m_damageToEnableMotion > 0 && GetHealth() < m_damageToEnableMotion ) + { + // only do this once + m_damageToEnableMotion = 0; + + // The damage that enables motion may have been enough damage to kill me if I'm breakable + // in which case my physics object is gone. + if ( VPhysicsGetObject() != NULL ) + { + EnableMotion(); + VPhysicsTakeDamage( info ); + } + } + + return ret; +} + + +//----------------------------------------------------------------------------- +// Mass / mass center +//----------------------------------------------------------------------------- +void CPhysicsProp::GetMassCenter( Vector *pMassCenter ) +{ + if ( !VPhysicsGetObject() ) + { + pMassCenter->Init(); + return; + } + + Vector vecLocal = VPhysicsGetObject()->GetMassCenterLocalSpace(); + VectorTransform( vecLocal, EntityToWorldTransform(), *pMassCenter ); +} + +float CPhysicsProp::GetMass() const +{ + return VPhysicsGetObject() ? VPhysicsGetObject()->GetMass() : 1.0f; +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CPhysicsProp::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + if (VPhysicsGetObject()) + { + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr),"Mass: %.2f kg / %.2f lb (%s)", VPhysicsGetObject()->GetMass(), kg2lbs(VPhysicsGetObject()->GetMass()), GetMassEquivalent(VPhysicsGetObject()->GetMass())); + EntityText( text_offset, tempstr, 0); + text_offset++; + + { + vphysics_objectstress_t stressOut; + float stress = CalculateObjectStress( VPhysicsGetObject(), this, &stressOut ); + Q_snprintf(tempstr, sizeof(tempstr),"Stress: %.2f (%.2f / %.2f)", stress, stressOut.exertedStress, stressOut.receivedStress ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + + if ( !VPhysicsGetObject()->IsMoveable() ) + { + Q_snprintf(tempstr, sizeof(tempstr),"Motion Disabled" ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + + if ( m_iszBasePropData != NULL_STRING ) + { + Q_snprintf(tempstr, sizeof(tempstr),"Base PropData: %s", STRING(m_iszBasePropData) ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + + if ( m_iNumBreakableChunks != 0 ) + { + IBreakableWithPropData *pBreakableInterface = assert_cast(this); + Q_snprintf(tempstr, sizeof(tempstr),"Breakable Chunks: %d (Max Size %d)", m_iNumBreakableChunks, pBreakableInterface->GetMaxBreakableSize() ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + + Q_snprintf(tempstr, sizeof(tempstr),"Skin: %d", m_nSkin.Get() ); + EntityText( text_offset, tempstr, 0); + text_offset++; + + Q_snprintf(tempstr, sizeof(tempstr),"Health: %d, collision group %d", GetHealth(), GetCollisionGroup() ); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + } + + return text_offset; +} + + +static CBreakableProp *BreakModelCreate_Prop( CBaseEntity *pOwner, breakmodel_t *pModel, const Vector &position, const QAngle &angles, const breakablepropparams_t ¶ms ) +{ + CBreakableProp *pEntity = (CBreakableProp *)CBaseEntity::CreateNoSpawn( "prop_physics", position, angles, pOwner ); + if ( pEntity ) + { + // UNDONE: Allow .qc to override spawnflags for child pieces + if ( pOwner ) + { + pEntity->AddSpawnFlags( pOwner->GetSpawnFlags() ); + + // We never want to be motion disabled + pEntity->RemoveSpawnFlags( SF_PHYSPROP_MOTIONDISABLED ); + } + pEntity->m_impactEnergyScale = params.impactEnergyScale; // assume the same material + // Inherit the base object's damage modifiers + CBreakableProp *pBreakableOwner = dynamic_cast(pOwner); + if ( pBreakableOwner ) + { + pEntity->SetDmgModBullet( pBreakableOwner->GetDmgModBullet() ); + pEntity->SetDmgModClub( pBreakableOwner->GetDmgModClub() ); + pEntity->SetDmgModExplosive( pBreakableOwner->GetDmgModExplosive() ); + + // Copy over the dx7 fade too + pEntity->CopyFadeFrom( pBreakableOwner ); + } + pEntity->SetModelName( AllocPooledString( pModel->modelName ) ); + pEntity->SetModel( STRING(pEntity->GetModelName()) ); + pEntity->SetCollisionGroup( pModel->collisionGroup ); + + if ( pModel->fadeMinDist > 0 && pModel->fadeMaxDist >= pModel->fadeMinDist ) + { + pEntity->SetFadeDistance( pModel->fadeMinDist, pModel->fadeMaxDist ); + } + + if ( pModel->fadeTime != 0 ) + { + pEntity->AddSpawnFlags( SF_PHYSPROP_IS_GIB ); + } + pEntity->Spawn(); + + // If we're burning, break into burning pieces + CBaseAnimating *pAnimating = dynamic_cast(pOwner); + if ( pAnimating && pAnimating->IsOnFire() ) + { + CEntityFlame *pOwnerFlame = dynamic_cast( pAnimating->GetEffectEntity() ); + + if ( pOwnerFlame ) + { + pEntity->Ignite( pOwnerFlame->GetRemainingLife(), false ); + pEntity->IgniteNumHitboxFires( pOwnerFlame->GetNumHitboxFires() ); + pEntity->IgniteHitboxFireScale( pOwnerFlame->GetHitboxFireScale() ); + } + else + { + // This should never happen + pEntity->Ignite( random->RandomFloat( 5, 10 ), false ); + } + } + } + + return pEntity; +} + +static CBaseAnimating *BreakModelCreate_Ragdoll( CBaseEntity *pOwner, breakmodel_t *pModel, const Vector &position, const QAngle &angles ) +{ + CBaseAnimating *pAnimating = CreateServerRagdollSubmodel( dynamic_cast(pOwner), pModel->modelName, position, angles, pModel->collisionGroup ); + return pAnimating; +} + +CBaseEntity *BreakModelCreateSingle( CBaseEntity *pOwner, breakmodel_t *pModel, const Vector &position, + const QAngle &angles, const Vector &velocity, const AngularImpulse &angVelocity, int nSkin, const breakablepropparams_t ¶ms ) +{ + CBaseAnimating *pEntity = NULL; + // stop creating gibs if too many + if ( g_ActiveGibCount >= ACTIVE_GIB_LIMIT ) + { + //DevMsg(1,"Gib limit on %s\n", pModel->modelName ); + return NULL; + } + + if ( !pModel->isRagdoll ) + { + pEntity = BreakModelCreate_Prop( pOwner, pModel, position, angles, params ); + } + else + { + pEntity = BreakModelCreate_Ragdoll( pOwner, pModel, position, angles ); + } + if ( pEntity ) + { + pEntity->m_nSkin = nSkin; + pEntity->m_iHealth = pModel->health; + if ( g_ActiveGibCount >= ACTIVE_GIB_FADE ) + { + pModel->fadeTime = MIN( 3, pModel->fadeTime ); + } + if ( pModel->fadeTime ) + { + pEntity->SUB_StartFadeOut( pModel->fadeTime, false ); + + CBreakableProp *pProp = dynamic_cast(pEntity); + if ( pProp && !pProp->GetNumBreakableChunks() && pProp->m_takedamage == DAMAGE_YES ) + { + pProp->m_takedamage = DAMAGE_EVENTS_ONLY; + } + } + + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int count = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + if ( count ) + { + for ( int i = 0; i < count; i++ ) + { + pList[i]->SetVelocity( &velocity, &angVelocity ); + } + } + else + { + // failed to create a physics object + UTIL_Remove( pEntity ); + return NULL; + } + } + + return pEntity; +} + +class CBreakModelsPrecached : public CAutoGameSystem +{ +public: + CBreakModelsPrecached() : CAutoGameSystem( "CBreakModelsPrecached" ) + { + m_modelList.SetLessFunc( BreakLessFunc ); + } + + struct breakable_precache_t + { + string_t iszModelName; + int iBreakableCount; + }; + + static bool BreakLessFunc( breakable_precache_t const &lhs, breakable_precache_t const &rhs ) + { + return ( lhs.iszModelName.ToCStr() < rhs.iszModelName.ToCStr() ); + } + + bool IsInList( string_t modelName, int *iBreakableCount ) + { + breakable_precache_t sEntry; + sEntry.iszModelName = modelName; + int iEntry = m_modelList.Find(sEntry); + if ( iEntry != m_modelList.InvalidIndex() ) + { + *iBreakableCount = m_modelList[iEntry].iBreakableCount; + return true; + } + + return false; + } + + void AddToList( string_t modelName, int iBreakableCount ) + { + breakable_precache_t sEntry; + sEntry.iszModelName = modelName; + sEntry.iBreakableCount = iBreakableCount; + m_modelList.Insert( sEntry ); + } + + void LevelShutdownPostEntity() + { + m_modelList.RemoveAll(); + } + +private: + CUtlRBTree m_modelList; +}; + +static CBreakModelsPrecached g_BreakModelsPrecached; + +int PropBreakablePrecacheAll( string_t modelName ) +{ + int iBreakables = 0; + if ( g_BreakModelsPrecached.IsInList( modelName, &iBreakables ) ) + return iBreakables; + + if ( modelName == NULL_STRING ) + { + Msg("Trying to precache breakable prop, but has no model name\n"); + return iBreakables; + } + + int modelIndex = CBaseEntity::PrecacheModel( STRING(modelName) ); + + CUtlVector list; + + BreakModelList( list, modelIndex, COLLISION_GROUP_NONE, 0 ); + iBreakables = list.Count(); + + g_BreakModelsPrecached.AddToList( modelName, iBreakables ); + + for ( int i = 0; i < iBreakables; i++ ) + { + string_t breakModelName = AllocPooledString(list[i].modelName); + if ( modelIndex <= 0 ) + { + iBreakables--; + continue; + } + + PropBreakablePrecacheAll( breakModelName ); + } + + return iBreakables; +} + +bool PropBreakableCapEdictsOnCreateAll(int modelindex, IPhysicsObject *pPhysics, const breakablepropparams_t ¶ms, CBaseEntity *pEntity, int iPrecomputedBreakableCount = -1 ) +{ + // @Note (toml 10-07-03): this is stop-gap to prevent this function from crashing the engine + const int BREATHING_ROOM = 64; + + CUtlVector list; + BreakModelList( list, modelindex, params.defBurstScale, params.defCollisionGroup ); + + int numToCreate = 0; + + if ( iPrecomputedBreakableCount != -1 ) + { + numToCreate = iPrecomputedBreakableCount; + } + else + { + if ( list.Count() ) + { + for ( int i = 0; i < list.Count(); i++ ) + { + int modelIndex = modelinfo->GetModelIndex( list[i].modelName ); + if ( modelIndex <= 0 ) + continue; + numToCreate++; + } + } + // Then see if the propdata specifies any breakable pieces + else if ( pEntity ) + { + IBreakableWithPropData *pBreakableInterface = dynamic_cast(pEntity); + if ( pBreakableInterface && pBreakableInterface->GetBreakableModel() != NULL_STRING && pBreakableInterface->GetBreakableCount() ) + { + numToCreate += pBreakableInterface->GetBreakableCount(); + } + } + } + + return ( !numToCreate || ( engine->GetEntityCount() + numToCreate + BREATHING_ROOM < MAX_EDICTS ) ); +} + + +//============================================================================================================= +// BASE PROP DOOR +//============================================================================================================= +// +// Private activities. +// +static int ACT_DOOR_OPEN = 0; +static int ACT_DOOR_LOCKED = 0; + +// +// Anim events. +// +enum +{ + AE_DOOR_OPEN = 1, // The door should start opening. +}; + + +void PlayLockSounds(CBaseEntity *pEdict, locksound_t *pls, int flocked, int fbutton); + +BEGIN_DATADESC_NO_BASE(locksound_t) + + DEFINE_FIELD( sLockedSound, FIELD_STRING), + DEFINE_FIELD( sLockedSentence, FIELD_STRING ), + DEFINE_FIELD( sUnlockedSound, FIELD_STRING ), + DEFINE_FIELD( sUnlockedSentence, FIELD_STRING ), + DEFINE_FIELD( iLockedSentence, FIELD_INTEGER ), + DEFINE_FIELD( iUnlockedSentence, FIELD_INTEGER ), + DEFINE_FIELD( flwaitSound, FIELD_FLOAT ), + DEFINE_FIELD( flwaitSentence, FIELD_FLOAT ), + DEFINE_FIELD( bEOFLocked, FIELD_CHARACTER ), + DEFINE_FIELD( bEOFUnlocked, FIELD_CHARACTER ), + +END_DATADESC() + +BEGIN_DATADESC(CBasePropDoor) + //DEFINE_FIELD(m_bLockedSentence, FIELD_CHARACTER), + //DEFINE_FIELD(m_bUnlockedSentence, FIELD_CHARACTER), + DEFINE_KEYFIELD(m_nHardwareType, FIELD_INTEGER, "hardware"), + DEFINE_KEYFIELD(m_flAutoReturnDelay, FIELD_FLOAT, "returndelay"), + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), + DEFINE_KEYFIELD(m_SoundMoving, FIELD_SOUNDNAME, "soundmoveoverride"), + DEFINE_KEYFIELD(m_SoundOpen, FIELD_SOUNDNAME, "soundopenoverride"), + DEFINE_KEYFIELD(m_SoundClose, FIELD_SOUNDNAME, "soundcloseoverride"), + DEFINE_KEYFIELD(m_ls.sLockedSound, FIELD_SOUNDNAME, "soundlockedoverride"), + DEFINE_KEYFIELD(m_ls.sUnlockedSound, FIELD_SOUNDNAME, "soundunlockedoverride"), + DEFINE_KEYFIELD(m_SlaveName, FIELD_STRING, "slavename" ), + DEFINE_FIELD(m_bLocked, FIELD_BOOLEAN), + //DEFINE_KEYFIELD(m_flBlockDamage, FIELD_FLOAT, "dmg"), + DEFINE_KEYFIELD( m_bForceClosed, FIELD_BOOLEAN, "forceclosed" ), + DEFINE_FIELD(m_eDoorState, FIELD_INTEGER), + DEFINE_FIELD( m_hMaster, FIELD_EHANDLE ), + DEFINE_FIELD( m_hBlocker, FIELD_EHANDLE ), + DEFINE_FIELD( m_bFirstBlocked, FIELD_BOOLEAN ), + //DEFINE_FIELD(m_hDoorList, FIELD_CLASSPTR), // Reconstructed + + DEFINE_INPUTFUNC(FIELD_VOID, "Open", InputOpen), + DEFINE_INPUTFUNC(FIELD_STRING, "OpenAwayFrom", InputOpenAwayFrom), + DEFINE_INPUTFUNC(FIELD_VOID, "Close", InputClose), + DEFINE_INPUTFUNC(FIELD_VOID, "Toggle", InputToggle), + DEFINE_INPUTFUNC(FIELD_VOID, "Lock", InputLock), + DEFINE_INPUTFUNC(FIELD_VOID, "Unlock", InputUnlock), +#ifdef MAPBASE + DEFINE_INPUTFUNC(FIELD_VOID, "AllowPlayerUse", InputAllowPlayerUse), + DEFINE_INPUTFUNC(FIELD_VOID, "DisallowPlayerUse", InputDisallowPlayerUse), +#endif + + DEFINE_OUTPUT(m_OnBlockedOpening, "OnBlockedOpening"), + DEFINE_OUTPUT(m_OnBlockedClosing, "OnBlockedClosing"), + DEFINE_OUTPUT(m_OnUnblockedOpening, "OnUnblockedOpening"), + DEFINE_OUTPUT(m_OnUnblockedClosing, "OnUnblockedClosing"), + DEFINE_OUTPUT(m_OnFullyClosed, "OnFullyClosed"), + DEFINE_OUTPUT(m_OnFullyOpen, "OnFullyOpen"), + DEFINE_OUTPUT(m_OnClose, "OnClose"), + DEFINE_OUTPUT(m_OnOpen, "OnOpen"), + DEFINE_OUTPUT(m_OnLockedUse, "OnLockedUse" ), + DEFINE_EMBEDDED( m_ls ), + + // Function Pointers + DEFINE_THINKFUNC(DoorOpenMoveDone), + DEFINE_THINKFUNC(DoorCloseMoveDone), + DEFINE_THINKFUNC(DoorAutoCloseThink), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST(CBasePropDoor, DT_BasePropDoor) +END_SEND_TABLE() + +CBasePropDoor::CBasePropDoor( void ) +{ + m_hMaster = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePropDoor::Spawn() +{ + BaseClass::Spawn(); + + DisableAutoFade(); + Precache(); + + DoorTeleportToSpawnPosition(); + + if (HasSpawnFlags(SF_DOOR_LOCKED)) + { + m_bLocked = true; + } + + SetMoveType(MOVETYPE_PUSH); + + if (m_flSpeed == 0) + { + m_flSpeed = 100; + } + + RemoveFlag(FL_STATICPROP); + + SetSolid(SOLID_VPHYSICS); + VPhysicsInitShadow(false, false); + AddSolidFlags( FSOLID_CUSTOMRAYTEST | FSOLID_CUSTOMBOXTEST ); + + SetBodygroup( DOOR_HARDWARE_GROUP, m_nHardwareType ); + if ((m_nHardwareType == 0) && (!HasSpawnFlags(SF_DOOR_LOCKED))) + { + // Doors with no hardware must always be locked. + DevWarning(1, "Unlocked prop_door '%s' at (%.0f %.0f %.0f) has no hardware. All openable doors must have hardware!\n", GetDebugName(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z); + } + + if ( !PropDataOverrodeBlockLOS() ) + { + CalculateBlockLOS(); + } + + SetDoorBlocker( NULL ); + + // Fills out the m_Soundxxx members. + CalcDoorSounds(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns our capabilities mask. +//----------------------------------------------------------------------------- +int CBasePropDoor::ObjectCaps() +{ + return BaseClass::ObjectCaps() | ( HasSpawnFlags( SF_DOOR_IGNORE_USE ) ? 0 : (FCAP_IMPULSE_USE|FCAP_USE_IN_RADIUS) ); +}; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePropDoor::Precache(void) +{ + BaseClass::Precache(); + + RegisterPrivateActivities(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePropDoor::RegisterPrivateActivities(void) +{ + static bool bRegistered = false; + + if (bRegistered) + return; + + REGISTER_PRIVATE_ACTIVITY( ACT_DOOR_OPEN ); + REGISTER_PRIVATE_ACTIVITY( ACT_DOOR_LOCKED ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePropDoor::Activate( void ) +{ + BaseClass::Activate(); + + UpdateAreaPortals( !IsDoorClosed() ); + + // If we have a name, we may be linked + if ( GetEntityName() != NULL_STRING ) + { + CBaseEntity *pTarget = NULL; + + // Find our slaves. + // If we have a specified slave name, then use that to find slaves. + // Otherwise, see if there are any other doors that match our name (Backwards compatability). + string_t iszSearchName = GetEntityName(); + if ( m_SlaveName != NULL_STRING ) + { + const char *pSlaveName = STRING(m_SlaveName); + if ( pSlaveName && pSlaveName[0] ) + { + iszSearchName = m_SlaveName; + } + } + + while ( ( pTarget = gEntList.FindEntityByName( pTarget, iszSearchName ) ) != NULL ) + { + if ( pTarget != this ) + { + CBasePropDoor *pDoor = dynamic_cast(pTarget); + + if ( pDoor != NULL && pDoor->HasSlaves() == false ) + { + m_hDoorList.AddToTail( pDoor ); + pDoor->SetMaster( this ); + pDoor->SetOwnerEntity( this ); + } + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePropDoor::HandleAnimEvent(animevent_t *pEvent) +{ + // Opening is called here via an animation event if the open sequence has one, + // otherwise it is called immediately when the open sequence is set. + if (pEvent->event == AE_DOOR_OPEN) + { + DoorActivate(); + } +} + + +// Only overwrite str1 if it's NULL_STRING. +#define ASSIGN_STRING_IF_NULL( str1, str2 ) \ + if ( ( str1 ) == NULL_STRING ) { ( str1 ) = ( str2 ); } + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePropDoor::CalcDoorSounds() +{ + ErrorIfNot( GetModel() != NULL, ( "prop_door with no model at %.2f %.2f %.2f\n", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ) ); + + string_t strSoundOpen = NULL_STRING; + string_t strSoundClose = NULL_STRING; + string_t strSoundMoving = NULL_STRING; + string_t strSoundLocked = NULL_STRING; + string_t strSoundUnlocked = NULL_STRING; + + bool bFoundSkin = false; + // Otherwise, use the sounds specified by the model keyvalues. These are looked up + // based on skin and hardware. + KeyValues *modelKeyValues = new KeyValues(""); + if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( GetModel() ), modelinfo->GetModelKeyValueText( GetModel() ) ) ) + { + KeyValues *pkvDoorSounds = modelKeyValues->FindKey("door_options"); + if ( pkvDoorSounds ) + { + // Open / close / move sounds are looked up by skin index. + char szSkin[80]; + int skin = m_nSkin; + Q_snprintf( szSkin, sizeof( szSkin ), "skin%d", skin ); + KeyValues *pkvSkinData = pkvDoorSounds->FindKey( szSkin ); + if ( pkvSkinData ) + { + strSoundOpen = AllocPooledString( pkvSkinData->GetString( "open" ) ); + strSoundClose = AllocPooledString( pkvSkinData->GetString( "close" ) ); + strSoundMoving = AllocPooledString( pkvSkinData->GetString( "move" ) ); + const char *pSurfaceprop = pkvSkinData->GetString( "surfaceprop" ); + if ( pSurfaceprop && VPhysicsGetObject() ) + { + bFoundSkin = true; + VPhysicsGetObject()->SetMaterialIndex( physprops->GetSurfaceIndex( pSurfaceprop ) ); + } + } + + // Locked / unlocked sounds are looked up by hardware index. + char szHardware[80]; + Q_snprintf( szHardware, sizeof( szHardware ), "hardware%d", m_nHardwareType ); + KeyValues *pkvHardwareData = pkvDoorSounds->FindKey( szHardware ); + if ( pkvHardwareData ) + { + strSoundLocked = AllocPooledString( pkvHardwareData->GetString( "locked" ) ); + strSoundUnlocked = AllocPooledString( pkvHardwareData->GetString( "unlocked" ) ); + } + + // If any sounds were missing, try the "defaults" block. + if ( ( strSoundOpen == NULL_STRING ) || ( strSoundClose == NULL_STRING ) || ( strSoundMoving == NULL_STRING ) || + ( strSoundLocked == NULL_STRING ) || ( strSoundUnlocked == NULL_STRING ) ) + { + KeyValues *pkvDefaults = pkvDoorSounds->FindKey( "defaults" ); + if ( pkvDefaults ) + { + ASSIGN_STRING_IF_NULL( strSoundOpen, AllocPooledString( pkvDefaults->GetString( "open" ) ) ); + ASSIGN_STRING_IF_NULL( strSoundClose, AllocPooledString( pkvDefaults->GetString( "close" ) ) ); + ASSIGN_STRING_IF_NULL( strSoundMoving, AllocPooledString( pkvDefaults->GetString( "move" ) ) ); + ASSIGN_STRING_IF_NULL( strSoundLocked, AllocPooledString( pkvDefaults->GetString( "locked" ) ) ); + ASSIGN_STRING_IF_NULL( strSoundUnlocked, AllocPooledString( pkvDefaults->GetString( "unlocked" ) ) ); + // NOTE: No default needed for surfaceprop, it's set by the model + } + } + } + } + modelKeyValues->deleteThis(); + modelKeyValues = NULL; + if ( !bFoundSkin && VPhysicsGetObject() ) + { + Warning( "%s has Door model (%s) with no door_options! Verify that SKIN is valid, and has a corresponding options block in the model QC file\n", GetDebugName(), modelinfo->GetModelName( GetModel() ) ); + VPhysicsGetObject()->SetMaterialIndex( physprops->GetSurfaceIndex("wood") ); + } + + // Any sound data members that are already filled out were specified as level designer overrides, + // so they should not be overwritten. + ASSIGN_STRING_IF_NULL( m_SoundOpen, strSoundOpen ); + ASSIGN_STRING_IF_NULL( m_SoundClose, strSoundClose ); + ASSIGN_STRING_IF_NULL( m_SoundMoving, strSoundMoving ); + ASSIGN_STRING_IF_NULL( m_ls.sLockedSound, strSoundLocked ); + ASSIGN_STRING_IF_NULL( m_ls.sUnlockedSound, strSoundUnlocked ); + + // Make sure we have real, precachable sound names in all cases. + UTIL_ValidateSoundName( m_SoundMoving, "DoorSound.Null" ); + UTIL_ValidateSoundName( m_SoundOpen, "DoorSound.Null" ); + UTIL_ValidateSoundName( m_SoundClose, "DoorSound.Null" ); + UTIL_ValidateSoundName( m_ls.sLockedSound, "DoorSound.Null" ); + UTIL_ValidateSoundName( m_ls.sUnlockedSound, "DoorSound.Null" ); + + PrecacheScriptSound( STRING( m_SoundMoving ) ); + PrecacheScriptSound( STRING( m_SoundOpen ) ); + PrecacheScriptSound( STRING( m_SoundClose ) ); + PrecacheScriptSound( STRING( m_ls.sLockedSound ) ); + PrecacheScriptSound( STRING( m_ls.sUnlockedSound ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : isOpen - +//----------------------------------------------------------------------------- +void CBasePropDoor::UpdateAreaPortals(bool isOpen) +{ + string_t name = GetEntityName(); + if (!name) + return; + + CBaseEntity *pPortal = NULL; +#ifdef MAPBASE + // For func_areaportal_oneway. + while ((pPortal = gEntList.FindEntityByClassname(pPortal, "func_areaportal*")) != NULL) +#else + while ((pPortal = gEntList.FindEntityByClassname(pPortal, "func_areaportal")) != NULL) +#endif + { + if (pPortal->HasTarget(name)) + { + // USE_ON means open the portal, off means close it + pPortal->Use(this, this, isOpen?USE_ON:USE_OFF, 0); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : state - +//----------------------------------------------------------------------------- +void CBasePropDoor::SetDoorBlocker( CBaseEntity *pBlocker ) +{ + m_hBlocker = pBlocker; + + if ( m_hBlocker == NULL ) + { + m_bFirstBlocked = false; + } +} +//----------------------------------------------------------------------------- +// Purpose: Called when the player uses the door. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CBasePropDoor::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value) +{ + if ( GetMaster() != NULL ) + { + // Tell our owner we've been used + GetMaster()->Use( pActivator, pCaller, useType, value ); + } + else + { + // Just let it through + OnUse( pActivator, pCaller, useType, value ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActivator - +// *pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CBasePropDoor::OnUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // If we're blocked while closing, open away from our blocker. This will + // liberate whatever bit of detritus is stuck in us. + if ( IsDoorBlocked() && IsDoorClosing() ) + { + m_hActivator = pActivator; + DoorOpen( m_hBlocker ); + return; + } + + if (IsDoorClosed() || (IsDoorOpen() && HasSpawnFlags(SF_DOOR_USE_CLOSES))) + { + // Ready to be opened or closed. + if (m_bLocked) + { + PropSetSequence(SelectWeightedSequence((Activity)ACT_DOOR_LOCKED)); + PlayLockSounds(this, &m_ls, TRUE, FALSE); + m_OnLockedUse.FireOutput( pActivator, pCaller ); + } + else + { + m_hActivator = pActivator; + + PlayLockSounds(this, &m_ls, FALSE, FALSE); + int nSequence = SelectWeightedSequence((Activity)ACT_DOOR_OPEN); + PropSetSequence(nSequence); + + if ((nSequence == -1) || !HasAnimEvent(nSequence, AE_DOOR_OPEN)) + { + // No open anim event, we need to open the door here. + DoorActivate(); + } + } + } + else if ( IsDoorOpening() && HasSpawnFlags(SF_DOOR_USE_CLOSES) ) + { + // We've been used while opening, close. + m_hActivator = pActivator; + DoorClose(); + } + else if ( IsDoorClosing() || IsDoorAjar() ) + { + m_hActivator = pActivator; + DoorOpen( m_hActivator ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Closes the door if it is not already closed. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputClose(inputdata_t &inputdata) +{ + if (!IsDoorClosed()) + { + m_OnClose.FireOutput(inputdata.pActivator, this); + DoorClose(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that locks the door. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputLock(inputdata_t &inputdata) +{ + Lock(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Opens the door if it is not already open. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputOpen(inputdata_t &inputdata) +{ + OpenIfUnlocked(inputdata.pActivator, NULL); +} + + +//----------------------------------------------------------------------------- +// Purpose: Opens the door away from a specified entity if it is not already open. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputOpenAwayFrom(inputdata_t &inputdata) +{ + CBaseEntity *pOpenAwayFrom = gEntList.FindEntityByName( NULL, inputdata.value.String(), NULL, inputdata.pActivator, inputdata.pCaller ); + OpenIfUnlocked(inputdata.pActivator, pOpenAwayFrom); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// +// FIXME: This function should be combined with DoorOpen, but doing that +// could break existing content. Fix after shipping! +// +// Input : *pOpenAwayFrom - +//----------------------------------------------------------------------------- +void CBasePropDoor::OpenIfUnlocked(CBaseEntity *pActivator, CBaseEntity *pOpenAwayFrom) +{ + // I'm locked, can't open + if (m_bLocked) + return; + + if (!IsDoorOpen() && !IsDoorOpening()) + { + // Play door unlock sounds. + PlayLockSounds(this, &m_ls, false, false); + m_OnOpen.FireOutput(pActivator, this); + DoorOpen(pOpenAwayFrom); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Opens the door if it is not already open. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputToggle(inputdata_t &inputdata) +{ + if (IsDoorClosed()) + { + // I'm locked, can't open + if (m_bLocked) + return; + + DoorOpen(NULL); + } + else if (IsDoorOpen()) + { + DoorClose(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that unlocks the door. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputUnlock(inputdata_t &inputdata) +{ + Unlock(); +} + + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Input handler that makes the door usable for players. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputAllowPlayerUse(inputdata_t &inputdata) +{ + RemoveSpawnFlags(SF_DOOR_IGNORE_USE); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler that makes the door unusable for players. +//----------------------------------------------------------------------------- +void CBasePropDoor::InputDisallowPlayerUse(inputdata_t &inputdata) +{ + AddSpawnFlags(SF_DOOR_IGNORE_USE); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Locks the door so that it cannot be opened. +//----------------------------------------------------------------------------- +void CBasePropDoor::Lock(void) +{ + m_bLocked = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Unlocks the door so that it can be opened. +//----------------------------------------------------------------------------- +void CBasePropDoor::Unlock(void) +{ + if (!m_nHardwareType) + { + // Doors with no hardware must always be locked. + DevWarning(1, "Unlocking prop_door '%s' at (%.0f %.0f %.0f) with no hardware. All openable doors must have hardware!\n", GetDebugName(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z); + } + + m_bLocked = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Causes the door to "do its thing", i.e. start moving, and cascade activation. +//----------------------------------------------------------------------------- +bool CBasePropDoor::DoorActivate( void ) +{ + if ( IsDoorOpen() && DoorCanClose( false ) ) + { + DoorClose(); + } + else + { + DoorOpen( m_hActivator ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Starts the door opening. +//----------------------------------------------------------------------------- +void CBasePropDoor::DoorOpen(CBaseEntity *pOpenAwayFrom) +{ + // Don't bother if we're already doing this + if ( IsDoorOpen() || IsDoorOpening() ) + return; + + UpdateAreaPortals(true); + + // It could be going-down, if blocked. + ASSERT( IsDoorClosed() || IsDoorClosing() || IsDoorAjar() ); + + // Emit door moving and stop sounds on CHAN_STATIC so that the multicast doesn't + // filter them out and leave a client stuck with looping door sounds! + if (!HasSpawnFlags(SF_DOOR_SILENT)) + { + EmitSound( STRING( m_SoundMoving ) ); + + if ( m_hActivator && m_hActivator->IsPlayer() && !HasSpawnFlags( SF_DOOR_SILENT_TO_NPCS ) ) + { + CSoundEnt::InsertSound( SOUND_PLAYER, GetAbsOrigin(), 512, 0.5, this );//<>//magic number + } + } + + SetDoorState( DOOR_STATE_OPENING ); + + SetMoveDone(&CBasePropDoor::DoorOpenMoveDone); + + // Virtual function that starts the door moving for whatever type of door this is. + BeginOpening(pOpenAwayFrom); + + m_OnOpen.FireOutput(this, this); + + // Tell all the slaves + if ( HasSlaves() ) + { + int numDoors = m_hDoorList.Count(); + + CBasePropDoor *pLinkedDoor = NULL; + + // Open all linked doors + for ( int i = 0; i < numDoors; i++ ) + { + pLinkedDoor = m_hDoorList[i]; + + if ( pLinkedDoor != NULL ) + { + // If the door isn't already moving, get it moving + pLinkedDoor->m_hActivator = m_hActivator; + pLinkedDoor->DoorOpen( pOpenAwayFrom ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: The door has reached the open position. Either close automatically +// or wait for another activation. +//----------------------------------------------------------------------------- +void CBasePropDoor::DoorOpenMoveDone(void) +{ + SetDoorBlocker( NULL ); + + if (!HasSpawnFlags(SF_DOOR_SILENT)) + { + EmitSound( STRING( m_SoundOpen ) ); + } + + ASSERT(IsDoorOpening()); + SetDoorState( DOOR_STATE_OPEN ); + + if (WillAutoReturn()) + { + // In flWait seconds, DoorClose will fire, unless wait is -1, then door stays open + SetMoveDoneTime(m_flAutoReturnDelay + 0.1); + SetMoveDone(&CBasePropDoor::DoorAutoCloseThink); + + if (m_flAutoReturnDelay == -1) + { + SetNextThink( TICK_NEVER_THINK ); + } + } + + CAI_BaseNPC *pNPC = dynamic_cast(m_hActivator.Get()); + if (pNPC) + { + // Notify the NPC that opened us. + pNPC->OnDoorFullyOpen(this); + } + + m_OnFullyOpen.FireOutput(this, this); + + // Let the leaf class do its thing. + OnDoorOpened(); + + m_hActivator = NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function that tries to close the door. Used for autoreturn. +//----------------------------------------------------------------------------- +void CBasePropDoor::DoorAutoCloseThink(void) +{ + // When autoclosing, we check both sides so that we don't close in the player's + // face, or in an NPC's face for that matter, because they might be shooting + // through the doorway. + if ( !DoorCanClose( true ) ) + { + if (m_flAutoReturnDelay == -1) + { + SetNextThink( TICK_NEVER_THINK ); + } + else + { + // In flWait seconds, DoorClose will fire, unless wait is -1, then door stays open + SetMoveDoneTime(m_flAutoReturnDelay + 0.1); + SetMoveDone(&CBasePropDoor::DoorAutoCloseThink); + } + + return; + } + + DoorClose(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the door closing. +//----------------------------------------------------------------------------- +void CBasePropDoor::DoorClose(void) +{ + // Don't bother if we're already doing this + if ( IsDoorClosed() || IsDoorClosing() ) + return; + + if (!HasSpawnFlags(SF_DOOR_SILENT)) + { + EmitSound( STRING( m_SoundMoving ) ); + + if ( m_hActivator && m_hActivator->IsPlayer() ) + { + CSoundEnt::InsertSound( SOUND_PLAYER, GetAbsOrigin(), 512, 0.5, this );//<>//magic number + } + } + + ASSERT(IsDoorOpen() || IsDoorOpening()); + SetDoorState( DOOR_STATE_CLOSING ); + + SetMoveDone(&CBasePropDoor::DoorCloseMoveDone); + + // This will set the movedone time. + BeginClosing(); + + m_OnClose.FireOutput(this, this); + + // Tell all the slaves + if ( HasSlaves() ) + { + int numDoors = m_hDoorList.Count(); + + CBasePropDoor *pLinkedDoor = NULL; + + // Open all linked doors + for ( int i = 0; i < numDoors; i++ ) + { + pLinkedDoor = m_hDoorList[i]; + + if ( pLinkedDoor != NULL ) + { + // If the door isn't already moving, get it moving + pLinkedDoor->DoorClose(); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: The door has reached the closed position. Return to quiescence. +//----------------------------------------------------------------------------- +void CBasePropDoor::DoorCloseMoveDone(void) +{ + SetDoorBlocker( NULL ); + + if (!HasSpawnFlags(SF_DOOR_SILENT)) + { + StopSound( STRING( m_SoundMoving ) ); + EmitSound( STRING( m_SoundClose ) ); + } + + ASSERT(IsDoorClosing()); + SetDoorState( DOOR_STATE_CLOSED ); + + m_OnFullyClosed.FireOutput(m_hActivator, this); + UpdateAreaPortals(false); + + // Let the leaf class do its thing. + OnDoorClosed(); + + m_hActivator = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pOther - +//----------------------------------------------------------------------------- +void CBasePropDoor::MasterStartBlocked( CBaseEntity *pOther ) +{ + if ( HasSlaves() ) + { + int numDoors = m_hDoorList.Count(); + + CBasePropDoor *pLinkedDoor = NULL; + + // Open all linked doors + for ( int i = 0; i < numDoors; i++ ) + { + pLinkedDoor = m_hDoorList[i]; + + if ( pLinkedDoor != NULL ) + { + // If the door isn't already moving, get it moving + pLinkedDoor->OnStartBlocked( pOther ); + } + } + } + + // Start ourselves blocked + OnStartBlocked( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called the first frame that the door is blocked while opening or closing. +// Input : pOther - The blocking entity. +//----------------------------------------------------------------------------- +void CBasePropDoor::StartBlocked( CBaseEntity *pOther ) +{ + m_bFirstBlocked = true; + + if ( GetMaster() != NULL ) + { + GetMaster()->MasterStartBlocked( pOther ); + return; + } + + // Start ourselves blocked + OnStartBlocked( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pOther - +//----------------------------------------------------------------------------- +void CBasePropDoor::OnStartBlocked( CBaseEntity *pOther ) +{ + if ( m_bFirstBlocked == false ) + { + DoorStop(); + } + + SetDoorBlocker( pOther ); + + if (!HasSpawnFlags(SF_DOOR_SILENT)) + { + StopSound( STRING( m_SoundMoving ) ); + } + + // + // Fire whatever events we need to due to our blocked state. + // + if (IsDoorClosing()) + { + // Closed into an NPC, open. + if ( pOther->MyNPCPointer() ) + { + DoorOpen( pOther ); + } + m_OnBlockedClosing.FireOutput(pOther, this); + } + else + { + // Opened into an NPC, close. + if ( pOther->MyNPCPointer() ) + { + DoorClose(); + } + + CAI_BaseNPC *pNPC = dynamic_cast(m_hActivator.Get()); + + if ( pNPC != NULL ) + { + // Notify the NPC that tried to open us. + pNPC->OnDoorBlocked( this ); + } + + m_OnBlockedOpening.FireOutput( pOther, this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called every frame when the door is blocked while opening or closing. +// Input : pOther - The blocking entity. +//----------------------------------------------------------------------------- +void CBasePropDoor::Blocked(CBaseEntity *pOther) +{ + // dvs: TODO: will prop_door apply any blocking damage? + // Hurt the blocker a little. + //if (m_flBlockDamage) + //{ + // pOther->TakeDamage(CTakeDamageInfo(this, this, m_flBlockDamage, DMG_CRUSH)); + //} + + if ( m_bForceClosed && ( pOther->GetMoveType() == MOVETYPE_VPHYSICS ) && + ( pOther->m_takedamage == DAMAGE_NO || pOther->m_takedamage == DAMAGE_EVENTS_ONLY ) ) + { + EntityPhysics_CreateSolver( this, pOther, true, 4.0f ); + } + else if ( m_bForceClosed && ( pOther->GetMoveType() == MOVETYPE_VPHYSICS ) && ( pOther->m_takedamage == DAMAGE_YES ) ) + { + pOther->TakeDamage( CTakeDamageInfo( this, this, pOther->GetHealth(), DMG_CRUSH ) ); + } + + // If we're set to force ourselves closed, keep going + if ( m_bForceClosed ) + return; + + // If a door has a negative wait, it would never come back if blocked, + // so let it just squash the object to death real fast. +// if (m_flAutoReturnDelay >= 0) +// { +// if (IsDoorClosing()) +// { +// DoorOpen(); +// } +// else +// { +// DoorClose(); +// } +// } + + // Block all door pieces with the same targetname here. +// if (GetEntityName() != NULL_STRING) +// { +// CBaseEntity pTarget = NULL; +// for (;;) +// { +// pTarget = gEntList.FindEntityByName(pTarget, GetEntityName() ); +// +// if (pTarget != this) +// { +// if (!pTarget) +// break; +// +// if (FClassnameIs(pTarget, "prop_door_rotating")) +// { +// CPropDoorRotating *pDoor = (CPropDoorRotating *)pTarget; +// +// if (pDoor->m_fAutoReturnDelay >= 0) +// { +// if (pDoor->GetAbsVelocity() == GetAbsVelocity() && pDoor->GetLocalAngularVelocity() == GetLocalAngularVelocity()) +// { +// // this is the most hacked, evil, bastardized thing I've ever seen. kjb +// if (FClassnameIs(pTarget, "prop_door_rotating")) +// { +// // set angles to realign rotating doors +// pDoor->SetLocalAngles(GetLocalAngles()); +// pDoor->SetLocalAngularVelocity(vec3_angle); +// } +// else +// //{ +// // // set origin to realign normal doors +// // pDoor->SetLocalOrigin(GetLocalOrigin()); +// // pDoor->SetAbsVelocity(vec3_origin);// stop! +// //} +// } +// +// if (IsDoorClosing()) +// { +// pDoor->DoorOpen(); +// } +// else +// { +// pDoor->DoorClose(); +// } +// } +// } +// } +// } +// } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called the first frame that the door is unblocked while opening or closing. +//----------------------------------------------------------------------------- +void CBasePropDoor::EndBlocked( void ) +{ + if ( GetMaster() != NULL ) + { + GetMaster()->EndBlocked(); + return; + } + + if ( HasSlaves() ) + { + int numDoors = m_hDoorList.Count(); + + CBasePropDoor *pLinkedDoor = NULL; + + // Check all links as well + for ( int i = 0; i < numDoors; i++ ) + { + pLinkedDoor = m_hDoorList[i]; + + if ( pLinkedDoor != NULL ) + { + // Make sure they can close as well + pLinkedDoor->OnEndBlocked(); + } + } + } + + // Emit door moving and stop sounds on CHAN_STATIC so that the multicast doesn't + // filter them out and leave a client stuck with looping door sounds! + if (!HasSpawnFlags(SF_DOOR_SILENT)) + { + EmitSound( STRING( m_SoundMoving ) ); + } + + // + // Fire whatever events we need to due to our unblocked state. + // + if (IsDoorClosing()) + { + m_OnUnblockedClosing.FireOutput(this, this); + } + else + { + m_OnUnblockedOpening.FireOutput(this, this); + } + + OnEndBlocked(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePropDoor::OnEndBlocked( void ) +{ + if ( m_bFirstBlocked ) + return; + + // Restart us going + DoorResume(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pNPC - +//----------------------------------------------------------------------------- +bool CBasePropDoor::NPCOpenDoor( CAI_BaseNPC *pNPC ) +{ + // dvs: TODO: use activator filter here + // dvs: TODO: outboard entity containing rules for whether door is operable? + + if ( IsDoorClosed() ) + { + // Use the door + Use( pNPC, pNPC, USE_ON, 0 ); + } + + return true; +} + +bool CBasePropDoor::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) +{ + if ( !VPhysicsGetObject() ) + return false; + + CStudioHdr *pStudioHdr = GetModelPtr( ); + if (!pStudioHdr) + return false; + + if ( !( pStudioHdr->contents() & mask ) ) + return false; + + physcollision->TraceBox( ray, VPhysicsGetObject()->GetCollide(), GetAbsOrigin(), GetAbsAngles(), &trace ); + + if ( trace.DidHit() ) + { + trace.surface.surfaceProps = VPhysicsGetObject()->GetMaterialIndex(); + return true; + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Custom trace filter for doors +// Will only test against entities and rejects physics objects below a mass threshold +//----------------------------------------------------------------------------- + +class CTraceFilterDoor : public CTraceFilterEntitiesOnly +{ +public: + // It does have a base, but we'll never network anything below here.. + DECLARE_CLASS_NOBASE( CTraceFilterDoor ); + + CTraceFilterDoor( const IHandleEntity *pDoor, const IHandleEntity *passentity, int collisionGroup ) + : m_pDoor(pDoor), m_pPassEnt(passentity), m_collisionGroup(collisionGroup) + { + } + + virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + if ( !StandardFilterRules( pHandleEntity, contentsMask ) ) + return false; + + if ( !PassServerEntityFilter( pHandleEntity, m_pDoor ) ) + return false; + + if ( !PassServerEntityFilter( pHandleEntity, m_pPassEnt ) ) + return false; + + // Don't test if the game code tells us we should ignore this collision... + CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity ); + + if ( pEntity ) + { + if ( !pEntity->ShouldCollide( m_collisionGroup, contentsMask ) ) + return false; + + if ( !g_pGameRules->ShouldCollide( m_collisionGroup, pEntity->GetCollisionGroup() ) ) + return false; + + // If objects are small enough and can move, close on them + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); + Assert(pPhysics); + + // Must either be squashable or very light + if ( pPhysics->IsMoveable() && pPhysics->GetMass() < 32 ) + return false; + } + +#ifdef MAPBASE + // They're children, for goodness sake! + if (pEntity->GetParent() == EntityFromEntityHandle(m_pDoor)) + return false; +#endif + } + + return true; + } + +private: + + const IHandleEntity *m_pDoor; + const IHandleEntity *m_pPassEnt; + int m_collisionGroup; +}; + +inline void TraceHull_Door( const CBasePropDoor *pDoor, const Vector &vecAbsStart, const Vector &vecAbsEnd, const Vector &hullMin, + const Vector &hullMax, unsigned int mask, const CBaseEntity *ignore, + int collisionGroup, trace_t *ptr ) +{ + Ray_t ray; + ray.Init( vecAbsStart, vecAbsEnd, hullMin, hullMax ); + CTraceFilterDoor traceFilter( pDoor, ignore, collisionGroup ); + enginetrace->TraceRay( ray, mask, &traceFilter, ptr ); +} + +#ifdef MAPBASE +// This was still broken when it was scrapped. +//#define DOOR_BREAKING_STUFF 1 +#endif + +// Check directions for door movement +enum doorCheck_e +{ + DOOR_CHECK_FORWARD, // Door's forward opening direction + DOOR_CHECK_BACKWARD, // Door's backward opening direction + DOOR_CHECK_FULL, // Door's complete movement volume +}; + + +enum PropDoorRotatingSpawnPos_t +{ + DOOR_SPAWN_CLOSED = 0, + DOOR_SPAWN_OPEN_FORWARD, + DOOR_SPAWN_OPEN_BACK, + DOOR_SPAWN_AJAR, +}; + +enum PropDoorRotatingOpenDirection_e +{ + DOOR_ROTATING_OPEN_BOTH_WAYS = 0, + DOOR_ROTATING_OPEN_FORWARD, + DOOR_ROTATING_OPEN_BACKWARD, +}; + +#ifdef DOOR_BREAKING_STUFF +enum PropDoorRotatingBreakType_e +{ + DOOR_ROTATING_BREAK_NORMAL = 0, // Base behavior. + DOOR_ROTATING_BREAK_PHYS, // Turn into a physics prop via phys_conversion. + DOOR_ROTATING_BREAK_PHYS_HINGE, // Same as above, but use a phys_hinge. +}; +#endif + +//=============================================== +// Rotating prop door +//=============================================== + +class CPropDoorRotating : public CBasePropDoor +{ + DECLARE_CLASS( CPropDoorRotating, CBasePropDoor ); + +public: + + ~CPropDoorRotating(); + + int DrawDebugTextOverlays(void); + + void Spawn( void ); + void MoveDone( void ); + void BeginOpening(CBaseEntity *pOpenAwayFrom); + void BeginClosing( void ); + void OnRestore( void ); + + void DoorTeleportToSpawnPosition(); + + void GetNPCOpenData(CAI_BaseNPC *pNPC, opendata_t &opendata); + + void DoorClose( void ); + bool DoorCanClose( bool bAutoClose ); + void DoorOpen( CBaseEntity *pOpenAwayFrom ); + + void OnDoorOpened(); + void OnDoorClosed(); + + void DoorResume( void ); + void DoorStop( void ); + + float GetOpenInterval(); + + bool OverridePropdata() { return true; } + + void InputSetSpeed(inputdata_t &inputdata); + +#ifdef DOOR_BREAKING_STUFF + void Break( CBaseEntity *pBreaker, const CTakeDamageInfo &info ); +#endif + +#ifdef MAPBASE + // Filters don't work well with the way doors are considered obstructions, so it's just a spawnflag that stops all NPCs for now. + virtual bool PassesDoorFilter(CBaseEntity *pEntity) { return !HasSpawnFlags(SF_DOOR_NONPCS); } +#endif + + DECLARE_DATADESC(); + +private: + + bool IsHingeOnLeft(); + + void AngularMove(const QAngle &vecDestAngle, float flSpeed); + void CalculateDoorVolume( QAngle closedAngles, QAngle openAngles, Vector *destMins, Vector *destMaxs ); + + bool CheckDoorClear( doorCheck_e state ); + + doorCheck_e GetOpenState( void ); + + void InputSetRotationDistance ( inputdata_t &inputdata ); // Set the degree difference between open and closed + + void CalcOpenAngles ( void ); // Subroutine to setup the m_angRotation QAngles based on the m_flDistance variable + + Vector m_vecAxis; // The axis of rotation. + float m_flDistance; // How many degrees we rotate between open and closed. + + PropDoorRotatingSpawnPos_t m_eSpawnPosition; + PropDoorRotatingOpenDirection_e m_eOpenDirection; +#ifdef DOOR_BREAKING_STUFF + PropDoorRotatingBreakType_e m_eBreakType; +#endif + + QAngle m_angRotationAjar; // Angles to spawn at if we are set to spawn ajar. + QAngle m_angRotationClosed; // Our angles when we are fully closed. + QAngle m_angRotationOpenForward; // Our angles when we are fully open towards our forward vector. + QAngle m_angRotationOpenBack; // Our angles when we are fully open away from our forward vector. + + QAngle m_angGoal; + + Vector m_vecForwardBoundsMin; + Vector m_vecForwardBoundsMax; + Vector m_vecBackBoundsMin; + Vector m_vecBackBoundsMax; + + CHandle m_hDoorBlocker; +}; + + +BEGIN_DATADESC(CPropDoorRotating) + DEFINE_KEYFIELD(m_eSpawnPosition, FIELD_INTEGER, "spawnpos"), + DEFINE_KEYFIELD(m_eOpenDirection, FIELD_INTEGER, "opendir" ), +#ifdef DOOR_BREAKING_STUFF + DEFINE_KEYFIELD(m_eBreakType, FIELD_INTEGER, "breaktype" ), +#endif + DEFINE_KEYFIELD(m_vecAxis, FIELD_VECTOR, "axis"), + DEFINE_KEYFIELD(m_flDistance, FIELD_FLOAT, "distance"), + DEFINE_KEYFIELD( m_angRotationAjar, FIELD_VECTOR, "ajarangles" ), + DEFINE_FIELD( m_angRotationClosed, FIELD_VECTOR ), + DEFINE_FIELD( m_angRotationOpenForward, FIELD_VECTOR ), + DEFINE_FIELD( m_angRotationOpenBack, FIELD_VECTOR ), + DEFINE_FIELD( m_angGoal, FIELD_VECTOR ), + DEFINE_FIELD( m_hDoorBlocker, FIELD_EHANDLE ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetRotationDistance", InputSetRotationDistance ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), + //m_vecForwardBoundsMin + //m_vecForwardBoundsMax + //m_vecBackBoundsMin + //m_vecBackBoundsMax +END_DATADESC() + +LINK_ENTITY_TO_CLASS(prop_door_rotating, CPropDoorRotating); + +//----------------------------------------------------------------------------- +// Destructor +//----------------------------------------------------------------------------- +CPropDoorRotating::~CPropDoorRotating( void ) +{ + // Remove our door blocker entity + if ( m_hDoorBlocker != NULL ) + { + UTIL_Remove( m_hDoorBlocker ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &mins1 - +// &maxs1 - +// &mins2 - +// &maxs2 - +// *destMins - +// *destMaxs - +//----------------------------------------------------------------------------- +void UTIL_ComputeAABBForBounds( const Vector &mins1, const Vector &maxs1, const Vector &mins2, const Vector &maxs2, Vector *destMins, Vector *destMaxs ) +{ + // Find the minimum extents + (*destMins)[0] = MIN( mins1[0], mins2[0] ); + (*destMins)[1] = MIN( mins1[1], mins2[1] ); + (*destMins)[2] = MIN( mins1[2], mins2[2] ); + + // Find the maximum extents + (*destMaxs)[0] = MAX( maxs1[0], maxs2[0] ); + (*destMaxs)[1] = MAX( maxs1[1], maxs2[1] ); + (*destMaxs)[2] = MAX( maxs1[2], maxs2[2] ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::Spawn() +{ + // Doors are built closed, so save the current angles as the closed angles. + m_angRotationClosed = GetLocalAngles(); + + // The axis of rotation must be along the z axis for now. + // NOTE: If you change this, be sure to change IsHingeOnLeft to account for it! + m_vecAxis = Vector(0, 0, 1); + + CalcOpenAngles(); + + // Call this last! It relies on stuff we calculated above. + BaseClass::Spawn(); + + // We have to call this after we call the base Spawn because it requires + // that the model already be set. + if ( IsHingeOnLeft() ) + { + ::V_swap( m_angRotationOpenForward, m_angRotationOpenBack ); + } + + // Figure out our volumes of movement as this door opens + CalculateDoorVolume( GetLocalAngles(), m_angRotationOpenForward, &m_vecForwardBoundsMin, &m_vecForwardBoundsMax ); + CalculateDoorVolume( GetLocalAngles(), m_angRotationOpenBack, &m_vecBackBoundsMin, &m_vecBackBoundsMax ); +} + +//----------------------------------------------------------------------------- +// Purpose: Setup the m_angRotationOpenForward and m_angRotationOpenBack variables based on +// the m_flDistance variable. Also restricts m_flDistance > 0. +//----------------------------------------------------------------------------- +void CPropDoorRotating::CalcOpenAngles() +{ + // HACK: convert the axis of rotation to dPitch dYaw dRoll + Vector vecMoveDir(m_vecAxis.y, m_vecAxis.z, m_vecAxis.x); + + if (m_flDistance == 0) + { + m_flDistance = 90; + } + m_flDistance = fabs(m_flDistance); + + // Calculate our orientation when we are fully open. + m_angRotationOpenForward.x = m_angRotationClosed.x - (vecMoveDir.x * m_flDistance); + m_angRotationOpenForward.y = m_angRotationClosed.y - (vecMoveDir.y * m_flDistance); + m_angRotationOpenForward.z = m_angRotationClosed.z - (vecMoveDir.z * m_flDistance); + + m_angRotationOpenBack.x = m_angRotationClosed.x + (vecMoveDir.x * m_flDistance); + m_angRotationOpenBack.y = m_angRotationClosed.y + (vecMoveDir.y * m_flDistance); + m_angRotationOpenBack.z = m_angRotationClosed.z + (vecMoveDir.z * m_flDistance); +} + +//----------------------------------------------------------------------------- +// Figures out whether the door's hinge is on its left or its right. +// Assumes: +// - that the door is hinged through its origin. +// - that the origin is at one edge of the door (revolving doors will give +// a random answer) +// - that the hinge axis lies along the z axis +//----------------------------------------------------------------------------- +bool CPropDoorRotating::IsHingeOnLeft() +{ + // + // Find the point farthest from the hinge in 2D. + // + Vector vecMins; + Vector vecMaxs; + CollisionProp()->WorldSpaceAABB( &vecMins, &vecMaxs ); + + vecMins -= GetAbsOrigin(); + vecMaxs -= GetAbsOrigin(); + + // Throw out z -- we only care about 2D distance. + // NOTE: if we allow for arbitrary hinge axes, this needs to change + vecMins.z = vecMaxs.z = 0; + + Vector vecPointCheck; + if ( vecMins.LengthSqr() > vecMaxs.LengthSqr() ) + { + vecPointCheck = vecMins; + } + else + { + vecPointCheck = vecMaxs; + } + + // + // See if the projection of that point lies along our right vector. + // If it does, the door is hinged on its left. + // + Vector vecRight; + GetVectors( NULL, &vecRight, NULL ); + float flDot = DotProduct( vecPointCheck, vecRight ); + + return ( flDot > 0 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +doorCheck_e CPropDoorRotating::GetOpenState( void ) +{ + return ( m_angGoal == m_angRotationOpenForward ) ? DOOR_CHECK_FORWARD : DOOR_CHECK_BACKWARD; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::OnDoorOpened( void ) +{ + if ( m_hDoorBlocker != NULL ) + { + // Allow passage through this blocker while open + m_hDoorBlocker->AddSolidFlags( FSOLID_NOT_SOLID ); + + if ( g_debug_doors.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), m_hDoorBlocker->CollisionProp()->OBBMins(), m_hDoorBlocker->CollisionProp()->OBBMaxs(), 0, 255, 0, true, 1.0f ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::OnDoorClosed( void ) +{ + if ( m_hDoorBlocker != NULL ) + { + // Destroy the blocker that was preventing NPCs from getting in our way. + UTIL_Remove( m_hDoorBlocker ); + + if ( g_debug_doors.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), m_hDoorBlocker->CollisionProp()->OBBMins(), m_hDoorBlocker->CollisionProp()->OBBMaxs(), 0, 255, 0, true, 1.0f ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns whether the way is clear for the door to close. +// Input : state - Which sides to check, forward, backward, or both. +// Output : Returns true if the door can close, false if the way is blocked. +//----------------------------------------------------------------------------- +bool CPropDoorRotating::DoorCanClose( bool bAutoClose ) +{ + if ( GetMaster() != NULL ) + return GetMaster()->DoorCanClose( bAutoClose ); + + // Check all slaves + if ( HasSlaves() ) + { + int numDoors = m_hDoorList.Count(); + + CPropDoorRotating *pLinkedDoor = NULL; + + // Check all links as well + for ( int i = 0; i < numDoors; i++ ) + { + pLinkedDoor = dynamic_cast((CBasePropDoor *)m_hDoorList[i]); + + if ( pLinkedDoor != NULL ) + { + if ( !pLinkedDoor->CheckDoorClear( bAutoClose ? DOOR_CHECK_FULL : pLinkedDoor->GetOpenState() ) ) + return false; + } + } + } + + // See if our path of movement is clear to allow us to shut + return CheckDoorClear( bAutoClose ? DOOR_CHECK_FULL : GetOpenState() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : closedAngles - +// openAngles - +// *destMins - +// *destMaxs - +//----------------------------------------------------------------------------- +void CPropDoorRotating::CalculateDoorVolume( QAngle closedAngles, QAngle openAngles, Vector *destMins, Vector *destMaxs ) +{ + // Save our current angles and move to our start angles + QAngle saveAngles = GetLocalAngles(); + SetLocalAngles( closedAngles ); + + // Find our AABB at the closed state + Vector closedMins, closedMaxs; + CollisionProp()->WorldSpaceAABB( &closedMins, &closedMaxs ); + + SetLocalAngles( openAngles ); + + // Find our AABB at the open state + Vector openMins, openMaxs; + CollisionProp()->WorldSpaceAABB( &openMins, &openMaxs ); + + // Reset our angles to our starting angles + SetLocalAngles( saveAngles ); + + // Find the minimum extents + UTIL_ComputeAABBForBounds( closedMins, closedMaxs, openMins, openMaxs, destMins, destMaxs ); + + // Move this back into local space + *destMins -= GetAbsOrigin(); + *destMaxs -= GetAbsOrigin(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::OnRestore( void ) +{ + BaseClass::OnRestore(); + + // Figure out our volumes of movement as this door opens + CalculateDoorVolume( GetLocalAngles(), m_angRotationOpenForward, &m_vecForwardBoundsMin, &m_vecForwardBoundsMax ); + CalculateDoorVolume( GetLocalAngles(), m_angRotationOpenBack, &m_vecBackBoundsMin, &m_vecBackBoundsMax ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : forward - +// mask - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropDoorRotating::CheckDoorClear( doorCheck_e state ) +{ + Vector moveMins; + Vector moveMaxs; + + switch ( state ) + { + case DOOR_CHECK_FORWARD: + moveMins = m_vecForwardBoundsMin; + moveMaxs = m_vecForwardBoundsMax; + break; + + case DOOR_CHECK_BACKWARD: + moveMins = m_vecBackBoundsMin; + moveMaxs = m_vecBackBoundsMax; + break; + + default: + case DOOR_CHECK_FULL: + UTIL_ComputeAABBForBounds( m_vecForwardBoundsMin, m_vecForwardBoundsMax, m_vecBackBoundsMin, m_vecBackBoundsMax, &moveMins, &moveMaxs ); + break; + } + + // Look for blocking entities, ignoring ourselves and the entity that opened us. + trace_t tr; + TraceHull_Door( this, GetAbsOrigin(), GetAbsOrigin(), moveMins, moveMaxs, MASK_SOLID, GetActivator(), COLLISION_GROUP_NONE, &tr ); + if ( tr.allsolid || tr.startsolid ) + { + if ( g_debug_doors.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), moveMins, moveMaxs, 255, 0, 0, true, 10.0f ); + + if ( tr.m_pEnt ) + { + NDebugOverlay::Box( tr.m_pEnt->GetAbsOrigin(), tr.m_pEnt->CollisionProp()->OBBMins(), tr.m_pEnt->CollisionProp()->OBBMaxs(), 220, 220, 0, true, 10.0f ); + } + } + + return false; + } + + if ( g_debug_doors.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), moveMins, moveMaxs, 0, 255, 0, true, 10.0f ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Puts the door in its appropriate position for spawning. +//----------------------------------------------------------------------------- +void CPropDoorRotating::DoorTeleportToSpawnPosition() +{ + QAngle angSpawn; + + // The Start Open spawnflag trumps the choices field + if ( ( HasSpawnFlags( SF_DOOR_START_OPEN_OBSOLETE ) ) || ( m_eSpawnPosition == DOOR_SPAWN_OPEN_FORWARD ) ) + { + angSpawn = m_angRotationOpenForward; + SetDoorState( DOOR_STATE_OPEN ); + } + else if ( m_eSpawnPosition == DOOR_SPAWN_OPEN_BACK ) + { + angSpawn = m_angRotationOpenBack; + SetDoorState( DOOR_STATE_OPEN ); + } + else if ( m_eSpawnPosition == DOOR_SPAWN_CLOSED ) + { + angSpawn = m_angRotationClosed; + SetDoorState( DOOR_STATE_CLOSED ); + } + else if ( m_eSpawnPosition == DOOR_SPAWN_AJAR ) + { + angSpawn = m_angRotationAjar; + SetDoorState( DOOR_STATE_AJAR ); + } + else + { + // Bogus spawn position setting! + Assert( false ); + angSpawn = m_angRotationClosed; + SetDoorState( DOOR_STATE_CLOSED ); + } + + SetLocalAngles( angSpawn ); + + // Doesn't relink; that's done in Spawn. +} + + +//----------------------------------------------------------------------------- +// Purpose: After rotating, set angle to exact final angle, call "move done" function. +//----------------------------------------------------------------------------- +void CPropDoorRotating::MoveDone() +{ + SetLocalAngles(m_angGoal); + SetLocalAngularVelocity(vec3_angle); + SetMoveDoneTime(-1); + BaseClass::MoveDone(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculate m_vecVelocity and m_flNextThink to reach vecDest from +// GetLocalOrigin() traveling at flSpeed. Just like LinearMove, but rotational. +// Input : vecDestAngle - +// flSpeed - +//----------------------------------------------------------------------------- +void CPropDoorRotating::AngularMove(const QAngle &vecDestAngle, float flSpeed) +{ + ASSERTSZ(flSpeed != 0, "AngularMove: no speed is defined!"); + + m_angGoal = vecDestAngle; + + // Already there? + if (vecDestAngle == GetLocalAngles()) + { + MoveDone(); + return; + } + + // Set destdelta to the vector needed to move. + QAngle vecDestDelta = vecDestAngle - GetLocalAngles(); + + // Divide by speed to get time to reach dest + float flTravelTime = vecDestDelta.Length() / flSpeed; + + // Call MoveDone when destination angles are reached. + SetMoveDoneTime(flTravelTime); + + // Scale the destdelta vector by the time spent traveling to get velocity. + SetLocalAngularVelocity(vecDestDelta * (1.0 / flTravelTime)); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::BeginOpening(CBaseEntity *pOpenAwayFrom) +{ + // Determine the direction to open. + QAngle angOpen = m_angRotationOpenForward; + doorCheck_e eDirCheck = DOOR_CHECK_FORWARD; + + if ( m_eOpenDirection == DOOR_ROTATING_OPEN_FORWARD ) + { + eDirCheck = DOOR_CHECK_FORWARD; + angOpen = m_angRotationOpenForward; + } + else if ( m_eOpenDirection == DOOR_ROTATING_OPEN_BACKWARD ) + { + eDirCheck = DOOR_CHECK_BACKWARD; + angOpen = m_angRotationOpenBack; + } + else // Can open either direction, test to see which is appropriate + { + if (pOpenAwayFrom != NULL) + { + Vector vecForwardDoor; + GetVectors(&vecForwardDoor, NULL, NULL); + + if (vecForwardDoor.Dot(pOpenAwayFrom->GetAbsOrigin()) > vecForwardDoor.Dot(GetAbsOrigin())) + { + angOpen = m_angRotationOpenBack; + eDirCheck = DOOR_CHECK_BACKWARD; + } + } + + // If player is opening us and we're opening away from them, and we'll be + // blocked if we open away from them, open toward them. + if (IsPlayerOpening() && (pOpenAwayFrom && pOpenAwayFrom->IsPlayer()) && !CheckDoorClear(eDirCheck)) + { + if (eDirCheck == DOOR_CHECK_FORWARD) + { + angOpen = m_angRotationOpenBack; + eDirCheck = DOOR_CHECK_BACKWARD; + } + else + { + angOpen = m_angRotationOpenForward; + eDirCheck = DOOR_CHECK_FORWARD; + } + } + } + + // Create the door blocker + Vector mins, maxs; + if ( eDirCheck == DOOR_CHECK_FORWARD ) + { + mins = m_vecForwardBoundsMin; + maxs = m_vecForwardBoundsMax; + } + else + { + mins = m_vecBackBoundsMin; + maxs = m_vecBackBoundsMax; + } + + if ( m_hDoorBlocker != NULL ) + { + UTIL_Remove( m_hDoorBlocker ); + } + + // Create a blocking entity to keep random entities out of our movement path + m_hDoorBlocker = CEntityBlocker::Create( GetAbsOrigin(), mins, maxs, pOpenAwayFrom, false ); + + Vector volumeCenter = ((mins+maxs) * 0.5f) + GetAbsOrigin(); + + // Ignoring the Z + float volumeRadius = MAX( fabs(mins.x), maxs.x ); + volumeRadius = MAX( volumeRadius, MAX( fabs(mins.y), maxs.y ) ); + + // Debug + if ( g_debug_doors.GetBool() ) + { + NDebugOverlay::Cross3D( volumeCenter, -Vector(volumeRadius,volumeRadius,volumeRadius), Vector(volumeRadius,volumeRadius,volumeRadius), 255, 0, 0, true, 1.0f ); + } + + // Make respectful entities move away from our path + if( !HasSpawnFlags(SF_DOOR_SILENT_TO_NPCS) ) + { + CSoundEnt::InsertSound( SOUND_MOVE_AWAY, volumeCenter, volumeRadius, 0.5f, pOpenAwayFrom ); + } + + // Do final setup + if ( m_hDoorBlocker != NULL ) + { + // Only block NPCs + m_hDoorBlocker->SetCollisionGroup( COLLISION_GROUP_DOOR_BLOCKER ); + + // If we hit something while opening, just stay unsolid until we try again + if ( CheckDoorClear( eDirCheck ) == false ) + { + m_hDoorBlocker->AddSolidFlags( FSOLID_NOT_SOLID ); + } + + if ( g_debug_doors.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), m_hDoorBlocker->CollisionProp()->OBBMins(), m_hDoorBlocker->CollisionProp()->OBBMaxs(), 255, 0, 0, true, 1.0f ); + } + } + + AngularMove(angOpen, m_flSpeed); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::BeginClosing( void ) +{ + if ( m_hDoorBlocker != NULL ) + { + // Become solid again unless we're already being blocked + if ( CheckDoorClear( GetOpenState() ) ) + { + m_hDoorBlocker->RemoveSolidFlags( FSOLID_NOT_SOLID ); + } + + if ( g_debug_doors.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), m_hDoorBlocker->CollisionProp()->OBBMins(), m_hDoorBlocker->CollisionProp()->OBBMaxs(), 255, 0, 0, true, 1.0f ); + } + } + + AngularMove(m_angRotationClosed, m_flSpeed); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::DoorStop( void ) +{ + SetLocalAngularVelocity( vec3_angle ); + SetMoveDoneTime( -1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Restart a door moving that was temporarily paused +//----------------------------------------------------------------------------- +void CPropDoorRotating::DoorResume( void ) +{ + // Restart our angular movement + AngularMove( m_angGoal, m_flSpeed ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : vecMoveDir - +// opendata - +//----------------------------------------------------------------------------- +void CPropDoorRotating::GetNPCOpenData(CAI_BaseNPC *pNPC, opendata_t &opendata) +{ + // dvs: TODO: finalize open position, direction, activity + Vector vecForward; + Vector vecRight; + AngleVectors(GetAbsAngles(), &vecForward, &vecRight, NULL); + + // + // Figure out where the NPC should stand to open this door, + // and what direction they should face. + // + opendata.vecStandPos = GetAbsOrigin() - (vecRight * 24); + opendata.vecStandPos.z -= 54; + + Vector vecNPCOrigin = pNPC->GetAbsOrigin(); + + if (pNPC->GetAbsOrigin().Dot(vecForward) > GetAbsOrigin().Dot(vecForward)) + { + // In front of the door relative to the door's forward vector. + opendata.vecStandPos += vecForward * 64; + opendata.vecFaceDir = -vecForward; + } + else + { + // Behind the door relative to the door's forward vector. + opendata.vecStandPos -= vecForward * 64; + opendata.vecFaceDir = vecForward; + } + + opendata.eActivity = ACT_OPEN_DOOR; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns how long it will take this door to open. +//----------------------------------------------------------------------------- +float CPropDoorRotating::GetOpenInterval() +{ + // set destdelta to the vector needed to move + QAngle vecDestDelta = m_angRotationOpenForward - GetLocalAngles(); + + // divide by speed to get time to reach dest + return vecDestDelta.Length() / m_flSpeed; +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CPropDoorRotating::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr),"Avelocity: %.2f %.2f %.2f", GetLocalAngularVelocity().x, GetLocalAngularVelocity().y, GetLocalAngularVelocity().z); + EntityText( text_offset, tempstr, 0); + text_offset++; + + if ( IsDoorOpen() ) + { + Q_strncpy(tempstr, "DOOR STATE: OPEN", sizeof(tempstr)); + } + else if ( IsDoorClosed() ) + { + Q_strncpy(tempstr, "DOOR STATE: CLOSED", sizeof(tempstr)); + } + else if ( IsDoorOpening() ) + { + Q_strncpy(tempstr, "DOOR STATE: OPENING", sizeof(tempstr)); + } + else if ( IsDoorClosing() ) + { + Q_strncpy(tempstr, "DOOR STATE: CLOSING", sizeof(tempstr)); + } + else if ( IsDoorAjar() ) + { + Q_strncpy(tempstr, "DOOR STATE: AJAR", sizeof(tempstr)); + } + EntityText( text_offset, tempstr, 0); + text_offset++; + } + + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: Change this door's distance (in degrees) between open and closed +//----------------------------------------------------------------------------- +void CPropDoorRotating::InputSetRotationDistance( inputdata_t &inputdata ) +{ + m_flDistance = inputdata.value.Float(); + + // Recalculate our open volume + CalcOpenAngles(); + CalculateDoorVolume( GetLocalAngles(), m_angRotationOpenForward, &m_vecForwardBoundsMin, &m_vecForwardBoundsMax ); + CalculateDoorVolume( GetLocalAngles(), m_angRotationOpenBack, &m_vecBackBoundsMin, &m_vecBackBoundsMax ); +} + +#ifdef DOOR_BREAKING_STUFF +//extern bool TransferPhysicsObject( CBaseEntity *pFrom, CBaseEntity *pTo, bool wakeUp ); +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropDoorRotating::Break( CBaseEntity *pBreaker, const CTakeDamageInfo &info ) +{ + if (m_eBreakType == DOOR_ROTATING_BREAK_NORMAL) + return BaseClass::Break( pBreaker, info ); + + if (m_eBreakType == DOOR_ROTATING_BREAK_PHYS || m_eBreakType == DOOR_ROTATING_BREAK_PHYS_HINGE) + { + DevMsg("Should break into physics\n"); + UnlinkFromParent( this ); + + CBaseEntity *pPhys = CreateNoSpawn( "prop_physics", GetLocalOrigin(), GetLocalAngles() ); + if ( pPhys ) + { + pPhys->SetModelName( GetModelName() ); + + pPhys->m_nRenderMode = m_nRenderMode; + pPhys->m_nRenderFX = m_nRenderFX; + const color32 rclr = GetRenderColor(); + pPhys->SetRenderColor(rclr.r, rclr.g, rclr.b, rclr.a); + + CBaseAnimating *pPhysAnimating = pPhys->GetBaseAnimating(); + + pPhysAnimating->m_nSkin = m_nSkin; + pPhysAnimating->m_nBody = m_nBody; + pPhysAnimating->SetModelScale(GetModelScale()); + + pPhys->SetName( GetEntityName() ); + + UTIL_TransferPoseParameters( this, pPhys ); + TransferChildren( this, pPhys ); + + AddSolidFlags( FSOLID_NOT_SOLID ); + AddEffects( EF_NODRAW ); + + + PhysBreakSound( this, VPhysicsGetObject(), WorldSpaceCenter() ); + + DispatchSpawn(pPhys); + + // Transferring the physics object in this case has proven to be buggy. + if (pPhys->VPhysicsGetObject()) //if ( !TransferPhysicsObject( this, pPhys, true ) ) + { + //pPhys->VPhysicsInitNormal( SOLID_VPHYSICS, 0, false ); + + pPhys->VPhysicsGetObject()->SetMaterialIndex( VPhysicsGetObject()->GetMaterialIndex() ); + } + + if (m_eBreakType == DOOR_ROTATING_BREAK_PHYS_HINGE) + { + DevMsg("Should break with hinge\n"); + // This is the point where names get a little weird. + if (GetEntityName() != NULL_STRING) + SetName(NULL_STRING); + else + { + // Since we don't have a name, but the designer wants a hinge, give the new prop a name for the hinge to target. + pPhys->SetName(AllocPooledString(UTIL_VarArgs("_physdoor%i", entindex()))); + } + + CBaseEntity *pHinge = CreateNoSpawn("phys_hinge", GetLocalOrigin(), GetLocalAngles()); + pHinge->SetName(AllocPooledString(UTIL_VarArgs("%s_createdhinge", STRING(pPhys->GetEntityName())))); + pHinge->KeyValue("attach1", STRING(pPhys->GetEntityName())); + pHinge->KeyValue("hingeaxis", m_vecAxis); + pHinge->KeyValue("breaksound", "Metal_Box.BulletImpact"); + + DispatchSpawn(pHinge); + } + + BaseClass::Break( pBreaker, info ); + + //UTIL_Remove( this ); + + pPhys->VPhysicsTakeDamage(info); + + //if (pPhys->VPhysicsGetObject()) + // pPhys->VPhysicsGetObject()->ApplyForceOffset(info.GetDamageForce(), info.GetDamagePosition()); + } + else + { + BaseClass::Break( pBreaker, info ); + } + } +} +#endif + +#ifdef MAPBASE +void CPropDoorRotating::InputSetSpeed(inputdata_t &inputdata) +{ + AssertMsg1(inputdata.value.Float() > 0.0f, "InputSetSpeed on %s called with negative parameter!", GetDebugName() ); + m_flSpeed = inputdata.value.Float(); + DoorResume(); +} +#endif + +// Debug sphere +class CPhysSphere : public CPhysicsProp +{ + DECLARE_CLASS( CPhysSphere, CPhysicsProp ); +#ifdef MAPBASE + DECLARE_DATADESC(); +#endif +public: +#ifdef MAPBASE + float m_fRadius; +#else + virtual bool OverridePropdata() { return true; } +#endif + bool CreateVPhysics() + { + SetSolid( SOLID_BBOX ); +#ifdef MAPBASE + SetCollisionBounds( -Vector(m_fRadius), Vector(m_fRadius) ); +#else + SetCollisionBounds( -Vector(12,12,12), Vector(12,12,12) ); +#endif + objectparams_t params = g_PhysDefaultObjectParams; + params.pGameData = static_cast(this); +#ifdef MAPBASE + IPhysicsObject *pPhysicsObject = physenv->CreateSphereObject( m_fRadius, GetModelPtr()->GetRenderHdr()->textureindex, GetAbsOrigin(), GetAbsAngles(), ¶ms, false ); +#else + IPhysicsObject *pPhysicsObject = physenv->CreateSphereObject( 12, 0, GetAbsOrigin(), GetAbsAngles(), ¶ms, false ); +#endif + if ( pPhysicsObject ) + { + VPhysicsSetObject( pPhysicsObject ); + SetMoveType( MOVETYPE_VPHYSICS ); + pPhysicsObject->Wake(); + } + + return true; + } +}; + +#ifdef MAPBASE +BEGIN_DATADESC( CPhysSphere ) + DEFINE_KEYFIELD( m_fRadius, FIELD_FLOAT, "radius"), +END_DATADESC() +#endif + +#ifndef MAPBASE // Yes, all I'm doing is moving this up a few lines and I'm still using the preprocessor. +void CPropDoorRotating::InputSetSpeed(inputdata_t &inputdata) +{ + AssertMsg1(inputdata.value.Float() > 0.0f, "InputSetSpeed on %s called with negative parameter!", GetDebugName() ); + m_flSpeed = inputdata.value.Float(); + DoorResume(); +} +#endif + +LINK_ENTITY_TO_CLASS( prop_sphere, CPhysSphere ); + + +#if defined(MAPBASE) && defined(HL2_EPISODIC) +// ------------------------------------------------------------------------------------------ // +// Flare class for higher interaction possibilities, inspired by Black Mesa +// ------------------------------------------------------------------------------------------ // +class CPropFlare : public CPhysicsProp +{ + DECLARE_CLASS( CPropFlare, CPhysicsProp ); + DECLARE_DATADESC(); +public: + + void Precache() + { + BaseClass::Precache(); + + if (GetModelName() != NULL_STRING) + { + PrecacheModel(STRING(GetModelName())); + } + else + { + PrecacheModel("models/props_junk/flare.mdl"); + } + } + + void Spawn() + { + if (GetModelName() == NULL_STRING) + { + // Must've been spawned with ent_create or something + SetModelName(AllocPooledString("models/props_junk/flare.mdl")); + //SetModel("models/props_junk/flare.mdl"); + } + + if (!HasInteraction(PROPINTER_PHYSGUN_CREATE_FLARE)) + { + SetInteraction(PROPINTER_PHYSGUN_CREATE_FLARE); + } + + SetClassname( "prop_physics" ); + + return BaseClass::Spawn(); + } + + bool OverridePropdata( void ) { return true; } + + virtual float GetFlareLifetime() { return m_flFlareLifetime; } + + void InputStartFlare( inputdata_t &inputdata ) + { + CreateFlare( PROP_FLARE_LIFETIME ); + } + void InputStopFlare( inputdata_t &inputdata ) + { + KillFlare( this, m_hFlareEnt, PROP_FLARE_IGNITE_SUBSTRACT ); + } + + void InputAddFlareLifetime( inputdata_t &inputdata ) + { + if (m_hFlareEnt) + { + KillFlare( this, m_hFlareEnt, (inputdata.value.Float() * -1) ); + } + else + { + CreateFlare( inputdata.value.Float() ); + } + } + + void InputRemoveFlare( inputdata_t &inputdata ) + { + UTIL_Remove(m_hFlareEnt); + m_nSkin = 1; + } + + void InputRestoreFlare( inputdata_t &inputdata ) + { + if (!HasInteraction(PROPINTER_PHYSGUN_CREATE_FLARE) && !m_hFlareEnt) + { + SetInteraction(PROPINTER_PHYSGUN_CREATE_FLARE); + m_OnRestored.FireOutput(inputdata.pActivator, inputdata.pCaller); + m_nSkin = 0; + } + } + + int DrawDebugTextOverlays(void) + { + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr), "Flare Duration: %f", GetEnvFlareLifetime(m_hFlareEnt)); + EntityText(text_offset, tempstr, 0); + text_offset++; + } + + return text_offset; + } + + COutputEvent m_OnRestored; + float m_flFlareLifetime = 30.0f; +}; + +LINK_ENTITY_TO_CLASS( prop_flare, CPropFlare ); + +BEGIN_DATADESC( CPropFlare ) + + DEFINE_KEYFIELD( m_flFlareLifetime, FIELD_FLOAT, "FlareLifetime" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartFlare", InputStartFlare ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopFlare", InputStopFlare ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "AddFlareLifetime", InputAddFlareLifetime ), + DEFINE_INPUTFUNC( FIELD_VOID, "RemoveFlare", InputRemoveFlare ), + DEFINE_INPUTFUNC( FIELD_VOID, "RestoreFlare", InputRestoreFlare ), + DEFINE_OUTPUT( m_OnRestored, "OnRestored" ), + +END_DATADESC() +#endif + + +// ------------------------------------------------------------------------------------------ // +// Special version of func_physbox. +// ------------------------------------------------------------------------------------------ // +class CPhysBoxMultiplayer : public CPhysBox, public IMultiplayerPhysics +{ +public: + DECLARE_CLASS( CPhysBoxMultiplayer, CPhysBox ); + + virtual int GetMultiplayerPhysicsMode() + { + return m_iPhysicsMode; + } + + virtual float GetMass() + { + return m_fMass; + } + + virtual bool IsAsleep() + { + return VPhysicsGetObject()->IsAsleep(); + } + + CNetworkVar( int, m_iPhysicsMode ); // One of the PHYSICS_MULTIPLAYER_ defines. + CNetworkVar( float, m_fMass ); + + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + virtual void Activate() + { + BaseClass::Activate(); + SetCollisionGroup( COLLISION_GROUP_PUSHAWAY ); + m_fMass = VPhysicsGetObject()->GetMass(); + } +}; + +LINK_ENTITY_TO_CLASS( func_physbox_multiplayer, CPhysBoxMultiplayer ); + +BEGIN_DATADESC( CPhysBoxMultiplayer ) +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPhysBoxMultiplayer, DT_PhysBoxMultiplayer ) + SendPropInt( SENDINFO( m_iPhysicsMode ), 1, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO( m_fMass ), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + + + +class CPhysicsPropMultiplayer : public CPhysicsProp, public IMultiplayerPhysics +{ + DECLARE_CLASS( CPhysicsPropMultiplayer, CPhysicsProp ); + + CNetworkVar( int, m_iPhysicsMode ); // One of the PHYSICS_MULTIPLAYER_ defines. + CNetworkVar( float, m_fMass ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CPhysicsPropMultiplayer() + { + m_iPhysicsMode = PHYSICS_MULTIPLAYER_AUTODETECT; + m_usingCustomCollisionBounds = false; + } + +// IBreakableWithPropData: + void SetPhysicsMode(int iMode) + { + m_iPhysicsMode = iMode; + } + + int GetPhysicsMode() { return m_iPhysicsMode; } + +// IMultiplayerPhysics: + int GetMultiplayerPhysicsMode() { return m_iPhysicsMode; } + float GetMass() { return m_fMass; } + bool IsAsleep() { return !m_bAwake; } + + bool IsDebris( void ) { return ( ( m_spawnflags & SF_PHYSPROP_DEBRIS ) != 0 ); } + + virtual void VPhysicsUpdate( IPhysicsObject *pPhysics ) + { + BaseClass::VPhysicsUpdate( pPhysics ); + + if ( sv_turbophysics.GetBool() ) + { + // If the object is set to debris, don't let turbo physics change it. + if ( IsDebris() ) + return; + + if ( m_bAwake ) + { + SetCollisionGroup( COLLISION_GROUP_PUSHAWAY ); + } + else if ( m_iPhysicsMode == PHYSICS_MULTIPLAYER_NON_SOLID ) + { + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } + else + { + SetCollisionGroup( COLLISION_GROUP_NONE ); + } + } + } + + virtual void Spawn( void ) + { + BaseClass::Spawn(); + + // if no physicsmode was defined by .QC or propdata.txt, + // use auto detect based on size & mass + if ( m_iPhysicsMode == PHYSICS_MULTIPLAYER_AUTODETECT ) + { + if ( VPhysicsGetObject() ) + { + m_iPhysicsMode = GetAutoMultiplayerPhysicsMode( + CollisionProp()->OBBSize(), VPhysicsGetObject()->GetMass() ); + } + else + { + UTIL_Remove( this ); + return; + } + } + + // check if map maker overrides physics mode to force a server-side entity + if ( GetSpawnFlags() & SF_PHYSPROP_FORCE_SERVER_SIDE ) + { + SetPhysicsMode( PHYSICS_MULTIPLAYER_NON_SOLID ); + } + + if ( m_iPhysicsMode == PHYSICS_MULTIPLAYER_CLIENTSIDE ) + { + if ( engine->IsInEditMode() ) + { + // in map edit mode always spawn as server phys prop + SetPhysicsMode( PHYSICS_MULTIPLAYER_NON_SOLID ); + } + else + { + // don't spawn clientside props on server + UTIL_Remove( this ); + return; + } + + } + + if ( GetCollisionGroup() == COLLISION_GROUP_NONE ) + SetCollisionGroup( COLLISION_GROUP_PUSHAWAY ); + + // Items marked as debris should be set as such. + if ( IsDebris() ) + { + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + } + + m_fMass = VPhysicsGetObject()->GetMass(); + + // VPhysicsGetObject() is NULL on the client, which prevents the client from finding a decent + // AABB surrounding the collision bounds. If we've got a VPhysicsGetObject()->GetCollide(), we'll + // grab it's unrotated bounds and use it to calculate our collision surrounding bounds. This + // can end up larger than the CollisionProp() would have calculated on its own, but it'll be + // identical on the client and the server. + m_usingCustomCollisionBounds = false; + if ( ( GetSolid() == SOLID_VPHYSICS ) && ( GetMoveType() == MOVETYPE_VPHYSICS ) ) + { + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if ( pPhysics && pPhysics->GetCollide() ) + { + physcollision->CollideGetAABB( &m_collisionMins.GetForModify(), &m_collisionMaxs.GetForModify(), pPhysics->GetCollide(), vec3_origin, vec3_angle ); + CollisionProp()->SetSurroundingBoundsType( USE_GAME_CODE ); + m_usingCustomCollisionBounds = true; + } + } + } + + virtual void ComputeWorldSpaceSurroundingBox( Vector *mins, Vector *maxs ) + { + Assert( m_usingCustomCollisionBounds ); + Assert( mins != NULL && maxs != NULL ); + if ( !mins || !maxs ) + return; + + // Take our saved collision bounds, and transform into world space + TransformAABB( EntityToWorldTransform(), m_collisionMins, m_collisionMaxs, *mins, *maxs ); + } + +private: + bool m_usingCustomCollisionBounds; + CNetworkVector( m_collisionMins ); + CNetworkVector( m_collisionMaxs ); +}; + +LINK_ENTITY_TO_CLASS( prop_physics_multiplayer, CPhysicsPropMultiplayer ); + +BEGIN_DATADESC( CPhysicsPropMultiplayer ) + DEFINE_KEYFIELD( m_iPhysicsMode, FIELD_INTEGER, "physicsmode" ), + DEFINE_FIELD( m_fMass, FIELD_FLOAT ), + DEFINE_FIELD( m_usingCustomCollisionBounds, FIELD_BOOLEAN ), + DEFINE_FIELD( m_collisionMins, FIELD_VECTOR ), + DEFINE_FIELD( m_collisionMaxs, FIELD_VECTOR ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPhysicsPropMultiplayer, DT_PhysicsPropMultiplayer ) + SendPropInt( SENDINFO( m_iPhysicsMode ), 2, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO( m_fMass ), 0, SPROP_NOSCALE ), + SendPropVector( SENDINFO( m_collisionMins ), 0, SPROP_NOSCALE ), + SendPropVector( SENDINFO( m_collisionMaxs ), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + +#define RESPAWNABLE_PROP_DEFAULT_TIME 60.0f + +class CPhysicsPropRespawnable : public CPhysicsProp +{ + DECLARE_CLASS( CPhysicsPropRespawnable, CPhysicsProp ); + DECLARE_DATADESC(); + +public: + + CPhysicsPropRespawnable(); + + virtual void Spawn( void ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + + void Materialize( void ); + +private: + + Vector m_vOriginalSpawnOrigin; + QAngle m_vOriginalSpawnAngles; + + Vector m_vOriginalMins; + Vector m_vOriginalMaxs; + + float m_flRespawnTime; +}; + +LINK_ENTITY_TO_CLASS( prop_physics_respawnable, CPhysicsPropRespawnable ); + +BEGIN_DATADESC( CPhysicsPropRespawnable ) + DEFINE_THINKFUNC( Materialize ), + DEFINE_KEYFIELD( m_flRespawnTime, FIELD_FLOAT, "RespawnTime" ), + DEFINE_FIELD( m_vOriginalSpawnOrigin, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vOriginalSpawnAngles, FIELD_VECTOR ), + DEFINE_FIELD( m_vOriginalMins, FIELD_VECTOR ), + DEFINE_FIELD( m_vOriginalMaxs, FIELD_VECTOR ), +END_DATADESC() + +CPhysicsPropRespawnable::CPhysicsPropRespawnable( void ) +{ + m_flRespawnTime = 0.0f; +} + +void CPhysicsPropRespawnable::Spawn( void ) +{ + BaseClass::Spawn(); + + m_vOriginalSpawnOrigin = GetAbsOrigin(); + m_vOriginalSpawnAngles = GetAbsAngles(); + + m_vOriginalMins = CollisionProp()->OBBMins(); + m_vOriginalMaxs = CollisionProp()->OBBMaxs(); + + if ( m_flRespawnTime == 0.0f ) + { + m_flRespawnTime = RESPAWNABLE_PROP_DEFAULT_TIME; + } + + SetOwnerEntity( NULL ); +} + +void CPhysicsPropRespawnable::Event_Killed( const CTakeDamageInfo &info ) +{ + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if ( pPhysics && !pPhysics->IsMoveable() ) + { + pPhysics->EnableMotion( true ); + VPhysicsTakeDamage( info ); + } + + Break( info.GetInflictor(), info ); + + PhysCleanupFrictionSounds( this ); + + VPhysicsDestroyObject(); + + CBaseEntity::PhysicsRemoveTouchedList( this ); + CBaseEntity::PhysicsRemoveGroundList( this ); + DestroyAllDataObjects(); + + AddEffects( EF_NODRAW ); + + if ( IsOnFire() || IsDissolving() ) + { + UTIL_Remove( GetEffectEntity() ); + } + + Teleport( &m_vOriginalSpawnOrigin, &m_vOriginalSpawnAngles, NULL ); + + SetContextThink( NULL, 0, "PROP_CLEARFLAGS" ); + + SetThink( &CPhysicsPropRespawnable::Materialize ); + SetNextThink( gpGlobals->curtime + m_flRespawnTime ); +} + +void CPhysicsPropRespawnable::Materialize( void ) +{ + trace_t tr; + UTIL_TraceHull( m_vOriginalSpawnOrigin, m_vOriginalSpawnOrigin, m_vOriginalMins, m_vOriginalMaxs, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.startsolid || tr.allsolid ) + { + //Try again in a second. + SetNextThink( gpGlobals->curtime + 1.0f ); + return; + } + + RemoveEffects( EF_NODRAW ); + Spawn(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Create a prop of the given type +//------------------------------------------------------------------------------ +void CC_Prop_Dynamic_Create( const CCommand &args ) +{ + if ( args.ArgC() != 2 ) + return; + + // Figure out where to place it + CBasePlayer* pPlayer = UTIL_GetCommandClient(); + Vector forward; + pPlayer->EyeVectors( &forward ); + + trace_t tr; + UTIL_TraceLine( pPlayer->EyePosition(), + pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH, MASK_NPCSOLID, + pPlayer, COLLISION_GROUP_NONE, &tr ); + + // No hit? We're done. + if ( tr.fraction == 1.0 ) + return; + + MDLCACHE_CRITICAL_SECTION(); + + char pModelName[512]; + Q_snprintf( pModelName, sizeof(pModelName), "models/%s", args[1] ); + Q_DefaultExtension( pModelName, ".mdl", sizeof(pModelName) ); + MDLHandle_t h = mdlcache->FindMDL( pModelName ); + if ( h == MDLHANDLE_INVALID ) + return; + + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + vcollide_t *pVCollide = mdlcache->GetVCollide( h ); + + Vector xaxis( 1.0f, 0.0f, 0.0f ); + Vector yaxis; + CrossProduct( tr.plane.normal, xaxis, yaxis ); + if ( VectorNormalize( yaxis ) < 1e-3 ) + { + xaxis.Init( 0.0f, 0.0f, 1.0f ); + CrossProduct( tr.plane.normal, xaxis, yaxis ); + VectorNormalize( yaxis ); + } + CrossProduct( yaxis, tr.plane.normal, xaxis ); + VectorNormalize( xaxis ); + + VMatrix entToWorld; + entToWorld.SetBasisVectors( xaxis, yaxis, tr.plane.normal ); + + QAngle angles; + MatrixToAngles( entToWorld, angles ); + + // Try to create entity + CDynamicProp *pProp = dynamic_cast< CDynamicProp * >( CreateEntityByName( "dynamic_prop" ) ); + if ( pProp ) + { + char buf[512]; + // Pass in standard key values + Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", tr.endpos.x, tr.endpos.y, tr.endpos.z ); + pProp->KeyValue( "origin", buf ); + Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", angles.x, angles.y, angles.z ); + pProp->KeyValue( "angles", buf ); + pProp->KeyValue( "model", pModelName ); + pProp->KeyValue( "solid", pVCollide ? "6" : "2" ); + pProp->KeyValue( "fademindist", "-1" ); + pProp->KeyValue( "fademaxdist", "0" ); + pProp->KeyValue( "fadescale", "1" ); + pProp->KeyValue( "MinAnimTime", "5" ); + pProp->KeyValue( "MaxAnimTime", "10" ); + pProp->Precache(); + DispatchSpawn( pProp ); + pProp->Activate(); + } + CBaseEntity::SetAllowPrecache( bAllowPrecache ); +} + +static ConCommand prop_dynamic_create("prop_dynamic_create", CC_Prop_Dynamic_Create, "Creates a dynamic prop with a specific .mdl aimed away from where the player is looking.\n\tArguments: {.mdl name}", FCVAR_CHEAT); + + + +//------------------------------------------------------------------------------ +// Purpose: Create a prop of the given type +//------------------------------------------------------------------------------ +void CC_Prop_Physics_Create( const CCommand &args ) +{ + if ( args.ArgC() != 2 ) + return; + + char pModelName[512]; + Q_snprintf( pModelName, sizeof(pModelName), "models/%s", args[1] ); + Q_DefaultExtension( pModelName, ".mdl", sizeof(pModelName) ); + + // Figure out where to place it + CBasePlayer* pPlayer = UTIL_GetCommandClient(); + Vector forward; + pPlayer->EyeVectors( &forward ); + + CreatePhysicsProp( pModelName, pPlayer->EyePosition(), pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH, pPlayer, true ); +} + +static ConCommand prop_physics_create("prop_physics_create", CC_Prop_Physics_Create, "Creates a physics prop with a specific .mdl aimed away from where the player is looking.\n\tArguments: {.mdl name}", FCVAR_CHEAT); + + +CPhysicsProp* CreatePhysicsProp( const char *pModelName, const Vector &vTraceStart, const Vector &vTraceEnd, const IHandleEntity *pTraceIgnore, bool bRequireVCollide, const char *pClassName ) +{ + MDLCACHE_CRITICAL_SECTION(); + + MDLHandle_t h = mdlcache->FindMDL( pModelName ); + if ( h == MDLHANDLE_INVALID ) + return NULL; + + // Must have vphysics to place as a physics prop + studiohdr_t *pStudioHdr = mdlcache->GetStudioHdr( h ); + if ( !pStudioHdr ) + return NULL; + + // Must have vphysics to place as a physics prop + if ( bRequireVCollide && !mdlcache->GetVCollide( h ) ) + return NULL; + + QAngle angles( 0.0f, 0.0f, 0.0f ); + Vector vecSweepMins = pStudioHdr->hull_min; + Vector vecSweepMaxs = pStudioHdr->hull_max; + + trace_t tr; + UTIL_TraceHull( vTraceStart, vTraceEnd, + vecSweepMins, vecSweepMaxs, MASK_NPCSOLID, pTraceIgnore, COLLISION_GROUP_NONE, &tr ); + + // No hit? We're done. + if ( (tr.fraction == 1.0 && (vTraceEnd-vTraceStart).Length() > 0.01) || tr.allsolid ) + return NULL; + + VectorMA( tr.endpos, 1.0f, tr.plane.normal, tr.endpos ); + + bool bAllowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + // Try to create entity + CPhysicsProp *pProp = dynamic_cast< CPhysicsProp * >( CreateEntityByName( pClassName ) ); + if ( pProp ) + { + char buf[512]; + // Pass in standard key values + Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", tr.endpos.x, tr.endpos.y, tr.endpos.z ); + pProp->KeyValue( "origin", buf ); + Q_snprintf( buf, sizeof(buf), "%.10f %.10f %.10f", angles.x, angles.y, angles.z ); + pProp->KeyValue( "angles", buf ); + pProp->KeyValue( "model", pModelName ); + pProp->KeyValue( "fademindist", "-1" ); + pProp->KeyValue( "fademaxdist", "0" ); + pProp->KeyValue( "fadescale", "1" ); + pProp->KeyValue( "inertiaScale", "1.0" ); + pProp->KeyValue( "physdamagescale", "0.1" ); + pProp->Precache(); + DispatchSpawn( pProp ); + pProp->Activate(); + } + CBaseEntity::SetAllowPrecache( bAllowPrecache ); + + return pProp; +} + +//----------------------------------------------------------------------------- +// Purpose: Scale the object to a new size, taking its render verts and physical verts into account +//----------------------------------------------------------------------------- +bool UTIL_CreateScaledPhysObject( CBaseAnimating *pInstance, float flScale ) +{ + // Don't scale NPCs + if ( pInstance->MyCombatCharacterPointer() ) + return false; + + // FIXME: This needs to work for ragdolls! + + // Get our object + IPhysicsObject *pObject = pInstance->VPhysicsGetObject(); + if ( pObject == NULL ) + { + AssertMsg( 0, "UTIL_CreateScaledPhysObject: Failed to scale physics for object-- It has no physics." ); + return false; + } + + // See if our current physics object is motion disabled + bool bWasMotionDisabled = ( pObject->IsMotionEnabled() == false ); + bool bWasStatic = ( pObject->IsStatic() ); + + vcollide_t *pCollide = modelinfo->GetVCollide( pInstance->GetModelIndex() ); + if ( pCollide == NULL || pCollide->solidCount == 0 ) + return NULL; + + CPhysCollide *pNewCollide = pCollide->solids[0]; // FIXME: Needs to iterate over the solids + + if ( flScale != 1.0f ) + { + // Create a query to get more information from the collision object + ICollisionQuery *pQuery = physcollision->CreateQueryModel( pCollide->solids[0] ); // FIXME: This should iterate over all solids! + if ( pQuery == NULL ) + return false; + + // Create a container to hold all the convexes we'll create + const int nNumConvex = pQuery->ConvexCount(); + CPhysConvex **pConvexes = (CPhysConvex **) stackalloc( sizeof(CPhysConvex *) * nNumConvex ); + + // For each convex, collect the verts and create a convex from it we'll retain for later + for ( int i = 0; i < nNumConvex; i++ ) + { + int nNumTris = pQuery->TriangleCount( i ); + int nNumVerts = nNumTris * 3; + // FIXME: Really? stackalloc? + Vector *pVerts = (Vector *) stackalloc( sizeof(Vector) * nNumVerts ); + Vector **ppVerts = (Vector **) stackalloc( sizeof(Vector *) * nNumVerts ); + for ( int j = 0; j < nNumTris; j++ ) + { + // Get all the verts for this triangle and scale them up + pQuery->GetTriangleVerts( i, j, pVerts+(j*3) ); + *(pVerts+(j*3)) *= flScale; + *(pVerts+(j*3)+1) *= flScale; + *(pVerts+(j*3)+2) *= flScale; + + // Setup our pointers (blech!) + *(ppVerts+(j*3)) = pVerts+(j*3); + *(ppVerts+(j*3)+1) = pVerts+(j*3)+1; + *(ppVerts+(j*3)+2) = pVerts+(j*3)+2; + } + + // Convert it back to a convex + pConvexes[i] = physcollision->ConvexFromVerts( ppVerts, nNumVerts ); + Assert( pConvexes[i] != NULL ); + if ( pConvexes[i] == NULL ) + return false; + } + + // Clean up + physcollision->DestroyQueryModel( pQuery ); + + // Create a collision model from all the convexes + pNewCollide = physcollision->ConvertConvexToCollide( pConvexes, nNumConvex ); + if ( pNewCollide == NULL ) + return false; + } + + // Get our solid info + solid_t tmpSolid; + if ( !PhysModelParseSolidByIndex( tmpSolid, pInstance, pInstance->GetModelIndex(), -1 ) ) + return false; + + // Physprops get keyvalues that effect the mass, this block is to respect those fields when we scale + CPhysicsProp *pPhysInstance = dynamic_cast( pInstance ); + if ( pPhysInstance ) + { + if ( pPhysInstance->GetMassScale() > 0 ) + { + tmpSolid.params.mass *= pPhysInstance->GetMassScale(); + } + + PhysSolidOverride( tmpSolid, pPhysInstance->GetPhysOverrideScript() ); + } + + // Scale our mass up as well + tmpSolid.params.mass *= flScale; + tmpSolid.params.volume = physcollision->CollideVolume( pNewCollide ); + + // Get our surface prop info + int surfaceProp = -1; + if ( tmpSolid.surfaceprop[0] ) + { + surfaceProp = physprops->GetSurfaceIndex( tmpSolid.surfaceprop ); + } + + // Now put it all back (phew!) + IPhysicsObject *pNewObject = NULL; + if ( bWasStatic ) + { + pNewObject = physenv->CreatePolyObjectStatic( pNewCollide, surfaceProp, pInstance->GetAbsOrigin(), pInstance->GetAbsAngles(), &tmpSolid.params ); + } + else + { + pNewObject = physenv->CreatePolyObject( pNewCollide, surfaceProp, pInstance->GetAbsOrigin(), pInstance->GetAbsAngles(), &tmpSolid.params ); + } + Assert( pNewObject ); + + pInstance->VPhysicsDestroyObject(); + pInstance->VPhysicsSetObject( pNewObject ); + + // Increase our model bounds + const model_t *pModel = modelinfo->GetModel( pInstance->GetModelIndex() ); + if ( pModel ) + { + Vector mins, maxs; + modelinfo->GetModelBounds( pModel, mins, maxs ); + pInstance->SetCollisionBounds( mins*flScale, maxs*flScale ); + } + + // Scale the base model as well + pInstance->SetModelScale( flScale ); + + if ( pInstance->GetParent() ) + { + pNewObject->SetShadow( 1e4, 1e4, false, false ); + pNewObject->UpdateShadow( pInstance->GetAbsOrigin(), pInstance->GetAbsAngles(), false, 0 ); + } + + if ( bWasMotionDisabled ) + { + pNewObject->EnableMotion( false ); + } + else + { + // Make sure we start awake! + pNewObject->Wake(); + } + + // Blargh + pInstance->SetScaledPhysics( ( flScale != 1.0f ) ? pNewObject : NULL ); + + return true; +} + + +//------------------------------------------------------------------------------ +// Rotates an entity +//------------------------------------------------------------------------------ +void CC_Ent_Rotate( const CCommand &args ) +{ + CBasePlayer* pPlayer = UTIL_GetCommandClient(); + CBaseEntity* pEntity = FindPickerEntity( pPlayer ); + if ( !pEntity ) + return; + + QAngle angles = pEntity->GetLocalAngles(); + float flAngle = (args.ArgC() == 2) ? atof( args[1] ) : 7.5f; + + VMatrix entToWorld, rot, newEntToWorld; + MatrixBuildRotateZ( rot, flAngle ); + MatrixFromAngles( angles, entToWorld ); + MatrixMultiply( entToWorld, rot, newEntToWorld ); + MatrixToAngles( newEntToWorld, angles ); + pEntity->SetLocalAngles( angles ); +} + +static ConCommand ent_rotate("ent_rotate", CC_Ent_Rotate, "Rotates an entity by a specified # of degrees", FCVAR_CHEAT); + +// This is a dummy. The entity is entirely clientside. +LINK_ENTITY_TO_CLASS( func_proprrespawnzone, CBaseEntity ); diff --git a/sp/src/game/server/props.h b/sp/src/game/server/props.h new file mode 100644 index 00000000..121dd5c3 --- /dev/null +++ b/sp/src/game/server/props.h @@ -0,0 +1,488 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#ifndef PROPS_H +#define PROPS_H +#ifdef _WIN32 +#pragma once +#endif + +#include "props_shared.h" +#include "baseanimating.h" +#include "physics_bone_follower.h" +#include "player_pickup.h" +#include "positionwatcher.h" + +//============================================================================================================= +// PROP TYPES +//============================================================================================================= +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CBaseProp : public CBaseAnimating +{ + DECLARE_CLASS( CBaseProp, CBaseAnimating ); +public: + + void Spawn( void ); + void Precache( void ); + void Activate( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void CalculateBlockLOS( void ); + int ParsePropData( void ); + + void DrawDebugGeometryOverlays( void ); + + // Don't treat as a live target + virtual bool IsAlive( void ) { return false; } + virtual bool OverridePropdata() { return true; } + +#ifdef MAPBASE + // Attempt to replace a dynamic_cast + virtual bool IsPropPhysics() { return false; } +#endif +}; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CBreakableProp : public CBaseProp, public IBreakableWithPropData, public CDefaultPlayerPickupVPhysics +{ +public: + CBreakableProp(); + + DECLARE_CLASS( CBreakableProp, CBaseProp ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + virtual void Spawn(); + virtual void Precache(); + virtual float GetAutoAimRadius() { return 24.0f; } + +#ifdef MAPBASE + virtual bool KeyValue( const char *szKeyName, const char *szValue ); +#endif + + void BreakablePropTouch( CBaseEntity *pOther ); + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + void Event_Killed( const CTakeDamageInfo &info ); +#ifdef MAPBASE + // Marks Break() as virtual + virtual +#endif + void Break( CBaseEntity *pBreaker, const CTakeDamageInfo &info ); + void BreakThink( void ); + void AnimateThink( void ); + + virtual void PlayPuntSound(); + + void InputBreak( inputdata_t &inputdata ); + void InputAddHealth( inputdata_t &inputdata ); + void InputRemoveHealth( inputdata_t &inputdata ); + void InputSetHealth( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetInteraction( inputdata_t &inputdata ); + void InputRemoveInteraction( inputdata_t &inputdata ); +#endif + + int GetNumBreakableChunks( void ) { return m_iNumBreakableChunks; } + + virtual bool OverridePropdata() { return false; } + virtual IPhysicsObject *GetRootPhysicsObjectForBreak(); + + bool PropDataOverrodeBlockLOS( void ) { return m_bBlockLOSSetByPropData; } + bool PropDataOverrodeAIWalkable( void ) { return m_bIsWalkableSetByPropData; } + + virtual bool HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ) + { + if ( HasInteraction( PROPINTER_PHYSGUN_LAUNCH_SPIN_Z ) ) + return true; + +#ifdef MAPBASE + if ( m_bUsesCustomCarryAngles ) + return true; +#endif + + return false; + } + + virtual QAngle PreferredCarryAngles( void ) { return m_preferredCarryAngles; } + + virtual void Ignite( float flFlameLifetime, bool bNPCOnly, float flSize = 0.0f, bool bCalledByLevelDesigner = false ); + + // Specific interactions + void HandleFirstCollisionInteractions( int index, gamevcollisionevent_t *pEvent ); + void HandleInteractionStick( int index, gamevcollisionevent_t *pEvent ); + void StickAtPosition( const Vector &stickPosition, const Vector &savePosition, const QAngle &saveAngles ); + +#ifdef MAPBASE + // Uses the new CBaseEntity interaction implementation + bool HandleInteraction( int interactionType, void *data, CBaseCombatCharacter* sourceEnt ); +#endif + + // Disable auto fading under dx7 or when level fades are specified + void DisableAutoFade(); + +public: + COutputEvent m_OnBreak; + COutputFloat m_OnHealthChanged; + COutputEvent m_OnTakeDamage; + + float m_impactEnergyScale; + + int m_iMinHealthDmg; + + QAngle m_preferredCarryAngles; +#ifdef MAPBASE + // Indicates whether the prop is using the keyvalue carry angles. + bool m_bUsesCustomCarryAngles; +#endif + +public: +// IBreakableWithPropData + void SetDmgModBullet( float flDmgMod ) { m_flDmgModBullet = flDmgMod; } + void SetDmgModClub( float flDmgMod ) { m_flDmgModClub = flDmgMod; } + void SetDmgModExplosive( float flDmgMod ) { m_flDmgModExplosive = flDmgMod; } + float GetDmgModBullet( void ) { return m_flDmgModBullet; } + float GetDmgModClub( void ) { return m_flDmgModClub; } + float GetDmgModExplosive( void ) { return m_flDmgModExplosive; } + void SetExplosiveRadius( float flRadius ) { m_explodeRadius = flRadius; } + void SetExplosiveDamage( float flDamage ) { m_explodeDamage = flDamage; } + float GetExplosiveRadius( void ) { return m_explodeRadius; } + float GetExplosiveDamage( void ) { return m_explodeDamage; } + void SetPhysicsDamageTable( string_t iszTableName ) { m_iszPhysicsDamageTableName = iszTableName; } + string_t GetPhysicsDamageTable( void ) { return m_iszPhysicsDamageTableName; } + void SetBreakableModel( string_t iszModel ) { m_iszBreakableModel = iszModel; } + string_t GetBreakableModel( void ) { return m_iszBreakableModel; } + void SetBreakableSkin( int iSkin ) { m_iBreakableSkin = iSkin; } + int GetBreakableSkin( void ) { return m_iBreakableSkin; } + void SetBreakableCount( int iCount ) { m_iBreakableCount = iCount; } + int GetBreakableCount( void ) { return m_iBreakableCount; } + void SetMaxBreakableSize( int iSize ) { m_iMaxBreakableSize = iSize; } + int GetMaxBreakableSize( void ) { return m_iMaxBreakableSize; } + void SetPropDataBlocksLOS( bool bBlocksLOS ) { m_bBlockLOSSetByPropData = true; SetBlocksLOS( bBlocksLOS ); } + void SetPropDataIsAIWalkable( bool b ) { m_bIsWalkableSetByPropData = true; SetAIWalkable( b ); } + void SetBasePropData( string_t iszBase ) { m_iszBasePropData = iszBase; } + string_t GetBasePropData( void ) { return m_iszBasePropData; } + void SetInteraction( propdata_interactions_t Interaction ) { m_iInteractions |= (1 << Interaction); } + void RemoveInteraction( propdata_interactions_t Interaction ) { m_iInteractions &= ~(1 << Interaction); } + bool HasInteraction( propdata_interactions_t Interaction ) { return ( m_iInteractions & (1 << Interaction) ) != 0; } + void SetMultiplayerBreakMode( mp_break_t mode ) { m_mpBreakMode = mode; } + mp_break_t GetMultiplayerBreakMode( void ) const { return m_mpBreakMode; } + +// derived by multiplayer phys props: + virtual void SetPhysicsMode(int iMode) {} + virtual int GetPhysicsMode() { return PHYSICS_MULTIPLAYER_SOLID; } + + // Copy fade from another breakable prop + void CopyFadeFrom( CBreakableProp *pSource ); + +protected: + + bool UpdateHealth( int iNewHealth, CBaseEntity *pActivator ); + virtual void OnBreak( const Vector &vecVelocity, const AngularImpulse &angVel, CBaseEntity *pBreaker ) {} + +protected: + + unsigned int m_createTick; + float m_flPressureDelay; + EHANDLE m_hBreaker; + + PerformanceMode_t m_PerformanceMode; + + // Prop data storage + float m_flDmgModBullet; + float m_flDmgModClub; + float m_flDmgModExplosive; + string_t m_iszPhysicsDamageTableName; + string_t m_iszBreakableModel; + int m_iBreakableSkin; + int m_iBreakableCount; + int m_iMaxBreakableSize; + string_t m_iszBasePropData; + int m_iInteractions; + float m_explodeDamage; + float m_explodeRadius; + string_t m_iszBreakModelMessage; + + // Count of how many pieces we'll break into, custom or generic + int m_iNumBreakableChunks; + + void SetEnableMotionPosition( const Vector &position, const QAngle &angles ); + bool GetEnableMotionPosition( Vector *pPosition, QAngle *pAngles ); + void ClearEnableMotionPosition(); +private: + CBaseEntity *FindEnableMotionFixup(); + +public: + virtual bool OnAttemptPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ); + virtual AngularImpulse PhysGunLaunchAngularImpulse(); + virtual CBasePlayer *HasPhysicsAttacker( float dt ); + +#ifdef HL2_EPISODIC +#ifdef MAPBASE + virtual float GetFlareLifetime() { return 30.0f; } +#endif + void CreateFlare( float flLifetime ); +#endif //HL2_EPISODIC + +protected: + void SetPhysicsAttacker( CBasePlayer *pEntity, float flTime ); + void CheckRemoveRagdolls(); + +private: + void InputEnablePhyscannonPickup( inputdata_t &inputdata ); + void InputDisablePhyscannonPickup( inputdata_t &inputdata ); + + void InputEnablePuntSound( inputdata_t &inputdata ) { m_bUsePuntSound = true; } + void InputDisablePuntSound( inputdata_t &inputdata ) { m_bUsePuntSound = false; } + + // Prevents fade scale from happening + void ForceFadeScaleToAlwaysVisible(); + void RampToDefaultFadeScale(); + +private: + enum PhysgunState_t + { + PHYSGUN_MUST_BE_DETACHED = 0, + PHYSGUN_IS_DETACHING, + PHYSGUN_CAN_BE_GRABBED, + PHYSGUN_ANIMATE_ON_PULL, + PHYSGUN_ANIMATE_IS_ANIMATING, + PHYSGUN_ANIMATE_FINISHED, + PHYSGUN_ANIMATE_IS_PRE_ANIMATING, + PHYSGUN_ANIMATE_IS_POST_ANIMATING, + }; + + CHandle m_hPhysicsAttacker; + float m_flLastPhysicsInfluenceTime; + bool m_bBlockLOSSetByPropData; + bool m_bIsWalkableSetByPropData; + bool m_bOriginalBlockLOS; // BlockLOS state before physgun pickup + char m_nPhysgunState; // Ripped-off state + COutputEvent m_OnPhysCannonDetach; // We've ripped it off! + COutputEvent m_OnPhysCannonAnimatePreStarted; // Started playing the pre-pull animation + COutputEvent m_OnPhysCannonAnimatePullStarted; // Player started the pull anim + COutputEvent m_OnPhysCannonAnimatePostStarted; // Started playing the post-pull animation + COutputEvent m_OnPhysCannonPullAnimFinished; // We've had our pull anim finished, or the post-pull has finished if there is one + float m_flDefaultFadeScale; // Things may temporarily change the fade scale, but this is its steady-state condition + + mp_break_t m_mpBreakMode; + + EHANDLE m_hLastAttacker; // Last attacker that harmed me. +#ifdef MAPBASE +protected: + // Needs to be protected for prop_flare entity usage + EHANDLE m_hFlareEnt; +private: +#else + EHANDLE m_hFlareEnt; +#endif + string_t m_iszPuntSound; + bool m_bUsePuntSound; +}; + +// Spawnflags +#define SF_DYNAMICPROP_USEHITBOX_FOR_RENDERBOX 64 +#define SF_DYNAMICPROP_NO_VPHYSICS 128 +#define SF_DYNAMICPROP_DISABLE_COLLISION 256 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CDynamicProp : public CBreakableProp, public IPositionWatcher +{ + DECLARE_CLASS( CDynamicProp, CBreakableProp ); + +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CDynamicProp(); + + void Spawn( void ); + bool CreateVPhysics( void ); + void CreateBoneFollowers(); + void UpdateOnRemove( void ); + void AnimThink( void ); + void PropSetSequence( int nSequence ); + void OnRestore( void ); + bool OverridePropdata( void ); + void HandleAnimEvent( animevent_t *pEvent ); + + // baseentity - watch dynamic hierarchy updates + virtual void SetParent( CBaseEntity* pNewParent, int iAttachment = -1 ); + bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); + + // breakable prop + virtual IPhysicsObject *GetRootPhysicsObjectForBreak(); + + // IPositionWatcher + virtual void NotifyPositionChanged( CBaseEntity *pEntity ); + + // Input handlers + void InputSetAnimation( inputdata_t &inputdata ); + void InputSetDefaultAnimation( inputdata_t &inputdata ); + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputDisableCollision( inputdata_t &inputdata ); + void InputEnableCollision( inputdata_t &inputdata ); + void InputSetPlaybackRate( inputdata_t &inputdata ); + + COutputEvent m_pOutputAnimBegun; + COutputEvent m_pOutputAnimOver; + + string_t m_iszDefaultAnim; + + int m_iGoalSequence; + int m_iTransitionDirection; + + // Random animations + bool m_bRandomAnimator; + float m_flNextRandAnim; + float m_flMinRandAnimTime; + float m_flMaxRandAnimTime; + short m_nPendingSequence; + + bool m_bStartDisabled; + bool m_bDisableBoneFollowers; + + CNetworkVar( bool, m_bUseHitboxesForRenderBox ); + +protected: + void FinishSetSequence( int nSequence ); + void PropSetAnim( const char *szAnim ); + void BoneFollowerHierarchyChanged(); + + // Contained Bone Follower manager + CBoneFollowerManager m_BoneFollowerManager; +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPhysicsProp : public CBreakableProp +{ + DECLARE_CLASS( CPhysicsProp, CBreakableProp ); + DECLARE_SERVERCLASS(); + +public: + ~CPhysicsProp(); + CPhysicsProp( void ) + { + } + + void Spawn( void ); + void Precache(); + bool CreateVPhysics( void ); + bool OverridePropdata( void ); + +#ifdef MAPBASE + // Attempt to replace a dynamic_cast + virtual bool IsPropPhysics() { return true; } +#endif + + virtual void VPhysicsUpdate( IPhysicsObject *pPhysics ); + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + + void InputWake( inputdata_t &inputdata ); + void InputSleep( inputdata_t &inputdata ); + void InputEnableMotion( inputdata_t &inputdata ); + void InputDisableMotion( inputdata_t &inputdata ); + void InputDisableFloating( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetDebris( inputdata_t &inputdata ); +#endif + + void EnableMotion( void ); + bool CanBePickedUpByPhyscannon( void ); + void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ); + + bool GetPropDataAngles( const char *pKeyName, QAngle &vecAngles ); + float GetCarryDistanceOffset( void ); + + int ObjectCaps(); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void GetMassCenter( Vector *pMassCenter ); + float GetMass() const; + + void ClearFlagsThink( void ); + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + int DrawDebugTextOverlays(void); + bool IsGib(); + DECLARE_DATADESC(); + + // Specific interactions + void HandleAnyCollisionInteractions( int index, gamevcollisionevent_t *pEvent ); + + string_t GetPhysOverrideScript( void ) { return m_iszOverrideScript; } + float GetMassScale( void ) { return m_massScale; } + +private: + // Compute impulse to apply to the enabled entity. + void ComputeEnablingImpulse( int index, gamevcollisionevent_t *pEvent ); + + COutputEvent m_MotionEnabled; + COutputEvent m_OnAwakened; + COutputEvent m_OnPhysGunPickup; + COutputEvent m_OnPhysGunPunt; + COutputEvent m_OnPhysGunOnlyPickup; + COutputEvent m_OnPhysGunDrop; + COutputEvent m_OnPlayerUse; + COutputEvent m_OnPlayerPickup; + COutputEvent m_OnOutOfWorld; + + float m_massScale; + float m_inertiaScale; + int m_damageType; + string_t m_iszOverrideScript; + int m_damageToEnableMotion; + float m_flForceToEnableMotion; + + bool m_bThrownByPlayer; + bool m_bFirstCollisionAfterLaunch; + +protected: + CNetworkVar( bool, m_bAwake ); +}; + + +// An interface so that objects parented to props can receive collision interaction events. +enum parentCollisionInteraction_t +{ + COLLISIONINTER_PARENT_FIRST_IMPACT = 1, +}; + + +abstract_class IParentPropInteraction +{ +public: + virtual void OnParentCollisionInteraction( parentCollisionInteraction_t eType, int index, gamevcollisionevent_t *pEvent ) = 0; + virtual void OnParentPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) = 0; +}; + + +// Used by prop_physics_create and the server benchmark. +// pModelName should not include the "models/" prefix. +CPhysicsProp* CreatePhysicsProp( const char *pModelName, const Vector &vTraceStart, const Vector &vTraceEnd, const IHandleEntity *pTraceIgnore, bool bRequireVCollide, const char *pClassName="physics_prop" ); + +bool UTIL_CreateScaledPhysObject( CBaseAnimating *pInstance, float flScale ); + +float GetBreakableDamage( const CTakeDamageInfo &inputInfo, IBreakableWithPropData *pProp = NULL ); +int PropBreakablePrecacheAll( string_t modelName ); + +extern ConVar func_breakdmg_bullet; +extern ConVar func_breakdmg_club; +extern ConVar func_breakdmg_explosive; + +#endif // PROPS_H diff --git a/sp/src/game/server/pushentity.h b/sp/src/game/server/pushentity.h new file mode 100644 index 00000000..f1c02d39 --- /dev/null +++ b/sp/src/game/server/pushentity.h @@ -0,0 +1,160 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= +#ifndef PUSHENTITY_H +#define PUSHENTITY_H +#ifdef _WIN32 +#pragma once +#endif + +#include "movetype_push.h" + +//----------------------------------------------------------------------------- +// Purpose: Keeps track of original positions of any entities that are being possibly pushed +// and handles restoring positions for those objects if the push is aborted +//----------------------------------------------------------------------------- +class CPhysicsPushedEntities +{ +public: + + DECLARE_CLASS_NOBASE( CPhysicsPushedEntities ); + + CPhysicsPushedEntities( void ); + + // Purpose: Tries to rotate an entity hierarchy, returns the blocker if any + CBaseEntity *PerformRotatePush( CBaseEntity *pRoot, float movetime ); + + // Purpose: Tries to linearly push an entity hierarchy, returns the blocker if any + CBaseEntity *PerformLinearPush( CBaseEntity *pRoot, float movetime ); + + int CountMovedEntities() { return m_rgMoved.Count(); } + void StoreMovedEntities( physicspushlist_t &list ); + void BeginPush( CBaseEntity *pRootEntity ); + +protected: + + // describes the per-frame incremental motion of a rotating MOVETYPE_PUSH + struct RotatingPushMove_t + { + Vector origin; + matrix3x4_t startLocalToWorld; + matrix3x4_t endLocalToWorld; + QAngle amove; // delta orientation + }; + + // Pushers + their original positions also (for touching triggers) + struct PhysicsPusherInfo_t + { + CBaseEntity *m_pEntity; + Vector m_vecStartAbsOrigin; + }; + + // Pushed entities + various state related to them being pushed + struct PhysicsPushedInfo_t + { + CBaseEntity *m_pEntity; + Vector m_vecStartAbsOrigin; + trace_t m_Trace; + bool m_bBlocked; + bool m_bPusherIsGround; + }; + + // Adds the specified entity to the list + void AddEntity( CBaseEntity *ent ); + + // If a move fails, restores all entities to their original positions + void RestoreEntities( ); + + // Compute the direction to move the rotation blocker + void ComputeRotationalPushDirection( CBaseEntity *pBlocker, const RotatingPushMove_t &rotPushMove, Vector *pMove, CBaseEntity *pRoot ); + + // Speculatively checks to see if all entities in this list can be pushed + bool SpeculativelyCheckPush( PhysicsPushedInfo_t &info, const Vector &vecAbsPush, bool bRotationalPush ); + + // Speculatively checks to see if all entities in this list can be pushed + virtual bool SpeculativelyCheckRotPush( const RotatingPushMove_t &rotPushMove, CBaseEntity *pRoot ); + + // Speculatively checks to see if all entities in this list can be pushed + virtual bool SpeculativelyCheckLinearPush( const Vector &vecAbsPush ); + + // Registers a blockage + CBaseEntity *RegisterBlockage(); + + // Some fixup for objects pushed by rotating objects + virtual void FinishRotPushedEntity( CBaseEntity *pPushedEntity, const RotatingPushMove_t &rotPushMove ); + + // Commits the speculative movement + void FinishPush( bool bIsRotPush = false, const RotatingPushMove_t *pRotPushMove = NULL ); + + // Generates a list of all entities potentially blocking all pushers + void GenerateBlockingEntityList(); + void GenerateBlockingEntityListAddBox( const Vector &vecMoved ); + + // Purpose: Gets a list of all entities hierarchically attached to the root + void SetupAllInHierarchy( CBaseEntity *pParent ); + + // Unlink + relink the pusher list so we can actually do the push + void UnlinkPusherList( int *pPusherHandles ); + void RelinkPusherList( int *pPusherHandles ); + + // Causes all entities in the list to touch triggers from their prev position + void FinishPushers(); + + // Purpose: Rotates the root entity, fills in the pushmove structure + void RotateRootEntity( CBaseEntity *pRoot, float movetime, RotatingPushMove_t &rotation ); + + // Purpose: Linearly moves the root entity + void LinearlyMoveRootEntity( CBaseEntity *pRoot, float movetime, Vector *pAbsPushVector ); + + bool IsPushedPositionValid( CBaseEntity *pBlocker ); + +protected: + + CUtlVector m_rgPusher; + CUtlVector m_rgMoved; + int m_nBlocker; + bool m_bIsUnblockableByPlayer; + Vector m_rootPusherStartLocalOrigin; + QAngle m_rootPusherStartLocalAngles; + float m_rootPusherStartLocaltime; + float m_flMoveTime; + + friend class CPushBlockerEnum; +}; + +class CTraceFilterPushMove : public CTraceFilterSimple +{ + DECLARE_CLASS( CTraceFilterPushMove, CTraceFilterSimple ); + +public: + CTraceFilterPushMove( CBaseEntity *pEntity, int nCollisionGroup ) + : CTraceFilterSimple( pEntity, nCollisionGroup ) + { + m_pRootParent = pEntity->GetRootMoveParent(); + } + + bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + Assert( dynamic_cast(pHandleEntity) ); + CBaseEntity *pTestEntity = static_cast(pHandleEntity); + + if ( UTIL_EntityHasMatchingRootParent( m_pRootParent, pTestEntity ) ) + return false; + + if ( pTestEntity->GetMoveType() == MOVETYPE_VPHYSICS && + pTestEntity->VPhysicsGetObject() && pTestEntity->VPhysicsGetObject()->IsMoveable() ) + return false; + + return BaseClass::ShouldHitEntity( pHandleEntity, contentsMask ); + } + +private: + + CBaseEntity *m_pRootParent; +}; + +extern CPhysicsPushedEntities *g_pPushedEntities; + +#endif // PUSHENTITY_H \ No newline at end of file diff --git a/sp/src/game/server/ragdoll_manager.cpp b/sp/src/game/server/ragdoll_manager.cpp new file mode 100644 index 00000000..74ab8e96 --- /dev/null +++ b/sp/src/game/server/ragdoll_manager.cpp @@ -0,0 +1,174 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" +#include "sendproxy.h" +#include "ragdoll_shared.h" +#include "ai_basenpc.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CRagdollManager : public CBaseEntity +{ +public: + DECLARE_CLASS( CRagdollManager, CBaseEntity ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CRagdollManager(); + + virtual void Activate(); + virtual int UpdateTransmitState(); + + void InputSetMaxRagdollCount(inputdata_t &data); + void InputSetMaxRagdollCountDX8(inputdata_t &data); + + int DrawDebugTextOverlays(void); + +public: + + void UpdateCurrentMaxRagDollCount(); + + CNetworkVar( int, m_iCurrentMaxRagdollCount ); + + int m_iDXLevel; + int m_iMaxRagdollCount; + int m_iMaxRagdollCountDX8; + + bool m_bSaveImportant; +}; + + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CRagdollManager, DT_RagdollManager ) + SendPropInt( SENDINFO( m_iCurrentMaxRagdollCount ), 6 ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( game_ragdoll_manager, CRagdollManager ); + +BEGIN_DATADESC( CRagdollManager ) + + //DEFINE_FIELD( m_iDXLevel, FIELD_INTEGER ), + + DEFINE_FIELD( m_iCurrentMaxRagdollCount, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_iMaxRagdollCount, FIELD_INTEGER, "MaxRagdollCount" ), + DEFINE_KEYFIELD( m_iMaxRagdollCountDX8, FIELD_INTEGER, "MaxRagdollCountDX8" ), + + DEFINE_KEYFIELD( m_bSaveImportant, FIELD_BOOLEAN, "SaveImportant" ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxRagdollCount", InputSetMaxRagdollCount ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetMaxRagdollCountDX8", InputSetMaxRagdollCountDX8 ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CRagdollManager::CRagdollManager( void ) +{ + m_iMaxRagdollCount = -1; + m_iMaxRagdollCountDX8 = -1; + m_iCurrentMaxRagdollCount = -1; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pInfo - +// Output : int +//----------------------------------------------------------------------------- +int CRagdollManager::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRagdollManager::Activate() +{ + BaseClass::Activate(); + + // Cache off the DX level for use later. + ConVarRef mat_dxlevel( "mat_dxlevel" ); + m_iDXLevel = mat_dxlevel.GetInt(); + + UpdateCurrentMaxRagDollCount(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CRagdollManager::UpdateCurrentMaxRagDollCount() +{ + if ( ( m_iDXLevel < 90 ) && ( m_iMaxRagdollCountDX8 >= 0 ) ) + { + m_iCurrentMaxRagdollCount = m_iMaxRagdollCountDX8; + } + else + { + m_iCurrentMaxRagdollCount = m_iMaxRagdollCount; + } + + s_RagdollLRU.SetMaxRagdollCount( m_iCurrentMaxRagdollCount ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CRagdollManager::InputSetMaxRagdollCount(inputdata_t &inputdata) +{ + m_iMaxRagdollCount = inputdata.value.Int(); + UpdateCurrentMaxRagDollCount(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CRagdollManager::InputSetMaxRagdollCountDX8(inputdata_t &inputdata) +{ + m_iMaxRagdollCountDX8 = inputdata.value.Int(); + UpdateCurrentMaxRagDollCount(); +} + +bool RagdollManager_SaveImportant( CAI_BaseNPC *pNPC ) +{ +#ifdef HL2_DLL + CRagdollManager *pEnt = (CRagdollManager *)gEntList.FindEntityByClassname( NULL, "game_ragdoll_manager" ); + + if ( pEnt == NULL ) + return false; + + if ( pEnt->m_bSaveImportant ) + { + if ( pNPC->Classify() == CLASS_PLAYER_ALLY || pNPC->Classify() == CLASS_PLAYER_ALLY_VITAL ) + { + return true; + } + } +#endif + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CRagdollManager::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + // print max ragdoll count + Q_snprintf(tempstr,sizeof(tempstr),"max ragdoll count: %d", m_iCurrentMaxRagdollCount.Get()); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + diff --git a/sp/src/game/server/recipientfilter.cpp b/sp/src/game/server/recipientfilter.cpp new file mode 100644 index 00000000..b7687fbe --- /dev/null +++ b/sp/src/game/server/recipientfilter.cpp @@ -0,0 +1,415 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "recipientfilter.h" +#include "team.h" +#include "ipredictionsystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static IPredictionSystem g_RecipientFilterPredictionSystem; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CRecipientFilter::CRecipientFilter() +{ + Reset(); +} + +CRecipientFilter::~CRecipientFilter() +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : src - +//----------------------------------------------------------------------------- +void CRecipientFilter::CopyFrom( const CRecipientFilter& src ) +{ + m_bReliable = src.IsReliable(); + m_bInitMessage = src.IsInitMessage(); + + m_bUsingPredictionRules = src.IsUsingPredictionRules(); + m_bIgnorePredictionCull = src.IgnorePredictionCull(); + + int c = src.GetRecipientCount(); + for ( int i = 0; i < c; ++i ) + { + m_Recipients.AddToTail( src.GetRecipientIndex( i ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRecipientFilter::Reset( void ) +{ + m_bReliable = false; + m_bInitMessage = false; + m_Recipients.RemoveAll(); + m_bUsingPredictionRules = false; + m_bIgnorePredictionCull = false; +} + +void CRecipientFilter::MakeReliable( void ) +{ + m_bReliable = true; +} + +bool CRecipientFilter::IsReliable( void ) const +{ + return m_bReliable; +} + +int CRecipientFilter::GetRecipientCount( void ) const +{ + return m_Recipients.Size(); +} + +int CRecipientFilter::GetRecipientIndex( int slot ) const +{ + if ( slot < 0 || slot >= GetRecipientCount() ) + return -1; + + return m_Recipients[ slot ]; +} + +void CRecipientFilter::AddAllPlayers( void ) +{ + m_Recipients.RemoveAll(); + + int i; + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( !pPlayer ) + { + continue; + } + + AddRecipient( pPlayer ); + } +} + +void CRecipientFilter::AddRecipient( CBasePlayer *player ) +{ + Assert( player ); + + if ( !player ) + return; + + int index = player->entindex(); + + // If we're predicting and this is not the first time we've predicted this sound + // then don't send it to the local player again. + if ( m_bUsingPredictionRules ) + { + // Only add local player if this is the first time doing prediction + if ( g_RecipientFilterPredictionSystem.GetSuppressHost() == player ) + { + return; + } + } + + // Already in list + if ( m_Recipients.Find( index ) != m_Recipients.InvalidIndex() ) + return; + + m_Recipients.AddToTail( index ); +} + +void CRecipientFilter::RemoveAllRecipients( void ) +{ + m_Recipients.RemoveAll(); +} + +void CRecipientFilter::RemoveRecipient( CBasePlayer *player ) +{ + Assert( player ); + if ( player ) + { + int index = player->entindex(); + + // Remove it if it's in the list + m_Recipients.FindAndRemove( index ); + } +} + +void CRecipientFilter::RemoveRecipientByPlayerIndex( int playerindex ) +{ + Assert( playerindex >= 1 && playerindex <= ABSOLUTE_PLAYER_LIMIT ); + + m_Recipients.FindAndRemove( playerindex ); +} + +void CRecipientFilter::AddRecipientsByTeam( CTeam *team ) +{ + Assert( team ); + + int i; + int c = team->GetNumPlayers(); + for ( i = 0 ; i < c ; i++ ) + { + CBasePlayer *player = team->GetPlayer( i ); + if ( !player ) + continue; + + AddRecipient( player ); + } +} + +void CRecipientFilter::RemoveRecipientsByTeam( CTeam *team ) +{ + Assert( team ); + + int i; + int c = team->GetNumPlayers(); + for ( i = 0 ; i < c ; i++ ) + { + CBasePlayer *player = team->GetPlayer( i ); + if ( !player ) + continue; + + RemoveRecipient( player ); + } +} + +void CRecipientFilter::RemoveRecipientsNotOnTeam( CTeam *team ) +{ + Assert( team ); + + int i; + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *player = UTIL_PlayerByIndex( i ); + if ( !player ) + continue; + + if ( player->GetTeam() != team ) + { + RemoveRecipient( player ); + } + } +} + +void CRecipientFilter::AddPlayersFromBitMask( CBitVec< ABSOLUTE_PLAYER_LIMIT >& playerbits ) +{ + int index = playerbits.FindNextSetBit( 0 ); + + while ( index > -1 ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( index + 1 ); + if ( pPlayer ) + { + AddRecipient( pPlayer ); + } + + index = playerbits.FindNextSetBit( index + 1 ); + } +} + +void CRecipientFilter::RemovePlayersFromBitMask( CBitVec< ABSOLUTE_PLAYER_LIMIT >& playerbits ) +{ + int index = playerbits.FindNextSetBit( 0 ); + + while ( index > -1 ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( index + 1 ); + if ( pPlayer ) + { + RemoveRecipient( pPlayer ); + } + + index = playerbits.FindNextSetBit( index + 1 ); + } +} + +void CRecipientFilter::AddRecipientsByPVS( const Vector& origin ) +{ + if ( gpGlobals->maxClients == 1 ) + { + AddAllPlayers(); + } + else + { + CBitVec< ABSOLUTE_PLAYER_LIMIT > playerbits; + engine->Message_DetermineMulticastRecipients( false, origin, playerbits ); + AddPlayersFromBitMask( playerbits ); + } +} + +void CRecipientFilter::RemoveRecipientsByPVS( const Vector& origin ) +{ + if ( gpGlobals->maxClients == 1 ) + { + m_Recipients.RemoveAll(); + } + else + { + CBitVec< ABSOLUTE_PLAYER_LIMIT > playerbits; + engine->Message_DetermineMulticastRecipients( false, origin, playerbits ); + RemovePlayersFromBitMask( playerbits ); + } +} + + + +void CRecipientFilter::AddRecipientsByPAS( const Vector& origin ) +{ + if ( gpGlobals->maxClients == 1 ) + { + AddAllPlayers(); + } + else + { + CBitVec< ABSOLUTE_PLAYER_LIMIT > playerbits; + engine->Message_DetermineMulticastRecipients( true, origin, playerbits ); + AddPlayersFromBitMask( playerbits ); + } +} + +bool CRecipientFilter::IsInitMessage( void ) const +{ + return m_bInitMessage; +} + +void CRecipientFilter::MakeInitMessage( void ) +{ + m_bInitMessage = true; +} + +void CRecipientFilter::UsePredictionRules( void ) +{ + if ( m_bUsingPredictionRules ) + return; + + m_bUsingPredictionRules = true; + + // Cull list now, if needed + if ( GetRecipientCount() == 0 ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( (CBaseEntity*)g_RecipientFilterPredictionSystem.GetSuppressHost() ); + + if ( pPlayer) + { + RemoveRecipient( pPlayer ); + } +} + +bool CRecipientFilter::IsUsingPredictionRules( void ) const +{ + return m_bUsingPredictionRules; +} + +bool CRecipientFilter:: IgnorePredictionCull( void ) const +{ + return m_bIgnorePredictionCull; +} + +void CRecipientFilter::SetIgnorePredictionCull( bool ignore ) +{ + m_bIgnorePredictionCull = ignore; +} + +//----------------------------------------------------------------------------- +// Purpose: Simple class to create a filter for all players on a given team +//----------------------------------------------------------------------------- +CTeamRecipientFilter::CTeamRecipientFilter( int team, bool isReliable ) +{ + if (isReliable) + MakeReliable(); + + RemoveAllRecipients(); + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( !pPlayer ) + { + continue; + } + + if ( pPlayer->GetTeamNumber() != team ) + { + //If we're in the spectator team then we should be getting whatever messages the person I'm spectating gets. + if ( pPlayer->GetTeamNumber() == TEAM_SPECTATOR && (pPlayer->GetObserverMode() == OBS_MODE_IN_EYE || pPlayer->GetObserverMode() == OBS_MODE_CHASE) ) + { + if ( pPlayer->GetObserverTarget() ) + { + if ( pPlayer->GetObserverTarget()->GetTeamNumber() != team ) + continue; + } + } + else + { + continue; + } + } + + AddRecipient( pPlayer ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : origin - +// ATTN_NORM - +//----------------------------------------------------------------------------- +void CPASAttenuationFilter::Filter( const Vector& origin, float attenuation /*= ATTN_NORM*/ ) +{ + // Don't crop for attenuation in single player + if ( gpGlobals->maxClients == 1 ) + return; + + // CPASFilter adds them by pure PVS in constructor + if ( attenuation <= 0 ) + return; + + // Now remove recipients that are outside sound radius + float distance, maxAudible; + Vector vecRelative; + + int c = GetRecipientCount(); + + for ( int i = c - 1; i >= 0; i-- ) + { + int index = GetRecipientIndex( i ); + + CBaseEntity *ent = CBaseEntity::Instance( index ); + if ( !ent || !ent->IsPlayer() ) + { + Assert( 0 ); + continue; + } + + CBasePlayer *player = ToBasePlayer( ent ); + if ( !player ) + { + Assert( 0 ); + continue; + } + +#ifndef _XBOX + // never remove the HLTV or Replay bot + if ( player->IsHLTV() || player->IsReplay() ) + continue; +#endif + + VectorSubtract( player->EarPosition(), origin, vecRelative ); + distance = VectorLength( vecRelative ); + maxAudible = ( 2 * SOUND_NORMAL_CLIP_DIST ) / attenuation; + if ( distance <= maxAudible ) + continue; + + RemoveRecipient( player ); + } +} \ No newline at end of file diff --git a/sp/src/game/server/recipientfilter.h b/sp/src/game/server/recipientfilter.h new file mode 100644 index 00000000..3f48e4fa --- /dev/null +++ b/sp/src/game/server/recipientfilter.h @@ -0,0 +1,241 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef RECIPIENTFILTER_H +#define RECIPIENTFILTER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "irecipientfilter.h" +#include "const.h" +#include "player.h" +#include "bitvec.h" + +//----------------------------------------------------------------------------- +// Purpose: A generic filter for determining whom to send message/sounds etc. to and +// providing a bit of additional state information +//----------------------------------------------------------------------------- +class CRecipientFilter : public IRecipientFilter +{ +public: + CRecipientFilter(); + virtual ~CRecipientFilter(); + + virtual bool IsReliable( void ) const; + virtual bool IsInitMessage( void ) const; + + virtual int GetRecipientCount( void ) const; + virtual int GetRecipientIndex( int slot ) const; + +public: + + void CopyFrom( const CRecipientFilter& src ); + + void Reset( void ); + + void MakeInitMessage( void ); + + void MakeReliable( void ); + + void AddAllPlayers( void ); + void AddRecipientsByPVS( const Vector& origin ); + void RemoveRecipientsByPVS( const Vector& origin ); + void AddRecipientsByPAS( const Vector& origin ); + void AddRecipient( CBasePlayer *player ); + void RemoveAllRecipients( void ); + void RemoveRecipient( CBasePlayer *player ); + void RemoveRecipientByPlayerIndex( int playerindex ); + void AddRecipientsByTeam( CTeam *team ); + void RemoveRecipientsByTeam( CTeam *team ); + void RemoveRecipientsNotOnTeam( CTeam *team ); + + void UsePredictionRules( void ); + bool IsUsingPredictionRules( void ) const; + + bool IgnorePredictionCull( void ) const; + void SetIgnorePredictionCull( bool ignore ); + + void AddPlayersFromBitMask( CBitVec< ABSOLUTE_PLAYER_LIMIT >& playerbits ); + void RemovePlayersFromBitMask( CBitVec< ABSOLUTE_PLAYER_LIMIT >& playerbits ); + +private: + + bool m_bReliable; + bool m_bInitMessage; + CUtlVector< int > m_Recipients; + + // If using prediction rules, the filter itself suppresses local player + bool m_bUsingPredictionRules; + // If ignoring prediction cull, then external systems can determine + // whether this is a special case where culling should not occur + bool m_bIgnorePredictionCull; +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple class to create a filter for a single player ( unreliable ) +//----------------------------------------------------------------------------- +class CSingleUserRecipientFilter : public CRecipientFilter +{ +public: + CSingleUserRecipientFilter( CBasePlayer *player ) + { + AddRecipient( player ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple class to create a filter for all players on a given team +//----------------------------------------------------------------------------- +class CTeamRecipientFilter : public CRecipientFilter +{ +public: + CTeamRecipientFilter( int team, bool isReliable = false ); +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple class to create a filter for all players ( unreliable ) +//----------------------------------------------------------------------------- +class CBroadcastRecipientFilter : public CRecipientFilter +{ +public: + CBroadcastRecipientFilter( void ) + { + AddAllPlayers(); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple class to create a filter for all players ( reliable ) +//----------------------------------------------------------------------------- +class CReliableBroadcastRecipientFilter : public CBroadcastRecipientFilter +{ +public: + CReliableBroadcastRecipientFilter( void ) + { + MakeReliable(); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple class to create a filter for all players except for one ( unreliable ) +//----------------------------------------------------------------------------- +class CBroadcastNonOwnerRecipientFilter : public CRecipientFilter +{ +public: + CBroadcastNonOwnerRecipientFilter( CBasePlayer *player ) + { + AddAllPlayers(); + RemoveRecipient( player ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: Add players in PAS to recipient list (unreliable) +//----------------------------------------------------------------------------- +class CPASFilter : public CRecipientFilter +{ +public: + CPASFilter( void ) + { + } + + CPASFilter( const Vector& origin ) + { + AddRecipientsByPAS( origin ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: Add players in PAS to list and if not in single player, use attenuation +// to remove those that are too far away from source origin +// Source origin can be stated as an entity or just a passed in origin +// (unreliable) +//----------------------------------------------------------------------------- +class CPASAttenuationFilter : public CPASFilter +{ +public: + CPASAttenuationFilter( void ) + { + } + + CPASAttenuationFilter( CBaseEntity *entity, soundlevel_t soundlevel ) : + CPASFilter( static_cast(entity->GetSoundEmissionOrigin()) ) + { + Filter( entity->GetSoundEmissionOrigin(), SNDLVL_TO_ATTN( soundlevel ) ); + } + + CPASAttenuationFilter( CBaseEntity *entity, float attenuation = ATTN_NORM ) : + CPASFilter( static_cast(entity->GetSoundEmissionOrigin()) ) + { + Filter( entity->GetSoundEmissionOrigin(), attenuation ); + } + + CPASAttenuationFilter( const Vector& origin, soundlevel_t soundlevel ) : + CPASFilter( origin ) + { + Filter( origin, SNDLVL_TO_ATTN( soundlevel ) ); + } + + CPASAttenuationFilter( const Vector& origin, float attenuation = ATTN_NORM ) : + CPASFilter( origin ) + { + Filter( origin, attenuation ); + } + + CPASAttenuationFilter( CBaseEntity *entity, const char *lookupSound ) : + CPASFilter( static_cast(entity->GetSoundEmissionOrigin()) ) + { + soundlevel_t level = CBaseEntity::LookupSoundLevel( lookupSound ); + float attenuation = SNDLVL_TO_ATTN( level ); + Filter( entity->GetSoundEmissionOrigin(), attenuation ); + } + + CPASAttenuationFilter( const Vector& origin, const char *lookupSound ) : + CPASFilter( origin ) + { + soundlevel_t level = CBaseEntity::LookupSoundLevel( lookupSound ); + float attenuation = SNDLVL_TO_ATTN( level ); + Filter( origin, attenuation ); + } + + CPASAttenuationFilter( CBaseEntity *entity, const char *lookupSound, HSOUNDSCRIPTHANDLE& handle ) : + CPASFilter( static_cast(entity->GetSoundEmissionOrigin()) ) + { + soundlevel_t level = CBaseEntity::LookupSoundLevel( lookupSound, handle ); + float attenuation = SNDLVL_TO_ATTN( level ); + Filter( entity->GetSoundEmissionOrigin(), attenuation ); + } + + CPASAttenuationFilter( const Vector& origin, const char *lookupSound, HSOUNDSCRIPTHANDLE& handle ) : + CPASFilter( origin ) + { + soundlevel_t level = CBaseEntity::LookupSoundLevel( lookupSound, handle ); + float attenuation = SNDLVL_TO_ATTN( level ); + Filter( origin, attenuation ); + } + + + + +public: + void Filter( const Vector& origin, float attenuation = ATTN_NORM ); +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple PVS based filter ( unreliable ) +//----------------------------------------------------------------------------- +class CPVSFilter : public CRecipientFilter +{ +public: + CPVSFilter( const Vector& origin ) + { + AddRecipientsByPVS( origin ); + } +}; + +#endif // RECIPIENTFILTER_H diff --git a/sp/src/game/server/rope.cpp b/sp/src/game/server/rope.cpp new file mode 100644 index 00000000..516015fe --- /dev/null +++ b/sp/src/game/server/rope.cpp @@ -0,0 +1,888 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "rope.h" +#include "entitylist.h" +#include "rope_shared.h" +#include "sendproxy.h" +#include "rope_helpers.h" +#include "te_effect_dispatch.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------- +// Rope Spawn Flags +//-------------------------------------------- +#define SF_ROPE_RESIZE 1 // Automatically resize the rope + +// -------------------------------------------------------------------------------- // +// Fun With Tables. +// -------------------------------------------------------------------------------- // + +LINK_ENTITY_TO_CLASS( move_rope, CRopeKeyframe ); +LINK_ENTITY_TO_CLASS( keyframe_rope, CRopeKeyframe ); + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CRopeKeyframe, DT_RopeKeyframe ) + SendPropEHandle(SENDINFO(m_hStartPoint)), + SendPropEHandle(SENDINFO(m_hEndPoint)), + SendPropInt( SENDINFO(m_iStartAttachment), 5, 0 ), + SendPropInt( SENDINFO(m_iEndAttachment), 5, 0 ), + + SendPropInt( SENDINFO(m_Slack), 12 ), + SendPropInt( SENDINFO(m_RopeLength), 15 ), + SendPropInt( SENDINFO(m_fLockedPoints), 4, SPROP_UNSIGNED ), +#ifdef MAPBASE + SendPropInt( SENDINFO(m_nChangeCount), 8, SPROP_UNSIGNED ), +#endif + SendPropInt( SENDINFO(m_RopeFlags), ROPE_NUMFLAGS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nSegments), 4, SPROP_UNSIGNED ), + SendPropBool( SENDINFO(m_bConstrainBetweenEndpoints) ), + SendPropInt( SENDINFO(m_iRopeMaterialModelIndex), 16, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_Subdiv), 4, SPROP_UNSIGNED ), + + SendPropFloat( SENDINFO(m_TextureScale), 10, 0, 0.1f, 10.0f ), + SendPropFloat( SENDINFO(m_Width), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flScrollSpeed), 0, SPROP_NOSCALE ), + + SendPropVector(SENDINFO(m_vecOrigin), -1, SPROP_COORD ), + SendPropEHandle(SENDINFO_NAME(m_hMoveParent, moveparent) ), + + SendPropInt (SENDINFO(m_iParentAttachment), NUM_PARENTATTACHMENT_BITS, SPROP_UNSIGNED), +END_SEND_TABLE() + + +BEGIN_DATADESC( CRopeKeyframe ) + + DEFINE_FIELD( m_RopeFlags, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_iNextLinkName, FIELD_STRING, "NextKey" ), + DEFINE_KEYFIELD( m_Slack, FIELD_INTEGER, "Slack" ), + DEFINE_KEYFIELD( m_Width, FIELD_FLOAT, "Width" ), + DEFINE_KEYFIELD( m_TextureScale, FIELD_FLOAT, "TextureScale" ), + DEFINE_FIELD( m_nSegments, FIELD_INTEGER ), + DEFINE_FIELD( m_bConstrainBetweenEndpoints, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_strRopeMaterialModel, FIELD_STRING ), + DEFINE_FIELD( m_iRopeMaterialModelIndex, FIELD_MODELINDEX ), + DEFINE_KEYFIELD( m_Subdiv, FIELD_INTEGER, "Subdiv" ), + DEFINE_FIELD( m_RopeLength, FIELD_INTEGER ), + DEFINE_FIELD( m_fLockedPoints, FIELD_INTEGER ), + DEFINE_FIELD( m_bCreatedFromMapFile, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_flScrollSpeed, FIELD_FLOAT, "ScrollSpeed" ), + + DEFINE_FIELD( m_bStartPointValid, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bEndPointValid, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_hStartPoint, FIELD_EHANDLE ), + DEFINE_FIELD( m_hEndPoint, FIELD_EHANDLE ), + DEFINE_FIELD( m_iStartAttachment, FIELD_SHORT ), + DEFINE_FIELD( m_iEndAttachment, FIELD_SHORT ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetScrollSpeed", InputSetScrollSpeed ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetForce", InputSetForce ), + DEFINE_INPUTFUNC( FIELD_VOID, "Break", InputBreak ), + +#ifdef MAPBASE + // Outputs + DEFINE_OUTPUT( m_OnBreak, "OnBreak" ), +#endif + +END_DATADESC() + + + +// -------------------------------------------------------------------------------- // +// CRopeKeyframe implementation. +// -------------------------------------------------------------------------------- // + +CRopeKeyframe::CRopeKeyframe() +{ + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + + m_takedamage = DAMAGE_YES; + + m_iStartAttachment = m_iEndAttachment = 0; + + m_Slack = 0; + m_Width = 2; + m_TextureScale = 4; // 4:1 + m_nSegments = 5; + m_RopeLength = 20; + m_fLockedPoints = (int) (ROPE_LOCK_START_POINT | ROPE_LOCK_END_POINT); // by default, both points are locked + m_flScrollSpeed = 0; +#ifdef MAPBASE + // Because of the keyvalue switch + m_RopeFlags = ROPE_SIMULATE | ROPE_INITIAL_HANG | ROPE_USE_WIND; +#else + m_RopeFlags = ROPE_SIMULATE | ROPE_INITIAL_HANG; +#endif + m_iRopeMaterialModelIndex = -1; + m_Subdiv = 2; + + m_bCreatedFromMapFile = true; +} + + +CRopeKeyframe::~CRopeKeyframe() +{ + // Release transmit state ownership. + SetStartPoint( NULL, 0 ); + SetEndPoint( NULL, 0 ); + SetParent( NULL, 0 ); +} + + +void CRopeKeyframe::SetAttachmentPoint( CBaseHandle &hOutEnt, short &iOutAttachment, CBaseEntity *pEnt, int iAttachment ) +{ + // Unforce our previously attached entity from transmitting. + CBaseEntity *pCurEnt = gEntList.GetBaseEntity( hOutEnt ); + if ( pCurEnt && pCurEnt->edict() ) + { + pCurEnt->DecrementTransmitStateOwnedCounter(); + pCurEnt->DispatchUpdateTransmitState(); + } + + hOutEnt = pEnt; + iOutAttachment = iAttachment; + + // Force this entity to transmit. + if ( pEnt ) + { + pEnt->SetTransmitState( FL_EDICT_ALWAYS ); + pEnt->IncrementTransmitStateOwnedCounter(); + } + + EndpointsChanged(); +} + + +void CRopeKeyframe::SetStartPoint( CBaseEntity *pStartPoint, int attachment ) +{ + SetAttachmentPoint( m_hStartPoint.GetForModify(), m_iStartAttachment.GetForModify(), pStartPoint, attachment ); +} + +void CRopeKeyframe::SetEndPoint( CBaseEntity *pEndPoint, int attachment ) +{ + SetAttachmentPoint( m_hEndPoint.GetForModify(), m_iEndAttachment.GetForModify(), pEndPoint, attachment ); +} + +void CRopeKeyframe::SetParent( CBaseEntity *pNewParent, int iAttachment ) +{ + CBaseEntity *pCurParent = GetMoveParent(); + if ( pCurParent ) + { + pCurParent->DecrementTransmitStateOwnedCounter(); + pCurParent->DispatchUpdateTransmitState(); + } + + // Make sure our move parent always transmits or we get asserts on the client. + if ( pNewParent ) + { + pNewParent->IncrementTransmitStateOwnedCounter(); + pNewParent->SetTransmitState( FL_EDICT_ALWAYS ); + } + + BaseClass::SetParent( pNewParent, iAttachment ); +} + +void CRopeKeyframe::EnablePlayerWeaponAttach( bool bAttach ) +{ + int newFlags = m_RopeFlags; + if ( bAttach ) + newFlags |= ROPE_PLAYER_WPN_ATTACH; + else + newFlags &= ~ROPE_PLAYER_WPN_ATTACH; + + if ( newFlags != m_RopeFlags ) + { + m_RopeFlags = newFlags; + } +} + + +CRopeKeyframe* CRopeKeyframe::Create( + CBaseEntity *pStartEnt, + CBaseEntity *pEndEnt, + int iStartAttachment, + int iEndAttachment, + int ropeWidth, + const char *pMaterialName, +#ifdef MAPBASE + int numSegments, + const char *pClassName +#else + int numSegments +#endif + ) +{ +#ifdef MAPBASE + CRopeKeyframe *pRet = (CRopeKeyframe*)CreateEntityByName( pClassName ); +#else + CRopeKeyframe *pRet = (CRopeKeyframe*)CreateEntityByName( "keyframe_rope" ); +#endif + if( !pRet ) + return NULL; + + pRet->SetStartPoint( pStartEnt, iStartAttachment ); + pRet->SetEndPoint( pEndEnt, iEndAttachment ); + pRet->m_bCreatedFromMapFile = false; + pRet->m_RopeFlags &= ~ROPE_INITIAL_HANG; + + pRet->Init(); + + pRet->SetMaterial( pMaterialName ); + pRet->m_Width = ropeWidth; + pRet->m_nSegments = clamp( numSegments, 2, ROPE_MAX_SEGMENTS ); +#ifdef MAPBASE + pRet->Spawn(); +#endif + + return pRet; +} + + +CRopeKeyframe* CRopeKeyframe::CreateWithSecondPointDetached( + CBaseEntity *pStartEnt, + int iStartAttachment, + int ropeLength, + int ropeWidth, + const char *pMaterialName, + int numSegments, +#ifdef MAPBASE + bool bInitialHang, + const char *pClassName +#else + bool bInitialHang +#endif + ) +{ +#ifdef MAPBASE + CRopeKeyframe *pRet = (CRopeKeyframe*)CreateEntityByName( pClassName ); +#else + CRopeKeyframe *pRet = (CRopeKeyframe*)CreateEntityByName( "keyframe_rope" ); +#endif + if( !pRet ) + return NULL; + + pRet->SetStartPoint( pStartEnt, iStartAttachment ); + pRet->SetEndPoint( NULL, 0 ); + pRet->m_bCreatedFromMapFile = false; + pRet->m_fLockedPoints.Set( ROPE_LOCK_START_POINT ); // Only attach the first point. + + if( !bInitialHang ) + { + pRet->m_RopeFlags &= ~ROPE_INITIAL_HANG; + } + + pRet->Init(); + + pRet->SetMaterial( pMaterialName ); + pRet->m_RopeLength = ropeLength; + pRet->m_Width = ropeWidth; + pRet->m_nSegments = clamp( numSegments, 2, ROPE_MAX_SEGMENTS ); + + return pRet; +} + +void CRopeKeyframe::ActivateStartDirectionConstraints( bool bEnable ) +{ + if (bEnable) + { + m_fLockedPoints.Set( m_fLockedPoints | ROPE_LOCK_START_DIRECTION ); + } + else + { + m_fLockedPoints &= ~((int)ROPE_LOCK_START_DIRECTION); + } +} + + +void CRopeKeyframe::ActivateEndDirectionConstraints( bool bEnable ) +{ + if (bEnable) + { + m_fLockedPoints.Set( m_fLockedPoints | ROPE_LOCK_END_DIRECTION ); + } + else + { + m_fLockedPoints &= ~((int)ROPE_LOCK_END_DIRECTION); + } +} + + +void CRopeKeyframe::ShakeRopes( const Vector &vCenter, float flRadius, float flMagnitude ) +{ + CEffectData shakeData; + shakeData.m_vOrigin = vCenter; + shakeData.m_flRadius = flRadius; + shakeData.m_flMagnitude = flMagnitude; + DispatchEffect( "ShakeRopes", shakeData ); +} + + +bool CRopeKeyframe::SetupHangDistance( float flHangDist ) +{ + CBaseEntity *pEnt1 = m_hStartPoint.Get(); + CBaseEntity *pEnt2 = m_hEndPoint.Get(); + if ( !pEnt1 || !pEnt2 ) + return false; + + // Calculate starting conditions so we can force it to hang down N inches. + Vector v1 = pEnt1->GetAbsOrigin(); + if ( pEnt1->GetBaseAnimating() ) + pEnt1->GetBaseAnimating()->GetAttachment( m_iStartAttachment, v1 ); + + Vector v2 = pEnt2->GetAbsOrigin(); + if ( pEnt2->GetBaseAnimating() ) + pEnt2->GetBaseAnimating()->GetAttachment( m_iEndAttachment, v2 ); + + float flSlack, flLen; + CalcRopeStartingConditions( v1, v2, ROPE_MAX_SEGMENTS, flHangDist, &flLen, &flSlack ); + + m_RopeLength = (int)flLen; + m_Slack = (int)flSlack; + return true; +} + + +void CRopeKeyframe::Init() +{ + SetLocalAngles( vec3_angle ); + RecalculateLength(); + + m_nSegments = clamp( (int) m_nSegments, 2, ROPE_MAX_SEGMENTS ); + + UpdateBBox( true ); + + m_bStartPointValid = (m_hStartPoint.Get() != NULL); + m_bEndPointValid = (m_hEndPoint.Get() != NULL); + +#ifdef MAPBASE + // Sanity-check the rope texture scale before it goes over the wire + if ( m_TextureScale < 0.1f ) + { + Vector origin = GetAbsOrigin(); + GetEndPointPos( 0, origin ); + DevMsg( "move_rope has TextureScale less than 0.1 at (%2.2f, %2.2f, %2.2f)\n", + origin.x, origin.y, origin.z ); + m_TextureScale = 0.1f; + } + else if ( m_TextureScale > 10.0f ) + { + Vector origin = GetAbsOrigin(); + GetEndPointPos( 0, origin ); + DevMsg( "move_rope has TextureScale greater than 10 at (%2.2f, %2.2f, %2.2f)\n", + origin.x, origin.y, origin.z ); + m_TextureScale = 10.0f; + } +#endif +} + + +void CRopeKeyframe::Activate() +{ + BaseClass::Activate(); + + if( !m_bCreatedFromMapFile ) + return; + + // Legacy support.. + if ( m_iRopeMaterialModelIndex == -1 ) + m_iRopeMaterialModelIndex = PrecacheModel( "cable/cable.vmt" ); + + // Find the next entity in our chain. + CBaseEntity *pEnt = gEntList.FindEntityByName( NULL, m_iNextLinkName ); + if( pEnt && pEnt->edict() ) + { + SetEndPoint( pEnt ); + + if( m_spawnflags & SF_ROPE_RESIZE ) + m_RopeFlags |= ROPE_RESIZE; + } + else + { + // If we're from the map file, and we don't have a target ent, and + // "Start Dangling" wasn't set, then this rope keyframe doesn't have + // any rope coming out of it. + if ( m_fLockedPoints & (int)ROPE_LOCK_END_POINT ) + { + m_RopeFlags &= ~ROPE_SIMULATE; + } + } + + // By default, our start point is our own entity. + SetStartPoint( this ); + + // If we don't do this here, then when we save/load, we won't "own" the transmit + // state of our parent, so the client might get our entity without our parent entity. + SetParent( GetParent(), GetParentAttachment() ); + + EndpointsChanged(); + + Init(); +} + +void CRopeKeyframe::EndpointsChanged() +{ + CBaseEntity *pStartEnt = m_hStartPoint.Get(); + if ( pStartEnt ) + { + if ( (pStartEnt != this) || GetMoveParent() ) + { + WatchPositionChanges( this, pStartEnt ); + } + } + CBaseEntity *pEndEnt = m_hEndPoint.Get(); + if ( pEndEnt ) + { + if ( (pEndEnt != this) || GetMoveParent() ) + { + WatchPositionChanges( this, pEndEnt ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Calculate the length of the rope +//----------------------------------------------------------------------------- +void CRopeKeyframe::RecalculateLength( void ) +{ + // Get my entities + if( m_hEndPoint.Get() ) + { + CBaseEntity *pStartEnt = m_hStartPoint.Get(); + CBaseEntity *pEndEnt = m_hEndPoint.Get(); + + // Set the length + m_RopeLength = (int)( pStartEnt->GetAbsOrigin() - pEndEnt->GetAbsOrigin() ).Length(); + } + else + { + m_RopeLength = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: This should remove the rope next time it reaches a resting state. +// Right now only the client knows when it reaches a resting state, so +// for now it just removes itself after a short time. +//----------------------------------------------------------------------------- +void CRopeKeyframe::DieAtNextRest( void ) +{ + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 1.0f ); +} + + +void CRopeKeyframe::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + if ( !pInfo->m_pTransmitEdict->Get( entindex() ) ) + { + BaseClass::SetTransmit( pInfo, bAlways ); + + // Make sure our target ents are sent too. + CBaseEntity *pEnt = m_hStartPoint; + if ( pEnt ) + pEnt->SetTransmit( pInfo, bAlways ); + + pEnt = m_hEndPoint; + if ( pEnt ) + pEnt->SetTransmit( pInfo, bAlways ); + } +} + + +bool CRopeKeyframe::GetEndPointPos2( CBaseEntity *pAttached, int iAttachment, Vector &vPos ) +{ + if( !pAttached ) + return false; + + if ( iAttachment > 0 ) + { + CBaseAnimating *pAnim = pAttached->GetBaseAnimating(); + if ( pAnim ) + { + if( !pAnim->GetAttachment( iAttachment, vPos ) ) + return false; + } + else + { + return false; + } + } + else + { + vPos = pAttached->GetAbsOrigin(); + } + + return true; +} + + +bool CRopeKeyframe::GetEndPointPos( int iPt, Vector &v ) +{ + if ( iPt == 0 ) + return GetEndPointPos2( m_hStartPoint, m_iStartAttachment, v ); + else + return GetEndPointPos2( m_hEndPoint, m_iEndAttachment, v ); +} + + +void CRopeKeyframe::UpdateBBox( bool bForceRelink ) +{ + Vector v1, v2; + Vector vMin, vMax; + if ( GetEndPointPos( 0, v1 ) ) + { + if ( GetEndPointPos( 1, v2 ) ) + { + VectorMin( v1, v2, vMin ); + VectorMax( v1, v2, vMax ); + + // Set our bounds to enclose both endpoints and relink. + vMin -= GetAbsOrigin(); + vMax -= GetAbsOrigin(); + } + else + { + vMin = vMax = v1 - GetAbsOrigin(); + } + } + else + { + vMin = vMax = Vector( 0, 0, 0 ); + } + + if ( WorldAlignMins() != vMin || WorldAlignMaxs() != vMax ) + { + UTIL_SetSize( this, vMin, vMax ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : Propagate force to each link in the rope. Check for loops +// Input : +// Output : +//------------------------------------------------------------------------------ +void CRopeKeyframe::PropagateForce(CBaseEntity *pActivator, CBaseEntity *pCaller, CBaseEntity *pFirstLink, float x, float y, float z) +{ + EntityMessageBegin( this, true ); + WRITE_FLOAT( x ); + WRITE_FLOAT( y ); + WRITE_FLOAT( z ); + MessageEnd(); + + // UNDONE: Doesn't deal with intermediate loops + // Propagate to next segment + CRopeKeyframe *pNextLink = dynamic_cast((CBaseEntity *)m_hEndPoint); + if (pNextLink && pNextLink != pFirstLink) + { + pNextLink->PropagateForce(pActivator, pCaller, pFirstLink, x, y, z); + } +} + +//------------------------------------------------------------------------------ +// Purpose: Set an instaneous force on the rope. +// Input : Force vector. +//------------------------------------------------------------------------------ +void CRopeKeyframe::InputSetForce( inputdata_t &inputdata ) +{ + Vector vecForce; + inputdata.value.Vector3D(vecForce); + PropagateForce( inputdata.pActivator, inputdata.pCaller, this, vecForce.x, vecForce.y, vecForce.z ); +} + +//----------------------------------------------------------------------------- +// Purpose: Breaks the rope if able +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CRopeKeyframe::InputBreak( inputdata_t &inputdata ) +{ + //Route through the damage code +#ifdef MAPBASE + Break(inputdata.pActivator); +#else + Break(); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Breaks the rope +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +bool CRopeKeyframe::Break( CBaseEntity *pActivator ) +#else +bool CRopeKeyframe::Break( void ) +#endif +{ + DetachPoint( 0 ); + +#ifdef MAPBASE + m_OnBreak.FireOutput(pActivator ? pActivator : this, this); +#endif + + // Find whoever references us and detach us from them. + // UNDONE: PERFORMANCE: This is very slow!!! + CRopeKeyframe *pTest = NULL; + pTest = gEntList.NextEntByClass( pTest ); + while ( pTest ) + { + if( stricmp( STRING(pTest->m_iNextLinkName), STRING(GetEntityName()) ) == 0 ) + { + pTest->DetachPoint( 1 ); + } + + pTest = gEntList.NextEntByClass( pTest ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRopeKeyframe::NotifyPositionChanged( CBaseEntity *pEntity ) +{ +#ifdef MAPBASE + ++m_nChangeCount; +#endif + + // Update our bbox? + UpdateBBox( false ); + + CBaseEntity *ents[2] = { m_hStartPoint.Get(), m_hEndPoint.Get() }; + if ( (m_RopeFlags & ROPE_RESIZE) && ents[0] && ents[0]->edict() && ents[1] && ents[1]->edict() ) + { + int len = (int)( ents[0]->GetAbsOrigin() - ents[1]->GetAbsOrigin() ).Length() + m_Slack; + if ( len != m_RopeLength ) + { + m_RopeLength = len; + } + } + + // Figure out if our attachment points have gone away and make sure to update the client if they have. + bool *pValid[2] = { &m_bStartPointValid, &m_bEndPointValid }; + for ( int i=0; i < 2; i++ ) + { + bool bCurrentlyValid = ( ents[i] != NULL ); + if ( *pValid[i] != bCurrentlyValid ) + { + *pValid[i] = bCurrentlyValid; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Take damage will break the rope +//----------------------------------------------------------------------------- +int CRopeKeyframe::OnTakeDamage( const CTakeDamageInfo &info ) +{ + // Only allow this if it's been marked + if( !(m_RopeFlags & ROPE_BREAKABLE) ) + return false; + +#ifdef MAPBASE + Break(info.GetAttacker()); +#else + Break(); +#endif + return 0; +} + + +void CRopeKeyframe::Precache() +{ + m_iRopeMaterialModelIndex = PrecacheModel( STRING( m_strRopeMaterialModel ) ); + BaseClass::Precache(); +} + +#ifdef MAPBASE +void CRopeKeyframe::Spawn( void ) +{ + BaseClass::Spawn(); + Precache(); +} +#endif + + +void CRopeKeyframe::DetachPoint( int iPoint ) +{ + Assert( iPoint == 0 || iPoint == 1 ); + + m_fLockedPoints &= ~(1 << iPoint); +} + + +void CRopeKeyframe::EnableCollision() +{ + if( !( m_RopeFlags & ROPE_COLLIDE ) ) + { + m_RopeFlags |= ROPE_COLLIDE; + } +} + +void CRopeKeyframe::EnableWind( bool bEnable ) +{ + int flag = 0; +#ifdef MAPBASE + if ( bEnable ) + flag |= ROPE_USE_WIND; + + if ( (m_RopeFlags & ROPE_USE_WIND) != flag ) +#else + if ( !bEnable ) + flag |= ROPE_NO_WIND; + + if ( (m_RopeFlags & ROPE_NO_WIND) != flag ) +#endif + { + m_RopeFlags |= flag; + } +} + + +bool CRopeKeyframe::KeyValue( const char *szKeyName, const char *szValue ) +{ + if( stricmp( szKeyName, "Breakable" ) == 0 ) + { + if( atoi( szValue ) == 1 ) + m_RopeFlags |= ROPE_BREAKABLE; + } + else if( stricmp( szKeyName, "Collide" ) == 0 ) + { + if( atoi( szValue ) == 1 ) + m_RopeFlags |= ROPE_COLLIDE; + } + else if( stricmp( szKeyName, "Barbed" ) == 0 ) + { + if( atoi( szValue ) == 1 ) + m_RopeFlags |= ROPE_BARBED; + } + else if( stricmp( szKeyName, "Dangling" ) == 0 ) + { + if( atoi( szValue ) == 1 ) + m_fLockedPoints &= ~ROPE_LOCK_END_POINT; // detach our dest point + + return true; + } + else if( stricmp( szKeyName, "Type" ) == 0 ) + { + int iType = atoi( szValue ); + if( iType == 0 ) + m_nSegments = ROPE_MAX_SEGMENTS; + else if( iType == 1 ) + m_nSegments = ROPE_TYPE1_NUMSEGMENTS; + else + m_nSegments = ROPE_TYPE2_NUMSEGMENTS; + } + else if ( stricmp( szKeyName, "RopeShader" ) == 0 ) + { + // Legacy support for the RopeShader parameter. + int iShader = atoi( szValue ); +#ifdef MAPBASE + if ( iShader == 0 ) + { + m_strRopeMaterialModel = AllocPooledString( "cable/cable.vmt" ); + } + else if ( iShader == 1 ) + { + m_strRopeMaterialModel = AllocPooledString( "cable/rope.vmt" ); + } + else + { + m_strRopeMaterialModel = AllocPooledString( "cable/chain.vmt" ); + } +#else + if ( iShader == 0 ) + { + m_iRopeMaterialModelIndex = PrecacheModel( "cable/cable.vmt" ); + } + else if ( iShader == 1 ) + { + m_iRopeMaterialModelIndex = PrecacheModel( "cable/rope.vmt" ); + } + else + { + m_iRopeMaterialModelIndex = PrecacheModel( "cable/chain.vmt" ); + } +#endif + } + else if ( stricmp( szKeyName, "RopeMaterial" ) == 0 ) + { + // Make sure we have a vmt extension. + if ( Q_stristr( szValue, ".vmt" ) ) + { + SetMaterial( szValue ); + } + else + { + char str[512]; + Q_snprintf( str, sizeof( str ), "%s.vmt", szValue ); + SetMaterial( str ); + } + } +#ifdef MAPBASE + else if( stricmp( szKeyName, "UseWind" ) == 0 ) + { + if( atoi( szValue ) == 1 ) + m_RopeFlags |= ROPE_USE_WIND; + else + m_RopeFlags &= ~ROPE_USE_WIND; + } +#endif + else if ( stricmp( szKeyName, "NoWind" ) == 0 ) + { +#ifdef MAPBASE + if ( atoi( szValue ) != 1 ) + m_RopeFlags |= ROPE_USE_WIND; + else + m_RopeFlags &= ~ROPE_USE_WIND; +#else + if ( atoi( szValue ) == 1 ) + { + m_RopeFlags |= ROPE_NO_WIND; + } +#endif + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that sets the scroll speed. +//----------------------------------------------------------------------------- +void CRopeKeyframe::InputSetScrollSpeed( inputdata_t &inputdata ) +{ + m_flScrollSpeed = inputdata.value.Float(); +} + + +void CRopeKeyframe::SetMaterial( const char *pName ) +{ + m_strRopeMaterialModel = AllocPooledString( pName ); +#ifndef MAPBASE + m_iRopeMaterialModelIndex = PrecacheModel( STRING( m_strRopeMaterialModel ) ); +#endif +} + +int CRopeKeyframe::UpdateTransmitState() +{ + // Certain entities like sprites and ropes are strewn throughout the level and they rarely change. + // For these entities, it's more efficient to transmit them once and then always leave them on + // the client. Otherwise, the server will have to send big bursts of data with the entity states + // as they come in and out of the PVS. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + + + diff --git a/sp/src/game/server/rope.h b/sp/src/game/server/rope.h new file mode 100644 index 00000000..74037800 --- /dev/null +++ b/sp/src/game/server/rope.h @@ -0,0 +1,226 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef ROPE_H +#define ROPE_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" + +#include "positionwatcher.h" + +class CRopeKeyframe : public CBaseEntity, public IPositionWatcher +{ + DECLARE_CLASS( CRopeKeyframe, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CRopeKeyframe(); + virtual ~CRopeKeyframe(); + + // Create a rope and attach it to two entities. + // Attachment points on the entities are optional. + static CRopeKeyframe* Create( + CBaseEntity *pStartEnt, + CBaseEntity *pEndEnt, + int iStartAttachment=0, + int iEndAttachment=0, + int ropeWidth = 2, + const char *pMaterialName = "cable/cable.vmt", // Note: whoever creates the rope must + // use PrecacheModel for whatever material + // it specifies here. +#ifdef MAPBASE + int numSegments = 5, + const char *pClassName = "keyframe_rope" +#else + int numSegments = 5 +#endif + ); + + static CRopeKeyframe* CreateWithSecondPointDetached( + CBaseEntity *pStartEnt, + int iStartAttachment = 0, // must be 0 if you don't want to use a specific model attachment. + int ropeLength = 20, + int ropeWidth = 2, + const char *pMaterialName = "cable/cable.vmt", // Note: whoever creates the rope + // use PrecacheModel for whatever material + // it specifies here. + int numSegments = 5, +#ifdef MAPBASE + bool bInitialHang = false, + const char *pClassName = "keyframe_rope" +#else + bool bInitialHang = false +#endif + ); + + bool SetupHangDistance( float flHangDist ); + void ActivateStartDirectionConstraints( bool bEnable ); + void ActivateEndDirectionConstraints( bool bEnable ); + + + // Shakes all ropes near vCenter. The higher flMagnitude is, the larger the shake will be. + static void ShakeRopes( const Vector &vCenter, float flRadius, float flMagnitude ); + + +// CBaseEntity overrides. +public: + + // don't cross transitions + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + virtual void Activate(); + virtual void Precache(); +#ifdef MAPBASE + virtual void Spawn(); +#endif + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + + void PropagateForce(CBaseEntity *pActivator, CBaseEntity *pCaller, CBaseEntity *pFirstLink, float x, float y, float z); + + // Once-off length recalculation + void RecalculateLength( void ); + + // Kill myself when I next come to rest + void DieAtNextRest( void ); + + virtual int UpdateTransmitState(void); + virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + virtual void SetParent( CBaseEntity *pParentEntity, int iAttachment ); + +// Input functions. +public: + + void InputSetScrollSpeed( inputdata_t &inputdata ); + void InputSetForce( inputdata_t &inputdata ); + void InputBreak( inputdata_t &inputdata ); + +public: + +#ifdef MAPBASE + bool Break( CBaseEntity *pActivator = NULL ); +#else + bool Break( void ); +#endif + void DetachPoint( int iPoint ); + + void EndpointsChanged(); + + // By default, ropes don't collide with the world. Call this to enable it. + void EnableCollision(); + + // Toggle wind. + void EnableWind( bool bEnable ); + +#ifdef MAPBASE + CBaseEntity* GetStartPoint() { return m_hStartPoint.Get(); } + int GetStartAttachment() { return m_iStartAttachment; }; +#else + // Unless this is called during initialization, the caller should have done + // PrecacheModel on whatever material they specify in here. + void SetMaterial( const char *pName ); +#endif + + CBaseEntity* GetEndPoint() { return m_hEndPoint.Get(); } +#ifdef MAPBASE + int GetEndAttachment() { return m_iEndAttachment; }; +#else + int GetEndAttachment() { return m_iStartAttachment; }; +#endif + + void SetStartPoint( CBaseEntity *pStartPoint, int attachment = 0 ); + void SetEndPoint( CBaseEntity *pEndPoint, int attachment = 0 ); + + // See ROPE_PLAYER_WPN_ATTACH for info. + void EnablePlayerWeaponAttach( bool bAttach ); + + + // IPositionWatcher + virtual void NotifyPositionChanged( CBaseEntity *pEntity ); + +private: + +#ifdef MAPBASE + // Unless this is called during initialization, the caller should have done + // PrecacheModel on whatever material they specify in here. + void SetMaterial( const char *pName ); + +protected: +#endif + + void SetAttachmentPoint( CBaseHandle &hOutEnt, short &iOutAttachment, CBaseEntity *pEnt, int iAttachment ); + + // This is normally called by Activate but if you create the rope at runtime, + // you must call it after you have setup its variables. +#ifdef MAPBASE + virtual void Init(); + +private: +#else + void Init(); +#endif + + // These work just like the client-side versions. + bool GetEndPointPos2( CBaseEntity *pEnt, int iAttachment, Vector &v ); + bool GetEndPointPos( int iPt, Vector &v ); + + void UpdateBBox( bool bForceRelink ); + + +public: + + CNetworkVar( int, m_RopeFlags ); // Combination of ROPE_ defines in rope_shared.h + + string_t m_iNextLinkName; + CNetworkVar( int, m_Slack ); + CNetworkVar( float, m_Width ); + CNetworkVar( float, m_TextureScale ); + CNetworkVar( int, m_nSegments ); // Number of segments. + CNetworkVar( bool, m_bConstrainBetweenEndpoints ); + + string_t m_strRopeMaterialModel; + CNetworkVar( int, m_iRopeMaterialModelIndex ); // Index of sprite model with the rope's material. + + // Number of subdivisions in between segments. + CNetworkVar( int, m_Subdiv ); + +#ifdef MAPBASE + // Used simply to wake up rope on the client side if it has gone to sleep + CNetworkVar( unsigned char, m_nChangeCount ); +#endif + + //EHANDLE m_hNextLink; + + CNetworkVar( int, m_RopeLength ); // Rope length at startup, used to calculate tension. + + CNetworkVar( int, m_fLockedPoints ); + + bool m_bCreatedFromMapFile; // set to false when creating at runtime + + CNetworkVar( float, m_flScrollSpeed ); + +private: + // Used to detect changes. + bool m_bStartPointValid; + bool m_bEndPointValid; + + CNetworkHandle( CBaseEntity, m_hStartPoint ); // StartPoint/EndPoint are entities + CNetworkHandle( CBaseEntity, m_hEndPoint ); + CNetworkVar( short, m_iStartAttachment ); // StartAttachment/EndAttachment are attachment points. + CNetworkVar( short, m_iEndAttachment ); + +#ifdef MAPBASE + COutputEvent m_OnBreak; +#endif +}; + + +#endif // ROPE_H diff --git a/sp/src/game/server/saverestore_gamedll.cpp b/sp/src/game/server/saverestore_gamedll.cpp new file mode 100644 index 00000000..a1f9505d --- /dev/null +++ b/sp/src/game/server/saverestore_gamedll.cpp @@ -0,0 +1,254 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "isaverestore.h" +#include "saverestoretypes.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: iterates through a typedescript data block, so it can insert key/value data into the block +// Input : *pObject - pointer to the struct or class the data is to be insterted into +// *pFields - description of the data +// iNumFields - number of fields contained in pFields +// char *szKeyName - name of the variable to look for +// char *szValue - value to set the variable to +// Output : Returns true if the variable is found and set, false if the key is not found. +//----------------------------------------------------------------------------- +bool ParseKeyvalue( void *pObject, typedescription_t *pFields, int iNumFields, const char *szKeyName, const char *szValue ) +{ + int i; + typedescription_t *pField; + + for ( i = 0; i < iNumFields; i++ ) + { + pField = &pFields[i]; + + int fieldOffset = pField->fieldOffset[ TD_OFFSET_NORMAL ]; + + // Check the nested classes, but only if they aren't in array form. + if ((pField->fieldType == FIELD_EMBEDDED) && (pField->fieldSize == 1)) + { + for ( datamap_t *dmap = pField->td; dmap != NULL; dmap = dmap->baseMap ) + { + void *pEmbeddedObject = (void*)((char*)pObject + fieldOffset); + if ( ParseKeyvalue( pEmbeddedObject, dmap->dataDesc, dmap->dataNumFields, szKeyName, szValue) ) + return true; + } + } + + if ( (pField->flags & FTYPEDESC_KEY) && !stricmp(pField->externalName, szKeyName) ) + { + switch( pField->fieldType ) + { + case FIELD_MODELNAME: + case FIELD_SOUNDNAME: + case FIELD_STRING: + (*(string_t *)((char *)pObject + fieldOffset)) = AllocPooledString( szValue ); + return true; + + case FIELD_TIME: + case FIELD_FLOAT: + (*(float *)((char *)pObject + fieldOffset)) = atof( szValue ); + return true; + + case FIELD_BOOLEAN: + (*(bool *)((char *)pObject + fieldOffset)) = (bool)(atoi( szValue ) != 0); + return true; + + case FIELD_CHARACTER: + (*(char *)((char *)pObject + fieldOffset)) = (char)atoi( szValue ); + return true; + + case FIELD_SHORT: + (*(short *)((char *)pObject + fieldOffset)) = (short)atoi( szValue ); + return true; + + case FIELD_INTEGER: + case FIELD_TICK: + (*(int *)((char *)pObject + fieldOffset)) = atoi( szValue ); + return true; + + case FIELD_POSITION_VECTOR: + case FIELD_VECTOR: + UTIL_StringToVector( (float *)((char *)pObject + fieldOffset), szValue ); + return true; + + case FIELD_VMATRIX: + case FIELD_VMATRIX_WORLDSPACE: + UTIL_StringToFloatArray( (float *)((char *)pObject + fieldOffset), 16, szValue ); + return true; + + case FIELD_MATRIX3X4_WORLDSPACE: + UTIL_StringToFloatArray( (float *)((char *)pObject + fieldOffset), 12, szValue ); + return true; + + case FIELD_COLOR32: + UTIL_StringToColor32( (color32 *) ((char *)pObject + fieldOffset), szValue ); + return true; + +#ifdef MAPBASE + case FIELD_EHANDLE: + ((CBaseHandle*)((char*)pObject + fieldOffset))->Set(gEntList.FindEntityByName(NULL, szValue)); + return true; + + case FIELD_INTERVAL: + extern interval_t ReadInterval( const char *pString ); + (*(interval_t*)((char *)pObject + fieldOffset)) = ReadInterval( szValue ); + return true; +#endif + + case FIELD_CUSTOM: + { + SaveRestoreFieldInfo_t fieldInfo = + { + (char *)pObject + fieldOffset, + pObject, + pField + }; + pField->pSaveRestoreOps->Parse( fieldInfo, szValue ); + return true; + } + + default: +#ifndef MAPBASE + case FIELD_INTERVAL: // Fixme, could write this if needed +#endif + case FIELD_CLASSPTR: + case FIELD_MODELINDEX: + case FIELD_MATERIALINDEX: + case FIELD_EDICT: + Warning( "Bad field in entity!!\n" ); + Assert(0); + break; + } + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: iterates through a typedescript data block, so it can insert key/value data into the block +// Input : *pObject - pointer to the struct or class the data is to be insterted into +// *pFields - description of the data +// iNumFields - number of fields contained in pFields +// char *szKeyName - name of the variable to look for +// char *szValue - value to set the variable to +// Output : Returns true if the variable is found and set, false if the key is not found. +//----------------------------------------------------------------------------- +bool ExtractKeyvalue( void *pObject, typedescription_t *pFields, int iNumFields, const char *szKeyName, char *szValue, int iMaxLen ) +{ + int i; + typedescription_t *pField; + + for ( i = 0; i < iNumFields; i++ ) + { + pField = &pFields[i]; + + int fieldOffset = pField->fieldOffset[ TD_OFFSET_NORMAL ]; + + // Check the nested classes, but only if they aren't in array form. + if ((pField->fieldType == FIELD_EMBEDDED) && (pField->fieldSize == 1)) + { + for ( datamap_t *dmap = pField->td; dmap != NULL; dmap = dmap->baseMap ) + { + void *pEmbeddedObject = (void*)((char*)pObject + fieldOffset); + if ( ExtractKeyvalue( pEmbeddedObject, dmap->dataDesc, dmap->dataNumFields, szKeyName, szValue, iMaxLen ) ) + return true; + } + } + + if ( (pField->flags & FTYPEDESC_KEY) && !stricmp(pField->externalName, szKeyName) ) + { + switch( pField->fieldType ) + { + case FIELD_MODELNAME: + case FIELD_SOUNDNAME: + case FIELD_STRING: + Q_strncpy( szValue, ((char *)pObject + fieldOffset), iMaxLen ); + return true; + + case FIELD_TIME: + case FIELD_FLOAT: + Q_snprintf( szValue, iMaxLen, "%f", (*(float *)((char *)pObject + fieldOffset)) ); + return true; + + case FIELD_BOOLEAN: + Q_snprintf( szValue, iMaxLen, "%d", (*(bool *)((char *)pObject + fieldOffset)) != 0); + return true; + + case FIELD_CHARACTER: + Q_snprintf( szValue, iMaxLen, "%d", (*(char *)((char *)pObject + fieldOffset)) ); + return true; + + case FIELD_SHORT: + Q_snprintf( szValue, iMaxLen, "%d", (*(short *)((char *)pObject + fieldOffset)) ); + return true; + + case FIELD_INTEGER: + case FIELD_TICK: + Q_snprintf( szValue, iMaxLen, "%d", (*(int *)((char *)pObject + fieldOffset)) ); + return true; + + case FIELD_POSITION_VECTOR: + case FIELD_VECTOR: + Q_snprintf( szValue, iMaxLen, "%f %f %f", + ((float *)((char *)pObject + fieldOffset))[0], + ((float *)((char *)pObject + fieldOffset))[1], + ((float *)((char *)pObject + fieldOffset))[2] ); + return true; + + case FIELD_VMATRIX: + case FIELD_VMATRIX_WORLDSPACE: + //UTIL_StringToFloatArray( (float *)((char *)pObject + fieldOffset), 16, szValue ); + return false; + + case FIELD_MATRIX3X4_WORLDSPACE: + //UTIL_StringToFloatArray( (float *)((char *)pObject + fieldOffset), 12, szValue ); + return false; + + case FIELD_COLOR32: + Q_snprintf( szValue, iMaxLen, "%d %d %d %d", + ((int *)((char *)pObject + fieldOffset))[0], + ((int *)((char *)pObject + fieldOffset))[1], + ((int *)((char *)pObject + fieldOffset))[2], + ((int *)((char *)pObject + fieldOffset))[3] ); + return true; + + case FIELD_CUSTOM: + { + /* + SaveRestoreFieldInfo_t fieldInfo = + { + (char *)pObject + fieldOffset, + pObject, + pField + }; + pField->pSaveRestoreOps->Parse( fieldInfo, szValue ); + */ + return false; + } + + default: + case FIELD_INTERVAL: // Fixme, could write this if needed + case FIELD_CLASSPTR: + case FIELD_MODELINDEX: + case FIELD_MATERIALINDEX: + case FIELD_EDICT: + Warning( "Bad field in entity!!\n" ); + Assert(0); + break; + } + } + } + + return false; +} diff --git a/sp/src/game/server/sceneentity.cpp b/sp/src/game/server/sceneentity.cpp new file mode 100644 index 00000000..0437c280 --- /dev/null +++ b/sp/src/game/server/sceneentity.cpp @@ -0,0 +1,6355 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include +#include "baseflex.h" +#include "entitylist.h" +#include "choreoevent.h" +#include "choreoactor.h" +#include "choreochannel.h" +#include "choreoscene.h" +#include "studio.h" +#include "networkstringtable_gamedll.h" +#include "ai_basenpc.h" +#include "engine/IEngineSound.h" +#include "ai_navigator.h" +#include "saverestore_utlvector.h" +#include "ai_baseactor.h" +#include "AI_Criteria.h" +#include "tier1/strtools.h" +#include "checksum_crc.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "utlbuffer.h" +#include "tier0/icommandline.h" +#include "sceneentity.h" +#include "datacache/idatacache.h" +#include "dt_utlvector_send.h" +#include "ichoreoeventcallback.h" +#include "scenefilecache/ISceneFileCache.h" +#include "SceneCache.h" +#include "scripted.h" +#include "env_debughistory.h" +#include "team.h" +#include "triggers.h" + +#ifdef HL2_EPISODIC +#include "npc_alyx_episodic.h" +#endif // HL2_EPISODIC + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern ISoundEmitterSystemBase *soundemitterbase; +extern ISceneFileCache *scenefilecache; + +class CSceneEntity; +class CBaseFlex; + +// VCDS are loaded from their compiled/binary format (much faster) +// Requies vcds be saved as compiled assets +//#define COMPILED_VCDS 1 + +static ConVar scene_forcecombined( "scene_forcecombined", "0", 0, "When playing back, force use of combined .wav files even in english." ); +static ConVar scene_maxcaptionradius( "scene_maxcaptionradius", "1200", 0, "Only show closed captions if recipient is within this many units of speaking actor (0==disabled)." ); + +// Assume sound system is 100 msec lagged (only used if we can't find snd_mixahead cvar!) +#define SOUND_SYSTEM_LATENCY_DEFAULT ( 0.1f ) + +// Think every 50 msec (FIXME: Try 10hz?) +#define SCENE_THINK_INTERVAL 0.001 // FIXME: make scene's think in concert with their npc's + +#define FINDNAMEDENTITY_MAX_ENTITIES 32 // max number of entities to be considered for random entity selection in FindNamedEntity + +// List of the last 5 lines of speech from NPCs for bug reports +static recentNPCSpeech_t speechListSounds[ SPEECH_LIST_MAX_SOUNDS ] = { { 0, "", "" }, { 0, "", "" }, { 0, "", "" }, { 0, "", "" }, { 0, "", "" } }; +static int speechListIndex = 0; + +// Only allow scenes to change their pitch within a range of values +#define SCENE_MIN_PITCH 0.25f +#define SCENE_MAX_PITCH 2.5f + +// New macros introduced for Mapbase's console message color changes. +#ifdef MAPBASE +#define ChoreoMsg( lvl, msg ) CGMsg( lvl, CON_GROUP_CHOREO, msg ) +#define ChoreoMsg1( lvl, msg, a ) CGMsg( lvl, CON_GROUP_CHOREO, msg, a ) +#define ChoreoMsg2( lvl, msg, a, b ) CGMsg( lvl, CON_GROUP_CHOREO, msg, a, b ) +#else +#define ChoreoMsg( lvl, msg ) DevMsg( lvl, msg ) +#define ChoreoMsg1( lvl, msg, a ) DevMsg( lvl, msg, a ) +#define ChoreoMsg2( lvl, msg, a, b ) DevMsg( lvl, msg, a, b ) +#endif + +//=========================================================================================================== +// SCENE LIST MANAGER +//=========================================================================================================== +#define SCENE_LIST_MANAGER_MAX_SCENES 16 + +//----------------------------------------------------------------------------- +// Purpose: Entity that manages a list of scenes +//----------------------------------------------------------------------------- +class CSceneListManager : public CLogicalEntity +{ + DECLARE_CLASS( CSceneListManager, CLogicalEntity ); +public: + DECLARE_DATADESC(); +#ifdef MAPBASE_VSCRIPT + DECLARE_ENT_SCRIPTDESC(); + + HSCRIPT ScriptGetScene( int iIndex ); +#endif + + virtual void Activate( void ); + + void ShutdownList( void ); + void SceneStarted( CBaseEntity *pSceneOrManager ); + void AddListManager( CSceneListManager *pManager ); + void RemoveScene( int iIndex ); + + // Inputs + void InputShutdown( inputdata_t &inputdata ); + +private: + CUtlVector< CHandle< CSceneListManager > > m_hListManagers; + string_t m_iszScenes[SCENE_LIST_MANAGER_MAX_SCENES]; + EHANDLE m_hScenes[SCENE_LIST_MANAGER_MAX_SCENES]; +}; + +//----------------------------------------------------------------------------- +// Purpose: This class exists solely to call think on all scene entities in a deterministic order +//----------------------------------------------------------------------------- +class CSceneManager : public CBaseEntity +{ + DECLARE_CLASS( CSceneManager, CBaseEntity ); + DECLARE_DATADESC(); + +public: + virtual void Spawn() + { + BaseClass::Spawn(); + SetNextThink( gpGlobals->curtime ); + } + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_DONT_SAVE; } + + virtual void Think(); + + void ClearAllScenes(); + + void AddSceneEntity( CSceneEntity *scene ); + void RemoveSceneEntity( CSceneEntity *scene ); + + void QueueRestoredSound( CBaseFlex *actor, char const *soundname, soundlevel_t soundlevel, float time_in_past ); + + void OnClientActive( CBasePlayer *player ); + + void RemoveActorFromScenes( CBaseFlex *pActor, bool bInstancedOnly, bool bNonIdleOnly, const char *pszThisSceneOnly ); + void RemoveScenesInvolvingActor( CBaseFlex *pActor ); + void PauseActorsScenes( CBaseFlex *pActor, bool bInstancedOnly ); + bool IsInInterruptableScenes( CBaseFlex *pActor ); + void ResumeActorsScenes( CBaseFlex *pActor, bool bInstancedOnly ); + void QueueActorsScenesToResume( CBaseFlex *pActor, bool bInstancedOnly ); + bool IsRunningScriptedScene( CBaseFlex *pActor, bool bIgnoreInstancedScenes ); + bool IsRunningScriptedSceneAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes ); + bool IsRunningScriptedSceneWithSpeech( CBaseFlex *pActor, bool bIgnoreInstancedScenes ); + bool IsRunningScriptedSceneWithSpeechAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes ); + +#ifdef MAPBASE + bool IsRunningScriptedSceneWithFlexAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes, const char *pszNotThisScene = NULL ); + + CUtlVector< CHandle< CSceneEntity > > *GetActiveSceneList(); +#endif + + +private: + + struct CRestoreSceneSound + { + CRestoreSceneSound() + { + actor = NULL; + soundname[ 0 ] = NULL; + soundlevel = SNDLVL_NORM; + time_in_past = 0.0f; + } + + CHandle< CBaseFlex > actor; + char soundname[ 128 ]; + soundlevel_t soundlevel; + float time_in_past; + }; + + CUtlVector< CHandle< CSceneEntity > > m_ActiveScenes; + + CUtlVector< CRestoreSceneSound > m_QueuedSceneSounds; +}; + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CSceneManager ) + + DEFINE_UTLVECTOR( m_ActiveScenes, FIELD_EHANDLE ), + // DEFINE_FIELD( m_QueuedSceneSounds, CUtlVector < CRestoreSceneSound > ), // Don't save/restore this, it's created and used by OnRestore only + +END_DATADESC() + +#ifdef DISABLE_DEBUG_HISTORY +#define LocalScene_Printf Scene_Printf +#else +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pFormat - +// ... - +// Output : static void +//----------------------------------------------------------------------------- +void LocalScene_Printf( const char *pFormat, ... ) +{ + va_list marker; + char msg[8192]; + + va_start(marker, pFormat); + Q_vsnprintf(msg, sizeof(msg), pFormat, marker); + va_end(marker); + + Scene_Printf( "%s", msg ); + ADD_DEBUG_HISTORY( HISTORY_SCENE_PRINT, UTIL_VarArgs( "(%0.2f) %s", gpGlobals->curtime, msg ) ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *filename - +// **buffer - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CopySceneFileIntoMemory( char const *pFilename, void **pBuffer, int *pSize ) +{ + size_t bufSize = scenefilecache->GetSceneBufferSize( pFilename ); + if ( bufSize > 0 ) + { + *pBuffer = new byte[bufSize]; + *pSize = bufSize; + return scenefilecache->GetSceneData( pFilename, (byte *)(*pBuffer), bufSize ); + } + + *pBuffer = 0; + *pSize = 0; + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void FreeSceneFileMemory( void *buffer ) +{ + delete[] (byte*) buffer; +} + +//----------------------------------------------------------------------------- +// Binary compiled VCDs get their strings from a pool +//----------------------------------------------------------------------------- +class CChoreoStringPool : public IChoreoStringPool +{ +public: + short FindOrAddString( const char *pString ) + { + // huh?, no compilation at run time, only fetches + Assert( 0 ); + return -1; + } + + bool GetString( short stringId, char *buff, int buffSize ) + { + // fetch from compiled pool + const char *pString = scenefilecache->GetSceneString( stringId ); + if ( !pString ) + { + V_strncpy( buff, "", buffSize ); + return false; + } + V_strncpy( buff, pString, buffSize ); + return true; + } +}; +CChoreoStringPool g_ChoreoStringPool; + +//----------------------------------------------------------------------------- +// Purpose: Singleton scene manager. Created by first placed scene or recreated it it's deleted for some unknown reason +// Output : CSceneManager +//----------------------------------------------------------------------------- +CSceneManager *GetSceneManager() +{ + // Create it if it doesn't exist + static CHandle< CSceneManager > s_SceneManager; + if ( s_SceneManager == NULL ) + { + s_SceneManager = ( CSceneManager * )CreateEntityByName( "scene_manager" ); + Assert( s_SceneManager ); + if ( s_SceneManager ) + { + s_SceneManager->Spawn(); + } + } + + Assert( s_SceneManager ); + return s_SceneManager; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +//----------------------------------------------------------------------------- +void SceneManager_ClientActive( CBasePlayer *player ) +{ + Assert( GetSceneManager() ); + + if ( GetSceneManager() ) + { + GetSceneManager()->OnClientActive( player ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: FIXME, need to deal with save/restore +//----------------------------------------------------------------------------- +class CSceneEntity : public CPointEntity, public IChoreoEventCallback +{ + friend class CInstancedSceneEntity; +public: + + enum + { + SCENE_ACTION_UNKNOWN = 0, + SCENE_ACTION_CANCEL, + SCENE_ACTION_RESUME, + }; + + enum + { + SCENE_BUSYACTOR_DEFAULT = 0, + SCENE_BUSYACTOR_WAIT, + SCENE_BUSYACTOR_INTERRUPT, + SCENE_BUSYACTOR_INTERRUPT_CANCEL, + }; + + + + + DECLARE_CLASS( CSceneEntity, CPointEntity ); + DECLARE_SERVERCLASS(); + // script description + DECLARE_ENT_SCRIPTDESC(); + + CSceneEntity( void ); + ~CSceneEntity( void ); + + // From IChoreoEventCallback + virtual void StartEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ); + virtual void EndEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ); + virtual void ProcessEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ); + virtual bool CheckEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ); + + + virtual int UpdateTransmitState(); + virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo ); + + void SetRecipientFilter( IRecipientFilter *filter ); + + virtual void Activate(); + + virtual void Precache( void ); + virtual void Spawn( void ); + virtual void UpdateOnRemove( void ); + + virtual void OnRestore(); + virtual void OnLoaded(); + + DECLARE_DATADESC(); + + virtual void OnSceneFinished( bool canceled, bool fireoutput ); + + virtual void DoThink( float frametime ); + virtual void PauseThink( void ); + + bool IsPlayingBack() const { return m_bIsPlayingBack; } + bool IsPaused() const { return m_bPaused; } + bool IsMultiplayer() const { return m_bMultiplayer; } + + bool IsInterruptable(); + virtual void ClearInterrupt(); + virtual void CheckInterruptCompletion(); + + virtual bool InterruptThisScene( CSceneEntity *otherScene ); + void RequestCompletionNotification( CSceneEntity *otherScene ); + + virtual void NotifyOfCompletion( CSceneEntity *interruptor ); + + void AddListManager( CSceneListManager *pManager ); + + void ClearActivatorTargets( void ); + + void SetBreakOnNonIdle( bool bBreakOnNonIdle ) { m_bBreakOnNonIdle = bBreakOnNonIdle; } + bool ShouldBreakOnNonIdle( void ) { return m_bBreakOnNonIdle; } + + // Inputs + void InputStartPlayback( inputdata_t &inputdata ); + void InputPausePlayback( inputdata_t &inputdata ); + void InputResumePlayback( inputdata_t &inputdata ); + void InputCancelPlayback( inputdata_t &inputdata ); + void InputCancelAtNextInterrupt( inputdata_t &inputdata ); + void InputPitchShiftPlayback( inputdata_t &inputdata ); + void InputTriggerEvent( inputdata_t &inputdata ); + + // If the scene is playing, finds an actor in the scene who can respond to the specified concept token + void InputInterjectResponse( inputdata_t &inputdata ); + + // If this scene is waiting on an actor, give up and quit trying. + void InputStopWaitingForActor( inputdata_t &inputdata ); + + virtual void StartPlayback( void ); + virtual void PausePlayback( void ); + virtual void ResumePlayback( void ); + virtual void CancelPlayback( void ); + virtual void PitchShiftPlayback( float fPitch ); + virtual void QueueResumePlayback( void ); + + bool ValidScene() const; + + // Scene load/unload + static CChoreoScene *LoadScene( const char *filename, IChoreoEventCallback *pCallback ); + + void UnloadScene( void ); + + struct SpeakEventSound_t + { + CUtlSymbol m_Symbol; + float m_flStartTime; + }; + + static bool SpeakEventSoundLessFunc( const SpeakEventSound_t& lhs, const SpeakEventSound_t& rhs ); + + bool GetSoundNameForPlayer( CChoreoEvent *event, CBasePlayer *player, char *buf, size_t buflen, CBaseEntity *pActor ); + + void BuildSortedSpeakEventSoundsPrefetchList( + CChoreoScene *scene, + CUtlSymbolTable& table, + CUtlRBTree< SpeakEventSound_t >& soundnames, + float timeOffset ); + void PrefetchSpeakEventSounds( CUtlSymbolTable& table, CUtlRBTree< SpeakEventSound_t >& soundnames ); + + // Event handlers + virtual void DispatchStartExpression( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchEndExpression( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartFlexAnimation( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchEndFlexAnimation( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartGesture( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchEndGesture( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartLookAt( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ); + virtual void DispatchEndLookAt( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartMoveTo( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ); + virtual void DispatchEndMoveTo( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartSpeak( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event, soundlevel_t iSoundlevel ); + virtual void DispatchEndSpeak( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartFace( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ); + virtual void DispatchEndFace( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartSequence( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchEndSequence( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartSubScene( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchStartInterrupt( CChoreoScene *scene, CChoreoEvent *event ); + virtual void DispatchEndInterrupt( CChoreoScene *scene, CChoreoEvent *event ); + virtual void DispatchStartGeneric( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchEndGeneric( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + + // NPC can play interstitial vcds (such as responding to the player doing something during a scene) + virtual void DispatchStartPermitResponses( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + virtual void DispatchEndPermitResponses( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ); + + + // Global events + virtual void DispatchProcessLoop( CChoreoScene *scene, CChoreoEvent *event ); + virtual void DispatchPauseScene( CChoreoScene *scene, const char *parameters ); + virtual void DispatchStopPoint( CChoreoScene *scene, const char *parameters ); + + virtual float EstimateLength( void ); + + void CancelIfSceneInvolvesActor( CBaseEntity *pActor ); + bool InvolvesActor( CBaseEntity *pActor ); // NOTE: returns false if scene hasn't loaded yet + + void GenerateSoundScene( CBaseFlex *pActor, const char *soundname ); + + virtual float GetPostSpeakDelay() { return 1.0; } + + bool HasUnplayedSpeech( void ); + bool HasFlexAnimation( void ); + + void SetCurrentTime( float t, bool forceClientSync ); + + void InputScriptPlayerDeath( inputdata_t &inputdata ); + + void AddBroadcastTeamTarget( int nTeamIndex ); + void RemoveBroadcastTeamTarget( int nTeamIndex ); +// Data +public: + string_t m_iszSceneFile; + + string_t m_iszResumeSceneFile; + EHANDLE m_hWaitingForThisResumeScene; + bool m_bWaitingForResumeScene; + + string_t m_iszTarget1; + string_t m_iszTarget2; + string_t m_iszTarget3; + string_t m_iszTarget4; + string_t m_iszTarget5; + string_t m_iszTarget6; + string_t m_iszTarget7; + string_t m_iszTarget8; + +#ifdef MAPBASE + void SetTarget(int nTarget, string_t pTargetName, CBaseEntity *pActivator = NULL, CBaseEntity *pCaller = NULL); + void InputSetTarget1(inputdata_t &inputdata); + void InputSetTarget2(inputdata_t &inputdata); + void InputSetTarget3(inputdata_t &inputdata); + void InputSetTarget4(inputdata_t &inputdata); + void InputSetTarget5(inputdata_t &inputdata); + void InputSetTarget6(inputdata_t &inputdata); + void InputSetTarget7(inputdata_t &inputdata); + void InputSetTarget8(inputdata_t &inputdata); +#endif + + EHANDLE m_hTarget1; + EHANDLE m_hTarget2; + EHANDLE m_hTarget3; + EHANDLE m_hTarget4; + EHANDLE m_hTarget5; + EHANDLE m_hTarget6; + EHANDLE m_hTarget7; + EHANDLE m_hTarget8; + + CNetworkVar( bool, m_bIsPlayingBack ); + CNetworkVar( bool, m_bPaused ); + CNetworkVar( bool, m_bMultiplayer ); + CNetworkVar( float, m_flForceClientTime ); + + float m_flCurrentTime; + float m_flFrameTime; + bool m_bCancelAtNextInterrupt; + + float m_fPitch; + + bool m_bAutomated; + int m_nAutomatedAction; + float m_flAutomationDelay; + float m_flAutomationTime; + + // A pause from an input requires another input to unpause (it's a hard pause) + bool m_bPausedViaInput; + + // Waiting for the actor to be able to speak. + bool m_bWaitingForActor; + + // Waiting for a point at which we can interrupt our actors + bool m_bWaitingForInterrupt; + bool m_bInterruptedActorsScenes; + + bool m_bBreakOnNonIdle; + +public: + virtual CBaseFlex *FindNamedActor( int index ); + virtual CBaseFlex *FindNamedActor( CChoreoActor *pChoreoActor ); + virtual CBaseFlex *FindNamedActor( const char *name ); + virtual CBaseEntity *FindNamedEntity( const char *name, CBaseEntity *pActor = NULL, bool bBaseFlexOnly = false, bool bUseClear = false ); + CBaseEntity *FindNamedTarget( string_t iszTarget, bool bBaseFlexOnly = false ); + virtual CBaseEntity *FindNamedEntityClosest( const char *name, CBaseEntity *pActor = NULL, bool bBaseFlexOnly = false, bool bUseClear = false, const char *pszSecondary = NULL ); + HSCRIPT ScriptFindNamedEntity( const char *name ); + bool ScriptLoadSceneFromString( const char * pszFilename, const char *pszData ); + + +private: + + CUtlVector< CHandle< CBaseFlex > > m_hActorList; + CUtlVector< CHandle< CBaseEntity > > m_hRemoveActorList; + +private: + + inline void SetRestoring( bool bRestoring ); + + // Prevent derived classed from using this! + virtual void Think( void ) {}; + + + void ClearSceneEvents( CChoreoScene *scene, bool canceled ); + void ClearSchedules( CChoreoScene *scene ); + + float GetSoundSystemLatency( void ); + void PrecacheScene( CChoreoScene *scene ); + + CChoreoScene *GenerateSceneForSound( CBaseFlex *pFlexActor, const char *soundname ); + + bool CheckActors(); + + void PrefetchAnimBlocks( CChoreoScene *scene ); + + bool ShouldNetwork() const; + // Set if we tried to async the scene but the FS returned that the data was not loadable + bool m_bSceneMissing; + + CChoreoScene *m_pScene; + CNetworkVar( int, m_nSceneStringIndex ); + + static const ConVar *m_pcvSndMixahead; + + COutputEvent m_OnStart; + COutputEvent m_OnCompletion; + COutputEvent m_OnCanceled; + COutputEvent m_OnTrigger1; + COutputEvent m_OnTrigger2; + COutputEvent m_OnTrigger3; + COutputEvent m_OnTrigger4; + COutputEvent m_OnTrigger5; + COutputEvent m_OnTrigger6; + COutputEvent m_OnTrigger7; + COutputEvent m_OnTrigger8; + COutputEvent m_OnTrigger9; + COutputEvent m_OnTrigger10; + COutputEvent m_OnTrigger11; + COutputEvent m_OnTrigger12; + COutputEvent m_OnTrigger13; + COutputEvent m_OnTrigger14; + COutputEvent m_OnTrigger15; + COutputEvent m_OnTrigger16; + + int m_nInterruptCount; + bool m_bInterrupted; + CHandle< CSceneEntity > m_hInterruptScene; + + bool m_bCompletedEarly; + + bool m_bInterruptSceneFinished; + CUtlVector< CHandle< CSceneEntity > > m_hNotifySceneCompletion; + CUtlVector< CHandle< CSceneListManager > > m_hListManagers; + + bool m_bRestoring; + + bool m_bGenerated; + string_t m_iszSoundName; + CHandle< CBaseFlex > m_hActor; + + EHANDLE m_hActivator; + + int m_BusyActor; + + int m_iPlayerDeathBehavior; + + CRecipientFilter *m_pRecipientFilter; + +public: + void SetBackground( bool bIsBackground ); + bool IsBackground( void ); +}; + +LINK_ENTITY_TO_CLASS( logic_choreographed_scene, CSceneEntity ); +LINK_ENTITY_TO_CLASS( scripted_scene, CSceneEntity ); + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CSceneEntity, DT_SceneEntity ) + SendPropInt(SENDINFO(m_nSceneStringIndex),MAX_CHOREO_SCENES_STRING_BITS,SPROP_UNSIGNED), + SendPropBool(SENDINFO(m_bIsPlayingBack)), + SendPropBool(SENDINFO(m_bPaused)), + SendPropBool(SENDINFO(m_bMultiplayer)), + SendPropFloat(SENDINFO(m_flForceClientTime)), + SendPropUtlVector( + SENDINFO_UTLVECTOR( m_hActorList ), + MAX_ACTORS_IN_SCENE, // max elements + SendPropEHandle( NULL, 0 ) ), +END_SEND_TABLE() + +BEGIN_DATADESC( CSceneEntity ) + + // Keys + DEFINE_KEYFIELD( m_iszSceneFile, FIELD_STRING, "SceneFile" ), + DEFINE_KEYFIELD( m_iszResumeSceneFile, FIELD_STRING, "ResumeSceneFile" ), + DEFINE_FIELD( m_hWaitingForThisResumeScene, FIELD_EHANDLE ), + DEFINE_FIELD( m_bWaitingForResumeScene, FIELD_BOOLEAN ), + + DEFINE_KEYFIELD( m_iszTarget1, FIELD_STRING, "target1" ), + DEFINE_KEYFIELD( m_iszTarget2, FIELD_STRING, "target2" ), + DEFINE_KEYFIELD( m_iszTarget3, FIELD_STRING, "target3" ), + DEFINE_KEYFIELD( m_iszTarget4, FIELD_STRING, "target4" ), + DEFINE_KEYFIELD( m_iszTarget5, FIELD_STRING, "target5" ), + DEFINE_KEYFIELD( m_iszTarget6, FIELD_STRING, "target6" ), + DEFINE_KEYFIELD( m_iszTarget7, FIELD_STRING, "target7" ), + DEFINE_KEYFIELD( m_iszTarget8, FIELD_STRING, "target8" ), + + DEFINE_KEYFIELD( m_BusyActor, FIELD_INTEGER, "busyactor" ), + + DEFINE_FIELD( m_hTarget1, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget2, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget3, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget4, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget5, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget6, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget7, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget8, FIELD_EHANDLE ), + + DEFINE_FIELD( m_bIsPlayingBack, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bPaused, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flCurrentTime, FIELD_FLOAT ), // relative, not absolute time + DEFINE_FIELD( m_flForceClientTime, FIELD_FLOAT ), + DEFINE_FIELD( m_flFrameTime, FIELD_FLOAT ), // last frametime + DEFINE_FIELD( m_bCancelAtNextInterrupt, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fPitch, FIELD_FLOAT ), + DEFINE_FIELD( m_bAutomated, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nAutomatedAction, FIELD_INTEGER ), + DEFINE_FIELD( m_flAutomationDelay, FIELD_FLOAT ), + DEFINE_FIELD( m_flAutomationTime, FIELD_FLOAT ), // relative, not absolute time + + DEFINE_FIELD( m_bPausedViaInput, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bWaitingForActor, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bWaitingForInterrupt, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bInterruptedActorsScenes, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bBreakOnNonIdle, FIELD_BOOLEAN ), + + DEFINE_UTLVECTOR( m_hActorList, FIELD_EHANDLE ), + DEFINE_UTLVECTOR( m_hRemoveActorList, FIELD_EHANDLE ), + + // DEFINE_FIELD( m_pScene, FIELD_XXXX ) // Special processing used for this + + // These are set up in the constructor + // DEFINE_FIELD( m_pcvSndMixahead, FIELD_XXXXX ), + // DEFINE_FIELD( m_bRestoring, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_nInterruptCount, FIELD_INTEGER ), + DEFINE_FIELD( m_bInterrupted, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hInterruptScene, FIELD_EHANDLE ), + DEFINE_FIELD( m_bCompletedEarly, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bInterruptSceneFinished, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_bGenerated, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iszSoundName, FIELD_STRING ), + DEFINE_FIELD( m_hActor, FIELD_EHANDLE ), + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), + + // DEFINE_FIELD( m_bSceneMissing, FIELD_BOOLEAN ), + DEFINE_UTLVECTOR( m_hNotifySceneCompletion, FIELD_EHANDLE ), + DEFINE_UTLVECTOR( m_hListManagers, FIELD_EHANDLE ), + + DEFINE_FIELD( m_bMultiplayer, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_nSceneStringIndex, FIELD_INTEGER ), + + // DEFINE_FIELD( m_pRecipientFilter, IRecipientFilter* ), // Multiplayer only + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Start", InputStartPlayback ), + DEFINE_INPUTFUNC( FIELD_VOID, "Pause", InputPausePlayback ), + DEFINE_INPUTFUNC( FIELD_VOID, "Resume", InputResumePlayback ), + DEFINE_INPUTFUNC( FIELD_VOID, "Cancel", InputCancelPlayback ), + DEFINE_INPUTFUNC( FIELD_VOID, "CancelAtNextInterrupt", InputCancelAtNextInterrupt ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "PitchShift", InputPitchShiftPlayback ), + DEFINE_INPUTFUNC( FIELD_STRING, "InterjectResponse", InputInterjectResponse ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopWaitingForActor", InputStopWaitingForActor ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "Trigger", InputTriggerEvent ), + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget1", InputSetTarget1 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget2", InputSetTarget2 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget3", InputSetTarget3 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget4", InputSetTarget4 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget5", InputSetTarget5 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget6", InputSetTarget6 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget7", InputSetTarget7 ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTarget8", InputSetTarget8 ), +#endif + + DEFINE_KEYFIELD( m_iPlayerDeathBehavior, FIELD_INTEGER, "onplayerdeath" ), + DEFINE_INPUTFUNC( FIELD_VOID, "ScriptPlayerDeath", InputScriptPlayerDeath ), + + // Outputs + DEFINE_OUTPUT( m_OnStart, "OnStart"), + DEFINE_OUTPUT( m_OnCompletion, "OnCompletion"), + DEFINE_OUTPUT( m_OnCanceled, "OnCanceled"), + DEFINE_OUTPUT( m_OnTrigger1, "OnTrigger1"), + DEFINE_OUTPUT( m_OnTrigger2, "OnTrigger2"), + DEFINE_OUTPUT( m_OnTrigger3, "OnTrigger3"), + DEFINE_OUTPUT( m_OnTrigger4, "OnTrigger4"), + DEFINE_OUTPUT( m_OnTrigger5, "OnTrigger5"), + DEFINE_OUTPUT( m_OnTrigger6, "OnTrigger6"), + DEFINE_OUTPUT( m_OnTrigger7, "OnTrigger7"), + DEFINE_OUTPUT( m_OnTrigger8, "OnTrigger8"), + DEFINE_OUTPUT( m_OnTrigger9, "OnTrigger9"), + DEFINE_OUTPUT( m_OnTrigger10, "OnTrigger10"), + DEFINE_OUTPUT( m_OnTrigger11, "OnTrigger11"), + DEFINE_OUTPUT( m_OnTrigger12, "OnTrigger12"), + DEFINE_OUTPUT( m_OnTrigger13, "OnTrigger13"), + DEFINE_OUTPUT( m_OnTrigger14, "OnTrigger14"), + DEFINE_OUTPUT( m_OnTrigger15, "OnTrigger15"), + DEFINE_OUTPUT( m_OnTrigger16, "OnTrigger16"), +END_DATADESC() + + +BEGIN_ENT_SCRIPTDESC( CSceneEntity, CBaseEntity, "Choreographed scene which controls animation and/or dialog on one or more actors." ) + DEFINE_SCRIPTFUNC( EstimateLength, "Returns length of this scene in seconds." ) + DEFINE_SCRIPTFUNC( IsPlayingBack, "If this scene is currently playing." ) + DEFINE_SCRIPTFUNC( IsPaused, "If this scene is currently paused." ) + DEFINE_SCRIPTFUNC( AddBroadcastTeamTarget, "Adds a team (by index) to the broadcast list" ) + DEFINE_SCRIPTFUNC( RemoveBroadcastTeamTarget, "Removes a team (by index) from the broadcast list" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptFindNamedEntity, "FindNamedEntity", "given an entity reference, such as !target, get actual entity from scene object" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptLoadSceneFromString, "LoadSceneFromString", "given a dummy scene name and a vcd string, load the scene" ) +END_SCRIPTDESC(); + +const ConVar *CSceneEntity::m_pcvSndMixahead = NULL; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CSceneEntity::CSceneEntity( void ) +{ + m_bWaitingForActor = false; + m_bWaitingForInterrupt = false; + m_bInterruptedActorsScenes = false; + m_bIsPlayingBack = false; + m_bPaused = false; + m_bMultiplayer = false; + m_fPitch = 1.0f; + m_iszSceneFile = NULL_STRING; + m_iszResumeSceneFile = NULL_STRING; + m_hWaitingForThisResumeScene = NULL; + m_bWaitingForResumeScene = false; + SetCurrentTime( 0.0f, false ); + m_bCancelAtNextInterrupt = false; + + m_bAutomated = false; + m_nAutomatedAction = SCENE_ACTION_UNKNOWN; + m_flAutomationDelay = 0.0f; + m_flAutomationTime = 0.0f; + + m_bPausedViaInput = false; + ClearInterrupt(); + + m_pScene = NULL; + + m_bCompletedEarly = false; + + if ( !m_pcvSndMixahead ) + m_pcvSndMixahead = cvar->FindVar( "snd_mixahead" ); + + m_BusyActor = SCENE_BUSYACTOR_DEFAULT; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CSceneEntity::~CSceneEntity( void ) +{ + delete m_pRecipientFilter; + m_pRecipientFilter = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Resets time such that the client version of the .vcd is also updated, if appropriate +// Input : t - +// forceClientSync - forces new timestamp down to client .dll via networking +//----------------------------------------------------------------------------- +void CSceneEntity::SetCurrentTime( float t, bool bForceClientSync ) +{ + m_flCurrentTime = t; + if ( gpGlobals->maxClients == 1 || bForceClientSync ) + { + m_flForceClientTime = t; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::UpdateOnRemove( void ) +{ + UnloadScene(); + BaseClass::UpdateOnRemove(); + + if ( GetSceneManager() ) + { + GetSceneManager()->RemoveSceneEntity( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *soundname - +// Output : CChoreoScene +//----------------------------------------------------------------------------- +CChoreoScene *CSceneEntity::GenerateSceneForSound( CBaseFlex *pFlexActor, const char *soundname ) +{ + float duration = CBaseEntity::GetSoundDuration( soundname, pFlexActor ? STRING( pFlexActor->GetModelName() ) : NULL ); + if( duration <= 0.0f ) + { + Warning( "CSceneEntity::GenerateSceneForSound: Couldn't determine duration of %s\n", soundname ); + return NULL; + } + + CChoreoScene *scene = new CChoreoScene( this ); + if ( !scene ) + { + Warning( "CSceneEntity::GenerateSceneForSound: Failed to allocated new scene!!!\n" ); + } + else + { + scene->SetPrintFunc( LocalScene_Printf ); + + + CChoreoActor *actor = scene->AllocActor(); + CChoreoChannel *channel = scene->AllocChannel(); + CChoreoEvent *event = scene->AllocEvent(); + + Assert( actor ); + Assert( channel ); + Assert( event ); + + if ( !actor || !channel || !event ) + { + Warning( "CSceneEntity::GenerateSceneForSound: Alloc of actor, channel, or event failed!!!\n" ); + delete scene; + return NULL; + } + + // Set us up the actorz + actor->SetName( "!self" ); // Could be pFlexActor->GetName()? + actor->SetActive( true ); + + // Set us up the channelz + channel->SetName( STRING( m_iszSceneFile ) ); + channel->SetActor( actor ); + + // Add to actor + actor->AddChannel( channel ); + + // Set us up the eventz + event->SetType( CChoreoEvent::SPEAK ); + event->SetName( soundname ); + event->SetParameters( soundname ); + event->SetStartTime( 0.0f ); + event->SetUsingRelativeTag( false ); + event->SetEndTime( duration ); + event->SnapTimes(); + + // Add to channel + channel->AddEvent( event ); + + // Point back to our owners + event->SetChannel( channel ); + event->SetActor( actor ); + + } + + return scene; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::Activate() +{ + if ( m_bGenerated && !m_pScene ) + { + m_pScene = GenerateSceneForSound( m_hActor, STRING( m_iszSoundName ) ); + } + + BaseClass::Activate(); + + if ( GetSceneManager() ) + { + GetSceneManager()->AddSceneEntity( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : float +//----------------------------------------------------------------------------- +float CSceneEntity::GetSoundSystemLatency( void ) +{ + if ( m_pcvSndMixahead ) + { + return m_pcvSndMixahead->GetFloat(); + } + + // Assume 100 msec sound system latency + return SOUND_SYSTEM_LATENCY_DEFAULT; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// I copied CSceneEntity's PrecacheScene to a unique static function so PrecacheInstancedScene() +// can precache loose scene files without having to use a CSceneEntity. +//----------------------------------------------------------------------------- +void PrecacheChoreoScene( CChoreoScene *scene ) +{ + Assert( scene ); + + // Iterate events and precache necessary resources + for ( int i = 0; i < scene->GetNumEvents(); i++ ) + { + CChoreoEvent *event = scene->GetEvent( i ); + if ( !event ) + continue; + + // load any necessary data + switch (event->GetType() ) + { + default: + break; + case CChoreoEvent::SPEAK: + { + // Defined in SoundEmitterSystem.cpp + // NOTE: The script entries associated with .vcds are forced to preload to avoid + // loading hitches during triggering + CBaseEntity::PrecacheScriptSound( event->GetParameters() ); + + if ( event->GetCloseCaptionType() == CChoreoEvent::CC_MASTER && + event->GetNumSlaves() > 0 ) + { + char tok[ CChoreoEvent::MAX_CCTOKEN_STRING ]; + if ( event->GetPlaybackCloseCaptionToken( tok, sizeof( tok ) ) ) + { + CBaseEntity::PrecacheScriptSound( tok ); + } + } + } + break; + case CChoreoEvent::SUBSCENE: + { + // Only allow a single level of subscenes for now + if ( !scene->IsSubScene() ) + { + CChoreoScene *subscene = event->GetSubScene(); + if ( !subscene ) + { + subscene = ChoreoLoadScene( event->GetParameters(), NULL, &g_TokenProcessor, LocalScene_Printf ); + subscene->SetSubScene( true ); + event->SetSubScene( subscene ); + + // Now precache it's resources, if any + PrecacheChoreoScene( subscene ); + } + } + } + break; + } + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *scene - +//----------------------------------------------------------------------------- +void CSceneEntity::PrecacheScene( CChoreoScene *scene ) +{ + Assert( scene ); + + // Iterate events and precache necessary resources + for ( int i = 0; i < scene->GetNumEvents(); i++ ) + { + CChoreoEvent *event = scene->GetEvent( i ); + if ( !event ) + continue; + + // load any necessary data + switch (event->GetType() ) + { + default: + break; + case CChoreoEvent::SPEAK: + { + // Defined in SoundEmitterSystem.cpp + // NOTE: The script entries associated with .vcds are forced to preload to avoid + // loading hitches during triggering + PrecacheScriptSound( event->GetParameters() ); + + if ( event->GetCloseCaptionType() == CChoreoEvent::CC_MASTER && + event->GetNumSlaves() > 0 ) + { + char tok[ CChoreoEvent::MAX_CCTOKEN_STRING ]; + if ( event->GetPlaybackCloseCaptionToken( tok, sizeof( tok ) ) ) + { + PrecacheScriptSound( tok ); + } + } + } + break; + case CChoreoEvent::SUBSCENE: + { + // Only allow a single level of subscenes for now + if ( !scene->IsSubScene() ) + { + CChoreoScene *subscene = event->GetSubScene(); + if ( !subscene ) + { + subscene = LoadScene( event->GetParameters(), this ); + subscene->SetSubScene( true ); + event->SetSubScene( subscene ); + + // Now precache it's resources, if any + PrecacheScene( subscene ); + } + } + } + break; + } + } +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::Precache( void ) +{ + if ( m_bGenerated ) + return; + + if ( m_iszSceneFile == NULL_STRING ) + return; + + if ( m_iszResumeSceneFile != NULL_STRING ) + { + PrecacheInstancedScene( STRING( m_iszResumeSceneFile ) ); + } + + PrecacheInstancedScene( STRING( m_iszSceneFile ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActor - +// *soundname - +//----------------------------------------------------------------------------- +void CSceneEntity::GenerateSoundScene( CBaseFlex *pActor, const char *soundname ) +{ + m_bGenerated = true; + m_iszSoundName = MAKE_STRING( soundname ); + m_hActor = pActor; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CSceneEntity::HasUnplayedSpeech( void ) +{ + if ( m_pScene ) + return m_pScene->HasUnplayedSpeech(); + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CSceneEntity::HasFlexAnimation( void ) +{ + if ( m_pScene ) + return m_pScene->HasFlexAnimation(); + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : +//----------------------------------------------------------------------------- + +void CSceneEntity::SetBackground( bool bIsBackground ) +{ + if ( m_pScene ) + { + m_pScene->SetBackground( bIsBackground ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- + +bool CSceneEntity::IsBackground( void ) +{ + if ( m_pScene ) + return m_pScene->IsBackground( ); + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::OnRestore() +{ + BaseClass::OnRestore(); + + // Fix saved games that have their pitch set to zero + if ( m_fPitch < SCENE_MIN_PITCH || m_fPitch > SCENE_MAX_PITCH ) + m_fPitch = 1.0f; + + if ( !m_bIsPlayingBack ) + return; + + if ( !m_pScene ) + { + m_pScene = LoadScene( STRING( m_iszSceneFile ), this ); + if ( !m_pScene ) + { + m_bSceneMissing = true; + return; + } + + OnLoaded(); + + if ( ShouldNetwork() ) + { + m_nSceneStringIndex = g_pStringTableClientSideChoreoScenes->AddString( CBaseEntity::IsServer(), STRING( m_iszSceneFile ) ); + } + + UpdateTransmitState(); + } + + m_bSceneMissing = false; + + int i; + for ( i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pTestActor = FindNamedActor( i ); + if ( !pTestActor ) + continue; + + if ( !pTestActor->MyCombatCharacterPointer() ) + continue; + + // Needed? + //if ( !pTestActor->MyCombatCharacterPointer()->IsAlive() ) + // return; + + pTestActor->StartChoreoScene( m_pScene ); + } + + float dt = SCENE_THINK_INTERVAL; + + bool paused = m_bPaused; + + m_bPaused = false; + + // roll back slightly so that pause events still trigger + m_pScene->ResetSimulation( true, m_flCurrentTime - SCENE_THINK_INTERVAL, m_flCurrentTime ); + m_pScene->SetTime( m_flCurrentTime - SCENE_THINK_INTERVAL ); + + SetCurrentTime( m_flCurrentTime, true ); + + // Robin: This causes a miscount of any interrupt events in the scene. + // All the variables are saved/restored properly, so we can safely leave them alone. + //ClearInterrupt(); + + SetRestoring( true ); + + DoThink( dt ); + + SetRestoring( false ); + + if ( paused ) + { + PausePlayback(); + } + + NetworkProp()->NetworkStateForceUpdate(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CSceneEntity::SetRestoring( bool bRestoring ) +{ + m_bRestoring = bRestoring; + if ( m_pScene ) + { + m_pScene->SetRestoring( bRestoring ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::Spawn( void ) +{ + Precache(); +} + +void CSceneEntity::PauseThink( void ) +{ + if ( !m_pScene ) + return; + + // Stay paused if pause occurred from interrupt + if ( m_bInterrupted ) + return; + + // If entity I/O paused the scene, then it'll have to resume/cancel the scene... + if ( m_bPausedViaInput ) + { + // If we're waiting for a resume scene to finish, continue when it's done + if ( m_bWaitingForResumeScene && !m_hWaitingForThisResumeScene ) + { + // Resume scene has finished, stop waiting for it + m_bWaitingForResumeScene = false; + } + else + { + return; + } + } + + if ( !m_bAutomated ) + { + // FIXME: Game code should check for AI waiting conditions being met, etc. + // + // + // + bool bAllFinished = m_pScene->CheckEventCompletion( ); + + if ( bAllFinished ) + { + // Perform action + switch ( m_nAutomatedAction ) + { + case SCENE_ACTION_RESUME: + ResumePlayback(); + break; + case SCENE_ACTION_CANCEL: + LocalScene_Printf( "%s : PauseThink canceling playback\n", STRING( m_iszSceneFile ) ); + CancelPlayback(); + break; + default: + ResumePlayback(); + break; + } + + // Reset + m_bAutomated = false; + m_nAutomatedAction = SCENE_ACTION_UNKNOWN; + m_flAutomationTime = 0.0f; + m_flAutomationDelay = 0.0f; + m_bPausedViaInput = false; + } + return; + } + + // Otherwise, see if resume/cancel is automatic and act accordingly if enough time + // has passed + m_flAutomationTime += (gpGlobals->frametime); + + if ( m_flAutomationDelay > 0.0f && + m_flAutomationTime < m_flAutomationDelay ) + return; + + // Perform action + switch ( m_nAutomatedAction ) + { + case SCENE_ACTION_RESUME: + LocalScene_Printf( "%s : Automatically resuming playback\n", STRING( m_iszSceneFile ) ); + ResumePlayback(); + break; + case SCENE_ACTION_CANCEL: + LocalScene_Printf( "%s : Automatically canceling playback\n", STRING( m_iszSceneFile ) ); + CancelPlayback(); + break; + default: + LocalScene_Printf( "%s : Unknown action %i, automatically resuming playback\n", STRING( m_iszSceneFile ), m_nAutomatedAction ); + ResumePlayback(); + break; + } + + // Reset + m_bAutomated = false; + m_nAutomatedAction = SCENE_ACTION_UNKNOWN; + m_flAutomationTime = 0.0f; + m_flAutomationDelay = 0.0f; + m_bPausedViaInput = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchPauseScene( CChoreoScene *scene, const char *parameters ) +{ + // Don't pause during restore, since we'll be restoring the pause state already + if ( m_bRestoring ) + return; + + // FIXME: Hook this up to AI, etc. somehow, perhaps poll each actor for conditions using + // scene resume condition iterator + PausePlayback(); + + char token[1024]; + + m_bPausedViaInput = false; + m_bAutomated = false; + m_nAutomatedAction = SCENE_ACTION_UNKNOWN; + m_flAutomationDelay = 0.0f; + m_flAutomationTime = 0.0f; + + // Check for auto resume/cancel + const char *buffer = parameters; + buffer = engine->ParseFile( buffer, token, sizeof( token ) ); + if ( !stricmp( token, "automate" ) ) + { + buffer = engine->ParseFile( buffer, token, sizeof( token ) ); + if ( !stricmp( token, "Cancel" ) ) + { + m_nAutomatedAction = SCENE_ACTION_CANCEL; + } + else if ( !stricmp( token, "Resume" ) ) + { + m_nAutomatedAction = SCENE_ACTION_RESUME; + } + + if ( m_nAutomatedAction != SCENE_ACTION_UNKNOWN ) + { + buffer = engine->ParseFile( buffer, token, sizeof( token ) ); + m_flAutomationDelay = (float)atof( token ); + + if ( m_flAutomationDelay > 0.0f ) + { + // Success + m_bAutomated = true; + m_flAutomationTime = 0.0f; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *scene - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchProcessLoop( CChoreoScene *scene, CChoreoEvent *event ) +{ + // Don't restore this event since it's implied in the current "state" of the scene timer, etc. + if ( m_bRestoring ) + return; + + Assert( scene ); + Assert( event->GetType() == CChoreoEvent::LOOP ); + + float backtime = (float)atof( event->GetParameters() ); + + bool process = true; + int counter = event->GetLoopCount(); + if ( counter != -1 ) + { + int remaining = event->GetNumLoopsRemaining(); + if ( remaining <= 0 ) + { + process = false; + } + else + { + event->SetNumLoopsRemaining( --remaining ); + } + } + + if ( !process ) + return; + + scene->LoopToTime( backtime ); + SetCurrentTime( backtime, true ); +} + +//----------------------------------------------------------------------------- +// Purpose: Flag the scene as already "completed" +// Input : *scene - +// *parameters - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStopPoint( CChoreoScene *scene, const char *parameters ) +{ + if ( m_bCompletedEarly ) + { + Assert( 0 ); + Warning( "Scene '%s' with two stop point events!\n", STRING( m_iszSceneFile ) ); + return; + } + // Fire completion trigger early + m_bCompletedEarly = true; + m_OnCompletion.FireOutput( this, this, 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CSceneEntity::IsInterruptable() +{ + return ( m_nInterruptCount > 0 ) ? true : false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *scene - +// *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartInterrupt( CChoreoScene *scene, CChoreoEvent *event ) +{ + // Don't re-interrupt during restore + if ( m_bRestoring ) + return; + + // If we're supposed to cancel at our next interrupt point, cancel now + if ( m_bCancelAtNextInterrupt ) + { + m_bCancelAtNextInterrupt = false; + LocalScene_Printf( "%s : cancelled via interrupt\n", STRING( m_iszSceneFile ) ); + CancelPlayback(); + return; + } + + ++m_nInterruptCount; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *scene - +// *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndInterrupt( CChoreoScene *scene, CChoreoEvent *event ) +{ + // Don't re-interrupt during restore + if ( m_bRestoring ) + return; + + --m_nInterruptCount; + + if ( m_nInterruptCount < 0 ) + { + m_nInterruptCount = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartExpression( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->AddSceneEvent( scene, event ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndExpression( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, false ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartFlexAnimation( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->AddSceneEvent( scene, event ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndFlexAnimation( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, false ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartGesture( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + // Ingore null gestures + if ( !Q_stricmp( event->GetName(), "NULL" ) ) + return; + + actor->AddSceneEvent( scene, event); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndGesture( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + // Ingore null gestures + if ( !Q_stricmp( event->GetName(), "NULL" ) ) + return; + + actor->RemoveSceneEvent( scene, event, m_bRestoring ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartGeneric( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + CBaseEntity *pTarget = FindNamedEntity( event->GetParameters2( ) ); +#ifdef MAPBASE + actor->AddSceneEvent( scene, event, pTarget, this ); +#else + actor->AddSceneEvent( scene, event, pTarget ); +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndGeneric( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, m_bRestoring ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *actor2 - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartLookAt( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ) +{ + actor->AddSceneEvent( scene, event, actor2 ); +} + + +void CSceneEntity::DispatchEndLookAt( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, m_bRestoring ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Move to spot/actor +// FIXME: Need to allow this to take arbitrary amount of time and pause playback +// while waiting for actor to move into position +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartMoveTo( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ) +{ + actor->AddSceneEvent( scene, event, actor2 ); +} + + +void CSceneEntity::DispatchEndMoveTo( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, m_bRestoring ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *token - +// listener - +// soundorigins - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool AttenuateCaption( const char *token, const Vector& listener, CUtlVector< Vector >& soundorigins ) +{ + if ( scene_maxcaptionradius.GetFloat() <= 0.0f ) + { + return false; + } + + int c = soundorigins.Count(); + + if ( c <= 0 ) + { + return false; + } + + float maxdistSqr = scene_maxcaptionradius.GetFloat() * scene_maxcaptionradius.GetFloat(); + + for ( int i = 0; i < c; ++i ) + { + const Vector& org = soundorigins[ i ]; + + float distSqr = ( org - listener ).LengthSqr(); + if ( distSqr <= maxdistSqr ) + { + return false; + } + } + + // All sound sources too far, don't show caption... + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *event - which event +// player - which recipient +// buf, buflen: where to put the data +// Output : Returns true if the sound should be played/prefetched +//----------------------------------------------------------------------------- +bool CSceneEntity::GetSoundNameForPlayer( CChoreoEvent *event, CBasePlayer *player, char *buf, size_t buflen, CBaseEntity *pActor ) +{ + Assert( event ); + Assert( player ); + Assert( buf ); + Assert( buflen > 0 ); + + bool ismasterevent = true; + char tok[ CChoreoEvent::MAX_CCTOKEN_STRING ]; + bool validtoken = false; + + tok[ 0 ] = 0; + + if ( event->GetCloseCaptionType() == CChoreoEvent::CC_SLAVE || + event->GetCloseCaptionType() == CChoreoEvent::CC_DISABLED ) + { + ismasterevent = false; + } + else + { + validtoken = event->GetPlaybackCloseCaptionToken( tok, sizeof( tok ) ); + } + + const char* pchToken = ""; + + if ( pActor && pActor->IsPlayer() ) + { + pchToken = dynamic_cast< CBasePlayer* >( pActor )->GetSceneSoundToken(); + } + + // Copy the sound name + CopySoundNameWithModifierToken( buf, event->GetParameters(), buflen, pchToken ); + + bool usingEnglish = true; + if ( !IsXbox() ) + { + char const *cvarvalue = engine->GetClientConVarValue( player->entindex(), "english" ); + if ( cvarvalue && *cvarvalue && Q_atoi( cvarvalue ) != 1 ) + { + usingEnglish = false; + } + + } + + // This makes it like they are running in another language + if ( scene_forcecombined.GetBool() ) + { + usingEnglish = false; + } + + if ( usingEnglish ) + { + // English sounds always play + return true; + } + + if ( ismasterevent ) + { + // Master event sounds always play too (master will be the combined .wav) + if ( validtoken ) + { + Q_strncpy( buf, tok, buflen ); + } + return true; + } + + // Slave events don't play any sound... + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Playback sound file that contains phonemes +// Input : *actor - +// *parameters - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartSpeak( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event, soundlevel_t iSoundlevel ) +{ + // Emit sound + if ( actor ) + { + CPASAttenuationFilter filter( actor ); + + if ( m_pRecipientFilter ) + { + int filterCount = filter.GetRecipientCount(); + int recipientPlayerCount = m_pRecipientFilter->GetRecipientCount(); + for ( int i = filterCount-1; i >= 0; --i ) + { + int playerindex = filter.GetRecipientIndex( i ); + + bool bFound = false; + + for ( int j = 0; j < recipientPlayerCount; ++j ) + { + if ( m_pRecipientFilter->GetRecipientIndex(j) == playerindex ) + { + bFound = true; + break; + } + } + + if ( !bFound ) + { + filter.RemoveRecipientByPlayerIndex( playerindex ); + } + } + } + + float time_in_past = m_flCurrentTime - event->GetStartTime() ; + + float soundtime = gpGlobals->curtime - time_in_past; + + if ( m_bRestoring ) + { + // Need to queue sounds on restore because the player has not yet connected + GetSceneManager()->QueueRestoredSound( actor, event->GetParameters(), iSoundlevel, time_in_past ); + + return; + } + + // Add padding to prevent any other talker talking right after I'm done, because I might + // be continuing speaking with another scene. + float flDuration = event->GetDuration() - time_in_past; + + CAI_BaseActor *pBaseActor = dynamic_cast(actor); + if ( pBaseActor ) + { + pBaseActor->NoteSpeaking( flDuration, GetPostSpeakDelay() ); + } + else if ( actor->IsNPC() ) + { + GetSpeechSemaphore( actor->MyNPCPointer() )->Acquire( flDuration + GetPostSpeakDelay(), actor ); + } + + EmitSound_t es; + es.m_nChannel = CHAN_VOICE; + es.m_flVolume = 1; + es.m_SoundLevel = iSoundlevel; + // Only specify exact delay in single player + es.m_flSoundTime = ( gpGlobals->maxClients == 1 ) ? soundtime : 0.0f; + if ( scene->ShouldIgnorePhonemes() ) + { + es.m_nFlags |= SND_IGNORE_PHONEMES; + } + + if ( actor->GetSpecialDSP() != 0 ) + { + es.m_nSpecialDSP = actor->GetSpecialDSP(); + } + + // No CC since we do it manually + // FIXME: This will change + es.m_bEmitCloseCaption = false; + + int c = filter.GetRecipientCount(); + for ( int i = 0; i < c; ++i ) + { + int playerindex = filter.GetRecipientIndex( i ); + CBasePlayer *player = UTIL_PlayerByIndex( playerindex ); + if ( !player ) + continue; + + CSingleUserRecipientFilter filter2( player ); + + char soundname[ 512 ]; + if ( !GetSoundNameForPlayer( event, player, soundname, sizeof( soundname ), actor ) ) + { + continue; + } + + es.m_pSoundName = soundname; + + // keep track of the last few sounds played for bug reports + speechListSounds[ speechListIndex ].time = gpGlobals->curtime; + Q_strncpy( speechListSounds[ speechListIndex ].name, soundname, sizeof( speechListSounds[ 0 ].name ) ); + Q_strncpy( speechListSounds[ speechListIndex ].sceneName, ( scene ) ? scene->GetFilename() : "", sizeof( speechListSounds[ 0 ].sceneName ) ); + + speechListIndex++; + if ( speechListIndex >= SPEECH_LIST_MAX_SOUNDS ) + { + speechListIndex = 0; + } + + // Warning( "Speak %s\n", soundname ); + + if ( m_fPitch != 1.0f ) + { + if ( es.m_nPitch ) + es.m_nPitch = static_cast( es.m_nPitch ) * m_fPitch; + else + es.m_nPitch = 100.0f * m_fPitch; + + es.m_nFlags |= SND_CHANGE_PITCH; + } + + EmitSound( filter2, actor->entindex(), es ); + actor->AddSceneEvent( scene, event ); + } + + // Close captioning only on master token no matter what... + if ( event->GetCloseCaptionType() == CChoreoEvent::CC_MASTER ) + { + char tok[ CChoreoEvent::MAX_CCTOKEN_STRING ]; + bool validtoken = event->GetPlaybackCloseCaptionToken( tok, sizeof( tok ) ); + if ( validtoken ) + { + char lowercase[ 256 ]; + Q_strncpy( lowercase, tok, sizeof( lowercase ) ); + Q_strlower( lowercase ); + + // Remove any players who don't want close captions + CBaseEntity::RemoveRecipientsIfNotCloseCaptioning( filter ); + + // Certain events are marked "don't attenuate", (breencast), skip those here + if ( !event->IsSuppressingCaptionAttenuation() && + ( filter.GetRecipientCount() > 0 ) ) + { + int c = filter.GetRecipientCount(); + for ( int i = c - 1 ; i >= 0; --i ) + { + CBasePlayer *player = UTIL_PlayerByIndex( filter.GetRecipientIndex( i ) ); + if ( !player ) + continue; + + Vector playerOrigin = player->GetAbsOrigin(); + + if ( AttenuateCaption( lowercase, playerOrigin, es.m_UtlVecSoundOrigin ) ) + { + // If the player has a view entity, measure the distance to that + if ( !player->GetViewEntity() || AttenuateCaption( lowercase, player->GetViewEntity()->GetAbsOrigin(), es.m_UtlVecSoundOrigin ) ) + { + filter.RemoveRecipient( player ); + } + } + } + } + + // Anyone left? + if ( filter.GetRecipientCount() > 0 ) + { + float endtime = event->GetLastSlaveEndTime(); + float durationShort = event->GetDuration(); + float durationLong = endtime - event->GetStartTime(); + + float duration = MAX( durationShort, durationLong ); + + + byte byteflags = CLOSE_CAPTION_WARNIFMISSING; // warnifmissing + /* + // Never for .vcds... + if ( fromplayer ) + { + byteflags |= CLOSE_CAPTION_FROMPLAYER; + } + */ + char const *pszActorModel = STRING( actor->GetModelName() ); + gender_t gender = soundemitterbase->GetActorGender( pszActorModel ); + + if ( gender == GENDER_MALE ) + { + byteflags |= CLOSE_CAPTION_GENDER_MALE; + } + else if ( gender == GENDER_FEMALE ) + { + byteflags |= CLOSE_CAPTION_GENDER_FEMALE; + } + + // Send caption and duration hint down to client + UserMessageBegin( filter, "CloseCaption" ); + WRITE_STRING( lowercase ); + WRITE_SHORT( MIN( 255, (int)( duration * 10.0f ) ) ); + WRITE_BYTE( byteflags ); // warn on missing + MessageEnd(); + } + } + } + } +} + +void CSceneEntity::DispatchEndSpeak( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, m_bRestoring ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *actor2 - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartFace( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ) +{ + actor->AddSceneEvent( scene, event, actor2 ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *actor2 - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndFace( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, m_bRestoring ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartSequence( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->AddSceneEvent( scene, event ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndSequence( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->RemoveSceneEvent( scene, event, m_bRestoring ); +} + +//----------------------------------------------------------------------------- +// Purpose: NPC can play interstitial vcds (such as responding to the player doing something during a scene) +// Input : *scene - +// *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartPermitResponses( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->SetPermitResponse( gpGlobals->curtime + event->GetDuration() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *scene - +// *actor - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchEndPermitResponses( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) +{ + actor->SetPermitResponse( 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CSceneEntity::EstimateLength( void ) +{ + if ( !m_pScene ) + { + return GetSceneDuration( STRING( m_iszSceneFile ) ); + } + return m_pScene->FindStopTime(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// NOTE: returns false if scene hasn't loaded yet +//----------------------------------------------------------------------------- +void CSceneEntity::CancelIfSceneInvolvesActor( CBaseEntity *pActor ) +{ + if ( InvolvesActor( pActor ) ) + { + LocalScene_Printf( "%s : cancelled for '%s'\n", STRING( m_iszSceneFile ), pActor->GetDebugName() ); + CancelPlayback(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// NOTE: returns false if scene hasn't loaded yet +//----------------------------------------------------------------------------- +bool CSceneEntity::InvolvesActor( CBaseEntity *pActor ) +{ + if ( !m_pScene ) + return false; + + int i; + for ( i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pTestActor = FindNamedActor( i ); + if ( !pTestActor ) + continue; + + if ( pTestActor == pActor ) + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::DoThink( float frametime ) +{ + CheckInterruptCompletion(); + + if ( m_bWaitingForActor || m_bWaitingForInterrupt ) + { + // Try to start playback. + StartPlayback(); + } + + if ( !m_pScene ) + return; + + if ( !m_bIsPlayingBack ) + return; + + // catch bad pitch shifting from old save games + Assert( m_fPitch >= SCENE_MIN_PITCH && m_fPitch <= SCENE_MAX_PITCH ); + m_fPitch = clamp( m_fPitch, SCENE_MIN_PITCH, SCENE_MAX_PITCH ); + + if ( m_bPaused ) + { + PauseThink(); + return; + } + + // Msg("%.2f %s\n", gpGlobals->curtime, STRING( m_iszSceneFile ) ); + + //Msg( "SV: %d, %f for %s\n", gpGlobals->tickcount, m_flCurrentTime, m_pScene->GetFilename() ); + + m_flFrameTime = frametime; + + m_pScene->SetSoundFileStartupLatency( GetSoundSystemLatency() ); + + // Tell scene to go + m_pScene->Think( m_flCurrentTime ); + + // Did we get to the end + if ( !m_bPaused ) + { + // Drive simulation time for scene + SetCurrentTime( m_flCurrentTime + m_flFrameTime * m_fPitch, false ); + + if ( m_pScene->SimulationFinished() ) + { + OnSceneFinished( false, true ); + + // Stop them from doing anything special + ClearSchedules( m_pScene ); + } + } + else + { + // Drive simulation time for scene + SetCurrentTime( m_pScene->GetTime(), true ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handlers +//----------------------------------------------------------------------------- +void CSceneEntity::InputStartPlayback( inputdata_t &inputdata ) +{ + // Already playing, ignore + if ( m_bIsPlayingBack ) + return; + + // Already waiting on someone. + if ( m_bWaitingForActor || m_bWaitingForInterrupt ) + return; + + ClearActivatorTargets(); + m_hActivator = inputdata.pActivator; + StartPlayback(); +} + +void CSceneEntity::InputPausePlayback( inputdata_t &inputdata ) +{ + PausePlayback(); + m_bPausedViaInput = true; +} + +void CSceneEntity::InputResumePlayback( inputdata_t &inputdata ) +{ + ResumePlayback(); +} + +void CSceneEntity::InputCancelPlayback( inputdata_t &inputdata ) +{ + LocalScene_Printf( "%s : cancelled via input\n", STRING( m_iszSceneFile ) ); + CancelPlayback(); +} + +void CSceneEntity::InputScriptPlayerDeath( inputdata_t &inputdata ) +{ + if ( m_iPlayerDeathBehavior == SCRIPT_CANCEL ) + { + LocalScene_Printf( "%s : cancelled via player death\n", STRING( m_iszSceneFile ) ); + CancelPlayback(); + } +} + + +void CSceneEntity::InputCancelAtNextInterrupt( inputdata_t &inputdata ) +{ + // If we're currently in an interruptable point, interrupt immediately + if ( IsInterruptable() ) + { + LocalScene_Printf( "%s : cancelled via input at interrupt point\n", STRING( m_iszSceneFile ) ); + CancelPlayback(); + return; + } + + // Otherwise, cancel when we next hit an interrupt point + m_bCancelAtNextInterrupt = true; +} + +void CSceneEntity::InputPitchShiftPlayback( inputdata_t &inputdata ) +{ + PitchShiftPlayback( inputdata.value.Float() ); +} + +void CSceneEntity::InputTriggerEvent( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + CBaseEntity *pActivator = inputdata.pActivator; +#else + CBaseEntity *pActivator = this; // at some point, find this from the inputdata +#endif + switch ( inputdata.value.Int() ) + { + case 1: + m_OnTrigger1.FireOutput( pActivator, this, 0 ); + break; + case 2: + m_OnTrigger2.FireOutput( pActivator, this, 0 ); + break; + case 3: + m_OnTrigger3.FireOutput( pActivator, this, 0 ); + break; + case 4: + m_OnTrigger4.FireOutput( pActivator, this, 0 ); + break; + case 5: + m_OnTrigger5.FireOutput( pActivator, this, 0 ); + break; + case 6: + m_OnTrigger6.FireOutput( pActivator, this, 0 ); + break; + case 7: + m_OnTrigger7.FireOutput( pActivator, this, 0 ); + break; + case 8: + m_OnTrigger8.FireOutput( pActivator, this, 0 ); + break; + case 9: + m_OnTrigger9.FireOutput( pActivator, this, 0 ); + break; + case 10: + m_OnTrigger10.FireOutput( pActivator, this, 0 ); + break; + case 11: + m_OnTrigger11.FireOutput( pActivator, this, 0 ); + break; + case 12: + m_OnTrigger12.FireOutput( pActivator, this, 0 ); + break; + case 13: + m_OnTrigger13.FireOutput( pActivator, this, 0 ); + break; + case 14: + m_OnTrigger14.FireOutput( pActivator, this, 0 ); + break; + case 15: + m_OnTrigger15.FireOutput( pActivator, this, 0 ); + break; + case 16: + m_OnTrigger16.FireOutput( pActivator, this, 0 ); + break; + } +} + +struct NPCInterjection +{ + AI_Response *response; + CAI_BaseActor *npc; +}; +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CSceneEntity::InputInterjectResponse( inputdata_t &inputdata ) +{ + // Not currently playing a scene + if ( !m_pScene ) + { + return; + } + + CUtlVector< CAI_BaseActor * > candidates; + int i; + for ( i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pTestActor = FindNamedActor( i ); + if ( !pTestActor ) + continue; + + CAI_BaseActor *pBaseActor = dynamic_cast(pTestActor); + if ( !pBaseActor ) + continue; + + if ( !pBaseActor->IsAlive() ) + continue; + + candidates.AddToTail( pBaseActor ); + } + + int c = candidates.Count(); + + if ( !c ) + { + return; + } + + int useIndex = 0; + if ( !m_bIsPlayingBack ) + { + // Use any actor if not playing a scene + useIndex = RandomInt( 0, c - 1 ); + } + else + { + CUtlVector< NPCInterjection > validResponses; + + char modifiers[ 512 ]; + Q_snprintf( modifiers, sizeof( modifiers ), "scene:%s", STRING( GetEntityName() ) ); + + for ( int i = 0; i < c; i++ ) + { + CAI_BaseActor *npc = candidates[ i ]; + Assert( npc ); + + AI_Response *response = npc->SpeakFindResponse( inputdata.value.String(), modifiers ); + if ( !response ) + continue; + + float duration = npc->GetResponseDuration( response ); + // Couldn't look it up + if ( duration <= 0.0f ) + continue; + + if ( !npc->PermitResponse( duration ) ) + { + delete response; + continue; + } + + // + NPCInterjection inter; + inter.response = response; + inter.npc = npc; + + validResponses.AddToTail( inter ); + } + + int rcount = validResponses.Count(); + if ( rcount >= 1 ) + { + int slot = RandomInt( 0, rcount - 1 ); + + for ( int i = 0; i < rcount; i++ ) + { + NPCInterjection *pInterjection = &validResponses[ i ]; + if ( i == slot ) + { + pInterjection->npc->SpeakDispatchResponse( inputdata.value.String(), pInterjection->response ); + } + else + { + delete pInterjection->response; + } + } + } + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CSceneEntity::SetTarget( int nTarget, string_t pTargetName, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + if (/*m_bIsPlayingBack ||*/ !nTarget) + { + return; + } + + switch (nTarget) + { + case 1: m_iszTarget1 = pTargetName; break; + case 2: m_iszTarget2 = pTargetName; break; + case 3: m_iszTarget3 = pTargetName; break; + case 4: m_iszTarget4 = pTargetName; break; + case 5: m_iszTarget5 = pTargetName; break; + case 6: m_iszTarget6 = pTargetName; break; + case 7: m_iszTarget7 = pTargetName; break; + case 8: m_iszTarget8 = pTargetName; break; + } + + // Reset our handle. + // Internal functions set them when they're null anyway. + switch (nTarget) + { + case 1: m_hTarget1 = NULL; break; + case 2: m_hTarget2 = NULL; break; + case 3: m_hTarget3 = NULL; break; + case 4: m_hTarget4 = NULL; break; + case 5: m_hTarget5 = NULL; break; + case 6: m_hTarget6 = NULL; break; + case 7: m_hTarget7 = NULL; break; + case 8: m_hTarget8 = NULL; break; + } + + //CBaseEntity *pTarget = gEntList.FindEntityByName(NULL, pTargetName, this, pActivator, pCaller); + //if (!pTarget) + //{ + // DevWarning("%s (%s) could not find SetTarget entity %s!\n", GetClassname(), GetDebugName(), pTargetName); + // return; + //} + + /* + switch (nTarget) + { + case 1: m_hTarget1 = pTarget; break; + case 2: m_hTarget2 = pTarget; break; + case 3: m_hTarget3 = pTarget; break; + case 4: m_hTarget4 = pTarget; break; + case 5: m_hTarget5 = pTarget; break; + case 6: m_hTarget6 = pTarget; break; + case 7: m_hTarget7 = pTarget; break; + case 8: m_hTarget8 = pTarget; break; + } + */ +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CSceneEntity::InputSetTarget1( inputdata_t &inputdata ) +{ + SetTarget(1, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} + +void CSceneEntity::InputSetTarget2( inputdata_t &inputdata ) +{ + SetTarget(2, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} + +void CSceneEntity::InputSetTarget3( inputdata_t &inputdata ) +{ + SetTarget(3, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} + +void CSceneEntity::InputSetTarget4( inputdata_t &inputdata ) +{ + SetTarget(4, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} + +void CSceneEntity::InputSetTarget5( inputdata_t &inputdata ) +{ + SetTarget(5, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} + +void CSceneEntity::InputSetTarget6( inputdata_t &inputdata ) +{ + SetTarget(6, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} + +void CSceneEntity::InputSetTarget7( inputdata_t &inputdata ) +{ + SetTarget(7, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} + +void CSceneEntity::InputSetTarget8( inputdata_t &inputdata ) +{ + SetTarget(8, inputdata.value.StringID(), inputdata.pActivator, inputdata.pCaller); +} +#endif + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CSceneEntity::InputStopWaitingForActor( inputdata_t &inputdata ) +{ + if( m_bIsPlayingBack ) + { + // Already started. + return; + } + + m_bWaitingForActor = false; +} + +bool CSceneEntity::CheckActors() +{ + Assert( m_pScene ); + if ( !m_pScene ) + return false; + + int i; + for ( i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pTestActor = FindNamedActor( i ); + if ( !pTestActor ) + continue; + + if ( !pTestActor->MyCombatCharacterPointer() ) + continue; + + if ( !pTestActor->MyCombatCharacterPointer()->IsAlive() ) + return false; + + if ( m_BusyActor == SCENE_BUSYACTOR_WAIT ) + { + CAI_BaseNPC *pActor = pTestActor->MyNPCPointer(); + + if ( pActor ) + { + bool bShouldWait = false; + if ( hl2_episodic.GetBool() ) + { + // Episodic waits until the NPC is fully finished with any .vcd with speech in it + if ( IsRunningScriptedSceneWithSpeech( pActor ) ) + { + bShouldWait = true; + } + +#ifdef HL2_EPISODIC + // HACK: Alyx cannot play scenes when she's in the middle of transitioning + if ( pActor->IsInAVehicle() ) + { + CNPC_Alyx *pAlyx = dynamic_cast(pActor); + if ( pAlyx != NULL && ( pAlyx->GetPassengerState() == PASSENGER_STATE_ENTERING || pAlyx->GetPassengerState() == PASSENGER_STATE_EXITING ) ) + { + bShouldWait = true; + } + } +#endif // HL2_EPISODIC + } + + if ( pActor->GetExpresser() && pActor->GetExpresser()->IsSpeaking() ) + { + bShouldWait = true; + } + + if ( bShouldWait ) + { + // One of the actors for this scene is talking already. + // Try again next think. + m_bWaitingForActor = true; + return false; + } + } + } + else if ( m_BusyActor == SCENE_BUSYACTOR_INTERRUPT || m_BusyActor == SCENE_BUSYACTOR_INTERRUPT_CANCEL ) + { + CBaseCombatCharacter *pActor = pTestActor->MyCombatCharacterPointer(); + if ( pActor && !IsInInterruptableScenes( pActor ) ) + { + // One of the actors is in a scene that's not at an interrupt point. + // Wait until the scene finishes or an interrupt point is reached. + m_bWaitingForInterrupt = true; + return false; + } + + if ( m_BusyActor == SCENE_BUSYACTOR_INTERRUPT_CANCEL ) + { + // Cancel existing scenes + RemoveActorFromScriptedScenes( pActor, false ); + } + else + { + // Pause existing scenes + PauseActorsScriptedScenes( pActor, false ); + m_bInterruptedActorsScenes = true; + } + } + + pTestActor->StartChoreoScene( m_pScene ); + } + + return true; +} + +#if !defined( _RETAIL ) +static ConVar scene_async_prefetch_spew( "scene_async_prefetch_spew", "0", 0, "Display async .ani file loading info." ); +#endif + +void CSceneEntity::PrefetchAnimBlocks( CChoreoScene *scene ) +{ + Assert( scene ); + + // Build a fast lookup, too + CUtlMap< CChoreoActor *, CBaseFlex *> actorMap( 0, 0, DefLessFunc( CChoreoActor * ) ); + + int spew = +#if !defined( _RETAIL ) + scene_async_prefetch_spew.GetInt(); +#else + 0; +#endif + + int resident = 0; + int checked = 0; + + // Iterate events and precache necessary resources + for ( int i = 0; i < scene->GetNumEvents(); i++ ) + { + CChoreoEvent *event = scene->GetEvent( i ); + if ( !event ) + continue; + + // load any necessary data + switch ( event->GetType() ) + { + default: + break; + case CChoreoEvent::SEQUENCE: + case CChoreoEvent::GESTURE: + { + CChoreoActor *actor = event->GetActor(); + if ( actor ) + { + CBaseFlex *pActor = NULL; + int idx = actorMap.Find( actor ); + if ( idx == actorMap.InvalidIndex() ) + { + pActor = FindNamedActor( actor ); + idx = actorMap.Insert( actor, pActor ); + } + else + { + pActor = actorMap[ idx ]; + } + + if ( pActor ) + { + int seq = pActor->LookupSequence( event->GetParameters() ); + if ( seq >= 0 ) + { + CStudioHdr *pStudioHdr = pActor->GetModelPtr(); + if ( pStudioHdr ) + { + // Now look up the animblock + mstudioseqdesc_t &seqdesc = pStudioHdr->pSeqdesc( seq ); + for ( int i = 0 ; i < seqdesc.groupsize[ 0 ] ; ++i ) + { + for ( int j = 0; j < seqdesc.groupsize[ 1 ]; ++j ) + { + int animation = seqdesc.anim( i, j ); + int baseanimation = pStudioHdr->iRelativeAnim( seq, animation ); + mstudioanimdesc_t &animdesc = pStudioHdr->pAnimdesc( baseanimation ); + + ++checked; + + if ( spew != 0 ) + { + Msg( "%s checking block %d\n", pStudioHdr->pszName(), animdesc.animblock ); + } + + // Async load the animation + int iFrame = 0; + const mstudioanim_t *panim = animdesc.pAnim( &iFrame ); + if ( panim ) + { + ++resident; + if ( spew > 1 ) + { + Msg( "%s:%s[%i:%i] was resident\n", pStudioHdr->pszName(), animdesc.pszName(), i, j ); + } + } + else + { + if ( spew != 0 ) + { + Msg( "%s:%s[%i:%i] async load\n", pStudioHdr->pszName(), animdesc.pszName(), i, j ); + } + } + } + } + } + } + } + } + } + break; + } + } + + if ( !spew || checked <= 0 ) + return; + + Msg( "%d of %d animations resident\n", resident, checked ); +} + +void CSceneEntity::OnLoaded() +{ + // Nothing +} + +//----------------------------------------------------------------------------- +// Purpose: Initiate scene playback +//----------------------------------------------------------------------------- +void CSceneEntity::StartPlayback( void ) +{ + if ( !m_pScene ) + { + if ( m_bSceneMissing ) + return; + + m_pScene = LoadScene( STRING( m_iszSceneFile ), this ); + if ( !m_pScene ) + { + ChoreoMsg1( 1, "%s missing from scenes.image\n", STRING( m_iszSceneFile ) ); + m_bSceneMissing = true; + return; + } + + OnLoaded(); + + if ( ShouldNetwork() ) + { + m_nSceneStringIndex = g_pStringTableClientSideChoreoScenes->AddString( CBaseEntity::IsServer(), STRING( m_iszSceneFile ) ); + } + + UpdateTransmitState(); + } + + if ( m_bIsPlayingBack ) + return; + + // Make sure actors are alive and able to handle this scene now, otherwise + // we'll wait for them to show up + if ( !CheckActors() ) + { + return; + } + + m_bCompletedEarly = false; + m_bWaitingForActor = false; + m_bWaitingForInterrupt = false; + m_bIsPlayingBack = true; + NetworkProp()->NetworkStateForceUpdate(); + m_bPaused = false; + SetCurrentTime( 0.0f, true ); + m_pScene->ResetSimulation(); + ClearInterrupt(); + + // Put face back in neutral pose + ClearSceneEvents( m_pScene, false ); + + m_OnStart.FireOutput( this, this, 0 ); + + // Aysnchronously load speak sounds + CUtlSymbolTable prefetchSoundSymbolTable; + CUtlRBTree< SpeakEventSound_t > soundnames( 0, 0, SpeakEventSoundLessFunc ); + + BuildSortedSpeakEventSoundsPrefetchList( m_pScene, prefetchSoundSymbolTable, soundnames, 0.0f ); + PrefetchSpeakEventSounds( prefetchSoundSymbolTable, soundnames ); + + // Tell any managers we're within that we've started + int c = m_hListManagers.Count(); + for ( int i = 0; i < c; i++ ) + { + if ( m_hListManagers[i] ) + { + m_hListManagers[i]->SceneStarted( this ); + } + } + + PrefetchAnimBlocks( m_pScene ); +} + +//----------------------------------------------------------------------------- +// Purpose: Static method used to sort by event start time +//----------------------------------------------------------------------------- +bool CSceneEntity::SpeakEventSoundLessFunc( const SpeakEventSound_t& lhs, const SpeakEventSound_t& rhs ) +{ + return lhs.m_flStartTime < rhs.m_flStartTime; +} + +//----------------------------------------------------------------------------- +// Purpose: Prefetches the list of sounds build by BuildSortedSpeakEventSoundsPrefetchList +//----------------------------------------------------------------------------- +void CSceneEntity::PrefetchSpeakEventSounds( CUtlSymbolTable& table, CUtlRBTree< SpeakEventSound_t >& soundnames ) +{ + for ( int i = soundnames.FirstInorder(); i != soundnames.InvalidIndex() ; i = soundnames.NextInorder( i ) ) + { + SpeakEventSound_t& sound = soundnames[ i ]; + // Look it up in the string table + char const *soundname = table.String( sound.m_Symbol ); + + // Warning( "Prefetch %s\n", soundname ); + + PrefetchScriptSound( soundname ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Builds list of sounds sorted by start time for prefetching +//----------------------------------------------------------------------------- +void CSceneEntity::BuildSortedSpeakEventSoundsPrefetchList( + CChoreoScene *scene, + CUtlSymbolTable& table, + CUtlRBTree< SpeakEventSound_t >& soundnames, + float timeOffset ) +{ + Assert( scene ); + + // Iterate events and precache necessary resources + for ( int i = 0; i < scene->GetNumEvents(); i++ ) + { + CChoreoEvent *event = scene->GetEvent( i ); + if ( !event ) + continue; + + // load any necessary data + switch (event->GetType() ) + { + default: + break; + case CChoreoEvent::SPEAK: + { + + // NOTE: The script entries associated with .vcds are forced to preload to avoid + // loading hitches during triggering + char soundname[ CChoreoEvent::MAX_CCTOKEN_STRING ]; + Q_strncpy( soundname, event->GetParameters(), sizeof( soundname ) ); + + if ( event->GetCloseCaptionType() == CChoreoEvent::CC_MASTER ) + { + event->GetPlaybackCloseCaptionToken( soundname, sizeof( soundname ) ); + } + + // In single player, try to use the combined or regular .wav files as needed + if ( gpGlobals->maxClients == 1 ) + { + CBasePlayer *player = UTIL_GetLocalPlayer(); + if ( player && !GetSoundNameForPlayer( event, player, soundname, sizeof( soundname ), player ) ) + { + // Skip to next event + continue; + } + } + /* + else + { + // UNDONE: Probably need some other solution in multiplayer... (not sure how to "prefetch" on certain players + // with one sound, but not prefetch the same sound for others...) + } + */ + + SpeakEventSound_t ses; + ses.m_Symbol = table.AddString( soundname ); + ses.m_flStartTime = timeOffset + event->GetStartTime(); + + soundnames.Insert( ses ); + } + break; + case CChoreoEvent::SUBSCENE: + { + // Only allow a single level of subscenes for now + if ( !scene->IsSubScene() ) + { + CChoreoScene *subscene = event->GetSubScene(); + if ( !subscene ) + { + subscene = LoadScene( event->GetParameters(), this ); + subscene->SetSubScene( true ); + event->SetSubScene( subscene ); + + // Now precache it's resources, if any + BuildSortedSpeakEventSoundsPrefetchList( subscene, table, soundnames, event->GetStartTime() ); + } + } + } + break; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::PausePlayback( void ) +{ + if ( !m_bIsPlayingBack ) + return; + + if ( m_bPaused ) + return; + + m_bPaused = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::ResumePlayback( void ) +{ + if ( !m_bIsPlayingBack ) + return; + + if ( !m_bPaused ) + return; + + Assert( m_pScene ); + if ( !m_pScene ) + { + // This should never happen!!!! + return; + } + + // FIXME: Iterate using m_pScene->IterateResumeConditionEvents and + // only resume if the event conditions have all been satisfied + + // FIXME: Just resume for now + m_pScene->ResumeSimulation(); + + m_bPaused = false; + m_bPausedViaInput = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::CancelPlayback( void ) +{ + if ( !m_bIsPlayingBack ) + return; + + m_bIsPlayingBack = false; + m_bPaused = false; + + m_OnCanceled.FireOutput( this, this, 0 ); + + LocalScene_Printf( "%s : %8.2f: canceled\n", STRING( m_iszSceneFile ), m_flCurrentTime ); + + OnSceneFinished( true, false ); +} + +void CSceneEntity::PitchShiftPlayback( float fPitch ) +{ + fPitch = clamp( fPitch, SCENE_MIN_PITCH, SCENE_MAX_PITCH ); + + m_fPitch = fPitch; + + if ( !m_pScene ) + return; + + for ( int iActor = 0 ; iActor < m_pScene->GetNumActors(); ++iActor ) + { + CBaseFlex *pTestActor = FindNamedActor( iActor ); + + if ( !pTestActor ) + continue; + + char szBuff[ 256 ]; + + if ( m_pScene->GetPlayingSoundName( szBuff, sizeof( szBuff ) ) ) + { + CPASAttenuationFilter filter( pTestActor ); + EmitSound_t params; + params.m_pSoundName = szBuff; + params.m_nPitch = 100.0f * fPitch; + params.m_nFlags = SND_CHANGE_PITCH; + pTestActor->EmitSound( filter, pTestActor->entindex(), params ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Start a resume scene, if we have one, and resume playing when it finishes +//----------------------------------------------------------------------------- +void CSceneEntity::QueueResumePlayback( void ) +{ + // Do we have a resume scene? + if ( m_iszResumeSceneFile != NULL_STRING ) + { + bool bStartedScene = false; + + // If it has ".vcd" somewhere in the string, try using it as a scene file first + if ( Q_stristr( STRING(m_iszResumeSceneFile), ".vcd" ) ) + { + bStartedScene = InstancedScriptedScene( NULL, STRING(m_iszResumeSceneFile), &m_hWaitingForThisResumeScene, 0, false ) != 0; + } + + // HACKHACK: For now, get the first target, and see if we can find a response for him + if ( !bStartedScene ) + { + CBaseFlex *pActor = FindNamedActor( 0 ); + if ( pActor ) + { + CAI_BaseActor *pBaseActor = dynamic_cast(pActor); + if ( pBaseActor ) + { + AI_Response *result = pBaseActor->SpeakFindResponse( STRING(m_iszResumeSceneFile), NULL ); + if ( result ) + { + char response[ 256 ]; + result->GetResponse( response, sizeof( response ) ); + bStartedScene = InstancedScriptedScene( NULL, response, &m_hWaitingForThisResumeScene, 0, false ) != 0; + } + } + } + } + + // If we started a scene/response, wait for it to finish + if ( bStartedScene ) + { + m_bWaitingForResumeScene = true; + } + else + { + // Failed to create the scene. Resume immediately. + ResumePlayback(); + } + } + else + { + // No resume scene, so just resume immediately + ResumePlayback(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Query whether the scene actually loaded. Only meaninful after Spawn() +//----------------------------------------------------------------------------- +bool CSceneEntity::ValidScene() const +{ + return ( m_pScene != NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActor - +// *scene - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::DispatchStartSubScene( CChoreoScene *scene, CBaseFlex *pActor, CChoreoEvent *event) +{ + if ( !scene->IsSubScene() ) + { + CChoreoScene *subscene = event->GetSubScene(); + if ( !subscene ) + { + Assert( 0 ); + /* + subscene = LoadScene( event->GetParameters() ); + subscene->SetSubScene( true ); + event->SetSubScene( subscene ); + */ + } + + if ( subscene ) + { +#ifdef MAPBASE + // Somes may not be created with a CSceneEntity as the event callback + if (!scene->GetEventCallbackInterface()) + scene->SetEventCallbackInterface( this ); +#endif + + subscene->ResetSimulation(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: All events are leading edge triggered +// Input : currenttime - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::StartEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) +{ + Assert( event ); + + if ( !Q_stricmp( event->GetName(), "NULL" ) ) + { + LocalScene_Printf( "%s : %8.2f: ignored %s\n", STRING( m_iszSceneFile ), currenttime, event->GetDescription() ); + return; + } + + + CBaseFlex *pActor = NULL; + CChoreoActor *actor = event->GetActor(); + if ( actor && (event->GetType() != CChoreoEvent::SCRIPT) && (event->GetType() != CChoreoEvent::CAMERA) ) + { + pActor = FindNamedActor( actor ); + if (pActor == NULL) + { + Warning( "CSceneEntity %s unable to find actor named \"%s\"\n", STRING(GetEntityName()), actor->GetName() ); + return; + } + } + + LocalScene_Printf( "%s : %8.2f: start %s\n", STRING( m_iszSceneFile ), currenttime, event->GetDescription() ); + + switch ( event->GetType() ) + { + case CChoreoEvent::SUBSCENE: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchStartSubScene( scene, pActor, event ); + } + } + break; + case CChoreoEvent::EXPRESSION: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchStartExpression( scene, pActor, event ); + } + } + break; + case CChoreoEvent::FLEXANIMATION: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchStartFlexAnimation( scene, pActor, event ); + } + } + break; + case CChoreoEvent::LOOKAT: + { + if ( pActor && !IsMultiplayer() ) + { + CBaseEntity *pActor2 = FindNamedEntity( event->GetParameters( ), pActor ); + if ( pActor2 ) + { + // Huh? + DispatchStartLookAt( scene, pActor, pActor2, event ); + } + else + { + Warning( "CSceneEntity %s unable to find actor named \"%s\"\n", STRING(GetEntityName()), event->GetParameters() ); + } + } + } + break; + case CChoreoEvent::SPEAK: + { + if ( pActor ) + { + // Speaking is edge triggered + + // FIXME: dB hack. soundlevel needs to be moved into inside of wav? + soundlevel_t iSoundlevel = SNDLVL_TALKING; + if (event->GetParameters2()) + { + iSoundlevel = (soundlevel_t)atoi( event->GetParameters2() ); + if (iSoundlevel == SNDLVL_NONE) + iSoundlevel = SNDLVL_TALKING; + } + + DispatchStartSpeak( scene, pActor, event, iSoundlevel ); + } + } + break; + case CChoreoEvent::MOVETO: + { + // FIXME: make sure moveto's aren't edge triggered + if ( !event->HasEndTime() ) + { + event->SetEndTime( event->GetStartTime() + 1.0 ); + } + + if ( pActor && !IsMultiplayer() ) + { + CBaseEntity *pActor2 = NULL; + if ( event->GetParameters3( ) && strlen( event->GetParameters3( ) ) > 0 ) + { + pActor2 = FindNamedEntityClosest( event->GetParameters( ), pActor, false, true, event->GetParameters3( ) ); + } + else + { + pActor2 = FindNamedEntity( event->GetParameters( ), pActor, false, true ); + } + if ( pActor2 ) + { + + DispatchStartMoveTo( scene, pActor, pActor2, event ); + } + else + { + Warning( "CSceneEntity %s unable to find actor named \"%s\"\n", STRING(GetEntityName()), event->GetParameters() ); + } + } + } + break; + case CChoreoEvent::FACE: + { + if ( pActor && !IsMultiplayer() ) + { + CBaseEntity *pActor2 = FindNamedEntity( event->GetParameters( ), pActor ); + if ( pActor2 ) + { + DispatchStartFace( scene, pActor, pActor2, event ); + } + else + { + Warning( "CSceneEntity %s unable to find actor named \"%s\"\n", STRING(GetEntityName()), event->GetParameters() ); + } + } + } + break; + case CChoreoEvent::GESTURE: + { + if ( pActor ) + { + DispatchStartGesture( scene, pActor, event ); + } + } + break; + case CChoreoEvent::GENERIC: + { + // If the first token in the parameters is "debugtext", print the rest of the text + if ( event->GetParameters() && !Q_strncmp( event->GetParameters(), "debugtext", 9 ) ) + { + const char *pszText = event->GetParameters() + 10; + + hudtextparms_s tTextParam; + tTextParam.x = -1; + tTextParam.y = 0.65; + tTextParam.effect = 0; + tTextParam.r1 = 255; + tTextParam.g1 = 170; + tTextParam.b1 = 0; + tTextParam.a1 = 255; + tTextParam.r2 = 255; + tTextParam.g2 = 170; + tTextParam.b2 = 0; + tTextParam.a2 = 255; + tTextParam.fadeinTime = 0; + tTextParam.fadeoutTime = 0; + tTextParam.holdTime = 3.1; + tTextParam.fxTime = 0; + tTextParam.channel = 1; + UTIL_HudMessageAll( tTextParam, pszText ); + break; + } + + if ( pActor ) + { + DispatchStartGeneric( scene, pActor, event ); + } + } + break; + + case CChoreoEvent::CAMERA: + { + // begin the camera shot + const char *pszShotType = event->GetParameters(); + + CBaseEntity *pActor1 = FindNamedEntity( event->GetParameters2( ), pActor ); + CBaseEntity *pActor2 = FindNamedEntity( event->GetParameters3( ), pActor ); + float duration = event->GetDuration(); + + // grab any camera we find in the map + // TODO: find camera that is nearest this scene entity? + CTriggerCamera *pCamera = (CTriggerCamera *)gEntList.FindEntityByClassname( NULL, "point_viewcontrol" ); + + if ( !pCamera ) + { + Warning( "CSceneEntity %s unable to find a camera (point_viewcontrol) in this map!\n", STRING(GetEntityName()) ); + } + else + { + pCamera->StartCameraShot( pszShotType, this, pActor1, pActor2, duration ); + } + } + break; + + case CChoreoEvent::SCRIPT: + { + // NOTE: this is only used by auto-generated vcds to embed script commands to map entities. + + // vscript call - param1 is entity name, param2 is function name, param3 is function parameter string + // calls a vscript function defined on the scope of the named CBaseEntity object/actor. + // script call is of the format FunctionName(pActor, pThisSceneEntity, pszScriptParameters, duration) + const char *pszActorName = event->GetParameters(); + const char *pszFunctionName = event->GetParameters2(); + const char *pszScriptParameters = event->GetParameters3(); + + float duration = event->GetDuration(); + + // TODO: should be new method CBaseEntity::CallScriptFunctionParams() + CBaseEntity *pEntity = (CBaseEntity *)gEntList.FindEntityByName( NULL, pszActorName ); + + //START_VMPROFILE + if ( !pEntity ) + { + Warning( "CSceneEntity::SCRIPT event - unable to find entity named '%s' in this map!\n", pszActorName ); + } + else + { + + if( !pEntity->ValidateScriptScope() ) + { + ChoreoMsg(1, "\n***\nCChoreoEvent::SCRIPT - FAILED to create private ScriptScope. ABORTING script call\n***\n"); + break; + } + + HSCRIPT hFunc = pEntity->m_ScriptScope.LookupFunction( pszFunctionName ); + + if( hFunc ) + { + pEntity->m_ScriptScope.Call( hFunc, NULL, ToHScript(this), pszScriptParameters, duration ); + pEntity->m_ScriptScope.ReleaseFunction( hFunc ); + + //UPDATE_VMPROFILE + } + else + { + Warning("CSceneEntity::SCRIPT event - '%s' entity has no script function '%s' defined!\n", pszActorName,pszFunctionName); + } + } + + } + break; + + + case CChoreoEvent::FIRETRIGGER: + { + if ( IsMultiplayer() ) + break; + + // Don't re-fire triggers during restore, the entities should already reflect all such state... + if ( m_bRestoring ) + { + break; + } + + CBaseEntity *pActivator = pActor; + if (!pActivator) + { + pActivator = this; + } + + // FIXME: how do I decide who fired it?? + switch( atoi( event->GetParameters() ) ) + { + case 1: + m_OnTrigger1.FireOutput( pActivator, this, 0 ); + break; + case 2: + m_OnTrigger2.FireOutput( pActivator, this, 0 ); + break; + case 3: + m_OnTrigger3.FireOutput( pActivator, this, 0 ); + break; + case 4: + m_OnTrigger4.FireOutput( pActivator, this, 0 ); + break; + case 5: + m_OnTrigger5.FireOutput( pActivator, this, 0 ); + break; + case 6: + m_OnTrigger6.FireOutput( pActivator, this, 0 ); + break; + case 7: + m_OnTrigger7.FireOutput( pActivator, this, 0 ); + break; + case 8: + m_OnTrigger8.FireOutput( pActivator, this, 0 ); + break; + case 9: + m_OnTrigger9.FireOutput( pActivator, this, 0 ); + break; + case 10: + m_OnTrigger10.FireOutput( pActivator, this, 0 ); + break; + case 11: + m_OnTrigger11.FireOutput( pActivator, this, 0 ); + break; + case 12: + m_OnTrigger12.FireOutput( pActivator, this, 0 ); + break; + case 13: + m_OnTrigger13.FireOutput( pActivator, this, 0 ); + break; + case 14: + m_OnTrigger14.FireOutput( pActivator, this, 0 ); + break; + case 15: + m_OnTrigger15.FireOutput( pActivator, this, 0 ); + break; + case 16: + m_OnTrigger16.FireOutput( pActivator, this, 0 ); + break; + } + } + break; + case CChoreoEvent::SEQUENCE: + { + if ( pActor ) + { + DispatchStartSequence( scene, pActor, event ); + } + } + break; + case CChoreoEvent::SECTION: + { + if ( IsMultiplayer() ) + break; + + // Pauses scene playback + DispatchPauseScene( scene, event->GetParameters() ); + } + break; + case CChoreoEvent::LOOP: + { + DispatchProcessLoop( scene, event ); + } + break; + case CChoreoEvent::INTERRUPT: + { + if ( IsMultiplayer() ) + break; + + DispatchStartInterrupt( scene, event ); + } + break; + + case CChoreoEvent::STOPPOINT: + { + if ( IsMultiplayer() ) + break; + + DispatchStopPoint( scene, event->GetParameters() ); + } + break; + + case CChoreoEvent::PERMIT_RESPONSES: + { + if ( IsMultiplayer() ) + break; + + if ( pActor ) + { + DispatchStartPermitResponses( scene, pActor, event ); + } + } + break; + default: + { + // FIXME: Unhandeled event + // Assert(0); + } + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : currenttime - +// *event - +//----------------------------------------------------------------------------- +void CSceneEntity::EndEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) +{ + Assert( event ); + + if ( !Q_stricmp( event->GetName(), "NULL" ) ) + { + return; + } + + CBaseFlex *pActor = NULL; + CChoreoActor *actor = event->GetActor(); + if ( actor ) + { + pActor = FindNamedActor( actor ); + } + + LocalScene_Printf( "%s : %8.2f: finish %s\n", STRING( m_iszSceneFile ), currenttime, event->GetDescription() ); + + switch ( event->GetType() ) + { + case CChoreoEvent::EXPRESSION: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchEndExpression( scene, pActor, event ); + } + } + break; + case CChoreoEvent::SPEAK: + { + if ( pActor ) + { + DispatchEndSpeak( scene, pActor, event ); + } + } + break; + case CChoreoEvent::FLEXANIMATION: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchEndFlexAnimation( scene, pActor, event ); + } + } + break; + + case CChoreoEvent::LOOKAT: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchEndLookAt( scene, pActor, event ); + } + } + break; + + + case CChoreoEvent::GESTURE: + { + if ( pActor ) + { + DispatchEndGesture( scene, pActor, event ); + } + } + break; + case CChoreoEvent::GENERIC: + { + // If the first token in the parameters is "debugtext", we printed it and we're done + if ( event->GetParameters() && !Q_strncmp( event->GetParameters(), "debugtext", 9 ) ) + break; + + if ( pActor ) + { + DispatchEndGeneric( scene, pActor, event ); + } + } + break; + + case CChoreoEvent::CAMERA: + { + // call the end of camera or call a dispatch function + } + break; + + case CChoreoEvent::SCRIPT: + { + // call the end of script or call a dispatch function + } + break; + + case CChoreoEvent::SEQUENCE: + { + if ( pActor ) + { + DispatchEndSequence( scene, pActor, event ); + } + } + break; + + case CChoreoEvent::FACE: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchEndFace( scene, pActor, event ); + } + } + break; + + case CChoreoEvent::MOVETO: + { + if ( pActor && !IsMultiplayer() ) + { + DispatchEndMoveTo( scene, pActor, event ); + } + } + break; + + case CChoreoEvent::SUBSCENE: + { + if ( IsMultiplayer() ) + break; + + CChoreoScene *subscene = event->GetSubScene(); + if ( subscene ) + { + subscene->ResetSimulation(); + } + } + break; + case CChoreoEvent::INTERRUPT: + { + if ( IsMultiplayer() ) + break; + + DispatchEndInterrupt( scene, event ); + } + break; + + case CChoreoEvent::PERMIT_RESPONSES: + { + if ( IsMultiplayer() ) + break; + + if ( pActor ) + { + DispatchEndPermitResponses( scene, pActor, event ); + } + } + break; + default: + break; + } +} + + + +//----------------------------------------------------------------------------- +// Purpose: Only spew one time per missing scene!!! +// Input : *scenename - +//----------------------------------------------------------------------------- +void MissingSceneWarning( char const *scenename ) +{ + static CUtlSymbolTable missing; + + // Make sure we only show the message once + if ( UTL_INVAL_SYMBOL == missing.Find( scenename ) ) + { + missing.AddString( scenename ); + + Warning( "Scene '%s' missing!\n", scenename ); + } +} + +bool CSceneEntity::ShouldNetwork() const +{ + if ( m_bMultiplayer ) + { + if ( m_pScene && + ( m_pScene->HasEventsOfType( CChoreoEvent::FLEXANIMATION ) || + m_pScene->HasEventsOfType( CChoreoEvent::EXPRESSION )|| + m_pScene->HasEventsOfType( CChoreoEvent::GESTURE ) || + m_pScene->HasEventsOfType( CChoreoEvent::SEQUENCE ) ) ) + { + return true; + } + } + else + { + if ( m_pScene && + ( m_pScene->HasEventsOfType( CChoreoEvent::FLEXANIMATION ) || + m_pScene->HasEventsOfType( CChoreoEvent::EXPRESSION ) ) ) + { + return true; + } + } + + return false; +} + +CChoreoScene *CSceneEntity::LoadScene( const char *filename, IChoreoEventCallback *pCallback ) +{ + ChoreoMsg1( 2, "Blocking load of scene from '%s'\n", filename ); + + char loadfile[MAX_PATH]; + Q_strncpy( loadfile, filename, sizeof( loadfile ) ); + Q_SetExtension( loadfile, ".vcd", sizeof( loadfile ) ); + Q_FixSlashes( loadfile ); + + // binary compiled vcd + void *pBuffer; +#ifdef MAPBASE + // + // Raw scene file support + // + CChoreoScene *pScene; + int fileSize; + + // First, check if it's in scenes.image... + if ( CopySceneFileIntoMemory( loadfile, &pBuffer, &fileSize ) ) + { + pScene = new CChoreoScene( NULL ); + CUtlBuffer buf( pBuffer, fileSize, CUtlBuffer::READ_ONLY ); + if ( !pScene->RestoreFromBinaryBuffer( buf, loadfile, &g_ChoreoStringPool ) ) + { + Warning( "CSceneEntity::LoadScene: Unable to load binary scene '%s'\n", loadfile ); + delete pScene; + pScene = NULL; + } + } + // Next, check if it's a loose file... + else if (filesystem->ReadFileEx( loadfile, "MOD", &pBuffer, true )) + { + g_TokenProcessor.SetBuffer((char*)pBuffer); + pScene = ChoreoLoadScene( loadfile, NULL, &g_TokenProcessor, LocalScene_Printf ); + } + // Okay, it's definitely missing. + else + { + MissingSceneWarning( loadfile ); + return NULL; + } + + if (pScene) + { + pScene->SetPrintFunc( LocalScene_Printf ); + pScene->SetEventCallbackInterface( pCallback ); + } +#else + int fileSize; + if ( !CopySceneFileIntoMemory( loadfile, &pBuffer, &fileSize ) ) + { + MissingSceneWarning( loadfile ); + return NULL; + } + + CChoreoScene *pScene = new CChoreoScene( NULL ); + CUtlBuffer buf( pBuffer, fileSize, CUtlBuffer::READ_ONLY ); + if ( !pScene->RestoreFromBinaryBuffer( buf, loadfile, &g_ChoreoStringPool ) ) + { + Warning( "CSceneEntity::LoadScene: Unable to load binary scene '%s'\n", loadfile ); + delete pScene; + pScene = NULL; + } + else + { + pScene->SetPrintFunc( LocalScene_Printf ); + pScene->SetEventCallbackInterface( pCallback ); + } +#endif + + FreeSceneFileMemory( pBuffer ); + return pScene; +} + +CChoreoScene *BlockingLoadScene( const char *filename ) +{ + return CSceneEntity::LoadScene( filename, NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::UnloadScene( void ) +{ + if ( m_pScene ) + { + ClearSceneEvents( m_pScene, false ); + + for ( int i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pTestActor = FindNamedActor( i ); + + if ( !pTestActor ) + continue; + + pTestActor->RemoveChoreoScene( m_pScene ); + } + } + delete m_pScene; + m_pScene = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Called every frame that an event is active (Start/EndEvent as also +// called) +// Input : *event - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +void CSceneEntity::ProcessEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) +{ + switch ( event->GetType() ) + { + case CChoreoEvent::SUBSCENE: + { + Assert( event->GetType() == CChoreoEvent::SUBSCENE ); + + CChoreoScene *subscene = event->GetSubScene(); + if ( !subscene ) + return; + + if ( subscene->SimulationFinished() ) + return; + + // Have subscenes think for appropriate time + subscene->Think( m_flFrameTime ); + } + break; + + default: + break; + } + + return; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Called for events that are part of a pause condition +// Input : *event - +// Output : Returns true on event completed, false on non-completion. +//----------------------------------------------------------------------------- +bool CSceneEntity::CheckEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) +{ + switch ( event->GetType() ) + { + case CChoreoEvent::SUBSCENE: + { + } + break; + default: + { + CBaseFlex *pActor = NULL; + CChoreoActor *actor = event->GetActor(); + if ( actor ) + { + pActor = FindNamedActor( actor ); + if (pActor == NULL) + { + Warning( "CSceneEntity %s unable to find actor \"%s\"\n", STRING(GetEntityName()), actor->GetName() ); + return true; + } + } + if (pActor) + { + return pActor->CheckSceneEvent( currenttime, scene, event ); + } + } + break; + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get a sticky version of a named actor +// Input : CChoreoActor +// Output : CBaseFlex +//----------------------------------------------------------------------------- + +CBaseFlex *CSceneEntity::FindNamedActor( int index ) +{ + if (m_hActorList.Count() == 0) + { + m_hActorList.SetCount( m_pScene->GetNumActors() ); + NetworkProp()->NetworkStateForceUpdate(); + } + + if ( !m_hActorList.IsValidIndex( index ) ) + { + DevWarning( "Scene %s has %d actors, but scene entity only has %d actors\n", m_pScene->GetFilename(), m_pScene->GetNumActors(), m_hActorList.Size() ); + return NULL; + } + + CBaseFlex *pActor = m_hActorList[ index ]; + + if (pActor == NULL || !pActor->IsAlive() ) + { + CChoreoActor *pChoreoActor = m_pScene->GetActor( index ); + if ( !pChoreoActor ) + return NULL; + + pActor = FindNamedActor( pChoreoActor->GetName() ); + + if (pActor) + { + // save who we found so we'll use them again + m_hActorList[ index ] = pActor; + NetworkProp()->NetworkStateForceUpdate(); + } + } + + return pActor; +} + +//----------------------------------------------------------------------------- +// Purpose: Get a sticky version of a named actor +// Input : CChoreoActor +// Output : CBaseFlex +//----------------------------------------------------------------------------- + +CBaseFlex *CSceneEntity::FindNamedActor( CChoreoActor *pChoreoActor ) +{ + int index = m_pScene->FindActorIndex( pChoreoActor ); + + if (index >= 0) + { + return FindNamedActor( index ); + } + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Search for an actor by name, make sure it can do face poses +// Input : *name - +// Output : CBaseFlex +//----------------------------------------------------------------------------- +CBaseFlex *CSceneEntity::FindNamedActor( const char *name ) +{ + CBaseEntity *entity = FindNamedEntity( name, NULL, true ); + + if ( !entity ) + { + // Couldn't find actor! + return NULL; + } + + // Make sure it can actually do facial animation, etc. + CBaseFlex *flexEntity = dynamic_cast< CBaseFlex * >( entity ); + if ( !flexEntity ) + { + // That actor was not a CBaseFlex! + return NULL; + } + + return flexEntity; +} + +//----------------------------------------------------------------------------- +// Purpose: Find an entity specified by a target name +// Input : *name - +// Output : CBaseEntity +//----------------------------------------------------------------------------- +CBaseEntity *CSceneEntity::FindNamedTarget( string_t iszTarget, bool bBaseFlexOnly ) +{ + if ( !stricmp( STRING(iszTarget), "!activator" ) ) + return m_hActivator; + + // If we don't have a wildcard in the target, just return the first entity found + if ( !strchr( STRING(iszTarget), '*' ) ) + return gEntList.FindEntityByName( NULL, iszTarget ); + + CBaseEntity *pTarget = NULL; + while ( (pTarget = gEntList.FindEntityByName( pTarget, iszTarget )) != NULL ) + { + if ( bBaseFlexOnly ) + { + // Make sure it can actually do facial animation, etc. + if ( dynamic_cast< CBaseFlex * >( pTarget ) ) + return pTarget; + } + else + { + return pTarget; + } + } + + // Failed to find one + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Filters entities only if they're clear +//----------------------------------------------------------------------------- +class CSceneFindMarkFilter : public IEntityFindFilter +{ +public: + void SetActor( CBaseEntity *pActor ) + { + m_hActor = pActor; + } + + bool ShouldFindEntity( CBaseEntity *pEntity ) + { + if ( !m_hActor ) + return true; + + // If we find no truly valid marks, we'll just use the first. + if ( !m_hEntityFound.Get() ) + { + m_hEntityFound = pEntity; + } + + // We only want marks that are clear + trace_t tr; + Vector vecOrigin = pEntity->GetAbsOrigin(); + AI_TraceHull( vecOrigin, vecOrigin, m_hActor->WorldAlignMins(), m_hActor->WorldAlignMaxs(), MASK_SOLID, m_hActor, COLLISION_GROUP_NONE, &tr ); + if ( tr.startsolid ) + { + return false; + } + m_hEntityFound = pEntity; + return true; + } + + CBaseEntity *GetFilterResult( void ) + { + return m_hEntityFound; + } + +private: + EHANDLE m_hActor; + + // To maintain backwards compatability, store off the first mark + // we find. If we find no truly valid marks, we'll just use the first. + EHANDLE m_hEntityFound; +}; + + +//----------------------------------------------------------------------------- +// Purpose: Finds the entity nearest to both entities, and is clear +//----------------------------------------------------------------------------- +class CSceneFindNearestMarkFilter : public IEntityFindFilter +{ +public: + + CSceneFindNearestMarkFilter( const CBaseEntity *pActor, const Vector &vecPos2, float flMaxRadius = MAX_TRACE_LENGTH ) + { + m_vecPos2 = vecPos2; + + m_flMaxSegmentDistance = flMaxRadius; + + m_flNearestToTarget = flMaxRadius; + m_pNearestToTarget = NULL; + m_flNearestToActor = flMaxRadius; + m_pNearestToActor = NULL; + + m_hActor = pActor; + if (pActor) + { + m_vecPos1 = pActor->GetAbsOrigin(); + m_flMaxSegmentDistance = MIN( flMaxRadius, (m_vecPos1 - m_vecPos2).Length() + 1.0 ); + if (m_flMaxSegmentDistance <= 1.0) + { + // must be closest to self + m_flMaxSegmentDistance = MIN( flMaxRadius, MAX_TRACE_LENGTH ); + } + } + } + + bool ShouldFindEntity( CBaseEntity *pEntity ) + { + if ( !m_hActor ) + return true; + + // If we find no truly valid marks, we'll just use the first. + if ( m_pNearestToActor == NULL ) + { + m_pNearestToActor = pEntity; + } + + // We only want marks that are clear + trace_t tr; + Vector vecOrigin = pEntity->GetAbsOrigin(); + AI_TraceHull( vecOrigin, vecOrigin, m_hActor->WorldAlignMins(), m_hActor->WorldAlignMaxs(), MASK_SOLID, m_hActor, COLLISION_GROUP_NONE, &tr ); + if ( !tr.startsolid || tr.m_pEnt == m_hActor) + { + float dist1 = (m_vecPos1 - pEntity->GetAbsOrigin()).Length(); + float dist2 = (m_vecPos2 - pEntity->GetAbsOrigin()).Length(); + /* + char text[256]; + Q_snprintf( text, sizeof( text ), "%.0f : %.0f", dist1, dist2 ); + NDebugOverlay::Text( pEntity->GetAbsOrigin() + Vector( 0, 0, 8 ), text, false, 5.0f ); + */ + // find the point closest to the actor + if (dist1 <= m_flNearestToActor) + { + m_pNearestToActor = pEntity; + m_flNearestToActor = dist2; + } + // find that node that's closest to both, but the distance to it from the actor isn't farther than + // the distance to the second node. This should keep the actor from walking past their point of interest + if (dist1 <= m_flMaxSegmentDistance && dist2 <= m_flMaxSegmentDistance && dist2 < m_flNearestToTarget) + { + m_pNearestToTarget = pEntity; + m_flNearestToTarget = dist2; + } + } + + return false; + } + + CBaseEntity *GetFilterResult( void ) + { + if (m_pNearestToTarget) + return m_pNearestToTarget; + return m_pNearestToActor; + } + +private: + EHANDLE m_hActor; + Vector m_vecPos1; + Vector m_vecPos2; + float m_flMaxSegmentDistance; + float m_flNearestToTarget; + CBaseEntity *m_pNearestToTarget; + float m_flNearestToActor; + CBaseEntity *m_pNearestToActor; +}; + +//----------------------------------------------------------------------------- +// Purpose: Search for an actor by name, make sure it can do face poses +// Input : *name - +// Output : CBaseFlex +//----------------------------------------------------------------------------- +CBaseEntity *CSceneEntity::FindNamedEntity( const char *name, CBaseEntity *pActor, bool bBaseFlexOnly, bool bUseClear ) +{ + CBaseEntity *entity = NULL; + + if ( !stricmp( name, "Player" ) || !stricmp( name, "!player" )) + { + entity = ( gpGlobals->maxClients == 1 ) ? ( CBaseEntity * )UTIL_GetLocalPlayer() : NULL; + } + else if ( !stricmp( name, "!target1" ) ) + { + if (m_hTarget1 == NULL) + { + m_hTarget1 = FindNamedTarget( m_iszTarget1, bBaseFlexOnly ); + } + return m_hTarget1; + } + else if ( !stricmp( name, "!target2" ) ) + { + if (m_hTarget2 == NULL) + { + m_hTarget2 = FindNamedTarget( m_iszTarget2, bBaseFlexOnly ); + } + return m_hTarget2; + } + else if ( !stricmp( name, "!target3" ) ) + { + if (m_hTarget3 == NULL) + { + m_hTarget3 = FindNamedTarget( m_iszTarget3, bBaseFlexOnly ); + } + return m_hTarget3; + } + else if ( !stricmp( name, "!target4" ) ) + { + if (m_hTarget4 == NULL) + { + m_hTarget4 = FindNamedTarget( m_iszTarget4, bBaseFlexOnly ); + } + return m_hTarget4; + } + else if ( !stricmp( name, "!target5" ) ) + { + if (m_hTarget5 == NULL) + { + m_hTarget5 = FindNamedTarget( m_iszTarget5, bBaseFlexOnly ); + } + return m_hTarget5; + } + else if ( !stricmp( name, "!target6" ) ) + { + if (m_hTarget6 == NULL) + { + m_hTarget6 = FindNamedTarget( m_iszTarget6, bBaseFlexOnly ); + } + return m_hTarget6; + } + else if ( !stricmp( name, "!target7" ) ) + { + if (m_hTarget7 == NULL) + { + m_hTarget7 = FindNamedTarget( m_iszTarget7, bBaseFlexOnly ); + } + return m_hTarget7; + } + else if ( !stricmp( name, "!target8" ) ) + { + if (m_hTarget8 == NULL) + { + m_hTarget8 = FindNamedTarget( m_iszTarget8, bBaseFlexOnly ); + } + return m_hTarget8; + } + else if (pActor && pActor->MyNPCPointer()) + { + CSceneFindMarkFilter *pFilter = NULL; + if ( bUseClear ) + { + pFilter = new CSceneFindMarkFilter(); + pFilter->SetActor( pActor ); + } + + entity = pActor->MyNPCPointer()->FindNamedEntity( name, pFilter ); + if ( !entity && pFilter ) + { + entity = pFilter->GetFilterResult(); + } + } + else + { + // search for up to 32 entities with the same name and choose one randomly + CBaseEntity *entityList[ FINDNAMEDENTITY_MAX_ENTITIES ]; + int iCount; + + entity = NULL; + for( iCount = 0; iCount < FINDNAMEDENTITY_MAX_ENTITIES; iCount++ ) + { + entity = gEntList.FindEntityByName( entity, name, NULL, pActor ); + if ( !entity ) + { + break; + } + entityList[ iCount ] = entity; + } + + if ( iCount > 0 ) + { + entity = entityList[ RandomInt( 0, iCount - 1 ) ]; + } + else + { + entity = NULL; + } + } + + return entity; +} + +#ifdef MAPBASE +const char *GetFirstSoundInScene(const char *pszScene) +{ + SceneCachedData_t sceneData; + if ( scenefilecache->GetSceneCachedData( pszScene, &sceneData ) ) + { + if ( sceneData.numSounds > 0 ) + { + // 0 is the first index...right? + short stringId = scenefilecache->GetSceneCachedSound( sceneData.sceneId, 0 ); + + // Trust that it's been precached + return scenefilecache->GetSceneString( stringId ); + } + } + else + { + void *pBuffer = NULL; + if (filesystem->ReadFileEx( pszScene, "MOD", &pBuffer, false, true )) + { + g_TokenProcessor.SetBuffer((char*)pBuffer); + CChoreoScene *pScene = ChoreoLoadScene( pszScene, NULL, &g_TokenProcessor, LocalScene_Printf ); + if (pScene) + { + for (int i = 0; i < pScene->GetNumEvents(); i++) + { + CChoreoEvent *pEvent = pScene->GetEvent(i); + + if (pEvent->GetType() == CChoreoEvent::SPEAK) + return pEvent->GetParameters(); + } + } + } + } + + return NULL; +} + +const char *GetFirstSoundInScene(CChoreoScene *scene) +{ + for ( int i = 0; i < scene->GetNumEvents(); i++ ) + { + CChoreoEvent *pEvent = scene->GetEvent( i ); + + if (pEvent->GetType() == CChoreoEvent::SPEAK) + return pEvent->GetParameters(); + } + + return NULL; +} + +CBaseEntity *UTIL_FindNamedSceneEntity(const char *name, CBaseEntity *pActor, CSceneEntity *scene, bool bBaseFlexOnly, bool bUseClear) +{ + if (scene) + { + CBaseEntity *pEnt = scene->FindNamedEntity(name, pActor, bBaseFlexOnly, bUseClear); + return pEnt; + } + else + { + //Warning("SCENE NOT FOUND!\n"); + return NULL; + } +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Search for an actor by name, make sure it can do face poses +// Input : *name - +// Output : CBaseFlex +//----------------------------------------------------------------------------- +CBaseEntity *CSceneEntity::FindNamedEntityClosest( const char *name, CBaseEntity *pActor, bool bBaseFlexOnly, bool bUseClear, const char *pszSecondary ) +{ + CBaseEntity *entity = NULL; + + if ( !stricmp( name, "!activator" ) ) + { + return m_hActivator; + } + else if ( !stricmp( name, "Player" ) || !stricmp( name, "!player" )) + { + entity = ( gpGlobals->maxClients == 1 ) ? ( CBaseEntity * )UTIL_GetLocalPlayer() : NULL; + return entity; + } + else if ( !stricmp( name, "!target1" ) ) + { + name = STRING( m_iszTarget1 ); + } + else if ( !stricmp( name, "!target2" ) ) + { + name = STRING( m_iszTarget2 ); + } + else if ( !stricmp( name, "!target3" ) ) + { + name = STRING( m_iszTarget3 ); + } + else if ( !stricmp( name, "!target4" ) ) + { + name = STRING( m_iszTarget4 ); + } + else if ( !stricmp( name, "!target5" ) ) + { + name = STRING( m_iszTarget5 ); + } + else if ( !stricmp( name, "!target6" ) ) + { + name = STRING( m_iszTarget6 ); + } + else if ( !stricmp( name, "!target7" ) ) + { + name = STRING( m_iszTarget7 ); + } + + if (pActor && pActor->MyNPCPointer()) + { + if (pszSecondary && strlen( pszSecondary ) > 0) + { + CBaseEntity *pActor2 = FindNamedEntityClosest( pszSecondary, pActor, false, false, NULL ); + + if (pActor2) + { + CSceneFindNearestMarkFilter *pFilter = new CSceneFindNearestMarkFilter( pActor, pActor2->GetAbsOrigin() ); + + entity = pActor->MyNPCPointer()->FindNamedEntity( name, pFilter ); + if (!entity && pFilter) + { + entity = pFilter->GetFilterResult(); + } + } + } + if (!entity) + { + CSceneFindMarkFilter *pFilter = NULL; + if ( bUseClear ) + { + pFilter = new CSceneFindMarkFilter(); + pFilter->SetActor( pActor ); + } + + entity = pActor->MyNPCPointer()->FindNamedEntity( name, pFilter ); + if (!entity && pFilter) + { + entity = pFilter->GetFilterResult(); + } + } + } + else + { + // search for up to 32 entities with the same name and choose one randomly + int iCount; + entity = NULL; + CBaseEntity *current = NULL; + for( iCount = 0; iCount < FINDNAMEDENTITY_MAX_ENTITIES; iCount++ ) + { + current = gEntList.FindEntityByName( current, name, NULL, pActor ); + if ( current ) + { + if (RandomInt( 0, iCount ) == 0) + entity = current; + } + } + + entity = NULL; + } + + return entity; +} + + +HSCRIPT CSceneEntity::ScriptFindNamedEntity(const char* name) +{ + return ToHScript(FindNamedEntity(name, NULL, false, false)); +} + +//----------------------------------------------------------------------------- +// Purpose: vscript - create a scene directly from a buffer containing +// a vcd description, and load it into the scene entity. +//----------------------------------------------------------------------------- +bool CSceneEntity::ScriptLoadSceneFromString(const char* pszFilename, const char* pszData) +{ + CChoreoScene* pScene = new CChoreoScene(NULL); + + // CSceneTokenProcessor SceneTokenProcessor; + // SceneTokenProcessor.SetBuffer( pszData ); + g_TokenProcessor.SetBuffer((char*)pszData); + + if (!pScene->ParseFromBuffer(pszFilename, &g_TokenProcessor)) //&SceneTokenProcessor ) ) + { + Warning("CSceneEntity::LoadSceneFromString: Unable to parse scene data '%s'\n", pszFilename); + delete pScene; + pScene = NULL; + } + else + { + pScene->SetPrintFunc(LocalScene_Printf); + pScene->SetEventCallbackInterface(this); + + + // precache all sounds for the newly constructed scene + PrecacheScene(pScene); + } + + if (pScene != NULL) + { + // release prior scene if present + UnloadScene(); + m_pScene = pScene; + return true; + } + else + { + return false; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Remove all "scene" expressions from all actors in this scene +//----------------------------------------------------------------------------- +void CSceneEntity::ClearSceneEvents( CChoreoScene *scene, bool canceled ) +{ + if ( !m_pScene ) + return; + + LocalScene_Printf( "%s : %8.2f: clearing events\n", STRING( m_iszSceneFile ), m_flCurrentTime ); + + int i; + for ( i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pActor = FindNamedActor( i ); + if ( !pActor ) + continue; + + // Clear any existing expressions + pActor->ClearSceneEvents( scene, canceled ); + } + + // Iterate events and precache necessary resources + for ( i = 0; i < scene->GetNumEvents(); i++ ) + { + CChoreoEvent *event = scene->GetEvent( i ); + if ( !event ) + continue; + + // load any necessary data + switch (event->GetType() ) + { + default: + break; + case CChoreoEvent::SUBSCENE: + { + // Only allow a single level of subscenes for now + if ( !scene->IsSubScene() ) + { + CChoreoScene *subscene = event->GetSubScene(); + if ( subscene ) + { + ClearSceneEvents( subscene, canceled ); + } + } + } + break; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Remove all imposed schedules from all actors in this scene +//----------------------------------------------------------------------------- +void CSceneEntity::ClearSchedules( CChoreoScene *scene ) +{ + if ( !m_pScene ) + return; + + int i; + for ( i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pActor = FindNamedActor( i ); + if ( !pActor ) + continue; + + CAI_BaseNPC *pNPC = pActor->MyNPCPointer(); + + if ( pNPC ) + { + /* + if ( pNPC->IsCurSchedule( SCHED_SCENE_GENERIC ) ) + pNPC->ClearSchedule( "Scene entity clearing all actor schedules" ); + */ + } + else + { + pActor->ResetSequence( pActor->SelectWeightedSequence( ACT_IDLE ) ); + pActor->SetCycle( 0 ); + } + // Clear any existing expressions + } + + // Iterate events and precache necessary resources + for ( i = 0; i < scene->GetNumEvents(); i++ ) + { + CChoreoEvent *event = scene->GetEvent( i ); + if ( !event ) + continue; + + // load any necessary data + switch (event->GetType() ) + { + default: + break; + case CChoreoEvent::SUBSCENE: + { + // Only allow a single level of subscenes for now + if ( !scene->IsSubScene() ) + { + CChoreoScene *subscene = event->GetSubScene(); + if ( subscene ) + { + ClearSchedules( subscene ); + } + } + } + break; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: If we are currently interruptable, pause this scene and wait for the other +// scene to finish +// Input : *otherScene - +//----------------------------------------------------------------------------- +bool CSceneEntity::InterruptThisScene( CSceneEntity *otherScene ) +{ + Assert( otherScene ); + + if ( !IsInterruptable() ) + { + return false; + } + + // Already interrupted + if ( m_bInterrupted ) + { + return false; + } + + m_bInterrupted = true; + m_hInterruptScene = otherScene; + + // Ask other scene to tell us when it's finished or canceled + otherScene->RequestCompletionNotification( this ); + + PausePlayback(); + return true; +} + +/* +void scene_interrupt( const CCommand &args ) +{ + if ( args.ArgC() != 3 ) + return; + + const char *scene1 = args[1]; + const char *scene2 = args[2]; + + CSceneEntity *s1 = dynamic_cast< CSceneEntity * >( gEntList.FindEntityByName( NULL, scene1 ) ); + CSceneEntity *s2 = dynamic_cast< CSceneEntity * >( gEntList.FindEntityByName( NULL, scene2 ) ); + + if ( !s1 || !s2 ) + return; + + if ( s1->InterruptThisScene( s2 ) ) + { + s2->StartPlayback(); + } +} + +static ConCommand interruptscene( "int", scene_interrupt, "interrupt scene 1 with scene 2.", FCVAR_CHEAT ); +*/ + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::CheckInterruptCompletion() +{ + if ( !m_bInterrupted ) + return; + + // If the interruptor goes away it's the same as having that scene finish up... + if ( m_hInterruptScene != NULL && + !m_bInterruptSceneFinished ) + { + return; + } + + m_bInterrupted = false; + m_hInterruptScene = NULL; + + ResumePlayback(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::ClearInterrupt() +{ + m_nInterruptCount = 0; + m_bInterrupted = false; + m_hInterruptScene = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Another scene is asking us to notify upon completion +// Input : *notify - +//----------------------------------------------------------------------------- +void CSceneEntity::RequestCompletionNotification( CSceneEntity *notify ) +{ + CHandle< CSceneEntity > h; + h = notify; + // Only add it once + if ( m_hNotifySceneCompletion.Find( h ) == m_hNotifySceneCompletion.InvalidIndex() ) + { + m_hNotifySceneCompletion.AddToTail( h ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: An interrupt scene has finished or been canceled, we can resume once we pick up this state in CheckInterruptCompletion +// Input : *interruptor - +//----------------------------------------------------------------------------- +void CSceneEntity::NotifyOfCompletion( CSceneEntity *interruptor ) +{ + Assert( m_bInterrupted ); + Assert( m_hInterruptScene == interruptor ); + m_bInterruptSceneFinished = true; + + CheckInterruptCompletion(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneEntity::AddListManager( CSceneListManager *pManager ) +{ + CHandle< CSceneListManager > h; + h = pManager; + // Only add it once + if ( m_hListManagers.Find( h ) == m_hListManagers.InvalidIndex() ) + { + m_hListManagers.AddToTail( h ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Clear any targets that a referencing !activator +//----------------------------------------------------------------------------- +void CSceneEntity::ClearActivatorTargets( void ) +{ + if ( !stricmp( STRING(m_iszTarget1), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget1 = NULL; + } + if ( !stricmp( STRING(m_iszTarget2), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget2 = NULL; + } + if ( !stricmp( STRING(m_iszTarget3), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget3 = NULL; + } + if ( !stricmp( STRING(m_iszTarget4), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget4 = NULL; + } + if ( !stricmp( STRING(m_iszTarget5), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget5 = NULL; + } + if ( !stricmp( STRING(m_iszTarget6), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget6 = NULL; + } + if ( !stricmp( STRING(m_iszTarget7), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget7 = NULL; + } + if ( !stricmp( STRING(m_iszTarget8), "!activator" ) ) + { + // We need to clear out actors so they're re-evaluated + m_hActorList.Purge(); + NetworkProp()->NetworkStateForceUpdate(); + m_hTarget8 = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called when a scene is completed or canceled +//----------------------------------------------------------------------------- +void CSceneEntity::OnSceneFinished( bool canceled, bool fireoutput ) +{ + if ( !m_pScene ) + return; + + LocalScene_Printf( "%s : %8.2f: finished\n", STRING( m_iszSceneFile ), m_flCurrentTime ); + + // Notify any listeners + int c = m_hNotifySceneCompletion.Count(); + int i; + for ( i = 0; i < c; i++ ) + { + CSceneEntity *ent = m_hNotifySceneCompletion[ i ].Get(); + if ( !ent ) + continue; + + ent->NotifyOfCompletion( this ); + } + m_hNotifySceneCompletion.RemoveAll(); + + // Clear simulation + m_pScene->ResetSimulation(); + m_bIsPlayingBack = false; + m_bPaused = false; + SetCurrentTime( 0.0f, false ); + + // Clear interrupt state if we were interrupted for some reason + ClearInterrupt(); + + if ( fireoutput && !m_bCompletedEarly) + { + m_OnCompletion.FireOutput( this, this, 0 ); + } + + // Put face back in neutral pose + ClearSceneEvents( m_pScene, canceled ); + + for ( i = 0 ; i < m_pScene->GetNumActors(); i++ ) + { + CBaseFlex *pTestActor = FindNamedActor( i ); + + if ( !pTestActor ) + continue; + + pTestActor->RemoveChoreoScene( m_pScene, canceled ); + + // If we interrupted the actor's previous scenes, resume them + if ( m_bInterruptedActorsScenes ) + { + QueueActorsScriptedScenesToResume( pTestActor, false ); + } + } +} + +//----------------------------------------------------------------------------- +// Should we transmit it to the client? +//----------------------------------------------------------------------------- +int CSceneEntity::UpdateTransmitState() +{ + if ( !ShouldNetwork() ) + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } + + if ( m_pRecipientFilter ) + { + return SetTransmitState( FL_EDICT_FULLCHECK ); + } + + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: Which clients should we be transmitting to? +//----------------------------------------------------------------------------- +int CSceneEntity::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + int result = BaseClass::ShouldTransmit( pInfo ); + + // if we have excluded them via our recipient filter, don't send + if ( m_pRecipientFilter && result != FL_EDICT_DONTSEND ) + { + bool bFound = false; + + // If we can't find them in the recipient list, exclude + int i; + for ( i=0; iGetRecipientCount();i++ ) + { + int iRecipient = m_pRecipientFilter->GetRecipientIndex(i); + + CBasePlayer *player = static_cast< CBasePlayer * >( CBaseEntity::Instance( iRecipient ) ); + + if ( player && player->edict() == pInfo->m_pClientEnt ) + { + bFound = true; + break; + } + } + + if ( !bFound ) + { + result = FL_EDICT_DONTSEND; + } + } + + return result; +} + +void CSceneEntity::SetRecipientFilter( IRecipientFilter *filter ) +{ + // create a copy of this filter + if ( filter ) + { + m_pRecipientFilter = new CRecipientFilter(); + m_pRecipientFilter->CopyFrom( (CRecipientFilter &)( *filter ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a player (by index) to the recipient filter +//----------------------------------------------------------------------------- +void CSceneEntity::AddBroadcastTeamTarget(int nTeamIndex) +{ + if (m_pRecipientFilter == NULL) + { + CRecipientFilter filter; + SetRecipientFilter(&filter); + } + + CTeam* pTeam = GetGlobalTeam(nTeamIndex); + Assert(pTeam); + if (pTeam == NULL) + return; + + m_pRecipientFilter->AddRecipientsByTeam(pTeam); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes a player (by index) from the recipient filter +//----------------------------------------------------------------------------- +void CSceneEntity::RemoveBroadcastTeamTarget(int nTeamIndex) +{ + if (m_pRecipientFilter == NULL) + { + CRecipientFilter filter; + SetRecipientFilter(&filter); + } + + CTeam* pTeam = GetGlobalTeam(nTeamIndex); + Assert(pTeam); + if (pTeam == NULL) + return; + + m_pRecipientFilter->RemoveRecipientsByTeam(pTeam); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +class CInstancedSceneEntity : public CSceneEntity +{ + DECLARE_DATADESC(); + DECLARE_CLASS( CInstancedSceneEntity, CSceneEntity ); +public: + EHANDLE m_hOwner; + bool m_bHadOwner; + float m_flPostSpeakDelay; + float m_flPreDelay; + char m_szInstanceFilename[ CChoreoScene::MAX_SCENE_FILENAME ]; + bool m_bIsBackground; + + virtual void StartPlayback( void ); + virtual void DoThink( float frametime ); + virtual CBaseFlex *FindNamedActor( const char *name ); + virtual CBaseEntity *FindNamedEntity( const char *name ); + virtual float GetPostSpeakDelay() { return m_flPostSpeakDelay; } + virtual void SetPostSpeakDelay( float flDelay ) { m_flPostSpeakDelay = flDelay; } + virtual float GetPreDelay() { return m_flPreDelay; } + virtual void SetPreDelay( float flDelay ) { m_flPreDelay = flDelay; } + + virtual void OnLoaded(); + + virtual void DispatchStartMoveTo( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ) + { + if (PassThrough( actor )) BaseClass::DispatchStartMoveTo( scene, actor, actor2, event ); + }; + + virtual void DispatchEndMoveTo( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) + { + if (PassThrough( actor )) BaseClass::DispatchEndMoveTo( scene, actor, event ); + }; + + virtual void DispatchStartFace( CChoreoScene *scene, CBaseFlex *actor, CBaseEntity *actor2, CChoreoEvent *event ) + { + if (PassThrough( actor )) BaseClass::DispatchStartFace( scene, actor, actor2, event ); + }; + + virtual void DispatchEndFace( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) + { + if (PassThrough( actor )) BaseClass::DispatchEndFace( scene, actor, event ); + }; + + virtual void DispatchStartSequence( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) + { + if ( IsMultiplayer() ) + { + BaseClass::DispatchStartSequence( scene, actor, event ); + } + }; + virtual void DispatchEndSequence( CChoreoScene *scene, CBaseFlex *actor, CChoreoEvent *event ) + { + if ( IsMultiplayer() ) + { + BaseClass::DispatchEndSequence( scene, actor, event ); + } + }; + virtual void DispatchPauseScene( CChoreoScene *scene, const char *parameters ) { /* suppress */ }; + + void OnRestore(); + + virtual float EstimateLength( void ); + +private: + bool PassThrough( CBaseFlex *actor ); +}; + +LINK_ENTITY_TO_CLASS( instanced_scripted_scene, CInstancedSceneEntity ); + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CInstancedSceneEntity ) + + DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ), + DEFINE_FIELD( m_bHadOwner, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flPostSpeakDelay, FIELD_FLOAT ), + DEFINE_FIELD( m_flPreDelay, FIELD_FLOAT ), + DEFINE_AUTO_ARRAY( m_szInstanceFilename, FIELD_CHARACTER ), + DEFINE_FIELD( m_bIsBackground, FIELD_BOOLEAN ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: create a one-shot scene, no movement, sequences, etc. +// Input : +// Output : +//----------------------------------------------------------------------------- +float InstancedScriptedScene( CBaseFlex *pActor, const char *pszScene, EHANDLE *phSceneEnt, + float flPostDelay, bool bIsBackground, AI_Response *response, + bool bMultiplayer, IRecipientFilter *filter /* = NULL */ ) +{ + VPROF( "InstancedScriptedScene" ); + + CInstancedSceneEntity *pScene = (CInstancedSceneEntity *)CBaseEntity::CreateNoSpawn( "instanced_scripted_scene", vec3_origin, vec3_angle ); + + // This code expands any $gender tags into male or female tags based on the gender of the actor (based on his/her .mdl) + if ( pActor ) + { + pActor->GenderExpandString( pszScene, pScene->m_szInstanceFilename, sizeof( pScene->m_szInstanceFilename ) ); + } + else + { + Q_strncpy( pScene->m_szInstanceFilename, pszScene, sizeof( pScene->m_szInstanceFilename ) ); + } + pScene->m_iszSceneFile = MAKE_STRING( pScene->m_szInstanceFilename ); + + // FIXME: I should set my output to fire something that kills me.... + + // FIXME: add a proper initialization function + pScene->m_hOwner = pActor; + pScene->m_bHadOwner = pActor != NULL; + pScene->m_bMultiplayer = bMultiplayer; + pScene->SetPostSpeakDelay( flPostDelay ); + DispatchSpawn( pScene ); + pScene->Activate(); + pScene->m_bIsBackground = bIsBackground; + + pScene->SetBackground( bIsBackground ); + pScene->SetRecipientFilter( filter ); + + if ( response ) + { + float flPreDelay = response->GetPreDelay(); + if ( flPreDelay ) + { + pScene->SetPreDelay( flPreDelay ); + } + } + + pScene->StartPlayback(); + + if ( response ) + { + // If the response wants us to abort on NPC state switch, remember that + pScene->SetBreakOnNonIdle( response->ShouldBreakOnNonIdle() ); + } + + if ( phSceneEnt ) + { + *phSceneEnt = pScene; + } + + return pScene->EstimateLength(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActor - +// *soundnmame - +// *phSceneEnt - +// Output : float +//----------------------------------------------------------------------------- +#ifdef MAPBASE +float InstancedAutoGeneratedSoundScene( CBaseFlex *pActor, const char *soundname, EHANDLE *phSceneEnt, + float flPostDelay, bool bIsBackground, AI_Response *response, + bool bMultiplayer, IRecipientFilter *filter /* = NULL */ ) +#else +float InstancedAutoGeneratedSoundScene( CBaseFlex *pActor, char const *soundname, EHANDLE *phSceneEnt /*= NULL*/ ) +#endif +{ + if ( !pActor ) + { + Warning( "InstancedAutoGeneratedSoundScene: Expecting non-NULL pActor for sound %s\n", soundname ); + return 0; + } + + CInstancedSceneEntity *pScene = (CInstancedSceneEntity *)CBaseEntity::CreateNoSpawn( "instanced_scripted_scene", vec3_origin, vec3_angle ); + + Q_strncpy( pScene->m_szInstanceFilename, UTIL_VarArgs( "AutoGenerated(%s)", soundname ), sizeof( pScene->m_szInstanceFilename ) ); + pScene->m_iszSceneFile = MAKE_STRING( pScene->m_szInstanceFilename ); + + pScene->m_hOwner = pActor; + pScene->m_bHadOwner = pActor != NULL; + + pScene->GenerateSoundScene( pActor, soundname ); + +#ifdef MAPBASE + pScene->m_bMultiplayer = bMultiplayer; + pScene->SetPostSpeakDelay( flPostDelay ); + DispatchSpawn( pScene ); + pScene->Activate(); + pScene->m_bIsBackground = bIsBackground; + + pScene->SetBackground( bIsBackground ); + pScene->SetRecipientFilter( filter ); + + if ( response ) + { + float flPreDelay = response->GetPreDelay(); + if ( flPreDelay ) + { + pScene->SetPreDelay( flPreDelay ); + } + } +#else + pScene->Spawn(); + pScene->Activate(); +#endif + pScene->StartPlayback(); + +#ifdef MAPBASE + if ( response ) + { + // If the response wants us to abort on NPC state switch, remember that + pScene->SetBreakOnNonIdle( response->ShouldBreakOnNonIdle() ); + } +#endif + + if ( phSceneEnt ) + { + *phSceneEnt = pScene; + } + + return pScene->EstimateLength(); +} + +//----------------------------------------------------------------------------- + +void StopScriptedScene( CBaseFlex *pActor, EHANDLE hSceneEnt ) +{ + CBaseEntity *pEntity = hSceneEnt; + CSceneEntity *pScene = dynamic_cast(pEntity); + + if ( pScene ) + { + LocalScene_Printf( "%s : stop scripted scene\n", STRING( pScene->m_iszSceneFile ) ); + pScene->CancelPlayback(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pszScene - +// Output : float +//----------------------------------------------------------------------------- +float GetSceneDuration( char const *pszScene ) +{ + unsigned int msecs = 0; + + SceneCachedData_t cachedData; + if ( scenefilecache->GetSceneCachedData( pszScene, &cachedData ) ) + { + msecs = cachedData.msecs; + } + + return (float)msecs * 0.001f; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pszScene - +// Output : int +//----------------------------------------------------------------------------- +int GetSceneSpeechCount( char const *pszScene ) +{ + SceneCachedData_t cachedData; + if ( scenefilecache->GetSceneCachedData( pszScene, &cachedData ) ) + { + return cachedData.numSounds; + } +#ifdef MAPBASE + else + { + void *pBuffer = NULL; + if (filesystem->ReadFileEx( pszScene, "MOD", &pBuffer, false, true )) + { + int iNumSounds = 0; + + g_TokenProcessor.SetBuffer((char*)pBuffer); + CChoreoScene *pScene = ChoreoLoadScene( pszScene, NULL, &g_TokenProcessor, LocalScene_Printf ); + if (pScene) + { + for (int i = 0; i < pScene->GetNumEvents(); i++) + { + CChoreoEvent *pEvent = pScene->GetEvent(i); + + if (pEvent->GetType() == CChoreoEvent::SPEAK) + iNumSounds++; + } + } + + return iNumSounds; + } + } +#endif + return 0; +} + +HSCRIPT ScriptCreateSceneEntity( const char* pszScene ) +{ + if ( IsEntityCreationAllowedInScripts() == false ) + { + Warning( "VScript error: A script attempted to create a scene entity mid-game. Entity creation from scripts is only allowed during map init.\n" ); + return NULL; + } + + g_pScriptVM->RegisterClass( GetScriptDescForClass( CSceneEntity ) ); + CSceneEntity *pScene = (CSceneEntity *)CBaseEntity::CreateNoSpawn( "logic_choreographed_scene", vec3_origin, vec3_angle ); + + if ( pScene ) + { + pScene->m_iszSceneFile = AllocPooledString( pszScene ); + DispatchSpawn( pScene ); + } + + return ToHScript( pScene ); +} + +//----------------------------------------------------------------------------- +// Purpose: Used for precaching instanced scenes +// Input : *pszScene - +//----------------------------------------------------------------------------- +void PrecacheInstancedScene( char const *pszScene ) +{ + static int nMakingReslists = -1; + + if ( nMakingReslists == -1 ) + { + nMakingReslists = CommandLine()->FindParm( "-makereslists" ) > 0 ? 1 : 0; + } + + if ( nMakingReslists == 1 ) + { + // Just stat the file to add to reslist + g_pFullFileSystem->Size( pszScene ); + } + + // verify existence, cache is pre-populated, should be there + SceneCachedData_t sceneData; + if ( !scenefilecache->GetSceneCachedData( pszScene, &sceneData ) ) + { +#ifdef MAPBASE + char loadfile[MAX_PATH]; + Q_strncpy( loadfile, pszScene, sizeof( loadfile ) ); + Q_SetExtension( loadfile, ".vcd", sizeof( loadfile ) ); + Q_FixSlashes( loadfile ); + + // Attempt to precache manually + void *pBuffer = NULL; + if (filesystem->ReadFileEx( loadfile, "MOD", &pBuffer, false, true )) + { + g_TokenProcessor.SetBuffer((char*)pBuffer); + CChoreoScene *pScene = ChoreoLoadScene( loadfile, NULL, &g_TokenProcessor, LocalScene_Printf ); + if (pScene) + { + PrecacheChoreoScene(pScene); + } + } +#else + // Scenes are sloppy and don't always exist. + // A scene that is not in the pre-built cache image, but on disk, is a true error. + if ( developer.GetInt() && ( IsX360() && ( g_pFullFileSystem->GetDVDMode() != DVDMODE_STRICT ) && g_pFullFileSystem->FileExists( pszScene, "GAME" ) ) ) + { + Warning( "PrecacheInstancedScene: Missing scene '%s' from scene image cache.\nRebuild scene image cache!\n", pszScene ); + } +#endif + } + else + { + for ( int i = 0; i < sceneData.numSounds; ++i ) + { + short stringId = scenefilecache->GetSceneCachedSound( sceneData.sceneId, i ); + CBaseEntity::PrecacheScriptSound( scenefilecache->GetSceneString( stringId ) ); + } + } + + g_pStringTableClientSideChoreoScenes->AddString( CBaseEntity::IsServer(), pszScene ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CInstancedSceneEntity::StartPlayback( void ) +{ + // Wait until our pre delay is over + if ( GetPreDelay() ) + return; + + BaseClass::StartPlayback(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CInstancedSceneEntity::DoThink( float frametime ) +{ + CheckInterruptCompletion(); + + if ( m_flPreDelay > 0 ) + { + m_flPreDelay = MAX( 0, m_flPreDelay - frametime ); + StartPlayback(); + if ( !m_bIsPlayingBack ) + return; + } + + if ( !m_pScene || !m_bIsPlayingBack || ( m_bHadOwner && m_hOwner == NULL ) ) + { + UTIL_Remove( this ); + return; + } + + // catch bad pitch shifting from old save games + Assert( m_fPitch >= SCENE_MIN_PITCH && m_fPitch <= SCENE_MAX_PITCH ); + m_fPitch = clamp( m_fPitch, SCENE_MIN_PITCH, SCENE_MAX_PITCH ); + + if ( m_bPaused ) + { + PauseThink(); + return; + } + + float dt = frametime; + + m_pScene->SetSoundFileStartupLatency( GetSoundSystemLatency() ); + + // Tell scene to go + m_pScene->Think( m_flCurrentTime ); + // Drive simulation time for scene + SetCurrentTime( m_flCurrentTime + dt * m_fPitch, false ); + + // Did we get to the end + if ( m_pScene->SimulationFinished() ) + { + OnSceneFinished( false, false ); + + UTIL_Remove( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Search for an actor by name, make sure it can do face poses +// Input : *name - +// Output : CBaseFlex +//----------------------------------------------------------------------------- +CBaseFlex *CInstancedSceneEntity::FindNamedActor( const char *name ) +{ + if ( m_pScene->GetNumActors() == 1 || stricmp( name, "!self" ) == 0 ) + { + if ( m_hOwner != NULL ) + { + CBaseCombatCharacter *pCharacter = m_hOwner->MyCombatCharacterPointer(); + if ( pCharacter ) + { + return pCharacter; + } + } + } + return BaseClass::FindNamedActor( name ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Search for an actor by name, make sure it can do face poses +// Input : *name - +// Output : CBaseFlex +//----------------------------------------------------------------------------- +CBaseEntity *CInstancedSceneEntity::FindNamedEntity( const char *name ) +{ + CBaseEntity *pOther = NULL; + + if (m_hOwner != NULL) + { + CAI_BaseNPC *npc = m_hOwner->MyNPCPointer(); + + if (npc) + { + pOther = npc->FindNamedEntity( name ); + } + else if ( m_hOwner->MyCombatCharacterPointer() ) + { + pOther = m_hOwner; + } + } + + if (!pOther) + { + pOther = BaseClass::FindNamedEntity( name ); + } + return pOther; +} + + +//----------------------------------------------------------------------------- +// Purpose: Suppress certain events when it's instanced since they're can cause odd problems +// Input : actor +// Output : true - the event should happen, false - it shouldn't +//----------------------------------------------------------------------------- + +bool CInstancedSceneEntity::PassThrough( CBaseFlex *actor ) +{ + if (!actor) + return false; + + CAI_BaseNPC *myNpc = actor->MyNPCPointer( ); + + if (!myNpc) + return false; + + if (myNpc->IsCurSchedule( SCHED_SCENE_GENERIC )) + { + return true; + } + + if (myNpc->GetCurSchedule()) + { + CAI_ScheduleBits testBits; + myNpc->GetCurSchedule()->GetInterruptMask( &testBits ); + + if (testBits.IsBitSet( COND_IDLE_INTERRUPT )) + { + return true; + } + } + + LocalScene_Printf( "%s : event suppressed\n", STRING( m_iszSceneFile ) ); + + return false; +} + + +//----------------------------------------------------------------------------- +void CInstancedSceneEntity::OnRestore() +{ + if ( m_bHadOwner && !m_hOwner ) + { + // probably just came back from a level transition + UTIL_Remove( this ); + return; + } + // reset background state + if ( m_pScene ) + { + m_pScene->SetBackground( m_bIsBackground ); + } + BaseClass::OnRestore(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CInstancedSceneEntity::EstimateLength( void ) +{ + return (BaseClass::EstimateLength() + GetPreDelay()); +} + + +void CInstancedSceneEntity::OnLoaded() +{ + BaseClass::OnLoaded(); + SetBackground( m_bIsBackground ); + +#ifdef MAPBASE + // It looks like !Target1 in those default NPC scenes was a freaking lie. + if (m_hOwner) + { + m_hTarget1 = m_hOwner; + m_iszTarget1 = m_hOwner->GetEntityName(); + } +#endif +} + +bool g_bClientFlex = true; + +LINK_ENTITY_TO_CLASS( scene_manager, CSceneManager ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneManager::Think() +{ + // Latch this only once per frame... + g_bClientFlex = scene_clientflex.GetBool(); + + // The manager is always thinking at 20 hz + SetNextThink( gpGlobals->curtime + SCENE_THINK_INTERVAL ); + float frameTime = ( gpGlobals->curtime - GetLastThink() ); + frameTime = MIN( 0.1, frameTime ); + + // stop if AI is diabled + if (CAI_BaseNPC::m_nDebugBits & bits_debugDisableAI) + return; + + bool needCleanupPass = false; + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *scene = m_ActiveScenes[ i ].Get(); + if ( !scene ) + { + needCleanupPass = true; + continue; + } + + scene->DoThink( frameTime ); + + if ( m_ActiveScenes.Count() < c ) + { + // Scene removed self while thinking. Adjust iteration. + c = m_ActiveScenes.Count(); + i--; + } + } + + // Now delete any invalid ones + if ( needCleanupPass ) + { + for ( int i = c - 1; i >= 0; i-- ) + { + CSceneEntity *scene = m_ActiveScenes[ i ].Get(); + if ( scene ) + continue; + + m_ActiveScenes.Remove( i ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneManager::ClearAllScenes() +{ + m_ActiveScenes.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *scene - +//----------------------------------------------------------------------------- +void CSceneManager::AddSceneEntity( CSceneEntity *scene ) +{ + CHandle< CSceneEntity > h; + + h = scene; + + // Already added/activated + if ( m_ActiveScenes.Find( h ) != m_ActiveScenes.InvalidIndex() ) + { + return; + } + + m_ActiveScenes.AddToTail( h ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *scene - +//----------------------------------------------------------------------------- +void CSceneManager::RemoveSceneEntity( CSceneEntity *scene ) +{ + CHandle< CSceneEntity > h; + + h = scene; + + m_ActiveScenes.FindAndRemove( h ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *player - +//----------------------------------------------------------------------------- +void CSceneManager::OnClientActive( CBasePlayer *player ) +{ + int c = m_QueuedSceneSounds.Count(); + for ( int i = 0; i < c; i++ ) + { + CRestoreSceneSound *sound = &m_QueuedSceneSounds[ i ]; + + if ( sound->actor == NULL ) + continue; + + // Blow off sounds too far in past to encode over networking layer + if ( fabs( 1000.0f * sound->time_in_past ) > MAX_SOUND_DELAY_MSEC ) + continue; + + CPASAttenuationFilter filter( sound->actor ); + + EmitSound_t es; + es.m_nChannel = CHAN_VOICE; + es.m_flVolume = 1; + es.m_pSoundName = sound->soundname; + es.m_SoundLevel = sound->soundlevel; + es.m_flSoundTime = gpGlobals->curtime - sound->time_in_past; + + EmitSound( filter, sound->actor->entindex(), es ); + } + + m_QueuedSceneSounds.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: Deletes scenes involving the specified actor +//----------------------------------------------------------------------------- +void CSceneManager::RemoveScenesInvolvingActor( CBaseFlex *pActor ) +{ + if ( !pActor ) + return; + + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) ) // NOTE: returns false if scene hasn't loaded yet + { + LocalScene_Printf( "%s : removed for '%s'\n", STRING( pScene->m_iszSceneFile ), pActor ? pActor->GetDebugName() : "NULL" ); + pScene->CancelPlayback(); + } + else + { + CInstancedSceneEntity *pInstancedScene = dynamic_cast< CInstancedSceneEntity * >( pScene ); + if ( pInstancedScene && pInstancedScene->m_hOwner ) + { + if ( pInstancedScene->m_hOwner == pActor ) + { + if ( pInstancedScene->m_bIsPlayingBack ) + { + pInstancedScene->OnSceneFinished( true, false ); + } + + LocalScene_Printf( "%s : removed for '%s'\n", STRING( pInstancedScene->m_iszSceneFile ), pActor ? pActor->GetDebugName() : "NULL" ); + UTIL_Remove( pInstancedScene ); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Stops scenes involving the specified actor +//----------------------------------------------------------------------------- +void CSceneManager::RemoveActorFromScenes( CBaseFlex *pActor, bool bInstancedOnly, bool bNonIdleOnly, const char *pszThisSceneOnly ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene ) + { + continue; + } + + // If only stopping instanced scenes, then skip it if it can't cast to an instanced scene + if ( bInstancedOnly && + ( dynamic_cast< CInstancedSceneEntity * >( pScene ) == NULL ) ) + { + continue; + } + + if ( bNonIdleOnly && !pScene->ShouldBreakOnNonIdle() ) + continue; + + if ( pScene->InvolvesActor( pActor ) ) + { + if ( pszThisSceneOnly && pszThisSceneOnly[0] ) + { + if ( Q_strcmp( pszThisSceneOnly, STRING(pScene->m_iszSceneFile) ) ) + continue; + } + + LocalScene_Printf( "%s : removed for '%s'\n", STRING( pScene->m_iszSceneFile ), pActor ? pActor->GetDebugName() : "NULL" ); + pScene->CancelPlayback(); + } + + } +} + +//----------------------------------------------------------------------------- +// Purpose: Pause scenes involving the specified actor +//----------------------------------------------------------------------------- +void CSceneManager::PauseActorsScenes( CBaseFlex *pActor, bool bInstancedOnly ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene ) + { + continue; + } + + // If only stopping instanced scenes, then skip it if it can't cast to an instanced scene + if ( bInstancedOnly && + ( dynamic_cast< CInstancedSceneEntity * >( pScene ) == NULL ) ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) && pScene->IsPlayingBack() ) + { + LocalScene_Printf( "Pausing actor %s scripted scene: %s\n", pActor->GetDebugName(), STRING(pScene->m_iszSceneFile) ); + + variant_t emptyVariant; + pScene->AcceptInput( "Pause", pScene, pScene, emptyVariant, 0 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if this Actor is only in scenes that are interruptable right now +//----------------------------------------------------------------------------- +bool CSceneManager::IsInInterruptableScenes( CBaseFlex *pActor ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene ) + continue; + + //Ignore background scenes since they're harmless. + if ( pScene->IsBackground() == true ) + continue; + + if ( pScene->InvolvesActor( pActor ) && pScene->IsPlayingBack() ) + { + if ( pScene->IsInterruptable() == false ) + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Resume any paused scenes involving the specified actor +//----------------------------------------------------------------------------- +void CSceneManager::ResumeActorsScenes( CBaseFlex *pActor, bool bInstancedOnly ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene ) + { + continue; + } + + // If only stopping instanced scenes, then skip it if it can't cast to an instanced scene + if ( bInstancedOnly && + ( dynamic_cast< CInstancedSceneEntity * >( pScene ) == NULL ) ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) && pScene->IsPlayingBack() ) + { + LocalScene_Printf( "Resuming actor %s scripted scene: %s\n", pActor->GetDebugName(), STRING(pScene->m_iszSceneFile) ); + + variant_t emptyVariant; + pScene->AcceptInput( "Resume", pScene, pScene, emptyVariant, 0 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set all paused, in-playback scenes to resume when the actor is ready +//----------------------------------------------------------------------------- +void CSceneManager::QueueActorsScenesToResume( CBaseFlex *pActor, bool bInstancedOnly ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene ) + { + continue; + } + + // If only stopping instanced scenes, then skip it if it can't cast to an instanced scene + if ( bInstancedOnly && + ( dynamic_cast< CInstancedSceneEntity * >( pScene ) == NULL ) ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) && pScene->IsPlayingBack() && pScene->IsPaused() ) + { + pScene->QueueResumePlayback(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: returns if there are scenes involving the specified actor +//----------------------------------------------------------------------------- +bool CSceneManager::IsRunningScriptedScene( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene || + !pScene->IsPlayingBack() || + ( bIgnoreInstancedScenes && dynamic_cast(pScene) != NULL ) + ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) ) + { + return true; + } + } + return false; +} + +bool CSceneManager::IsRunningScriptedSceneAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene || + !pScene->IsPlayingBack() || + pScene->IsPaused() || + ( bIgnoreInstancedScenes && dynamic_cast(pScene) != NULL ) + ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) ) + { + return true; + } + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActor - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CSceneManager::IsRunningScriptedSceneWithSpeech( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene || + !pScene->IsPlayingBack() || + ( bIgnoreInstancedScenes && dynamic_cast(pScene) != NULL ) + ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) ) + { + if ( pScene->HasUnplayedSpeech() ) + return true; + } + } + return false; +} + + +bool CSceneManager::IsRunningScriptedSceneWithSpeechAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene || + !pScene->IsPlayingBack() || + pScene->IsPaused() || + ( bIgnoreInstancedScenes && dynamic_cast(pScene) != NULL ) + ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) ) + { + if ( pScene->HasUnplayedSpeech() ) + return true; + } + } + return false; +} + + +#ifdef MAPBASE +bool CSceneManager::IsRunningScriptedSceneWithFlexAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes, const char *pszNotThisScene ) +{ + int c = m_ActiveScenes.Count(); + for ( int i = 0; i < c; i++ ) + { + CSceneEntity *pScene = m_ActiveScenes[ i ].Get(); + if ( !pScene || + !pScene->IsPlayingBack() || + pScene->IsPaused() || + ( bIgnoreInstancedScenes && dynamic_cast(pScene) != NULL ) || + ( pszNotThisScene == NULL || Q_strcmp( pszNotThisScene, STRING(pScene->m_iszSceneFile) ) == 0 ) + ) + { + continue; + } + + if ( pScene->InvolvesActor( pActor ) ) + { + if ( pScene->HasFlexAnimation() ) + return true; + } + } + return false; +} + + +CUtlVector< CHandle< CSceneEntity > > *CSceneManager::GetActiveSceneList() +{ + return &m_ActiveScenes; +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *soundname - +// soundlevel - +// soundtime - +//----------------------------------------------------------------------------- +void CSceneManager::QueueRestoredSound( CBaseFlex *actor, char const *soundname, soundlevel_t soundlevel, float time_in_past ) +{ + CRestoreSceneSound e; + e.actor = actor; + Q_strncpy( e.soundname, soundname, sizeof( e.soundname ) ); + e.soundlevel = soundlevel; + e.time_in_past = time_in_past; + + m_QueuedSceneSounds.AddToTail( e ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void RemoveActorFromScriptedScenes( CBaseFlex *pActor, bool instancedscenesonly, bool nonidlescenesonly, const char *pszThisSceneOnly ) +{ + GetSceneManager()->RemoveActorFromScenes( pActor, instancedscenesonly, nonidlescenesonly, pszThisSceneOnly ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void RemoveAllScenesInvolvingActor( CBaseFlex *pActor ) +{ + GetSceneManager()->RemoveScenesInvolvingActor( pActor ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void PauseActorsScriptedScenes( CBaseFlex *pActor, bool instancedscenesonly ) +{ + GetSceneManager()->PauseActorsScenes( pActor, instancedscenesonly ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool IsInInterruptableScenes( CBaseFlex *pActor ) +{ + return GetSceneManager()->IsInInterruptableScenes( pActor ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void ResumeActorsScriptedScenes( CBaseFlex *pActor, bool instancedscenesonly ) +{ + GetSceneManager()->ResumeActorsScenes( pActor, instancedscenesonly ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void QueueActorsScriptedScenesToResume( CBaseFlex *pActor, bool instancedscenesonly ) +{ + GetSceneManager()->QueueActorsScenesToResume( pActor, instancedscenesonly ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool IsRunningScriptedScene( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + return GetSceneManager()->IsRunningScriptedScene( pActor, bIgnoreInstancedScenes ); +} + +bool IsRunningScriptedSceneAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + return GetSceneManager()->IsRunningScriptedSceneAndNotPaused( pActor, bIgnoreInstancedScenes ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool IsRunningScriptedSceneWithSpeech( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + return GetSceneManager()->IsRunningScriptedSceneWithSpeech( pActor, bIgnoreInstancedScenes ); +} + +bool IsRunningScriptedSceneWithSpeechAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes ) +{ + return GetSceneManager()->IsRunningScriptedSceneWithSpeechAndNotPaused( pActor, bIgnoreInstancedScenes ); +} + +#ifdef MAPBASE +bool IsRunningScriptedSceneWithFlexAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes, const char *pszNotThisScene ) +{ + return GetSceneManager()->IsRunningScriptedSceneWithFlexAndNotPaused( pActor, bIgnoreInstancedScenes, pszNotThisScene ); +} + +CUtlVector< CHandle< CSceneEntity > > *GetActiveSceneList() +{ + return GetSceneManager()->GetActiveSceneList(); +} +#endif + + +//=========================================================================================================== +// SCENE LIST MANAGER +//=========================================================================================================== +LINK_ENTITY_TO_CLASS( logic_scene_list_manager, CSceneListManager ); + +BEGIN_DATADESC( CSceneListManager ) + DEFINE_UTLVECTOR( m_hListManagers, FIELD_EHANDLE ), + + // Keys + DEFINE_KEYFIELD( m_iszScenes[0], FIELD_STRING, "scene0" ), + DEFINE_KEYFIELD( m_iszScenes[1], FIELD_STRING, "scene1" ), + DEFINE_KEYFIELD( m_iszScenes[2], FIELD_STRING, "scene2" ), + DEFINE_KEYFIELD( m_iszScenes[3], FIELD_STRING, "scene3" ), + DEFINE_KEYFIELD( m_iszScenes[4], FIELD_STRING, "scene4" ), + DEFINE_KEYFIELD( m_iszScenes[5], FIELD_STRING, "scene5" ), + DEFINE_KEYFIELD( m_iszScenes[6], FIELD_STRING, "scene6" ), + DEFINE_KEYFIELD( m_iszScenes[7], FIELD_STRING, "scene7" ), + DEFINE_KEYFIELD( m_iszScenes[8], FIELD_STRING, "scene8" ), + DEFINE_KEYFIELD( m_iszScenes[9], FIELD_STRING, "scene9" ), + DEFINE_KEYFIELD( m_iszScenes[10], FIELD_STRING, "scene10" ), + DEFINE_KEYFIELD( m_iszScenes[11], FIELD_STRING, "scene11" ), + DEFINE_KEYFIELD( m_iszScenes[12], FIELD_STRING, "scene12" ), + DEFINE_KEYFIELD( m_iszScenes[13], FIELD_STRING, "scene13" ), + DEFINE_KEYFIELD( m_iszScenes[14], FIELD_STRING, "scene14" ), + DEFINE_KEYFIELD( m_iszScenes[15], FIELD_STRING, "scene15" ), + + DEFINE_FIELD( m_hScenes[0], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[1], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[2], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[3], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[4], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[5], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[6], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[7], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[8], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[9], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[10], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[11], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[12], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[13], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[14], FIELD_EHANDLE ), + DEFINE_FIELD( m_hScenes[15], FIELD_EHANDLE ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Shutdown", InputShutdown ), +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +BEGIN_ENT_SCRIPTDESC( CSceneListManager, CBaseEntity, "Stores choreo scenes and cleans them up when a later scene in the list begins playing." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetScene, "GetScene", "Gets the specified scene index from this manager." ) + +END_SCRIPTDESC(); +#endif + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneListManager::Activate( void ) +{ + BaseClass::Activate(); + + // Hook up scenes, but not after loading a game because they're saved. + if ( gpGlobals->eLoadType != MapLoad_LoadGame ) + { + for ( int i = 0; i < SCENE_LIST_MANAGER_MAX_SCENES; i++ ) + { + if ( m_iszScenes[i] != NULL_STRING ) + { + m_hScenes[i] = gEntList.FindEntityByName( NULL, STRING(m_iszScenes[i]) ); + if ( m_hScenes[i] ) + { + CSceneEntity *pScene = dynamic_cast(m_hScenes[i].Get()); + if ( pScene ) + { + pScene->AddListManager( this ); + } + else + { + CSceneListManager *pList = dynamic_cast(m_hScenes[i].Get()); + if ( pList ) + { + pList->AddListManager( this ); + } + else + { + Warning( "%s(%s) found an entity that wasn't a logic_choreographed_scene or logic_scene_list_manager in slot %d, named %s\n", GetDebugName(), GetClassname(), i, STRING(m_iszScenes[i]) ); + m_hScenes[i] = NULL; + } + } + } + else + { + Warning( "%s(%s) could not find scene %d, named %s\n", GetDebugName(), GetClassname(), i, STRING(m_iszScenes[i]) ); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: A scene or manager in our list has started playing. +// Remove all scenes earlier in the list. +//----------------------------------------------------------------------------- +void CSceneListManager::SceneStarted( CBaseEntity *pSceneOrManager ) +{ + // Move backwards and call remove on all scenes / managers earlier in the list to the fired one + bool bFoundStart = false; + for ( int i = SCENE_LIST_MANAGER_MAX_SCENES-1; i >= 0; i-- ) + { + if ( !m_hScenes[i] ) + continue; + + if ( bFoundStart ) + { + RemoveScene( i ); + } + else if ( m_hScenes[i] == pSceneOrManager ) + { + bFoundStart = true; + } + } + + // Tell any managers we're within that we've started a scene + if ( bFoundStart ) + { + int c = m_hListManagers.Count(); + for ( int i = 0; i < c; i++ ) + { + if ( m_hListManagers[i] ) + { + m_hListManagers[i]->SceneStarted( this ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneListManager::AddListManager( CSceneListManager *pManager ) +{ + CHandle< CSceneListManager > h; + h = pManager; + // Only add it once + if ( m_hListManagers.Find( h ) == m_hListManagers.InvalidIndex() ) + { + m_hListManagers.AddToTail( h ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Shut down all scenes, and then remove this entity +//----------------------------------------------------------------------------- +void CSceneListManager::InputShutdown( inputdata_t &inputdata ) +{ + ShutdownList(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneListManager::ShutdownList( void ) +{ + for ( int i = 0; i < SCENE_LIST_MANAGER_MAX_SCENES; i++ ) + { + if ( m_hScenes[i] ) + { + RemoveScene(i); + } + } + + UTIL_Remove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CSceneListManager::RemoveScene( int iIndex ) +{ + CSceneEntity *pScene = dynamic_cast(m_hScenes[iIndex].Get()); + if ( pScene ) + { + // Remove the scene + UTIL_Remove( pScene ); + return; + } + + // Tell the list manager to shut down all scenes + CSceneListManager *pList = dynamic_cast(m_hScenes[iIndex].Get()); + if ( pList ) + { + pList->ShutdownList(); + } +} + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +HSCRIPT CSceneListManager::ScriptGetScene( int iIndex ) +{ + if ( iIndex < 0 || iIndex >= SCENE_LIST_MANAGER_MAX_SCENES ) + return NULL; + + return ToHScript( m_hScenes[iIndex] ); +} +#endif + +void ReloadSceneFromDisk( CBaseEntity *ent ) +{ + CSceneEntity *scene = dynamic_cast< CSceneEntity * >( ent ); + if ( !scene ) + return; + + Assert( 0 ); +} + +// Purpose: +// Input : *ent - +// Output : char const +//----------------------------------------------------------------------------- +char const *GetSceneFilename( CBaseEntity *ent ) +{ + CSceneEntity *scene = dynamic_cast< CSceneEntity * >( ent ); + if ( !scene ) + return ""; + + return STRING( scene->m_iszSceneFile ); +} + +//----------------------------------------------------------------------------- +// Purpose: Return a list of the last 5 lines of speech from NPCs for bug reports +// Input : +// Output : speech - last 5 sound files played as speech +// returns the number of sounds in the returned list +//----------------------------------------------------------------------------- + +int GetRecentNPCSpeech( recentNPCSpeech_t speech[ SPEECH_LIST_MAX_SOUNDS ] ) +{ + int i; + int num; + int index; + + // clear out the output list + for( i = 0; i < SPEECH_LIST_MAX_SOUNDS; i++ ) + { + speech[ i ].time = 0.0f; + speech[ i ].name[ 0 ] = 0; + speech[ i ].sceneName[ 0 ] = 0; + } + + // copy the sound names into the list in order they were played + num = 0; + index = speechListIndex; + for( i = 0; i < SPEECH_LIST_MAX_SOUNDS; i++ ) + { + if ( speechListSounds[ index ].name[ 0 ] ) + { + // only copy names that are not zero length + speech[ num ] = speechListSounds[ index ]; + num++; + } + + index++; + if ( index >= SPEECH_LIST_MAX_SOUNDS ) + { + index = 0; + } + } + + return num; +} + +//----------------------------------------------------------------------------- +// Purpose: Displays a list of the last 5 lines of speech from NPCs +// Input : +// Output : +//----------------------------------------------------------------------------- + +static void ListRecentNPCSpeech( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + recentNPCSpeech_t speech[ SPEECH_LIST_MAX_SOUNDS ]; + int num; + int i; + + // get any sounds that were spoken by NPCs recently + num = GetRecentNPCSpeech( speech ); + Msg( "Recent NPC speech:\n" ); + for( i = 0; i < num; i++ ) + { + Msg( " time: %6.3f sound name: %s scene: %s\n", speech[ i ].time, speech[ i ].name, speech[ i ].sceneName ); + } + Msg( "Current time: %6.3f\n", gpGlobals->curtime ); +} + +static ConCommand ListRecentNPCSpeechCmd( "listRecentNPCSpeech", ListRecentNPCSpeech, "Displays a list of the last 5 lines of speech from NPCs.", FCVAR_DONTRECORD|FCVAR_GAMEDLL ); + +CON_COMMAND( scene_flush, "Flush all .vcds from the cache and reload from disk." ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + Msg( "Reloading\n" ); + scenefilecache->Reload(); + Msg( " done\n" ); +} diff --git a/sp/src/game/server/sceneentity.h b/sp/src/game/server/sceneentity.h new file mode 100644 index 00000000..6e005a60 --- /dev/null +++ b/sp/src/game/server/sceneentity.h @@ -0,0 +1,64 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// +#ifndef SCENEENTITY_H +#define SCENEENTITY_H +#ifdef _WIN32 +#pragma once +#endif + +// List of the last 5 lines of speech from NPCs for bug reports +#define SPEECH_LIST_MAX_SOUNDS 5 + +class AI_Response; + +struct recentNPCSpeech_t +{ + float time; + char name[ 512 ]; + char sceneName[ 128 ]; +}; + +int GetRecentNPCSpeech( recentNPCSpeech_t speech[ SPEECH_LIST_MAX_SOUNDS ] ); +float InstancedScriptedScene( CBaseFlex *pActor, const char *pszScene, EHANDLE *phSceneEnt = NULL, float flPostDelay = 0.0f, bool bIsBackground = false, AI_Response *response = NULL, bool bMultiplayer = false, IRecipientFilter *filter = NULL ); +#ifdef MAPBASE +float InstancedAutoGeneratedSoundScene( CBaseFlex *pActor, char const *soundname, EHANDLE *phSceneEnt = NULL, float flPostDelay = 0.0f, bool bIsBackground = false, AI_Response *response = NULL, bool bMultiplayer = false, IRecipientFilter *filter = NULL ); +#else +float InstancedAutoGeneratedSoundScene( CBaseFlex *pActor, char const *soundname, EHANDLE *phSceneEnt = NULL ); +#endif +void StopScriptedScene( CBaseFlex *pActor, EHANDLE hSceneEnt ); +void RemoveActorFromScriptedScenes( CBaseFlex *pActor, bool instancedscenesonly, bool nonidlescenesonly = false, const char *pszThisSceneOnly = NULL ); +void RemoveAllScenesInvolvingActor( CBaseFlex *pActor ); +void PauseActorsScriptedScenes( CBaseFlex *pActor, bool instancedscenesonly ); +void ResumeActorsScriptedScenes( CBaseFlex *pActor, bool instancedscenesonly ); +void QueueActorsScriptedScenesToResume( CBaseFlex *pActor, bool instancedscenesonly ); +bool IsRunningScriptedScene( CBaseFlex *pActor, bool bIgnoreInstancedScenes = true ); +bool IsRunningScriptedSceneAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes = true ); +bool IsRunningScriptedSceneWithSpeech( CBaseFlex *pActor, bool bIgnoreInstancedScenes = false ); +bool IsRunningScriptedSceneWithSpeechAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes = false ); +#ifdef MAPBASE +bool IsRunningScriptedSceneWithFlexAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes = false, const char *pszNotThisScene = NULL ); +CUtlVector< CHandle< CSceneEntity > > *GetActiveSceneList(); +#endif +float GetSceneDuration( char const *pszScene ); +int GetSceneSpeechCount( char const *pszScene ); +bool IsInInterruptableScenes( CBaseFlex *pActor ); + +void PrecacheInstancedScene( char const *pszScene ); +HSCRIPT ScriptCreateSceneEntity( char const *pszScene ); + +char const *GetSceneFilename( CBaseEntity *ent ); +void ReloadSceneFromDisk( CBaseEntity *ent ); + +#ifdef MAPBASE +const char *GetFirstSoundInScene(const char *pszScene); +const char *GetFirstSoundInScene(CChoreoScene *scene); + +CBaseEntity *UTIL_FindNamedSceneEntity(const char *name, CBaseEntity *pActor, CSceneEntity *scene, bool bBaseFlexOnly = false, bool bUseClear = false); +#endif + + +#endif // SCENEENTITY_H diff --git a/sp/src/game/server/scratchpad_gamedll_helpers.cpp b/sp/src/game/server/scratchpad_gamedll_helpers.cpp new file mode 100644 index 00000000..153bdba8 --- /dev/null +++ b/sp/src/game/server/scratchpad_gamedll_helpers.cpp @@ -0,0 +1,95 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "scratchpad_gamedll_helpers.h" +#include "iscratchpad3d.h" +#include "player.h" +#include "collisionproperty.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void ScratchPad_DrawWorldToScratchPad( + IScratchPad3D *pPad, + unsigned long flags ) +{ + pPad->SetRenderState( IScratchPad3D::RS_FillMode, IScratchPad3D::FillMode_Wireframe ); + + if ( flags & SPDRAWWORLD_DRAW_WORLD ) + { + engine->DrawMapToScratchPad( pPad, 0 ); + } + + if ( flags & (SPDRAWWORLD_DRAW_PLAYERS | SPDRAWWORLD_DRAW_ENTITIES) ) + { + CBaseEntity *pCur = gEntList.FirstEnt(); + while ( pCur ) + { + bool bPlayer = ( dynamic_cast< CBasePlayer* >( pCur ) != 0 ); + if ( (bPlayer && !( flags & SPDRAWWORLD_DRAW_PLAYERS )) || + (!bPlayer && !( flags & SPDRAWWORLD_DRAW_ENTITIES )) ) + { + pCur = gEntList.NextEnt( pCur ); + continue; + } + + ScratchPad_DrawEntityToScratchPad( + pPad, + flags, + pCur, + bPlayer ? Vector( 1.0, 0.5, 0 ) : Vector( 0.3, 0.3, 1.0 ) + ); + + pCur = gEntList.NextEnt( pCur ); + } + } +} + + +void ScratchPad_DrawEntityToScratchPad( + IScratchPad3D *pPad, + unsigned long flags, + CBaseEntity *pEnt, + const Vector &vColor ) +{ + // Draw the entity's bbox [todo: draw OBBs here too]. + Vector mins, maxs; + pEnt->CollisionProp()->WorldSpaceAABB( &mins, &maxs ); + + pPad->DrawWireframeBox( mins, maxs, vColor ); + + // Draw the edict's index or class? + char str[512]; + str[0] = 0; + if ( flags & SPDRAWWORLD_DRAW_EDICT_INDICES ) + { + char tempStr[512]; + Q_snprintf( tempStr, sizeof( tempStr ), "edict: %d", pEnt->entindex() ); + Q_strncat( str, tempStr, sizeof( str ), COPY_ALL_CHARACTERS ); + } + + if ( flags & SPDRAWWORLD_DRAW_ENTITY_CLASSNAMES ) + { + if ( str[0] != 0 ) + Q_strncat( str, ", ", sizeof( str ), COPY_ALL_CHARACTERS ); + + char tempStr[512]; + Q_snprintf( tempStr, sizeof( tempStr ), "class: %s", pEnt->GetClassname() ); + Q_strncat( str, tempStr, sizeof( str ), COPY_ALL_CHARACTERS ); + } + + if ( str[0] != 0 ) + { + CTextParams params; + params.m_vPos = (mins + maxs) * 0.5f; + params.m_bCentered = true; + params.m_flLetterWidth = 2; + pPad->DrawText( str, params ); + } +} + + diff --git a/sp/src/game/server/scratchpad_gamedll_helpers.h b/sp/src/game/server/scratchpad_gamedll_helpers.h new file mode 100644 index 00000000..61503197 --- /dev/null +++ b/sp/src/game/server/scratchpad_gamedll_helpers.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef SCRATCHPAD_GAMEDLL_HELPERS_H +#define SCRATCHPAD_GAMEDLL_HELPERS_H +#ifdef _WIN32 +#pragma once +#endif + + +class IScratchPad3D; + + +#define SPDRAWWORLD_DRAW_WORLD 0x001 +#define SPDRAWWORLD_DRAW_PLAYERS 0x002 // Draw player entities? +#define SPDRAWWORLD_DRAW_ENTITIES 0x004 // Draw entities other than players? +#define SPDRAWWORLD_DRAW_ENTITY_CLASSNAMES 0x008 +#define SPDRAWWORLD_DRAW_EDICT_INDICES 0x010 + +#define SPDRAWWORLD_DRAW_ALL 0xFFFFFFFF + + +// Draws the world and various things in it into the scratchpad. +// flags is a combination of the SPDRAWWORLD_ flags. +void ScratchPad_DrawWorldToScratchPad( + IScratchPad3D *pPad, + unsigned long flags ); + +// Draw a specific entity into the scratch pad. +void ScratchPad_DrawEntityToScratchPad( + IScratchPad3D *pPad, + unsigned long flags, + CBaseEntity *pEnt, + const Vector &vColor ); + + +#endif // SCRATCHPAD_GAMEDLL_HELPERS_H diff --git a/sp/src/game/server/scripted.cpp b/sp/src/game/server/scripted.cpp new file mode 100644 index 00000000..19fc6784 --- /dev/null +++ b/sp/src/game/server/scripted.cpp @@ -0,0 +1,2511 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implementation of entities that cause NPCs to participate in +// scripted events. These entities find and temporarily possess NPCs +// within a given search radius. +// +// Multiple scripts with the same targetname will start frame-synchronized. +// +// Scripts will find available NPCs by name or class name and grab them +// to play the script. If the NPC is already playing a script, the +// new script may enqueue itself unless there is already a non critical +// script in the queue. +// +//=============================================================================// + +#include "cbase.h" +#include "ai_schedule.h" +#include "ai_default.h" +#include "ai_motor.h" +#include "ai_hint.h" +#include "ai_networkmanager.h" +#include "ai_network.h" +#include "engine/IEngineSound.h" +#include "animation.h" +#include "scripted.h" +#include "entitylist.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +ConVar ai_task_pre_script( "ai_task_pre_script", "0", FCVAR_NONE ); + +// New macros introduced for Mapbase's console message color changes. +#ifdef MAPBASE +#define ScriptMsg( lvl, msg ) CGMsg( lvl, CON_GROUP_NPC_SCRIPTS, msg ) +#define ScriptMsg1( lvl, msg, a ) CGMsg( lvl, CON_GROUP_NPC_SCRIPTS, msg, a ) +#define ScriptMsg2( lvl, msg, a, b ) CGMsg( lvl, CON_GROUP_NPC_SCRIPTS, msg, a, b ) +#define ScriptMsg3( lvl, msg, a, b, c ) CGMsg( lvl, CON_GROUP_NPC_SCRIPTS, msg, a, b, c ) +#define ScriptMsg4( lvl, msg, a, b, c, d ) CGMsg( lvl, CON_GROUP_NPC_SCRIPTS, msg, a, b, c, d ) +#define ScriptMsg5( lvl, msg, a, b, c, d, e ) CGMsg( lvl, CON_GROUP_NPC_SCRIPTS, msg, a, b, c, d, e ) +#else +#define ScriptMsg( lvl, msg ) DevMsg( lvl, msg ) +#define ScriptMsg1( lvl, msg, a ) DevMsg( lvl, msg, a ) +#define ScriptMsg2( lvl, msg, a, b ) DevMsg( lvl, msg, a, b ) +#define ScriptMsg3( lvl, msg, a, b, c ) DevMsg( lvl, msg, a, b, c ) +#define ScriptMsg4( lvl, msg, a, b, c, d ) DevMsg( lvl, msg, a, b, c, d ) +#define ScriptMsg5( lvl, msg, a, b, c, d, e ) DevMsg( lvl, msg, a, b, c, d, e ) +#endif + + +// +// targetname "me" - there can be more than one with the same name, and they act in concert +// target "the_entity_I_want_to_start_playing" or "class entity_classname" will pick the closest inactive scientist +// play "name_of_sequence" +// idle "name of idle sequence to play before starting" +// moveto - if set the NPC first moves to this nodes position +// range # - only search this far to find the target +// spawnflags - (stop if blocked, stop if player seen) +// + +BEGIN_DATADESC( CAI_ScriptedSequence ) + + DEFINE_KEYFIELD( m_iszEntry, FIELD_STRING, "m_iszEntry" ), + DEFINE_KEYFIELD( m_iszPreIdle, FIELD_STRING, "m_iszIdle" ), + DEFINE_KEYFIELD( m_iszPlay, FIELD_STRING, "m_iszPlay" ), + DEFINE_KEYFIELD( m_iszPostIdle, FIELD_STRING, "m_iszPostIdle" ), + DEFINE_KEYFIELD( m_iszCustomMove, FIELD_STRING, "m_iszCustomMove" ), + DEFINE_KEYFIELD( m_iszNextScript, FIELD_STRING, "m_iszNextScript" ), + DEFINE_KEYFIELD( m_iszEntity, FIELD_STRING, "m_iszEntity" ), + DEFINE_KEYFIELD( m_fMoveTo, FIELD_INTEGER, "m_fMoveTo" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "m_flRadius" ), + DEFINE_KEYFIELD( m_flRepeat, FIELD_FLOAT, "m_flRepeat" ), + + DEFINE_FIELD( m_bIsPlayingEntry, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_bLoopActionSequence, FIELD_BOOLEAN, "m_bLoopActionSequence" ), + DEFINE_KEYFIELD( m_bSynchPostIdles, FIELD_BOOLEAN, "m_bSynchPostIdles" ), + DEFINE_KEYFIELD( m_bIgnoreGravity, FIELD_BOOLEAN, "m_bIgnoreGravity" ), + DEFINE_KEYFIELD( m_bDisableNPCCollisions, FIELD_BOOLEAN, "m_bDisableNPCCollisions" ), + + DEFINE_FIELD( m_iDelay, FIELD_INTEGER ), + DEFINE_FIELD( m_bDelayed, FIELD_BOOLEAN ), + DEFINE_FIELD( m_startTime, FIELD_TIME ), + DEFINE_FIELD( m_bWaitForBeginSequence, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_saved_effects, FIELD_INTEGER ), + DEFINE_FIELD( m_savedFlags, FIELD_INTEGER ), + DEFINE_FIELD( m_savedCollisionGroup, FIELD_INTEGER ), + + DEFINE_FIELD( m_interruptable, FIELD_BOOLEAN ), + DEFINE_FIELD( m_sequenceStarted, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hTargetEnt, FIELD_EHANDLE ), + DEFINE_FIELD( m_hNextCine, FIELD_EHANDLE ), + DEFINE_FIELD( m_hLastFoundEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_hForcedTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_bDontCancelOtherSequences, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bForceSynch, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_bThinking, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bInitiatedSelfDelete, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_bIsTeleportingDueToMoveTo, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_matInteractionPosition, FIELD_VMATRIX ), + DEFINE_FIELD( m_hInteractionRelativeEntity, FIELD_EHANDLE ), + + DEFINE_FIELD( m_bTargetWasAsleep, FIELD_BOOLEAN ), + + // Function Pointers + DEFINE_THINKFUNC( ScriptThink ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "MoveToPosition", InputMoveToPosition ), + DEFINE_INPUTFUNC( FIELD_VOID, "BeginSequence", InputBeginSequence ), + DEFINE_INPUTFUNC( FIELD_VOID, "CancelSequence", InputCancelSequence ), + + DEFINE_KEYFIELD( m_iPlayerDeathBehavior, FIELD_INTEGER, "onplayerdeath" ), + DEFINE_INPUTFUNC( FIELD_VOID, "ScriptPlayerDeath", InputScriptPlayerDeath ), + +#ifdef MAPBASE + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), +#endif + + // Outputs + DEFINE_OUTPUT(m_OnBeginSequence, "OnBeginSequence"), + DEFINE_OUTPUT(m_OnEndSequence, "OnEndSequence"), + DEFINE_OUTPUT(m_OnPostIdleEndSequence, "OnPostIdleEndSequence"), + DEFINE_OUTPUT(m_OnCancelSequence, "OnCancelSequence"), + DEFINE_OUTPUT(m_OnCancelFailedSequence, "OnCancelFailedSequence"), + DEFINE_OUTPUT(m_OnScriptEvent[0], "OnScriptEvent01"), + DEFINE_OUTPUT(m_OnScriptEvent[1], "OnScriptEvent02"), + DEFINE_OUTPUT(m_OnScriptEvent[2], "OnScriptEvent03"), + DEFINE_OUTPUT(m_OnScriptEvent[3], "OnScriptEvent04"), + DEFINE_OUTPUT(m_OnScriptEvent[4], "OnScriptEvent05"), + DEFINE_OUTPUT(m_OnScriptEvent[5], "OnScriptEvent06"), + DEFINE_OUTPUT(m_OnScriptEvent[6], "OnScriptEvent07"), + DEFINE_OUTPUT(m_OnScriptEvent[7], "OnScriptEvent08"), +#ifdef MAPBASE + DEFINE_OUTPUT(m_OnPreIdleSequence, "OnPreIdleSequence"), + DEFINE_OUTPUT(m_OnFoundNPC, "OnFoundNPC"), +#endif + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( scripted_sequence, CAI_ScriptedSequence ); +#define CLASSNAME "scripted_sequence" + +//----------------------------------------------------------------------------- +// Purpose: Cancels the given scripted sequence. +// Input : pentCine - +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::ScriptEntityCancel( CBaseEntity *pentCine, bool bPretendSuccess ) +{ + // make sure they are a scripted_sequence + if ( FClassnameIs( pentCine, CLASSNAME ) ) + { + CAI_ScriptedSequence *pCineTarget = (CAI_ScriptedSequence *)pentCine; + + // make sure they have a NPC in mind for the script + CBaseEntity *pEntity = pCineTarget->GetTarget(); + CAI_BaseNPC *pTarget = NULL; + if ( pEntity ) + pTarget = pEntity->MyNPCPointer(); + + if (pTarget) + { + // make sure their NPC is actually playing a script + if ( pTarget->m_NPCState == NPC_STATE_SCRIPT ) + { + // tell them do die + pTarget->m_scriptState = CAI_BaseNPC::SCRIPT_CLEANUP; + + // We have to save off the flags here, because the NPC's m_hCine is cleared in CineCleanup() + int iSavedFlags = (pTarget->m_hCine ? pTarget->m_hCine->m_savedFlags : 0); + +#ifdef HL1_DLL + //if we didn't have FL_FLY before the script, remove it + // for some reason hl2 doesn't have to do this *before* + // restoring the position ( which checks FL_FLY ) in CineCleanup + // Let's not risk breaking anything at this stage and just remove it. + pCineTarget->FixFlyFlag( pTarget, iSavedFlags ); +#endif + // do it now + pTarget->CineCleanup( ); + pCineTarget->FixScriptNPCSchedule( pTarget, iSavedFlags ); + } + else + { + // Robin HACK: If a script is started and then cancelled before an NPC gets to + // think, we have to manually clear it out of scripted state, or it'll never recover. + pCineTarget->SetTarget( NULL ); + pTarget->SetEffects( pCineTarget->m_saved_effects ); + pTarget->m_hCine = NULL; + pTarget->SetTarget( NULL ); + pTarget->SetGoalEnt( NULL ); + pTarget->SetIdealState( NPC_STATE_IDLE ); + } + } + + // FIXME: this needs to be done in a cine cleanup function + pCineTarget->m_iDelay = 0; + + if ( bPretendSuccess ) + { + // We need to pretend that this sequence actually finished fully +#ifdef MAPBASE + pCineTarget->m_OnEndSequence.FireOutput(pEntity, pCineTarget); + pCineTarget->m_OnPostIdleEndSequence.FireOutput(pEntity, pCineTarget); +#else + pCineTarget->m_OnEndSequence.FireOutput(NULL, pCineTarget); + pCineTarget->m_OnPostIdleEndSequence.FireOutput(NULL, pCineTarget); +#endif + } + else + { + // Fire the cancel +#ifdef MAPBASE + pCineTarget->m_OnCancelSequence.FireOutput(pEntity, pCineTarget); +#else + pCineTarget->m_OnCancelSequence.FireOutput(NULL, pCineTarget); +#endif + + if ( pCineTarget->m_startTime == 0 ) + { + // If start time is 0, this sequence never actually ran. Fire the failed output. +#ifdef MAPBASE + pCineTarget->m_OnCancelFailedSequence.FireOutput(pEntity, pCineTarget); +#else + pCineTarget->m_OnCancelFailedSequence.FireOutput(NULL, pCineTarget); +#endif + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after keyvalues have been parsed. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + + // + // If we have no name or we are set to start immediately, find the NPC and + // have them move to their script position now. + // + if ( !GetEntityName() || ( m_spawnflags & SF_SCRIPT_START_ON_SPAWN ) ) + { + StartThink(); + SetNextThink( gpGlobals->curtime + 1.0f ); + + // + // If we have a name, wait for a BeginSequence input to play the + // action animation. Otherwise, we'll play the action animation + // as soon as the NPC reaches the script position. + // + if ( GetEntityName() != NULL_STRING ) + { + m_bWaitForBeginSequence = true; + } + } + + if ( m_spawnflags & SF_SCRIPT_NOINTERRUPT ) + { + m_interruptable = false; + } + else + { + m_interruptable = true; + } + + m_sequenceStarted = false; + m_startTime = 0; + m_hNextCine = NULL; + + m_hLastFoundEntity = NULL; +} + +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::UpdateOnRemove(void) +{ + ScriptEntityCancel( this ); + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::StartThink() +{ + m_sequenceStarted = false; + m_bThinking = true; + SetThink( &CAI_ScriptedSequence::ScriptThink ); +} + +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::StopThink() +{ + if ( m_bThinking ) + { + Assert( !m_bInitiatedSelfDelete ); + SetThink( NULL); + m_bThinking = false; + } +} +//----------------------------------------------------------------------------- +// Purpose: Returns true if this scripted sequence can possess entities +// regardless of state. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::FCanOverrideState( void ) +{ + if ( m_spawnflags & SF_SCRIPT_OVERRIDESTATE ) + return true; + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Fires a script event by number. +// Input : nEvent - One based index of the script event from the , from 1 to 8. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::FireScriptEvent( int nEvent ) +{ + if ( ( nEvent >= 1 ) && ( nEvent <= MAX_SCRIPT_EVENTS ) ) + { + m_OnScriptEvent[nEvent - 1].FireOutput( this, this ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that causes the NPC to move to the script position. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::InputMoveToPosition( inputdata_t &inputdata ) +{ + if ( m_bInitiatedSelfDelete ) + return; + + // Have I already grabbed an NPC? + CBaseEntity *pEntity = GetTarget(); + CAI_BaseNPC *pTarget = NULL; + + if ( pEntity ) + { + pTarget = pEntity->MyNPCPointer(); + } + + if ( pTarget != NULL ) + { + // Yes, are they already playing this script? + if ( pTarget->m_scriptState == CAI_BaseNPC::SCRIPT_PLAYING || pTarget->m_scriptState == CAI_BaseNPC::SCRIPT_POST_IDLE ) + { + // Yes, see if we can enqueue ourselves. + if ( pTarget->CanPlaySequence( FCanOverrideState(), SS_INTERRUPT_BY_NAME ) ) + { + StartScript(); + m_bWaitForBeginSequence = true; + } + } + + // No, presumably they are moving to position or waiting for the BeginSequence input. + } + else + { + // No, grab the NPC but make them wait until BeginSequence is fired. They'll play + // their pre-action idle animation until BeginSequence is fired. + StartThink(); + SetNextThink( gpGlobals->curtime ); + m_bWaitForBeginSequence = true; + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Sets our target NPC with the generic SetTarget input. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::InputSetTarget( inputdata_t &inputdata ) +{ + m_hActivator = inputdata.pActivator; + m_iszEntity = AllocPooledString(inputdata.value.String()); + m_hTargetEnt = NULL; +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that activates the scripted sequence. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::InputBeginSequence( inputdata_t &inputdata ) +{ + if ( m_bInitiatedSelfDelete ) + return; + + // Start the script as soon as possible. + m_bWaitForBeginSequence = false; + +#ifdef MAPBASE + m_hActivator = inputdata.pActivator; + + // TODO: Investigate whether this is necessary + //if (FStrEq(STRING(m_iszEntity), "!activator")) + // SetTarget(NULL); +#endif + + // do I already know who I should use? + CBaseEntity *pEntity = GetTarget(); + CAI_BaseNPC *pTarget = NULL; + + if ( !pEntity && m_hForcedTarget ) + { + if ( FindEntity() ) + { + pEntity = GetTarget(); + } + } + + if ( pEntity ) + { + pTarget = pEntity->MyNPCPointer(); + } + + if ( pTarget ) + { + // Are they already playing a script? + if ( pTarget->m_scriptState == CAI_BaseNPC::SCRIPT_PLAYING || pTarget->m_scriptState == CAI_BaseNPC::SCRIPT_POST_IDLE ) + { + // See if we can enqueue ourselves after the current script. + if ( pTarget->CanPlaySequence( FCanOverrideState(), SS_INTERRUPT_BY_NAME ) ) + { + StartScript(); + } + } + } + else + { + // if not, try finding them + StartThink(); + + // Because we are directly calling the new "think" function ScriptThink, assume we're done + // This fixes the following bug (along with the WokeThisTick() code herein: + // A zombie is created in the asleep state and then, the mapper fires both Wake and BeginSequence + // messages to have it jump up out of the slime, e.g. What happens before this change is that + // the Wake code removed EF_NODRAW, but so the zombie is transmitted to the client, but the script + // hasn't started and won't start until the next Think time (2 ticks on xbox) at which time the + // actual sequence starts causing the zombie to quickly lie down. + // The changes here are to track what tick we "awoke" on and get rid of the lag between Wake and + // ScriptThink by actually calling ScriptThink directly on the same frame and checking for the + // zombie having woken up and been instructed to play a sequence in the same frame. + SetNextThink( TICK_NEVER_THINK ); + ScriptThink(); + } + +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that activates the scripted sequence. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::InputCancelSequence( inputdata_t &inputdata ) +{ + if ( m_bInitiatedSelfDelete ) + return; + + // + // We don't call CancelScript because entity I/O will handle dispatching + // this input to all other scripts with our same name. + // + ScriptMsg1( 2, "InputCancelScript: Cancelling script '%s'\n", STRING( m_iszPlay )); + StopThink(); + ScriptEntityCancel( this ); +} + +void CAI_ScriptedSequence::InputScriptPlayerDeath( inputdata_t &inputdata ) +{ + if ( m_iPlayerDeathBehavior == SCRIPT_CANCEL ) + { + if ( m_bInitiatedSelfDelete ) + return; + + // + // We don't call CancelScript because entity I/O will handle dispatching + // this input to all other scripts with our same name. + // + ScriptMsg1( 2, "InputCancelScript: Cancelling script '%s'\n", STRING( m_iszPlay )); + StopThink(); + ScriptEntityCancel( this ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if it is time for this script to start, false if the +// NPC should continue waiting. +// +// Scripts wait for two reasons: +// +// 1. To frame-syncronize with other scripts of the same name. +// 2. To wait indefinitely for the BeginSequence input after the NPC +// moves to the script position. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::IsTimeToStart( void ) +{ + Assert( !m_bWaitForBeginSequence ); + + return ( m_iDelay == 0 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the script is still waiting to call StartScript() +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::IsWaitingForBegin( void ) +{ + return m_bWaitForBeginSequence; +} + +//----------------------------------------------------------------------------- +// Purpose: This doesn't really make sense since only MOVETYPE_PUSH get 'Blocked' events +// Input : pOther - The entity blocking us. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::Blocked( CBaseEntity *pOther ) +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pOther - The entity touching us. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::Touch( CBaseEntity *pOther ) +{ +/* + ScriptMsg( 2, "Cine Touch\n" ); + if (m_pentTarget && OFFSET(pOther->pev) == OFFSET(m_pentTarget)) + { + CAI_BaseNPC *pTarget = GetClassPtr((CAI_BaseNPC *)VARS(m_pentTarget)); + pTarget->m_NPCState == NPC_STATE_SCRIPT; + } +*/ +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::Die( void ) +{ + SetThink( &CAI_ScriptedSequence::SUB_Remove ); + m_bThinking = false; + m_bInitiatedSelfDelete = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::Pain( void ) +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : eMode - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +CAI_BaseNPC *CAI_ScriptedSequence::FindScriptEntity( ) +{ + CAI_BaseNPC *pEnqueueNPC = NULL; + + CBaseEntity *pEntity; + int interrupt; + if ( m_hForcedTarget ) + { + interrupt = SS_INTERRUPT_BY_NAME; + pEntity = m_hForcedTarget; + } + else + { + interrupt = SS_INTERRUPT_BY_NAME; + +#ifdef MAPBASE + pEntity = gEntList.FindEntityByNameWithin( m_hLastFoundEntity, STRING( m_iszEntity ), GetAbsOrigin(), m_flRadius, this, m_hActivator ); +#else + pEntity = gEntList.FindEntityByNameWithin( m_hLastFoundEntity, STRING( m_iszEntity ), GetAbsOrigin(), m_flRadius ); +#endif + if (!pEntity) + { + pEntity = gEntList.FindEntityByClassnameWithin( m_hLastFoundEntity, STRING( m_iszEntity ), GetAbsOrigin(), m_flRadius ); + interrupt = SS_INTERRUPT_BY_CLASS; + } + } + + while ( pEntity != NULL ) + { + CAI_BaseNPC *pNPC = pEntity->MyNPCPointer( ); + if ( pNPC ) + { + // + // If they can play the sequence... + // + CanPlaySequence_t eCanPlay = pNPC->CanPlaySequence( FCanOverrideState(), interrupt ); + if ( eCanPlay == CAN_PLAY_NOW ) + { + // If they can play it now, we're done! + return pNPC; + } + else if ( eCanPlay == CAN_PLAY_ENQUEUED ) + { + // They can play it, but only enqueued. We'll use them as a last resort. + pEnqueueNPC = pNPC; + } + else if (!(m_spawnflags & SF_SCRIPT_NO_COMPLAINTS)) + { + // They cannot play the script. + ScriptMsg1( 1, "Found %s, but can't play!\n", STRING( m_iszEntity )); + } + } + + if ( m_hForcedTarget ) + { + Warning( "Code forced %s(%s), to be the target of scripted sequence %s, but it can't play it.\n", + pEntity->GetClassname(), pEntity->GetDebugName(), GetDebugName() ); + pEntity = NULL; + UTIL_Remove( this ); + return NULL; + } + else + { + if ( interrupt == SS_INTERRUPT_BY_NAME ) + pEntity = gEntList.FindEntityByNameWithin( pEntity, STRING( m_iszEntity ), GetAbsOrigin(), m_flRadius ); + else + pEntity = gEntList.FindEntityByClassnameWithin( pEntity, STRING( m_iszEntity ), GetAbsOrigin(), m_flRadius ); + } + } + + // + // If we found an NPC that will enqueue the script, use them. + // + if ( pEnqueueNPC != NULL ) + { + return pEnqueueNPC; + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::FindEntity( void ) +{ + CAI_BaseNPC *pTarget = FindScriptEntity( ); + + if ( (m_spawnflags & SF_SCRIPT_SEARCH_CYCLICALLY)) + { + // next time this is called, start searching from the one found last time + m_hLastFoundEntity = pTarget; + } + + SetTarget( pTarget ); + + return pTarget != NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Make the entity enter a scripted sequence. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::StartScript( void ) +{ + CBaseEntity *pEntity = GetTarget(); + CAI_BaseNPC *pTarget = NULL; + if ( pEntity ) + pTarget = pEntity->MyNPCPointer(); + + if ( pTarget ) + { + pTarget->RemoveSpawnFlags( SF_NPC_WAIT_FOR_SCRIPT ); + + // + // If the NPC is in another script, just enqueue ourselves and bail out. + // We'll possess the NPC when the current script finishes with the NPC. + // Note that we only enqueue one deep currently, so if there is someone + // else in line we'll stomp them. + // + if ( pTarget->m_hCine != NULL ) + { + if ( pTarget->m_hCine->m_hNextCine != NULL ) + { + // + // Kicking another script out of the queue. + // + CAI_ScriptedSequence *pCine = ( CAI_ScriptedSequence * )pTarget->m_hCine->m_hNextCine.Get(); + + if (pTarget->m_hCine->m_hNextCine != pTarget->m_hCine) + { + // Don't clear the currently playing script's target! + pCine->SetTarget( NULL ); + } + ScriptMsg2( 2, "script \"%s\" kicking script \"%s\" out of the queue\n", GetDebugName(), pCine->GetDebugName() ); + } + + pTarget->m_hCine->m_hNextCine = this; + return; + } + + // + // If no next script is specified, clear it out so other scripts can enqueue themselves + // after us. + // + if ( !m_iszNextScript ) + { + m_hNextCine = NULL; + } + + // UNDONE: Do this to sync up multi-entity scripts? + //pTarget->SetNextThink( gpGlobals->curtime ); + + pTarget->SetGoalEnt( this ); + pTarget->ForceDecisionThink(); + pTarget->m_hCine = this; + pTarget->SetTarget( this ); + + // Notify the NPC tat we're stomping them into a scene! + pTarget->OnStartScene(); + + { + m_bTargetWasAsleep = ( pTarget->GetSleepState() != AISS_AWAKE ) ? true : false; + bool justAwoke = pTarget->WokeThisTick(); + if ( m_bTargetWasAsleep || justAwoke ) + { + // Note, Wake() will remove the EF_NODRAW flag, but if we are starting a seq on a hidden entity + // we don't want it to draw on the client until the sequence actually starts to play + // Make sure it stays hidden!!! + if ( m_bTargetWasAsleep ) + { + pTarget->Wake(); + } + m_bTargetWasAsleep = true; + + // Even if awakened this frame, temporarily keep the entity hidden for now + pTarget->AddEffects( EF_NODRAW ); + } + } + + // If the entity was asleep at the start, make sure we don't make it invisible + // AFTER the script finishes (can't think of a case where you'd want that to happen) + m_saved_effects = pTarget->GetEffects() & ~EF_NODRAW; + pTarget->AddEffects( GetEffects() ); + m_savedFlags = pTarget->GetFlags(); + m_savedCollisionGroup = pTarget->GetCollisionGroup(); + + if ( m_bDisableNPCCollisions ) + { + pTarget->SetCollisionGroup( COLLISION_GROUP_NPC_SCRIPTED ); + } + + switch (m_fMoveTo) + { + case CINE_MOVETO_WAIT: + case CINE_MOVETO_WAIT_FACING: + pTarget->m_scriptState = CAI_BaseNPC::SCRIPT_WAIT; + + if ( m_bIgnoreGravity ) + { + pTarget->AddFlag( FL_FLY ); + pTarget->SetGroundEntity( NULL ); + } + + break; + + case CINE_MOVETO_WALK: + pTarget->m_scriptState = CAI_BaseNPC::SCRIPT_WALK_TO_MARK; + break; + + case CINE_MOVETO_RUN: + pTarget->m_scriptState = CAI_BaseNPC::SCRIPT_RUN_TO_MARK; + break; + + case CINE_MOVETO_CUSTOM: + pTarget->m_scriptState = CAI_BaseNPC::SCRIPT_CUSTOM_MOVE_TO_MARK; + break; + + case CINE_MOVETO_TELEPORT: + m_bIsTeleportingDueToMoveTo = true; + pTarget->Teleport( &GetAbsOrigin(), NULL, &vec3_origin ); + m_bIsTeleportingDueToMoveTo = false; + pTarget->GetMotor()->SetIdealYaw( GetLocalAngles().y ); + pTarget->SetLocalAngularVelocity( vec3_angle ); + pTarget->IncrementInterpolationFrame(); + QAngle angles = pTarget->GetLocalAngles(); + angles.y = GetLocalAngles().y; + pTarget->SetLocalAngles( angles ); + pTarget->m_scriptState = CAI_BaseNPC::SCRIPT_WAIT; + + if ( m_bIgnoreGravity ) + { + pTarget->AddFlag( FL_FLY ); + pTarget->SetGroundEntity( NULL ); + } + + // UNDONE: Add a flag to do this so people can fixup physics after teleporting NPCs + //pTarget->SetGroundEntity( NULL ); + break; + } + //ScriptMsg2( 2, "\"%s\" found and used (INT: %s)\n", STRING( pTarget->m_iName ), FBitSet(m_spawnflags, SF_SCRIPT_NOINTERRUPT)?"No":"Yes" ); + + + // Wait until all scripts of the same name are ready to play. + m_bDelayed = false; + DelayStart( true ); + + pTarget->SetIdealState(NPC_STATE_SCRIPT); + + // FIXME: not sure why this is happening, or what to do about truely dormant NPCs + if ( pTarget->IsEFlagSet( EFL_NO_THINK_FUNCTION ) && pTarget->GetNextThink() != TICK_NEVER_THINK ) + { + DevWarning( "scripted_sequence %d:%s - restarting dormant entity %d:%s : %.1f:%.1f\n", entindex(), GetDebugName(), pTarget->entindex(), pTarget->GetDebugName(), gpGlobals->curtime, pTarget->GetNextThink() ); + pTarget->SetNextThink( gpGlobals->curtime ); + } + +#ifdef MAPBASE + m_OnFoundNPC.FireOutput( pTarget, this ); +#endif + } +} + + +//----------------------------------------------------------------------------- +// Purpose: First think after activation. Grabs an NPC and makes it do things. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::ScriptThink( void ) +{ + if ( g_pAINetworkManager && !g_pAINetworkManager->IsInitialized() ) + { + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else if (FindEntity()) + { + StartScript( ); + ScriptMsg5( 2, "scripted_sequence %d:\"%s\" using NPC %d:\"%s\"(%s)\n", entindex(), GetDebugName(), GetTarget()->entindex(), GetTarget()->GetEntityName().ToCStr(), STRING( m_iszEntity ) ); + } + else + { + CancelScript( ); + ScriptMsg3( 2, "scripted_sequence %d:\"%s\" can't find NPC \"%s\"\n", entindex(), GetDebugName(), STRING( m_iszEntity ) ); + // FIXME: just trying again is bad. This should fire an output instead. + // FIXME: Think about puting output triggers in both StartScript() and CancelScript(). + SetNextThink( gpGlobals->curtime + 1.0f ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Callback for firing the begin sequence output. Called by the NPC that +// is running the script as it starts the action seqeunce. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +void CAI_ScriptedSequence::OnBeginSequence( CBaseEntity *pActor ) +{ + m_OnBeginSequence.FireOutput( pActor, this ); +} + +void CAI_ScriptedSequence::OnPreIdleSequence( CBaseEntity *pActor ) +{ + m_OnPreIdleSequence.FireOutput( pActor, this ); +} +#else +void CAI_ScriptedSequence::OnBeginSequence( void ) +{ + m_OnBeginSequence.FireOutput( this, this ); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Look up a sequence name and setup the target NPC to play it. +// Input : pTarget - +// iszSeq - +// completeOnEmpty - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::StartSequence( CAI_BaseNPC *pTarget, string_t iszSeq, bool completeOnEmpty ) +{ + Assert( pTarget ); + m_sequenceStarted = true; + m_bIsPlayingEntry = (iszSeq == m_iszEntry); + + if ( !iszSeq && completeOnEmpty ) + { + SequenceDone( pTarget ); + return false; + } + + int nSequence = pTarget->LookupSequence( STRING( iszSeq ) ); + if (nSequence == -1) + { + Warning( "%s: unknown scripted sequence \"%s\"\n", pTarget->GetDebugName(), STRING( iszSeq )); + nSequence = 0; + } + + // look for the activity that this represents + Activity act = pTarget->GetSequenceActivity( nSequence ); + if (act == ACT_INVALID) + act = ACT_IDLE; + + pTarget->SetActivityAndSequence( act, nSequence, act, act ); + + // If the target was hidden even though we woke it up, only make it drawable if we're not still on the preidle seq... + if ( m_bTargetWasAsleep && + iszSeq != m_iszPreIdle ) + { + m_bTargetWasAsleep = false; + // Show it + pTarget->RemoveEffects( EF_NODRAW ); + // Don't blend... + pTarget->IncrementInterpolationFrame(); + } + //ScriptMsg4( 2, "%s (%s): started \"%s\":INT:%s\n", STRING( pTarget->m_iName ), pTarget->GetClassname(), STRING( iszSeq), (m_spawnflags & SF_SCRIPT_NOINTERRUPT) ? "No" : "Yes" ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Called when a scripted sequence is ready to start playing the sequence +// Input : pNPC - Pointer to the NPC that the sequence possesses. +//----------------------------------------------------------------------------- + +void CAI_ScriptedSequence::SynchronizeSequence( CAI_BaseNPC *pNPC ) +{ + //Msg("%s (for %s) called SynchronizeSequence() at %0.2f\n", GetTarget()->GetDebugName(), GetDebugName(), gpGlobals->curtime); + + Assert( m_iDelay == 0 ); + Assert( m_bWaitForBeginSequence == false ); + m_bForceSynch = false; + + // Reset cycle position + float flCycleRate = pNPC->GetSequenceCycleRate( pNPC->GetSequence() ); + float flInterval = gpGlobals->curtime - m_startTime; + + // Msg("%.2f \"%s\" %s : %f (%f): interval %f\n", gpGlobals->curtime, GetEntityName().ToCStr(), pNPC->GetClassname(), pNPC->m_flAnimTime.Get(), m_startTime, flInterval ); + //Assert( flInterval >= 0.0 && flInterval <= 0.15 ); + flInterval = clamp( flInterval, 0.f, 0.15f ); + + if (flInterval == 0) + return; + + // do the movement for the missed portion of the sequence + pNPC->SetCycle( 0.0f ); + pNPC->AutoMovement( flInterval ); + + // reset the cycle to a common basis + pNPC->SetCycle( flInterval * flCycleRate ); +} + +//----------------------------------------------------------------------------- +// Purpose: Moves to the next action sequence if the scripted_sequence wants to, +// or returns true if it wants to leave the action sequence +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::FinishedActionSequence( CAI_BaseNPC *pNPC ) +{ + // Restart the action sequence when the entry finishes, or when the action + // finishes and we're set to loop it. + if ( IsPlayingEntry() ) + { + if ( GetEntityName() != NULL_STRING ) + { + // Force all matching ss's to synchronize their action sequences + SynchNewSequence( CAI_BaseNPC::SCRIPT_PLAYING, m_iszPlay, true ); + } + else + { + StartSequence( pNPC, m_iszPlay, true ); + } + return false; + } + + // Let the core action sequence continue to loop + if ( ShouldLoopActionSequence() ) + { + // If the NPC has reached post idle state, we need to stop looping the action sequence + if ( pNPC->m_scriptState == CAI_BaseNPC::SCRIPT_POST_IDLE ) + return true; + + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Called when a scripted sequence animation sequence is done playing +// (or when an AI Scripted Sequence doesn't supply an animation sequence +// to play). +// Input : pNPC - Pointer to the NPC that the sequence possesses. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::SequenceDone( CAI_BaseNPC *pNPC ) +{ + //ScriptMsg1( 2, "Sequence %s finished\n", STRING( pNPC->m_hCine->m_iszPlay ) ); + + //Msg("%s SequenceDone() at %0.2f\n", pNPC->GetDebugName(), gpGlobals->curtime ); + + // If we're part of a synchronised post-idle sequence, we need to do things differently + if ( m_bSynchPostIdles && GetEntityName() != NULL_STRING ) + { + // If we're already in POST_IDLE state, then one of the other scripted + // sequences we're synching with has already kicked us into running + // the post idle sequence, so we do nothing. + if ( pNPC->m_scriptState != CAI_BaseNPC::SCRIPT_POST_IDLE ) + { + if ( ( m_iszPostIdle != NULL_STRING ) && ( m_hNextCine == NULL ) ) + { + SynchNewSequence( CAI_BaseNPC::SCRIPT_POST_IDLE, m_iszPostIdle, true ); + } + else + { + PostIdleDone( pNPC ); + } + } + } + else + { + // + // If we have a post idle set, and no other script is in the queue for this + // NPC, start playing the idle now. + // + if ( ( m_iszPostIdle != NULL_STRING ) && ( m_hNextCine == NULL ) ) + { + // + // First time we've gotten here for this script. Start playing the post idle + // if one is specified. + // + pNPC->m_scriptState = CAI_BaseNPC::SCRIPT_POST_IDLE; + StartSequence( pNPC, m_iszPostIdle, false ); // false to prevent recursion here! + } + else + { + PostIdleDone( pNPC ); + } + } + +#ifdef MAPBASE + m_OnEndSequence.FireOutput(pNPC, this); +#else + m_OnEndSequence.FireOutput(NULL, this); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::SynchNewSequence( CAI_BaseNPC::SCRIPTSTATE newState, string_t iszSequence, bool bSynchOtherScenes ) +{ + // Do we need to synchronize all our matching scripted scenes? + if ( bSynchOtherScenes ) + { + //Msg("%s (for %s) forcing synch of %s at %0.2f\n", GetTarget()->GetDebugName(), GetDebugName(), iszSequence, gpGlobals->curtime); + + CBaseEntity *pentCine = gEntList.FindEntityByName( NULL, GetEntityName(), NULL ); + while ( pentCine ) + { + CAI_ScriptedSequence *pScene = dynamic_cast(pentCine); + if ( pScene && pScene != this ) + { + pScene->SynchNewSequence( newState, iszSequence, false ); + } + pentCine = gEntList.FindEntityByName( pentCine, GetEntityName(), NULL ); + } + } + + // Now force this one to start the post idle? + if ( !GetTarget() ) + return; + CAI_BaseNPC *pNPC = GetTarget()->MyNPCPointer(); + if ( !pNPC ) + return; + + //Msg("%s (for %s) starting %s seq at %0.2f\n", pNPC->GetDebugName(), GetDebugName(), iszSequence, gpGlobals->curtime); + + m_startTime = gpGlobals->curtime; + pNPC->m_scriptState = newState; + StartSequence( pNPC, iszSequence, false ); + + // Force the NPC to synchronize animations on their next think + m_bForceSynch = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Called when a scripted sequence animation sequence is done playing +// (or when an AI Scripted Sequence doesn't supply an animation sequence +// to play). +// Input : pNPC - Pointer to the NPC that the sequence possesses. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::PostIdleDone( CAI_BaseNPC *pNPC ) +{ + // + // If we're set to keep the NPC in a scripted idle, hack them back into the script, + // but allow another scripted sequence to take control of the NPC if it wants to, + // unless another script has stolen them from us. + // + if ( ( m_iszPostIdle != NULL_STRING ) && ( m_spawnflags & SF_SCRIPT_LOOP_IN_POST_IDLE ) && ( m_hNextCine == NULL ) ) + { + // + // First time we've gotten here for this script. Start playing the post idle + // if one is specified. + // Only do so if we're selected, to prevent spam + if ( pNPC->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) + { + ScriptMsg2( 2, "Post Idle %s finished for %s\n", STRING( pNPC->m_hCine->m_iszPostIdle ), pNPC->GetDebugName() ); + } + + pNPC->m_scriptState = CAI_BaseNPC::SCRIPT_POST_IDLE; + StartSequence( pNPC, m_iszPostIdle, false ); + } + else + { + if ( !( m_spawnflags & SF_SCRIPT_REPEATABLE ) ) + { + SetThink( &CAI_ScriptedSequence::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + m_bThinking = false; + m_bInitiatedSelfDelete = true; + } + + // + // This is done so that another sequence can take over the NPC when triggered + // by the first. + // + pNPC->CineCleanup(); + + // We have to pass in the flags here, because the NPC's m_hCine was cleared in CineCleanup() + FixScriptNPCSchedule( pNPC, m_savedFlags ); + + // + // If another script is waiting to grab this NPC, start the next script + // immediately. + // + if ( m_hNextCine != NULL ) + { + CAI_ScriptedSequence *pNextCine = ( CAI_ScriptedSequence * )m_hNextCine.Get(); + + // + // Don't link ourselves in if we are going to be deleted. + // TODO: use EHANDLEs instead of pointers to scripts! + // + if ( ( pNextCine != this ) || ( m_spawnflags & SF_SCRIPT_REPEATABLE ) ) + { + pNextCine->SetTarget( pNPC ); + pNextCine->StartScript(); + } + } + } + + //Msg("%s finished post idle at %0.2f\n", pNPC->GetDebugName(), gpGlobals->curtime ); +#ifdef MAPBASE + m_OnPostIdleEndSequence.FireOutput(pNPC, this); +#else + m_OnPostIdleEndSequence.FireOutput(NULL, this); +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: When a NPC finishes a scripted sequence, we have to fix up its state +// and schedule for it to return to a normal AI NPC. +// Scripted sequences just dirty the Schedule and drop the NPC in Idle State. +// Input : *pNPC - +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::FixScriptNPCSchedule( CAI_BaseNPC *pNPC, int iSavedCineFlags ) +{ + if ( pNPC->GetIdealState() != NPC_STATE_DEAD ) + { + pNPC->SetIdealState( NPC_STATE_IDLE ); + } + + if ( pNPC == NULL ) + return; + + FixFlyFlag( pNPC, iSavedCineFlags ); + + pNPC->ClearSchedule( "Finished scripted sequence" ); +} + +void CAI_ScriptedSequence::FixFlyFlag( CAI_BaseNPC *pNPC, int iSavedCineFlags ) +{ + //Adrian: We NEED to clear this or the NPC's FL_FLY flag will never be removed cause of ClearSchedule! + if ( pNPC->GetTask() && ( pNPC->GetTask()->iTask == TASK_PLAY_SCRIPT || pNPC->GetTask()->iTask == TASK_PLAY_SCRIPT_POST_IDLE ) ) + { + if ( !(iSavedCineFlags & FL_FLY) ) + { + if ( pNPC->GetFlags() & FL_FLY ) + { + pNPC->RemoveFlag( FL_FLY ); + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : fAllow - +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::AllowInterrupt( bool fAllow ) +{ + if ( m_spawnflags & SF_SCRIPT_NOINTERRUPT ) + return; + m_interruptable = fAllow; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::CanInterrupt( void ) +{ + if ( !m_interruptable ) + return false; + + CBaseEntity *pTarget = GetTarget(); + + if ( pTarget != NULL && pTarget->IsAlive() ) + return true; + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::RemoveIgnoredConditions( void ) +{ + if ( CanInterrupt() ) + { + return; + } + + CBaseEntity *pEntity = GetTarget(); + if ( pEntity == NULL ) + { + return; + } + + CAI_BaseNPC *pTarget = pEntity->MyNPCPointer(); + if ( pTarget == NULL ) + { + return; + } + + + if ( pTarget ) + { + pTarget->ClearCondition(COND_LIGHT_DAMAGE); + pTarget->ClearCondition(COND_HEAVY_DAMAGE); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if another script is allowed to enqueue itself after +// this script. The enqueued script will begin immediately after the +// current script without dropping the NPC into AI. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSequence::CanEnqueueAfter( void ) +{ + if ( m_hNextCine == NULL ) + { + return true; + } + + if ( m_iszNextScript != NULL_STRING ) + { + ScriptMsg1( 2, "%s is specified as the 'Next Script' and cannot be kicked out of the queue\n", m_hNextCine->GetDebugName() ); + return false; + } + + if ( !m_hNextCine->HasSpawnFlags( SF_SCRIPT_HIGH_PRIORITY ) ) + { + return true; + } + + ScriptMsg1( 2, "%s is a priority script and cannot be kicked out of the queue\n", m_hNextCine->GetDebugName() ); + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::StopActionLoop( bool bStopSynchronizedScenes ) +{ + // Stop looping our action sequence. Next time the loop finishes, + // we'll move to the post idle sequence instead. + m_bLoopActionSequence = false; + + // If we have synchronized scenes, and we're supposed to stop them, do so + if ( !bStopSynchronizedScenes || GetEntityName() == NULL_STRING ) + return; + + CBaseEntity *pentCine = gEntList.FindEntityByName( NULL, GetEntityName(), NULL ); + while ( pentCine ) + { + CAI_ScriptedSequence *pScene = dynamic_cast(pentCine); + if ( pScene && pScene != this ) + { + pScene->StopActionLoop( false ); + } + + pentCine = gEntList.FindEntityByName( pentCine, GetEntityName(), NULL ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Code method of forcing a scripted sequence entity to use a particular NPC. +// Useful when you don't know if the NPC has a unique targetname. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::ForceSetTargetEntity( CAI_BaseNPC *pTarget, bool bDontCancelOtherSequences ) +{ + m_hForcedTarget = pTarget; + m_iszEntity = m_hForcedTarget->GetEntityName(); // Not guaranteed to be unique + m_bDontCancelOtherSequences = bDontCancelOtherSequences; +} + +//----------------------------------------------------------------------------- +// Purpose: Setup this scripted sequence to maintain the desired position offset +// to the other NPC in the scripted interaction. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::SetupInteractionPosition( CBaseEntity *pRelativeEntity, VMatrix &matDesiredLocalToWorld ) +{ + m_matInteractionPosition = matDesiredLocalToWorld; + m_hInteractionRelativeEntity = pRelativeEntity; +} + +extern ConVar ai_debug_dyninteractions; +//----------------------------------------------------------------------------- +// Purpose: Modify the target AutoMovement() position before the NPC moves. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::ModifyScriptedAutoMovement( Vector *vecNewPos ) +{ + if ( m_hInteractionRelativeEntity ) + { + // If we have an entry animation, only do it on the entry + if ( m_iszEntry != NULL_STRING && !m_bIsPlayingEntry ) + return; + + Vector vecRelativeOrigin = m_hInteractionRelativeEntity->GetAbsOrigin(); + QAngle angRelativeAngles = m_hInteractionRelativeEntity->GetAbsAngles(); + + CAI_BaseNPC *pNPC = m_hInteractionRelativeEntity->MyNPCPointer(); + if ( pNPC ) + { + angRelativeAngles[YAW] = pNPC->GetInteractionYaw(); + } + + bool bDebug = ai_debug_dyninteractions.GetInt() == 2; + if ( bDebug ) + { + Msg("--\n%s current org: %f %f\n", m_hTargetEnt->GetDebugName(), m_hTargetEnt->GetAbsOrigin().x, m_hTargetEnt->GetAbsOrigin().y ); + Msg("%s current org: %f %f", m_hInteractionRelativeEntity->GetDebugName(), vecRelativeOrigin.x, vecRelativeOrigin.y ); + } + + CBaseAnimating *pAnimating = dynamic_cast(m_hInteractionRelativeEntity.Get()); + if ( pAnimating ) + { + Vector vecDeltaPos; + QAngle vecDeltaAngles; + pAnimating->GetSequenceMovement( pAnimating->GetSequence(), 0.0f, pAnimating->GetCycle(), vecDeltaPos, vecDeltaAngles ); + VectorYawRotate( vecDeltaPos, pAnimating->GetLocalAngles().y, vecDeltaPos ); + + if ( bDebug ) + { + NDebugOverlay::Box( vecRelativeOrigin, -Vector(2,2,2), Vector(2,2,2), 0,255,0, 8, 0.1 ); + } + vecRelativeOrigin -= vecDeltaPos; + if ( bDebug ) + { + Msg(", relative to sequence start: %f %f\n", vecRelativeOrigin.x, vecRelativeOrigin.y ); + NDebugOverlay::Box( vecRelativeOrigin, -Vector(3,3,3), Vector(3,3,3), 255,0,0, 8, 0.1 ); + } + } + + // We've been asked to maintain a specific position relative to the other NPC + // we're interacting with. Lerp towards the relative position. + VMatrix matMeToWorld, matLocalToWorld; + matMeToWorld.SetupMatrixOrgAngles( vecRelativeOrigin, angRelativeAngles ); + MatrixMultiply( matMeToWorld, m_matInteractionPosition, matLocalToWorld ); + + // Get the desired NPC position in worldspace + Vector vecOrigin; + QAngle angAngles; + vecOrigin = matLocalToWorld.GetTranslation(); + MatrixToAngles( matLocalToWorld, angAngles ); + + if ( bDebug ) + { + Msg("Desired Origin for %s: %f %f\n", m_hTargetEnt->GetDebugName(), vecOrigin.x, vecOrigin.y ); + NDebugOverlay::Axis( vecOrigin, angAngles, 5, true, 0.1 ); + } + + // Lerp to it over time + Vector vecToTarget = (vecOrigin - *vecNewPos); + if ( bDebug ) + { + Msg("Automovement's output origin: %f %f\n", (*vecNewPos).x, (*vecNewPos).y ); + Msg("Vector from automovement to desired: %f %f\n", vecToTarget.x, vecToTarget.y ); + } + *vecNewPos += (vecToTarget * pAnimating->GetCycle()); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Find all the cinematic entities with my targetname and stop them +// from playing. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::CancelScript( void ) +{ + ScriptMsg1( 2, "Cancelling script: %s\n", STRING( m_iszPlay )); + + // Don't cancel matching sequences if we're asked not to, unless we didn't actually + // succeed in starting, in which case we should always cancel. This fixes + // dynamic interactions where an NPC was killed the same frame another NPC + // started a dynamic interaction with him. + bool bDontCancelOther = ((m_bDontCancelOtherSequences || HasSpawnFlags( SF_SCRIPT_ALLOW_DEATH ) )&& (m_startTime != 0)); + if ( bDontCancelOther || !GetEntityName() ) + { + ScriptEntityCancel( this ); + return; + } + + CBaseEntity *pentCineTarget = gEntList.FindEntityByName( NULL, GetEntityName() ); + + while ( pentCineTarget ) + { + ScriptEntityCancel( pentCineTarget ); + pentCineTarget = gEntList.FindEntityByName( pentCineTarget, GetEntityName() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Find all the cinematic entities with my targetname and tell them to +// wait before starting. This ensures that all scripted sequences with +// the same name are frame-synchronized. +// +// When triggered, scripts call this first with a state of 1 to indicate that +// they are not ready to play (while NPCs move to their cue positions, etc). +// Once they are ready to play, they call it with a state of 0. When all +// the scripts are ready, they all are told to start. +// +// Input : bDelay - true means this script is not ready, false means it is ready. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::DelayStart( bool bDelay ) +{ + //Msg("SSEQ: %.2f \"%s\" (%d) DelayStart( %d ). Current m_iDelay is: %d\n", gpGlobals->curtime, GetDebugName(), entindex(), bDelay, m_iDelay ); + + if ( ai_task_pre_script.GetBool() ) + { + if ( bDelay == m_bDelayed ) + return; + + m_bDelayed = bDelay; + } + + // Without a name, we cannot synchronize with anything else + if ( GetEntityName() == NULL_STRING ) + { + m_iDelay = bDelay; + m_startTime = gpGlobals->curtime; + return; + } + + CBaseEntity *pentCine = gEntList.FindEntityByName( NULL, GetEntityName() ); + + while ( pentCine ) + { + if ( FClassnameIs( pentCine, "scripted_sequence" ) ) + { + CAI_ScriptedSequence *pTarget = (CAI_ScriptedSequence *)pentCine; + if (bDelay) + { + // if delaying, add up the number of other scripts in the group + m_iDelay++; + + //Msg("SSEQ: (%d) Found matching SS (%d). Incrementing MY m_iDelay to %d.\n", entindex(), pTarget->entindex(), m_iDelay ); + } + else + { + // if ready, decrement each of other scripts in the group + // members not yet delayed will decrement below zero. + pTarget->m_iDelay--; + + //Msg("SSEQ: (%d) Found matching SS (%d). Decrementing THEIR m_iDelay to %d.\n", entindex(), pTarget->entindex(), pTarget->m_iDelay ); + + // once everything is balanced, everyone will start. + if (pTarget->m_iDelay == 0) + { + pTarget->m_startTime = gpGlobals->curtime; + + //Msg("SSEQ: STARTING SEQUENCE for \"%s\" (%d) (m_iDelay reached 0).\n", pTarget->GetDebugName(), pTarget->entindex() ); + } + } + } + pentCine = gEntList.FindEntityByName( pentCine, GetEntityName() ); + } + + //Msg("SSEQ: Exited DelayStart() with m_iDelay of: %d.\n", m_iDelay ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Find an entity that I'm interested in and precache the sounds he'll +// need in the sequence. +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::Activate( void ) +{ + BaseClass::Activate(); + + // + // See if there is another script specified to run immediately after this one. + // + m_hNextCine = gEntList.FindEntityByName( NULL, m_iszNextScript ); + if ( m_hNextCine == NULL ) + { + m_iszNextScript = NULL_STRING; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSequence::DrawDebugGeometryOverlays( void ) +{ + BaseClass::DrawDebugGeometryOverlays(); + + if ( m_debugOverlays & OVERLAY_TEXT_BIT ) + { + if ( GetTarget() ) + { + NDebugOverlay::HorzArrow( GetAbsOrigin(), GetTarget()->GetAbsOrigin(), 16, 0, 255, 0, 64, true, 0.0f ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Input : +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CAI_ScriptedSequence::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + Q_snprintf(tempstr,sizeof(tempstr),"Target: %s", GetTarget() ? GetTarget()->GetDebugName() : "None" ) ; + EntityText(text_offset,tempstr,0); + text_offset++; + + switch (m_fMoveTo) + { + case CINE_MOVETO_WAIT: + Q_snprintf(tempstr,sizeof(tempstr),"Moveto: Wait" ); + break; + case CINE_MOVETO_WAIT_FACING: + Q_snprintf(tempstr,sizeof(tempstr),"Moveto: Wait Facing" ); + break; + case CINE_MOVETO_WALK: + Q_snprintf(tempstr,sizeof(tempstr),"Moveto: Walk to Mark" ); + break; + case CINE_MOVETO_RUN: + Q_snprintf(tempstr,sizeof(tempstr),"Moveto: Run to Mark" ); + break; + case CINE_MOVETO_CUSTOM: + Q_snprintf(tempstr,sizeof(tempstr),"Moveto: Custom move to Mark" ); + break; + case CINE_MOVETO_TELEPORT: + Q_snprintf(tempstr,sizeof(tempstr),"Moveto: Teleport to Mark" ); + break; + } + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Thinking: %s", m_bThinking ? "Yes" : "No" ) ; + EntityText(text_offset,tempstr,0); + text_offset++; + + if ( GetEntityName() != NULL_STRING ) + { + Q_snprintf(tempstr,sizeof(tempstr),"Delay: %d", m_iDelay ) ; + EntityText(text_offset,tempstr,0); + text_offset++; + } + + Q_snprintf(tempstr,sizeof(tempstr),"Start Time: %f", m_startTime ) ; + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Sequence has started: %s", m_sequenceStarted ? "Yes" : "No" ) ; + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Cancel Other Sequences: %s", m_bDontCancelOtherSequences ? "No" : "Yes" ) ; + EntityText(text_offset,tempstr,0); + text_offset++; + + if ( m_bWaitForBeginSequence ) + { + Q_snprintf(tempstr,sizeof(tempstr),"Is waiting for BeingSequence" ); + EntityText(text_offset,tempstr,0); + text_offset++; + } + + if ( m_bIsPlayingEntry ) + { + Q_snprintf(tempstr,sizeof(tempstr),"Is playing entry" ); + EntityText(text_offset,tempstr,0); + text_offset++; + } + + if ( m_bLoopActionSequence ) + { + Q_snprintf(tempstr,sizeof(tempstr),"Will loop action sequence" ); + EntityText(text_offset,tempstr,0); + text_offset++; + } + + if ( m_bSynchPostIdles ) + { + Q_snprintf(tempstr,sizeof(tempstr),"Will synch post idles" ); + EntityText(text_offset,tempstr,0); + text_offset++; + } + } + + return text_offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Modifies an NPC's AI state without taking it out of its AI. +//----------------------------------------------------------------------------- + +class CAI_ScriptedSchedule : public CBaseEntity +{ + DECLARE_CLASS( CAI_ScriptedSchedule, CBaseEntity ); +public: + CAI_ScriptedSchedule( void ); + +private: + + void StartSchedule( CAI_BaseNPC *pTarget ); + void StopSchedule( CAI_BaseNPC *pTarget ); + void ScriptThink( void ); + + // Input handlers + void InputStartSchedule( inputdata_t &inputdata ); + void InputStopSchedule( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetTarget( inputdata_t &inputdata ); +#endif + + CAI_BaseNPC *FindScriptEntity( bool bCyclic ); + + //--------------------------------- + + enum Schedule_t + { + SCHED_SCRIPT_NONE = 0, + SCHED_SCRIPT_WALK_TO_GOAL, + SCHED_SCRIPT_RUN_TO_GOAL, + SCHED_SCRIPT_ENEMY_IS_GOAL, + SCHED_SCRIPT_WALK_PATH_GOAL, + SCHED_SCRIPT_RUN_PATH_GOAL, + SCHED_SCRIPT_ENEMY_IS_GOAL_AND_RUN_TO_GOAL, + }; + + //--------------------------------- + + EHANDLE m_hLastFoundEntity; + EHANDLE m_hActivator; // Held from the input to allow procedural calls + + string_t m_iszEntity; // Entity that is wanted for this script + float m_flRadius; // Range to search for an NPC to possess. + + string_t m_sGoalEnt; + Schedule_t m_nSchedule; + int m_nForceState; + + bool m_bGrabAll; + + Interruptability_t m_Interruptability; + + bool m_bDidFireOnce; + + //--------------------------------- + + DECLARE_DATADESC(); + +}; + +BEGIN_DATADESC( CAI_ScriptedSchedule ) + + DEFINE_FIELD( m_hLastFoundEntity, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "m_flRadius" ), + + DEFINE_KEYFIELD( m_iszEntity, FIELD_STRING, "m_iszEntity" ), + DEFINE_KEYFIELD( m_nSchedule, FIELD_INTEGER, "schedule" ), + DEFINE_KEYFIELD( m_nForceState, FIELD_INTEGER, "forcestate" ), + DEFINE_KEYFIELD( m_sGoalEnt, FIELD_STRING, "goalent" ), + DEFINE_KEYFIELD( m_bGrabAll, FIELD_BOOLEAN, "graball" ), + DEFINE_KEYFIELD( m_Interruptability, FIELD_INTEGER, "interruptability"), + + DEFINE_FIELD( m_bDidFireOnce, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), + + DEFINE_THINKFUNC( ScriptThink ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartSchedule", InputStartSchedule ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopSchedule", InputStopSchedule ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( aiscripted_schedule, CAI_ScriptedSchedule ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CAI_ScriptedSchedule::CAI_ScriptedSchedule( void ) : m_hActivator( NULL ) +{ +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_ScriptedSchedule::ScriptThink( void ) +{ + bool success = false; + CAI_BaseNPC *pTarget; + + if ( !m_bGrabAll ) + { + pTarget = FindScriptEntity( (m_spawnflags & SF_SCRIPT_SEARCH_CYCLICALLY) != 0 ); + if ( pTarget ) + { + ScriptMsg3( 2, "scripted_schedule \"%s\" using NPC \"%s\"(%s)\n", GetDebugName(), STRING( m_iszEntity ), pTarget->GetEntityName().ToCStr() ); + StartSchedule( pTarget ); + success = true; + } + } + else + { + m_hLastFoundEntity = NULL; + while ( ( pTarget = FindScriptEntity( true ) ) != NULL ) + { + ScriptMsg3( 2, "scripted_schedule \"%s\" using NPC \"%s\"(%s)\n", GetDebugName(), pTarget->GetEntityName().ToCStr(), STRING( m_iszEntity ) ); + StartSchedule( pTarget ); + success = true; + } + } + + if ( !success ) + { + ScriptMsg2( 2, "scripted_schedule \"%s\" can't find NPC \"%s\"\n", GetDebugName(), STRING( m_iszEntity ) ); + // FIXME: just trying again is bad. This should fire an output instead. + // FIXME: Think about puting output triggers on success true and sucess false + // FIXME: also needs to check the result of StartSchedule(), which can fail and not complain + SetNextThink( gpGlobals->curtime + 1.0f ); + } + else + { + m_bDidFireOnce = true; + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CAI_BaseNPC *CAI_ScriptedSchedule::FindScriptEntity( bool bCyclic ) +{ + CBaseEntity *pEntity = gEntList.FindEntityGenericWithin( m_hLastFoundEntity, STRING( m_iszEntity ), GetAbsOrigin(), m_flRadius, this, m_hActivator ); + + while ( pEntity != NULL ) + { + CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); + if ( pNPC && pNPC->IsAlive() && pNPC->IsInterruptable()) + { + if ( bCyclic ) + { + // next time this is called, start searching from the one found last time + m_hLastFoundEntity = pNPC; + } + + return pNPC; + } + + pEntity = gEntList.FindEntityGenericWithin( pEntity, STRING( m_iszEntity ), GetAbsOrigin(), m_flRadius, this, NULL ); + } + + m_hLastFoundEntity = NULL; + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Make the entity carry out the scripted instructions, but without +// destroying the NPC's state. +//----------------------------------------------------------------------------- +void CAI_ScriptedSchedule::StartSchedule( CAI_BaseNPC *pTarget ) +{ + if ( pTarget == NULL ) + return; + + CBaseEntity *pGoalEnt = gEntList.FindEntityGeneric( NULL, STRING( m_sGoalEnt ), this, NULL ); + + // NOTE: !!! all possible choices require a goal ent currently + if ( !pGoalEnt ) + { + CHintCriteria hintCriteria; + hintCriteria.SetGroup( m_sGoalEnt ); + hintCriteria.SetHintType( HINT_ANY ); + hintCriteria.AddIncludePosition( pTarget->GetAbsOrigin(), FLT_MAX ); + CAI_Hint *pHint = CAI_HintManager::FindHint( pTarget->GetAbsOrigin(), hintCriteria ); + if ( !pHint ) + { + ScriptMsg2( 1, "Can't find goal entity %s\nCan't execute script %s\n", STRING(m_sGoalEnt), GetDebugName() ); + return; + } + pGoalEnt = pHint; + } + + static NPC_STATE forcedStatesMap[] = + { + NPC_STATE_NONE, + NPC_STATE_IDLE, + NPC_STATE_ALERT, + NPC_STATE_COMBAT + }; + + if ( pTarget->GetSleepState() > AISS_AWAKE ) + pTarget->Wake(); + + pTarget->ForceDecisionThink(); + + Assert( m_nForceState >= 0 && m_nForceState < ARRAYSIZE(forcedStatesMap) ); + + NPC_STATE forcedState = forcedStatesMap[m_nForceState]; + + // trap if this isn't a legal thing to do + Assert( pTarget->IsInterruptable() ); + + if ( forcedState != NPC_STATE_NONE ) + pTarget->SetState( forcedState ); + + // + // Set enemy and make the NPC aware of the enemy's current position. + // + if ( m_nSchedule == SCHED_SCRIPT_ENEMY_IS_GOAL || m_nSchedule == SCHED_SCRIPT_ENEMY_IS_GOAL_AND_RUN_TO_GOAL ) + { + if ( pGoalEnt && pGoalEnt->MyCombatCharacterPointer() ) + { + pTarget->SetEnemy( pGoalEnt ); + pTarget->UpdateEnemyMemory( pGoalEnt, pGoalEnt->GetAbsOrigin() ); + pTarget->SetCondition( COND_SCHEDULE_DONE ); + } + else + ScriptMsg2( 1, "Scripted schedule %s specified an invalid enemy %s\n", STRING( GetEntityName() ), STRING( m_sGoalEnt ) ); + } + + bool bDidSetSchedule = false; + + switch ( m_nSchedule ) + { + // + // Walk or run to position. + // + case SCHED_SCRIPT_WALK_TO_GOAL: + case SCHED_SCRIPT_RUN_TO_GOAL: + case SCHED_SCRIPT_ENEMY_IS_GOAL_AND_RUN_TO_GOAL: + { + Activity movementActivity = ( m_nSchedule == SCHED_SCRIPT_WALK_TO_GOAL ) ? ACT_WALK : ACT_RUN; + bool bIsFlying = (pTarget->GetMoveType() == MOVETYPE_FLY) || (pTarget->GetMoveType() == MOVETYPE_FLYGRAVITY); + if ( bIsFlying ) + { + movementActivity = ACT_FLY; + } + + if (!pTarget->ScheduledMoveToGoalEntity( SCHED_IDLE_WALK, pGoalEnt, movementActivity )) + { + if (!(m_spawnflags & SF_SCRIPT_NO_COMPLAINTS)) + { + ScriptMsg2( 1, "ScheduledMoveToGoalEntity to goal entity %s failed\nCan't execute script %s\n", STRING(m_sGoalEnt), GetDebugName() ); + } + return; + } + bDidSetSchedule = true; + + break; + } + + case SCHED_SCRIPT_WALK_PATH_GOAL: + case SCHED_SCRIPT_RUN_PATH_GOAL: + { + Activity movementActivity = ( m_nSchedule == SCHED_SCRIPT_WALK_PATH_GOAL ) ? ACT_WALK : ACT_RUN; + bool bIsFlying = (pTarget->GetMoveType() == MOVETYPE_FLY) || (pTarget->GetMoveType() == MOVETYPE_FLYGRAVITY); + if ( bIsFlying ) + { + movementActivity = ACT_FLY; + } + if (!pTarget->ScheduledFollowPath( SCHED_IDLE_WALK, pGoalEnt, movementActivity )) + { + if (!(m_spawnflags & SF_SCRIPT_NO_COMPLAINTS)) + { + ScriptMsg2( 1, "ScheduledFollowPath to goal entity %s failed\nCan't execute script %s\n", STRING(m_sGoalEnt), GetDebugName() ); + } + return; + } + bDidSetSchedule = true; + break; + } + } + + if ( bDidSetSchedule ) + { + // Chain this to the target so that it can add the base and any custom interrupts to this + pTarget->SetScriptedScheduleIgnoreConditions( m_Interruptability ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler to activate the scripted schedule. Finds the NPC to +// act on and sets a think for the near future to do the real work. +//----------------------------------------------------------------------------- +void CAI_ScriptedSchedule::InputStartSchedule( inputdata_t &inputdata ) +{ + if (( m_nForceState == 0 ) && ( m_nSchedule == 0 )) + { + ScriptMsg( 2, "aiscripted_schedule - no schedule or state has been set!\n" ); + } + + if ( !m_bDidFireOnce || ( m_spawnflags & SF_SCRIPT_REPEATABLE ) ) + { + // DVS TODO: Is the NPC already playing the script? + m_hActivator = inputdata.pActivator; + SetThink( &CAI_ScriptedSchedule::ScriptThink ); + SetNextThink( gpGlobals->curtime ); + } + else + { + ScriptMsg( 2, "aiscripted_schedule - not playing schedule again: not flagged to repeat\n" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler to stop a previously activated scripted schedule. +//----------------------------------------------------------------------------- +void CAI_ScriptedSchedule::InputStopSchedule( inputdata_t &inputdata ) +{ + if ( !m_bDidFireOnce ) + { + ScriptMsg( 2, "aiscripted_schedule - StopSchedule called, but schedule's never started.\n" ); + return; + } + + CAI_BaseNPC *pTarget; + if ( !m_bGrabAll ) + { + pTarget = FindScriptEntity( (m_spawnflags & SF_SCRIPT_SEARCH_CYCLICALLY) != 0 ); + if ( pTarget ) + { + StopSchedule( pTarget ); + } + } + else + { + m_hLastFoundEntity = NULL; + while ( ( pTarget = FindScriptEntity( true ) ) != NULL ) + { + StopSchedule( pTarget ); + } + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Sets our target NPC with the generic SetTarget input. +//----------------------------------------------------------------------------- +void CAI_ScriptedSchedule::InputSetTarget( inputdata_t &inputdata ) +{ + m_hActivator = inputdata.pActivator; + m_iszEntity = AllocPooledString( inputdata.value.String() ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: If the target entity appears to be running this scripted schedule break it +//----------------------------------------------------------------------------- +void CAI_ScriptedSchedule::StopSchedule( CAI_BaseNPC *pTarget ) +{ + if ( pTarget->IsCurSchedule( SCHED_IDLE_WALK ) ) + { + ScriptMsg3( 2, "%s (%s): StopSchedule called on NPC %s.\n", GetClassname(), GetDebugName(), pTarget->GetDebugName() ); + pTarget->ClearSchedule( "Stopping scripted schedule" ); + } +} + +class CAI_ScriptedSentence : public CPointEntity +{ +public: + DECLARE_CLASS( CAI_ScriptedSentence, CPointEntity ); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void FindThink( void ); + void DelayThink( void ); + int ObjectCaps( void ) { return (BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); } + + // Input handlers + void InputBeginSentence( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + + CAI_BaseNPC *FindEntity( void ); + bool AcceptableSpeaker( CAI_BaseNPC *pNPC ); + int StartSentence( CAI_BaseNPC *pTarget ); + +private: + string_t m_iszSentence; // string index for sentence name + string_t m_iszEntity; // entity that is wanted for this sentence + float m_flRadius; // range to search + float m_flDelay; // How long the sentence lasts + float m_flRepeat; // repeat rate + soundlevel_t m_iSoundLevel; + int m_TempAttenuation; + float m_flVolume; + bool m_active; + string_t m_iszListener; // name of entity to look at while talking + CBaseEntity *m_pActivator; + + COutputEvent m_OnBeginSentence; + COutputEvent m_OnEndSentence; +}; + + +#define SF_SENTENCE_ONCE 0x0001 +#define SF_SENTENCE_FOLLOWERS 0x0002 // only say if following player +#define SF_SENTENCE_INTERRUPT 0x0004 // force talking except when dead +#define SF_SENTENCE_CONCURRENT 0x0008 // allow other people to keep talking +#define SF_SENTENCE_SPEAKTOACTIVATOR 0x0010 + +BEGIN_DATADESC( CAI_ScriptedSentence ) + + DEFINE_KEYFIELD( m_iszSentence, FIELD_STRING, "sentence" ), + DEFINE_KEYFIELD( m_iszEntity, FIELD_STRING, "entity" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + DEFINE_KEYFIELD( m_flDelay, FIELD_FLOAT, "delay" ), + DEFINE_KEYFIELD( m_flRepeat, FIELD_FLOAT, "refire" ), + DEFINE_KEYFIELD( m_iszListener, FIELD_STRING, "listener" ), + + DEFINE_KEYFIELD( m_TempAttenuation, FIELD_INTEGER, "attenuation" ), + + DEFINE_FIELD( m_iSoundLevel, FIELD_INTEGER ), + DEFINE_FIELD( m_flVolume, FIELD_FLOAT ), + DEFINE_FIELD( m_active, FIELD_BOOLEAN ), + DEFINE_FIELD( m_pActivator, FIELD_EHANDLE ), + + // Function Pointers + DEFINE_FUNCTION( FindThink ), + DEFINE_FUNCTION( DelayThink ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "BeginSentence", InputBeginSentence), + + // Outputs + DEFINE_OUTPUT(m_OnBeginSentence, "OnBeginSentence"), + DEFINE_OUTPUT(m_OnEndSentence, "OnEndSentence"), + +END_DATADESC() + + + +LINK_ENTITY_TO_CLASS( scripted_sentence, CAI_ScriptedSentence ); + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : szKeyName - +// szValue - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSentence::KeyValue( const char *szKeyName, const char *szValue ) +{ + if(FStrEq(szKeyName, "volume")) + { + m_flVolume = atof( szValue ) * 0.1; + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for starting the scripted sentence. +//----------------------------------------------------------------------------- +void CAI_ScriptedSentence::InputBeginSentence( inputdata_t &inputdata ) +{ + if ( !m_active ) + return; + + m_pActivator = inputdata.pActivator; + + //Msg( "Firing sentence: %s\n", STRING( m_iszSentence )); + SetThink( &CAI_ScriptedSentence::FindThink ); + SetNextThink( gpGlobals->curtime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSentence::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + + m_active = true; + // if no targetname, start now + if ( !GetEntityName() ) + { + SetThink( &CAI_ScriptedSentence::FindThink ); + SetNextThink( gpGlobals->curtime + 1.0f ); + } + + switch( m_TempAttenuation ) + { + case 1: // Medium radius + m_iSoundLevel = SNDLVL_80dB; + break; + + case 2: // Large radius + m_iSoundLevel = SNDLVL_85dB; + break; + + case 3: //EVERYWHERE + m_iSoundLevel = SNDLVL_NONE; + break; + + default: + case 0: // Small radius + m_iSoundLevel = SNDLVL_70dB; + break; + } + m_TempAttenuation = 0; + + // No volume, use normal + if ( m_flVolume <= 0 ) + m_flVolume = 1.0; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSentence::FindThink( void ) +{ + CAI_BaseNPC *pNPC = FindEntity(); + if ( pNPC ) + { + int index = StartSentence( pNPC ); + float length = engine->SentenceLength(index); + + m_OnEndSentence.FireOutput(NULL, this, length + m_flRepeat); + + if ( m_spawnflags & SF_SENTENCE_ONCE ) + UTIL_Remove( this ); + + float delay = m_flDelay + length + 0.1; + if ( delay < 0 ) + delay = 0; + + SetThink( &CAI_ScriptedSentence::DelayThink ); + // calculate delay dynamically because this could play a sentence group + // rather than a single sentence. + // add 0.1 because the sound engine mixes ahead -- the sentence will actually start ~0.1 secs from now + SetNextThink( gpGlobals->curtime + delay + m_flRepeat ); + m_active = false; + //Msg( "%s: found NPC %s\n", STRING(m_iszSentence), STRING(m_iszEntity) ); + } + else + { + //Msg( "%s: can't find NPC %s\n", STRING(m_iszSentence), STRING(m_iszEntity) ); + SetNextThink( gpGlobals->curtime + m_flRepeat + 0.5 ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_ScriptedSentence::DelayThink( void ) +{ + m_active = true; + if ( !GetEntityName() ) + SetNextThink( gpGlobals->curtime + 0.1f ); + SetThink( &CAI_ScriptedSentence::FindThink ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pNPC - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_ScriptedSentence::AcceptableSpeaker( CAI_BaseNPC *pNPC ) +{ + if ( pNPC ) + { + if ( m_spawnflags & SF_SENTENCE_FOLLOWERS ) + { + if ( pNPC->GetTarget() == NULL || !pNPC->GetTarget()->IsPlayer() ) + return false; + } + bool override; + if ( m_spawnflags & SF_SENTENCE_INTERRUPT ) + override = true; + else + override = false; + if ( pNPC->CanPlaySentence( override ) ) + return true; + } + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : +//----------------------------------------------------------------------------- +CAI_BaseNPC *CAI_ScriptedSentence::FindEntity( void ) +{ + CBaseEntity *pentTarget; + CAI_BaseNPC *pNPC; + + pentTarget = gEntList.FindEntityByName( NULL, m_iszEntity ); + pNPC = NULL; + + while (pentTarget) + { + pNPC = pentTarget->MyNPCPointer(); + if ( pNPC != NULL ) + { + if ( AcceptableSpeaker( pNPC ) ) + return pNPC; + //Msg( "%s (%s), not acceptable\n", pNPC->GetClassname(), pNPC->GetDebugName() ); + } + pentTarget = gEntList.FindEntityByName( pentTarget, m_iszEntity ); + } + + CBaseEntity *pEntity = NULL; + for ( CEntitySphereQuery sphere( GetAbsOrigin(), m_flRadius, FL_NPC ); ( pEntity = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) + { + if (FClassnameIs( pEntity, STRING(m_iszEntity))) + { + pNPC = pEntity->MyNPCPointer( ); + if ( AcceptableSpeaker( pNPC ) ) + return pNPC; + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pTarget - +// Output : +//----------------------------------------------------------------------------- +int CAI_ScriptedSentence::StartSentence( CAI_BaseNPC *pTarget ) +{ + if ( !pTarget ) + { + ScriptMsg1( 2, "Not Playing sentence %s\n", STRING(m_iszSentence) ); + return -1; + } + + bool bConcurrent = false; + if ( !(m_spawnflags & SF_SENTENCE_CONCURRENT) ) + bConcurrent = true; + + CBaseEntity *pListener = NULL; + + if ( m_spawnflags & SF_SENTENCE_SPEAKTOACTIVATOR ) + { + pListener = m_pActivator; + } + else if (m_iszListener != NULL_STRING) + { + float radius = m_flRadius; + + if ( FStrEq( STRING(m_iszListener ), "!player" ) ) + radius = MAX_TRACE_LENGTH; // Always find the player + + pListener = gEntList.FindEntityGenericNearest( STRING( m_iszListener ), pTarget->GetAbsOrigin(), radius, this, NULL ); + } + + int sentenceIndex = pTarget->PlayScriptedSentence( STRING(m_iszSentence), m_flDelay, m_flVolume, m_iSoundLevel, bConcurrent, pListener ); + ScriptMsg1( 2, "Playing sentence %s\n", STRING(m_iszSentence) ); + + m_OnBeginSentence.FireOutput(NULL, this); + + return sentenceIndex; +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// This isn't exclusive to NPCs, so it could be moved if needed. +//----------------------------------------------------------------------------- +class CScriptedSound : public CPointEntity +{ +public: + DECLARE_CLASS( CScriptedSound, CPointEntity ); + DECLARE_DATADESC(); + + void Precache(); + + CBaseEntity *GetTarget(inputdata_t &inputdata); + + // Input handlers + void InputPlaySound( inputdata_t &inputdata ); + void InputPlaySoundOnEntity( inputdata_t &inputdata ); + void InputStopSound( inputdata_t &inputdata ); + void InputSetSound( inputdata_t &inputdata ); + +private: + string_t m_message; + + bool m_bGrabAll; +}; + + +BEGIN_DATADESC( CScriptedSound ) + + DEFINE_KEYFIELD( m_message, FIELD_STRING, "message" ), + DEFINE_KEYFIELD( m_bGrabAll, FIELD_BOOLEAN, "GrabAll" ), + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "PlaySound", InputPlaySound), + DEFINE_INPUTFUNC(FIELD_EHANDLE, "PlaySoundOnEntity", InputPlaySoundOnEntity), + DEFINE_INPUTFUNC(FIELD_VOID, "StopSound", InputStopSound), + DEFINE_INPUTFUNC(FIELD_STRING, "SetSound", InputSetSound), + +END_DATADESC() + + + +LINK_ENTITY_TO_CLASS( scripted_sound, CScriptedSound ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CScriptedSound::Precache() +{ + //PrecacheScriptSound(STRING(m_message)); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +CBaseEntity *CScriptedSound::GetTarget(inputdata_t &inputdata) +{ + CBaseEntity *pEntity = NULL; + if (m_target == NULL_STRING) + { + // Use this as the default source entity + pEntity = this; + m_bGrabAll = false; + } + else + { + pEntity = gEntList.FindEntityGeneric(NULL, STRING(m_target), this, inputdata.pActivator, inputdata.pCaller); + } + + return pEntity; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CScriptedSound::InputPlaySound( inputdata_t &inputdata ) +{ + PrecacheScriptSound(STRING(m_message)); + + CBaseEntity *pEntity = GetTarget(inputdata); + const char *sound = STRING(m_message); + if (m_bGrabAll) + { + //if (pEntity) + //{ + // pEntity->PrecacheScriptSound(sound); + //} + + while (pEntity) + { + pEntity->EmitSound(sound); + pEntity = gEntList.FindEntityGeneric(pEntity, STRING(m_target), this, inputdata.pActivator, inputdata.pCaller); + } + } + else if (pEntity) + { + //pEntity->PrecacheScriptSound(sound); + pEntity->EmitSound(sound); + } + else + { + Warning("%s unable to find target entity %s!\n", GetDebugName(), STRING(m_target)); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CScriptedSound::InputPlaySoundOnEntity( inputdata_t &inputdata ) +{ + if (inputdata.value.Entity()) + { + inputdata.value.Entity()->PrecacheScriptSound(STRING(m_message)); + inputdata.value.Entity()->EmitSound(STRING(m_message)); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CScriptedSound::InputStopSound( inputdata_t &inputdata ) +{ + CBaseEntity *pEntity = GetTarget(inputdata); + const char *sound = STRING(m_message); + if (m_bGrabAll) + { + while (pEntity) + { + pEntity->StopSound(sound); + pEntity = gEntList.FindEntityGeneric(pEntity, STRING(m_target), this, inputdata.pActivator, inputdata.pCaller); + } + } + else if (pEntity) + { + pEntity->StopSound(sound); + } + else + { + Warning("%s unable to find target entity %s!\n", GetDebugName(), STRING(m_target)); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void CScriptedSound::InputSetSound( inputdata_t &inputdata ) +{ + PrecacheScriptSound(inputdata.value.String()); + m_message = inputdata.value.StringID(); +} +#endif + + + +// HACKHACK: This is a little expensive with the dynamic_cast<> and all, but it lets us solve +// the problem of matching scripts back to entities without new state. +const char *CAI_ScriptedSequence::GetSpawnPreIdleSequenceForScript( CBaseEntity *pEntity ) +{ + CAI_ScriptedSequence *pScript = gEntList.NextEntByClass( (CAI_ScriptedSequence *)NULL ); + while ( pScript ) + { + if ( pScript->HasSpawnFlags( SF_SCRIPT_START_ON_SPAWN ) && pScript->m_iszEntity == pEntity->GetEntityName() ) + { + if ( pScript->m_iszPreIdle != NULL_STRING ) + { + return STRING(pScript->m_iszPreIdle); + } + return NULL; + } + pScript = gEntList.NextEntByClass( pScript ); + } + return NULL; +} + diff --git a/sp/src/game/server/scripted.h b/sp/src/game/server/scripted.h new file mode 100644 index 00000000..1b7398a4 --- /dev/null +++ b/sp/src/game/server/scripted.h @@ -0,0 +1,246 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef SCRIPTED_H +#define SCRIPTED_H +#ifdef _WIN32 +#pragma once +#endif + +#ifndef SCRIPTEVENT_H +#include "scriptevent.h" +#endif + +#include "ai_basenpc.h" + + +// +// The number of unique outputs that a script can fire from animation events. +// These are fired via SCRIPT_EVENT_FIREEVENT in CAI_BaseNPC::HandleAnimEvent. +// +#define MAX_SCRIPT_EVENTS 8 + + +#define SF_SCRIPT_WAITTILLSEEN 1 +#define SF_SCRIPT_EXITAGITATED 2 +#define SF_SCRIPT_REPEATABLE 4 // Whether the script can be played more than once. +#define SF_SCRIPT_LEAVECORPSE 8 +#define SF_SCRIPT_START_ON_SPAWN 16 +#define SF_SCRIPT_NOINTERRUPT 32 +#define SF_SCRIPT_OVERRIDESTATE 64 +#define SF_SCRIPT_DONT_TELEPORT_AT_END 128 // Don't fixup end position with a teleport when the SS is finished +#define SF_SCRIPT_LOOP_IN_POST_IDLE 256 // Loop in the post idle animation after playing the action animation. +#define SF_SCRIPT_HIGH_PRIORITY 512 // If set, we don't allow other scripts to steal our spot in the queue. +#define SF_SCRIPT_SEARCH_CYCLICALLY 1024 // Start search from last entity found. +#define SF_SCRIPT_NO_COMPLAINTS 2048 // doesn't bitch if it can't find anything +#define SF_SCRIPT_ALLOW_DEATH 4096 // the actor using this scripted sequence may die without interrupting the scene (used for scripted deaths) + + +enum script_moveto_t +{ + CINE_MOVETO_WAIT = 0, + CINE_MOVETO_WALK = 1, + CINE_MOVETO_RUN = 2, + CINE_MOVETO_CUSTOM = 3, + CINE_MOVETO_TELEPORT = 4, + CINE_MOVETO_WAIT_FACING = 5, +}; + +enum SCRIPT_PLAYER_DEATH +{ + SCRIPT_DO_NOTHING = 0, + SCRIPT_CANCEL = 1, +}; + + +// +// Interrupt levels for grabbing NPCs to act out scripted events. These indicate +// how important it is to get a specific NPC, and can affect how they respond. +// +enum SS_INTERRUPT +{ + SS_INTERRUPT_BY_CLASS = 0, // Indicates that we are asking for this NPC by class + SS_INTERRUPT_BY_NAME, // Indicates that we are asking for this NPC by name +}; + + +// when a NPC finishes an AI scripted sequence, we can choose +// a schedule to place them in. These defines are the aliases to +// resolve worldcraft input to real schedules (sjb) +#define SCRIPT_FINISHSCHED_DEFAULT 0 +#define SCRIPT_FINISHSCHED_AMBUSH 1 + +class CAI_ScriptedSequence : public CBaseEntity +{ + DECLARE_CLASS( CAI_ScriptedSequence, CBaseEntity ); +public: + void Spawn( void ); + virtual void Blocked( CBaseEntity *pOther ); + virtual void Touch( CBaseEntity *pOther ); + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); } + virtual void Activate( void ); + virtual void UpdateOnRemove( void ); + void StartThink(); + void ScriptThink( void ); + void StopThink(); + + DECLARE_DATADESC(); + + void Pain( void ); + void Die( void ); + void DelayStart( bool bDelay ); + bool FindEntity( void ); + void StartScript( void ); + void FireScriptEvent( int nEvent ); +#ifdef MAPBASE + void OnBeginSequence( CBaseEntity *pActor ); + void OnPreIdleSequence( CBaseEntity *pActor ); +#else + void OnBeginSequence( void ); +#endif + + void SetTarget( CBaseEntity *pTarget ) { m_hTargetEnt = pTarget; }; + CBaseEntity *GetTarget( void ) { return m_hTargetEnt; }; + + // Input handlers + void InputBeginSequence( inputdata_t &inputdata ); + void InputCancelSequence( inputdata_t &inputdata ); + void InputMoveToPosition( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetTarget( inputdata_t &inputdata ); +#endif + + bool IsTimeToStart( void ); + bool IsWaitingForBegin( void ); + void ReleaseEntity( CAI_BaseNPC *pEntity ); + void CancelScript( void ); + bool StartSequence( CAI_BaseNPC *pTarget, string_t iszSeq, bool completeOnEmpty ); + void SynchronizeSequence( CAI_BaseNPC *pNPC ); + bool FCanOverrideState ( void ); + void SequenceDone( CAI_BaseNPC *pNPC ); + void PostIdleDone( CAI_BaseNPC *pNPC ); + void FixScriptNPCSchedule( CAI_BaseNPC *pNPC, int iSavedCineFlags ); + void FixFlyFlag( CAI_BaseNPC *pNPC, int iSavedCineFlags ); + bool CanInterrupt( void ); + void AllowInterrupt( bool fAllow ); + void RemoveIgnoredConditions( void ); + bool PlayedSequence( void ) { return m_sequenceStarted; } + bool CanEnqueueAfter( void ); + + // Entry & Action loops + bool IsPlayingEntry( void ) { return m_bIsPlayingEntry; } + bool IsPlayingAction( void ) { return ( m_sequenceStarted && !m_bIsPlayingEntry ); } + bool FinishedActionSequence( CAI_BaseNPC *pNPC ); + void SetLoopActionSequence( bool bLoop ) { m_bLoopActionSequence = bLoop; } + bool ShouldLoopActionSequence( void ) { return m_bLoopActionSequence; } + void StopActionLoop( bool bStopSynchronizedScenes ); + void SetSynchPostIdles( bool bSynch ) { m_bSynchPostIdles = bSynch; } + void SynchNewSequence( CAI_BaseNPC::SCRIPTSTATE newState, string_t iszSequence, bool bSynchOtherScenes ); + + // Dynamic scripted sequence spawning + void ForceSetTargetEntity( CAI_BaseNPC *pTarget, bool bDontCancelOtherSequences ); + + // Dynamic interactions + void SetupInteractionPosition( CBaseEntity *pRelativeEntity, VMatrix &matDesiredLocalToWorld ); + void ModifyScriptedAutoMovement( Vector *vecNewPos ); + + bool IsTeleportingDueToMoveTo( void ) { return m_bIsTeleportingDueToMoveTo; } + + // Debug + virtual int DrawDebugTextOverlays( void ); + virtual void DrawDebugGeometryOverlays( void ); + + void InputScriptPlayerDeath( inputdata_t &inputdata ); + +private: + friend class CAI_BaseNPC; // should probably try to eliminate this relationship + + string_t m_iszEntry; // String index for animation that must be played before entering the main action anim + string_t m_iszPreIdle; // String index for idle animation to play before playing the action anim (only played while waiting for the script to begin) + string_t m_iszPlay; // String index for scripted action animation + string_t m_iszPostIdle; // String index for idle animation to play before playing the action anim + string_t m_iszCustomMove; // String index for custom movement animation + string_t m_iszNextScript; // Name of the script to run immediately after this one. + string_t m_iszEntity; // Entity that is wanted for this script + + int m_fMoveTo; + bool m_bIsPlayingEntry; + bool m_bLoopActionSequence; + bool m_bSynchPostIdles; + bool m_bIgnoreGravity; + bool m_bDisableNPCCollisions; // Used when characters must interpenetrate while riding on elevators, trains, etc. + + float m_flRadius; // Range to search for an NPC to possess. + float m_flRepeat; // Repeat rate + + int m_iDelay; // A counter indicating how many scripts are NOT ready to start. + + bool m_bDelayed; // This moderately hacky hack ensures that we don't calls to DelayStart(true) or DelayStart(false) + // twice in succession. This is necessary because we didn't want to remove the call to DelayStart(true) + // from StartScript, even though DelayStart(true) is called from TASK_PRE_SCRIPT. + // All of this is necessary in case the NPCs schedule gets cleared during the script and then they + // reselect the schedule to play the script. Without this you can get NPCs stuck with m_iDelay = -1 + + float m_startTime; // Time when script actually started, used for synchronization + bool m_bWaitForBeginSequence; // Set to true when we are told to MoveToPosition. Holds the actor in the pre-action idle until BeginSequence is called. + + int m_saved_effects; + int m_savedFlags; + int m_savedCollisionGroup; + + bool m_interruptable; + bool m_sequenceStarted; + + EHANDLE m_hTargetEnt; + + EHANDLE m_hNextCine; // The script to hand the NPC off to when we finish with them. + + bool m_bThinking; + bool m_bInitiatedSelfDelete; + + bool m_bIsTeleportingDueToMoveTo; + + CAI_BaseNPC *FindScriptEntity( void ); + EHANDLE m_hLastFoundEntity; + + // Code forced us to use a specific NPC + EHANDLE m_hForcedTarget; + bool m_bDontCancelOtherSequences; + bool m_bForceSynch; + + bool m_bTargetWasAsleep; + + COutputEvent m_OnBeginSequence; + COutputEvent m_OnEndSequence; + COutputEvent m_OnPostIdleEndSequence; + COutputEvent m_OnCancelSequence; + COutputEvent m_OnCancelFailedSequence; // Fired when a scene is cancelled before it's ever run + COutputEvent m_OnScriptEvent[MAX_SCRIPT_EVENTS]; +#ifdef MAPBASE + COutputEvent m_OnPreIdleSequence; + COutputEvent m_OnFoundNPC; +#endif + + static void ScriptEntityCancel( CBaseEntity *pentCine, bool bPretendSuccess = false ); + + static const char *GetSpawnPreIdleSequenceForScript( CBaseEntity *pTargetEntity ); + + // Dynamic interactions + // For now, store just a single one of these. To synchronize positions + // with multiple other NPCs, this needs to be an array of NPCs & desired position matrices. + VMatrix m_matInteractionPosition; + EHANDLE m_hInteractionRelativeEntity; + + int m_iPlayerDeathBehavior; + +#ifdef MAPBASE + // !activator functionality + EHANDLE m_hActivator; +#endif +}; + + +#endif // SCRIPTED_H diff --git a/sp/src/game/server/scriptedtarget.cpp b/sp/src/game/server/scriptedtarget.cpp new file mode 100644 index 00000000..b74217da --- /dev/null +++ b/sp/src/game/server/scriptedtarget.cpp @@ -0,0 +1,362 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_default.h" +#include "scriptedtarget.h" +#include "entitylist.h" +#include "ndebugoverlay.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//========================================================= +// Interactions +//========================================================= +int g_interactionScriptedTarget = 0; + +LINK_ENTITY_TO_CLASS( scripted_target, CScriptedTarget ); + +BEGIN_DATADESC( CScriptedTarget ) + + DEFINE_FIELD( m_vLastPosition, FIELD_POSITION_VECTOR ), + + DEFINE_KEYFIELD( m_iDisabled, FIELD_INTEGER, "StartDisabled" ), + DEFINE_KEYFIELD( m_iszEntity, FIELD_STRING, "m_iszEntity" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "m_flRadius" ), + + DEFINE_KEYFIELD( m_nMoveSpeed, FIELD_INTEGER, "MoveSpeed" ), + DEFINE_KEYFIELD( m_flPauseDuration, FIELD_FLOAT, "PauseDuration" ), + DEFINE_FIELD( m_flPauseDoneTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_flEffectDuration, FIELD_FLOAT, "EffectDuration" ), + + // Function Pointers + DEFINE_THINKFUNC( ScriptThink ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + // Outputs + DEFINE_OUTPUT(m_AtTarget, "AtTarget" ), + DEFINE_OUTPUT(m_LeaveTarget, "LeaveTarget" ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CScriptedTarget::InputEnable( inputdata_t &inputdata ) +{ + TurnOn(); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CScriptedTarget::InputDisable( inputdata_t &inputdata ) +{ + TurnOff(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CScriptedTarget::TurnOn( void ) +{ + m_vLastPosition = GetAbsOrigin(); + SetThink( &CScriptedTarget::ScriptThink ); + m_iDisabled = false; + SetNextThink( gpGlobals->curtime ); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CScriptedTarget::TurnOff( void ) +{ + SetThink( NULL ); + m_iDisabled = true; + + // If I have a target entity, free him + if (GetTarget()) + { + CAI_BaseNPC* pNPC = GetTarget()->MyNPCPointer(); + pNPC->DispatchInteraction( g_interactionScriptedTarget, NULL, NULL ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CScriptedTarget::Spawn( void ) +{ + if (g_interactionScriptedTarget == 0) + { + g_interactionScriptedTarget = CBaseCombatCharacter::GetInteractionID(); + } + + SetSolid( SOLID_NONE ); + + m_vLastPosition = GetAbsOrigin(); + + if (!m_iDisabled ) + { + TurnOn(); + } +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +CScriptedTarget* CScriptedTarget::NextScriptedTarget(void) +{ + // ---------------------------------------------------------------------- + // If I just hit my target, set how long I'm supposed to pause here + // ---------------------------------------------------------------------- + if (m_flPauseDoneTime == 0) + { + m_flPauseDoneTime = gpGlobals->curtime + m_flPauseDuration; + m_AtTarget.FireOutput( GetTarget(), this ); + } + + // ------------------------------------------------------------- + // If I'm done pausing move on to next burn target + // ------------------------------------------------------------- + if (gpGlobals->curtime >= m_flPauseDoneTime) + { + m_flPauseDoneTime = 0; + + // ---------------------------------------------------------- + // Fire output that current Scripted target has been reached + // ---------------------------------------------------------- + m_LeaveTarget.FireOutput( GetTarget(), this ); + + // ------------------------------------------------------------ + // Get next target. + // ------------------------------------------------------------ + CScriptedTarget* pNextTarget = ((CScriptedTarget*)GetNextTarget()); + + // -------------------------------------------- + // Fire output if last one has been reached + // -------------------------------------------- + if (!pNextTarget) + { + TurnOff(); + SetTarget( NULL ); + } + // ------------------------------------------------ + // Otherwise, turn myself off, the next target on + // and pass on my target entity + // ------------------------------------------------ + else + { + // ---------------------------------------------------- + // Make sure there is a LOS between these two targets + // ---------------------------------------------------- + trace_t tr; + UTIL_TraceLine(GetAbsOrigin(), pNextTarget->GetAbsOrigin(), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); + if (tr.fraction != 1.0) + { + Warning( "WARNING: Scripted Target from (%s) to (%s) is occluded!\n",GetDebugName(),pNextTarget->GetDebugName() ); + } + + pNextTarget->TurnOn(); + pNextTarget->SetTarget( GetTarget() ); + + SetTarget( NULL ); + TurnOff(); + } + // -------------------------------------------- + // Return new target + // -------------------------------------------- + return pNextTarget; + } + // ------------------------------------------------------------- + // Otherwise keep the same scripted target until pause is done + // ------------------------------------------------------------- + else + { + return this; + } +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +CBaseEntity* CScriptedTarget::FindEntity( void ) +{ + // --------------------------------------------------- + // First try to find the entity by name + // --------------------------------------------------- + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_iszEntity ); + if (pEntity && pEntity->GetFlags() & FL_NPC) + { + CAI_BaseNPC* pNPC = pEntity->MyNPCPointer(); + if (pNPC->DispatchInteraction( g_interactionScriptedTarget, NULL, this )) + { + return pEntity; + } + } + + // --------------------------------------------------- + // If that fails, assume we were given a classname + // and find nearest entity in radius of that class + // --------------------------------------------------- + float flNearestDist = MAX_COORD_RANGE; + CBaseEntity* pNearestEnt = NULL; + CBaseEntity* pTestEnt = NULL; + + for ( CEntitySphereQuery sphere( GetAbsOrigin(), m_flRadius ); ( pTestEnt = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) + { + if (pTestEnt->GetFlags() & FL_NPC) + { + if (FClassnameIs( pTestEnt, STRING(m_iszEntity))) + { + float flTestDist = (pTestEnt->GetAbsOrigin() - GetAbsOrigin()).Length(); + if (flTestDist < flNearestDist) + { + flNearestDist = flTestDist; + pNearestEnt = pTestEnt; + } + } + } + } + + // UNDONE: If nearest fails, try next nearest + if (pNearestEnt) + { + CAI_BaseNPC* pNPC = pNearestEnt->MyNPCPointer(); + if (pNPC->DispatchInteraction( g_interactionScriptedTarget, NULL, this )) + { + return pNearestEnt; + } + } + + return NULL; +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CScriptedTarget::ScriptThink( void ) +{ + // -------------------------------------------- + // If I don't have target entity look for one + // -------------------------------------------- + if (GetTarget() == NULL) + { + m_flPauseDoneTime = 0; + SetTarget( FindEntity() ); + } + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CScriptedTarget::DrawDebugTextOverlays(void) +{ + // Skip AIClass debug overlays + int text_offset = CBaseEntity::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + // -------------- + // Print State + // -------------- + char tempstr[512]; + if (m_iDisabled) + { + Q_strncpy(tempstr,"State: Off",sizeof(tempstr)); + } + else + { + Q_strncpy(tempstr,"State: On",sizeof(tempstr)); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + // ----------------- + // Print Next Entity + // ----------------- + CBaseEntity *pTarget = GetNextTarget(); + if (pTarget) + { + Q_snprintf(tempstr,sizeof(tempstr),"Next: %s",pTarget->GetDebugName() ); + } + else + { + Q_strncpy(tempstr,"Next: -NONE-",sizeof(tempstr)); + } + EntityText(text_offset,tempstr,0); + text_offset++; + + // -------------- + // Print Target + // -------------- + if (GetTarget()!=NULL) + { + Q_snprintf(tempstr,sizeof(tempstr),"User: %s",GetTarget()->GetDebugName() ); + } + else if (m_iDisabled) + { + Q_strncpy(tempstr,"User: -NONE-",sizeof(tempstr)); + } + else + { + Q_strncpy(tempstr,"User: -LOOKING-",sizeof(tempstr)); + } + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Override base class to add display of paths +//----------------------------------------------------------------------------- +void CScriptedTarget::DrawDebugGeometryOverlays(void) +{ + // ---------------------------------------------- + // Draw line to next target is bbox is selected + // ---------------------------------------------- + if (m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_ABSBOX_BIT)) + { + if (m_iDisabled) + { + NDebugOverlay::Box(GetAbsOrigin(), Vector(-5,-5,-5), Vector(5,5,5), 200,100,100, 0 ,0); + } + else + { + NDebugOverlay::Cross3D(m_vLastPosition, Vector(-8,-8,-8),Vector(8,8,8),255,0,0,true,0.1); + NDebugOverlay::Box(GetAbsOrigin(), Vector(-5,-5,-5), Vector(5,5,5), 255,0,0, 0 ,0); + NDebugOverlay::Line(GetAbsOrigin(),m_vLastPosition,255,0,0,true,0.0); + } + + CBaseEntity *pTarget = GetNextTarget(); + if (pTarget) + { + NDebugOverlay::Line(GetAbsOrigin(),pTarget->GetAbsOrigin(),200,100,100,true,0.0); + } + if (GetTarget() != NULL) + { + NDebugOverlay::Line(GetAbsOrigin(),GetTarget()->EyePosition(),0,255,0,true,0.0); + } + + } + CBaseEntity::DrawDebugGeometryOverlays(); +} + diff --git a/sp/src/game/server/scriptedtarget.h b/sp/src/game/server/scriptedtarget.h new file mode 100644 index 00000000..4f1c1e0d --- /dev/null +++ b/sp/src/game/server/scriptedtarget.h @@ -0,0 +1,64 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SCRIPTEDTARGET_H +#define SCRIPTEDTARGET_H +#ifdef _WIN32 +#pragma once +#endif + +#ifndef SCRIPTEVENT_H +#include "scriptevent.h" +#endif + +#include "ai_basenpc.h" + +class CScriptedTarget : public CAI_BaseNPC +{ + DECLARE_CLASS( CScriptedTarget, CAI_BaseNPC ); +public: + DECLARE_DATADESC(); + + void Spawn( void ); + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); } + + + void ScriptThink( void ); + CBaseEntity* FindEntity( void ); + + void TurnOn(void); + void TurnOff(void); + + // Input handlers + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + CScriptedTarget* NextScriptedTarget(void); + float MoveSpeed(void) { return m_nMoveSpeed; }; + float EffectDuration(void) { return m_flEffectDuration; }; + + int DrawDebugTextOverlays(void); + void DrawDebugGeometryOverlays(void); + float PercentComplete(void); + + Vector m_vLastPosition; // Last position that's been reached + +private: + int m_iDisabled; // Initial state + string_t m_iszEntity; // entity that is wanted for this script + float m_flRadius; // range to search + + int m_nMoveSpeed; // How fast do I burn from target to target + float m_flPauseDuration; // How long to pause at this target + float m_flPauseDoneTime; // When is pause over + float m_flEffectDuration; // How long should any associated effect last? + + COutputEvent m_AtTarget; // Fired when scripted target has been reached + COutputEvent m_LeaveTarget; // Fired when scripted target is left +}; + +#endif // SCRIPTEDTARGET_H diff --git a/sp/src/game/server/sendproxy.cpp b/sp/src/game/server/sendproxy.cpp new file mode 100644 index 00000000..ad017c64 --- /dev/null +++ b/sp/src/game/server/sendproxy.cpp @@ -0,0 +1,197 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: implements various common send proxies +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "sendproxy.h" +#include "basetypes.h" +#include "baseentity.h" +#include "team.h" +#include "player.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void SendProxy_Color32ToInt( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + color32 *pIn = (color32*)pData; + *((unsigned int*)&pOut->m_Int) = ((unsigned int)pIn->r << 24) | ((unsigned int)pIn->g << 16) | ((unsigned int)pIn->b << 8) | ((unsigned int)pIn->a); +} + +void SendProxy_EHandleToInt( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID) +{ + CBaseHandle *pHandle = (CBaseHandle*)pVarData; + + if ( pHandle && pHandle->Get() ) + { + int iSerialNum = pHandle->GetSerialNumber() & (1 << NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS) - 1; + pOut->m_Int = pHandle->GetEntryIndex() | (iSerialNum << MAX_EDICT_BITS); + } + else + { + pOut->m_Int = INVALID_NETWORKED_EHANDLE_VALUE; + } +} + +void SendProxy_IntAddOne( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID) +{ + int *pInt = (int *)pVarData; + + pOut->m_Int = (*pInt) + 1; +} + +void SendProxy_ShortAddOne( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID) +{ + short *pInt = (short *)pVarData; + + pOut->m_Int = (*pInt) + 1; +} + +SendProp SendPropBool( + const char *pVarName, + int offset, + int sizeofVar ) +{ + Assert( sizeofVar == sizeof( bool ) ); + return SendPropInt( pVarName, offset, sizeofVar, 1, SPROP_UNSIGNED ); +} + + +SendProp SendPropEHandle( + const char *pVarName, + int offset, + int sizeofVar, + int flags, + SendVarProxyFn proxyFn ) +{ + return SendPropInt( pVarName, offset, sizeofVar, NUM_NETWORKED_EHANDLE_BITS, SPROP_UNSIGNED|flags, proxyFn ); +} + +SendProp SendPropIntWithMinusOneFlag( const char *pVarName, int offset, int sizeofVar, int nBits, SendVarProxyFn proxyFn ) +{ + return SendPropInt( pVarName, offset, sizeofVar, nBits, SPROP_UNSIGNED, proxyFn ); +} + +//----------------------------------------------------------------------------- +// Purpose: Proxy that only sends data to team members +// Input : *pStruct - +// *pData - +// *pOut - +// objectID - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +void* SendProxy_OnlyToTeam( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ) +{ + CBaseEntity *pEntity = (CBaseEntity*)pStruct; + if ( pEntity ) + { + CTeam *pTeam = pEntity->GetTeam(); + if ( pTeam ) + { + pRecipients->ClearAllRecipients(); + for ( int i=0; i < pTeam->GetNumPlayers(); i++ ) + pRecipients->SetRecipient( pTeam->GetPlayer( i )->GetClientIndex() ); + + return (void*)pVarData; + } + } + + return NULL; +} +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_OnlyToTeam ); + +#define TIME_BITS 24 + +// This table encodes edict data. +static void SendProxy_Time( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ) +{ + float clock_base = floor( gpGlobals->curtime ); + float t = *( float * )pVarData; + float dt = t - clock_base; + int addt = Floor2Int( 1000.0f * dt + 0.5f ); + // TIME_BITS bits gives us TIME_BITS-1 bits plus sign bit + int maxoffset = 1 << ( TIME_BITS - 1); + + addt = clamp( addt, -maxoffset, maxoffset ); + + pOut->m_Int = addt; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pVarName - +// sizeofVar - +// flags - +// pId - +// Output : SendProp +//----------------------------------------------------------------------------- +SendProp SendPropTime( + const char *pVarName, + int offset, + int sizeofVar ) +{ +// return SendPropInt( pVarName, offset, sizeofVar, TIME_BITS, 0, SendProxy_Time ); + // FIXME: Re-enable above when it doesn't cause lots of deltas + return SendPropFloat( pVarName, offset, sizeofVar, -1, SPROP_NOSCALE ); +} + +#if !defined( NO_ENTITY_PREDICTION ) + +#define PREDICTABLE_ID_BITS 31 + +//----------------------------------------------------------------------------- +// Purpose: Converts a predictable Id to an integer +// Input : *pStruct - +// *pVarData - +// *pOut - +// iElement - +// objectID - +//----------------------------------------------------------------------------- +static void SendProxy_PredictableIdToInt( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ) +{ + CPredictableId* pId = ( CPredictableId * )pVarData; + if ( pId ) + { + pOut->m_Int = pId->GetRaw(); + } + else + { + pOut->m_Int = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pVarName - +// sizeofVar - +// flags - +// pId - +// Output : SendProp +//----------------------------------------------------------------------------- +SendProp SendPropPredictableId( + const char *pVarName, + int offset, + int sizeofVar ) +{ + return SendPropInt( pVarName, offset, sizeofVar, PREDICTABLE_ID_BITS, SPROP_UNSIGNED, SendProxy_PredictableIdToInt ); +} + +#endif + +void SendProxy_StringT_To_String( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ) +{ + string_t &str = *((string_t*)pVarData); + pOut->m_pString = (char*)STRING( str ); +} + + +SendProp SendPropStringT( const char *pVarName, int offset, int sizeofVar ) +{ + // Make sure it's the right type. + Assert( sizeofVar == sizeof( string_t ) ); + + return SendPropString( pVarName, offset, DT_MAX_STRING_BUFFERSIZE, 0, SendProxy_StringT_To_String ); +} diff --git a/sp/src/game/server/sendproxy.h b/sp/src/game/server/sendproxy.h new file mode 100644 index 00000000..99527b1e --- /dev/null +++ b/sp/src/game/server/sendproxy.h @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: implements various common send proxies +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SENDPROXY_H +#define SENDPROXY_H + + +#include "dt_send.h" + + +class DVariant; + +void SendProxy_Color32ToInt( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_EHandleToInt( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_IntAddOne( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ); +void SendProxy_ShortAddOne( const SendProp *pProp, const void *pStruct, const void *pVarData, DVariant *pOut, int iElement, int objectID ); + +SendProp SendPropBool( + const char *pVarName, + int offset, + int sizeofVar ); + +SendProp SendPropEHandle( + const char *pVarName, + int offset, + int sizeofVar=SIZEOF_IGNORE, + int flags = 0, + SendVarProxyFn proxyFn=SendProxy_EHandleToInt ); + +SendProp SendPropTime( + const char *pVarName, + int offset, + int sizeofVar=SIZEOF_IGNORE ); + +#if !defined( NO_ENTITY_PREDICTION ) +SendProp SendPropPredictableId( + const char *pVarName, + int offset, + int sizeofVar=SIZEOF_IGNORE ); +#endif + +SendProp SendPropIntWithMinusOneFlag( + const char *pVarName, + int offset, + int sizeofVar=SIZEOF_IGNORE, + int bits=-1, + SendVarProxyFn proxyFn=SendProxy_IntAddOne ); + + +// Send a string_t as a string property. +SendProp SendPropStringT( const char *pVarName, int offset, int sizeofVar ); + +//----------------------------------------------------------------------------- +// Purpose: Proxy that only sends data to team members +//----------------------------------------------------------------------------- +void* SendProxy_OnlyToTeam( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ); + + +#endif // SENDPROXY_H diff --git a/sp/src/game/server/server_base.vpc b/sp/src/game/server/server_base.vpc new file mode 100644 index 00000000..e0b5cec1 --- /dev/null +++ b/sp/src/game/server/server_base.vpc @@ -0,0 +1,1037 @@ +//----------------------------------------------------------------------------- +// SERVER_BASE.VPC +// +// Project Base Script +//----------------------------------------------------------------------------- + +$Macro OUTBINNAME "server" +$Macro OUTBINDIR "$SRCDIR\..\game\$GAMENAME\bin" +$Macro DEVKITBINDIR "$GAMENAME\bin" [$X360] +// It's important to include $GAMENAME in the generated_proto directory +// to avoid race conditions when multiple games are in one solution. +$Macro GENERATED_PROTO_DIR "$SRCDIR\game\server\generated_proto_$GAMENAME" + +$MacroRequired "GAMENAME" + +$include "$SRCDIR\vpc_scripts\source_dll_base.vpc" +$include "$SRCDIR\vpc_scripts\protobuf_builder.vpc" +$Include "$SRCDIR\vpc_scripts\source_replay.vpc" [$TF] +$Include "$SRCDIR\game\protobuf_include.vpc" + +// Mapbase stuff +$Include "$SRCDIR\game\server\server_mapbase.vpc" [$MAPBASE] +$Include "csm.vpc" + +$Configuration "Debug" +{ + $General + { + $OutputDirectory ".\Debug_$GAMENAME" [$WIN32] + $IntermediateDirectory ".\Debug_$GAMENAME" [$WIN32] + + $OutputDirectory ".\Debug_$GAMENAME_360" [$X360] + $IntermediateDirectory ".\Debug_$GAMENAME_360" [$X360] + } +} + +$Configuration "Release" +{ + $General + { + $OutputDirectory ".\Release_$GAMENAME" [$WIN32] + $IntermediateDirectory ".\Release_$GAMENAME" [$WIN32] + + $OutputDirectory ".\Release_$GAMENAME_360" [$X360] + $IntermediateDirectory ".\Release_$GAMENAME_360" [$X360] + } +} + +$Configuration +{ + $General + { + $OutputDirectory ".\$GAMENAME" [$OSXALL] + } + + $Compiler + { + $AdditionalIncludeDirectories "$BASE;.\;$SRCDIR\game\shared;$SRCDIR\utils\common;$SRCDIR\game\shared\econ;$SRCDIR\game\server\NextBot" + $PreprocessorDefinitions "$BASE;GAME_DLL;VECTOR;VERSION_SAFE_STEAM_API_INTERFACES;PROTECTED_THINGS_ENABLE;sprintf=use_Q_snprintf_instead_of_sprintf;strncpy=use_Q_strncpy_instead;_snprintf=use_Q_snprintf_instead" + $PreprocessorDefinitions "$BASE;SWDS" [$POSIX] + $PreprocessorDefinitions "$BASE;fopen=dont_use_fopen" [$WINDOWS||$X360] + $Create/UsePrecompiledHeader "Use Precompiled Header (/Yu)" + $Create/UsePCHThroughFile "cbase.h" + } + + $Linker + { + $AdditionalDependencies "$BASE winmm.lib" [$WIN32] + $SystemLibraries "iconv" [$OSXALL] + } +} + +$Project +{ + $Folder "Replay" + { + $File "gamedll_replay.cpp" + $File "$SRCDIR\common\replay\ireplaysessionrecorder.h" + + $File "$SRCDIR\game\shared\replay_gamestats_shared.cpp" [$BUILD_REPLAY] + $File "$SRCDIR\game\shared\replay_gamestats_shared.h" [$BUILD_REPLAY] + } + + $Folder "Source Files" + { + + $Folder "Terrain Morphing" + { + $File "terrainmorph\cenv_terrainmorph.cpp" + $File "terrainmorph\terrainmodmgr.cpp" + $File "terrainmorph\terrainmodmgr.h" + $File "terrainmorph\terrainmod_functions.h" + $File "terrainmorph\terrainmod_functions.cpp" + } + + $File "$SRCDIR\game\shared\achievement_saverestore.cpp" + $File "$SRCDIR\game\shared\achievement_saverestore.h" + $File "$SRCDIR\game\shared\achievementmgr.cpp" + $File "$SRCDIR\game\shared\achievementmgr.h" + $File "$SRCDIR\game\shared\achievements_hlx.cpp" + $File "$SRCDIR\game\shared\activitylist.cpp" + $File "$SRCDIR\game\shared\activitylist.h" + $File "ai_activity.cpp" + $File "$SRCDIR\game\shared\ai_activity.h" + $File "ai_baseactor.cpp" + $File "ai_baseactor.h" + $File "ai_basehumanoid.cpp" + $File "ai_basehumanoid.h" + $File "ai_basenpc.cpp" + $File "ai_basenpc.h" + $File "ai_basenpc_flyer.cpp" + $File "ai_basenpc_flyer.h" + $File "ai_basenpc_flyer_new.cpp" + $File "ai_basenpc_flyer_new.h" + $File "ai_basenpc_movement.cpp" + $File "ai_basenpc_physicsflyer.cpp" + $File "ai_basenpc_physicsflyer.h" + $File "ai_basenpc_schedule.cpp" + $File "ai_basenpc_squad.cpp" + $File "ai_behavior.cpp" + $File "ai_behavior.h" + $File "ai_behavior_assault.cpp" + $File "ai_behavior_assault.h" + $File "ai_behavior_fear.cpp" + $File "ai_behavior_fear.h" + $File "ai_behavior_follow.cpp" + $File "ai_behavior_follow.h" + $File "ai_behavior_lead.cpp" + $File "ai_behavior_lead.h" + $File "ai_behavior_rappel.cpp" + $File "ai_behavior_rappel.h" + $File "ai_behavior_standoff.cpp" + $File "ai_behavior_standoff.h" + $File "ai_blended_movement.cpp" + $File "ai_blended_movement.h" + $File "ai_component.h" + $File "ai_concommands.cpp" + $File "ai_condition.cpp" + $File "ai_condition.h" + $File "AI_Criteria.cpp" + $File "AI_Criteria.h" + $File "ai_debug.h" + $File "$SRCDIR\game\shared\ai_debug_shared.h" + $File "ai_default.cpp" + $File "ai_default.h" + $File "ai_dynamiclink.cpp" + $File "ai_dynamiclink.h" + $File "ai_event.cpp" + $File "ai_goalentity.cpp" + $File "ai_goalentity.h" + $File "ai_hint.cpp" + $File "ai_hint.h" + $File "ai_hull.cpp" + $File "ai_hull.h" + $File "ai_initutils.cpp" + $File "ai_initutils.h" + $File "AI_Interest_Target.cpp" + $File "AI_Interest_Target.h" + $File "ai_link.cpp" + $File "ai_link.h" + $File "ai_localnavigator.cpp" + $File "ai_localnavigator.h" + $File "ai_looktarget.cpp" + $File "ai_looktarget.h" + $File "ai_memory.cpp" + $File "ai_memory.h" + $File "ai_motor.cpp" + $File "ai_motor.h" + $File "ai_moveprobe.cpp" + $File "ai_moveprobe.h" + $File "ai_moveshoot.cpp" + $File "ai_moveshoot.h" + $File "ai_movesolver.cpp" + $File "ai_movesolver.h" + $File "ai_movetypes.h" + $File "ai_namespaces.cpp" + $File "ai_namespaces.h" + $File "ai_navgoaltype.h" + $File "ai_navigator.cpp" + $File "ai_navigator.h" + $File "ai_navtype.h" + $File "ai_network.cpp" + $File "ai_network.h" + $File "ai_networkmanager.cpp" + $File "ai_networkmanager.h" + $File "ai_node.cpp" + $File "ai_node.h" + $File "ai_npcstate.h" + $File "ai_obstacle_type.h" + $File "ai_pathfinder.cpp" + $File "ai_pathfinder.h" + $File "ai_planesolver.cpp" + $File "ai_planesolver.h" + $File "ai_playerally.cpp" + $File "ai_playerally.h" + $File "AI_ResponseSystem.cpp" + $File "AI_ResponseSystem.h" + $File "ai_route.cpp" + $File "ai_route.h" + $File "ai_routedist.h" + $File "ai_saverestore.cpp" + $File "ai_saverestore.h" + $File "ai_schedule.cpp" + $File "ai_schedule.h" + $File "ai_scriptconditions.cpp" + $File "ai_scriptconditions.h" + $File "ai_senses.cpp" + $File "ai_senses.h" + $File "ai_sentence.cpp" + $File "ai_sentence.h" + $File "ai_speech.cpp" + $File "ai_speech.h" + $File "ai_speechfilter.cpp" + $File "ai_speechfilter.h" + $File "ai_squad.cpp" + $File "ai_squad.h" + $File "ai_squadslot.cpp" + $File "ai_squadslot.h" + $File "ai_tacticalservices.cpp" + $File "ai_tacticalservices.h" + $File "ai_task.cpp" + $File "ai_task.h" + $File "ai_trackpather.cpp" + $File "ai_trackpather.h" + $File "ai_utils.cpp" + $File "ai_utils.h" + $File "ai_waypoint.cpp" + $File "ai_waypoint.h" + $File "$SRCDIR\game\shared\ammodef.cpp" + $File "$SRCDIR\game\shared\animation.cpp" + $File "$SRCDIR\game\shared\animation.h" + $File "$SRCDIR\game\shared\apparent_velocity_helper.h" + $File "$SRCDIR\game\shared\base_playeranimstate.cpp" + $File "base_transmit_proxy.cpp" + $File "$SRCDIR\game\shared\baseachievement.cpp" + $File "$SRCDIR\game\shared\baseachievement.h" + $File "baseanimating.cpp" + $File "baseanimating.h" + $File "BaseAnimatingOverlay.cpp" + $File "BaseAnimatingOverlay.h" + $File "basecombatcharacter.cpp" + $File "basecombatcharacter.h" + $File "$SRCDIR\game\shared\basecombatcharacter_shared.cpp" + $File "basecombatweapon.cpp" + $File "basecombatweapon.h" + $File "$SRCDIR\game\shared\basecombatweapon_shared.cpp" + $File "$SRCDIR\game\shared\basecombatweapon_shared.h" + $File "baseentity.cpp" + $File "baseentity.h" + $File "$SRCDIR\game\shared\baseentity_shared.cpp" + $File "$SRCDIR\game\shared\baseentity_shared.h" + $File "baseflex.cpp" + $File "baseflex.h" + $File "$SRCDIR\game\shared\basegrenade_shared.cpp" + $File "$SRCDIR\game\shared\basegrenade_shared.h" + $File "basemultiplayerplayer.cpp" + $File "basemultiplayerplayer.h" + $File "$SRCDIR\game\shared\baseparticleentity.cpp" + $File "$SRCDIR\game\shared\baseparticleentity.h" + $File "$SRCDIR\game\shared\baseplayer_shared.cpp" + $File "$SRCDIR\game\shared\baseplayer_shared.h" + $File "$SRCDIR\game\shared\baseprojectile.cpp" + $File "$SRCDIR\game\shared\baseprojectile.h" + $File "BasePropDoor.h" + $File "basetoggle.h" + $File "baseviewmodel.cpp" + $File "baseviewmodel.h" + $File "$SRCDIR\game\shared\baseviewmodel_shared.cpp" + $File "$SRCDIR\game\shared\baseviewmodel_shared.h" + $File "$SRCDIR\game\shared\beam_shared.cpp" + $File "$SRCDIR\game\shared\beam_shared.h" + $File "bitstring.cpp" + $File "bitstring.h" + $File "bmodels.cpp" + $File "$SRCDIR\public\bone_setup.h" + $File "buttons.cpp" + $File "buttons.h" + $File "cbase.cpp" + $File "cbase.h" + $File "$SRCDIR\game\shared\choreoactor.h" + $File "$SRCDIR\game\shared\choreochannel.h" + $File "$SRCDIR\game\shared\choreoevent.h" + $File "$SRCDIR\game\shared\choreoscene.h" + $File "client.cpp" + $File "client.h" + $File "$SRCDIR\game\shared\collisionproperty.cpp" + $File "$SRCDIR\game\shared\collisionproperty.h" + $File "$SRCDIR\public\collisionutils.h" + $File "colorcorrection.cpp" + $File "colorcorrectionvolume.cpp" + $File "CommentarySystem.cpp" + $File "controlentities.cpp" + $File "cplane.cpp" + $File "CRagdollMagnet.cpp" + $File "CRagdollMagnet.h" + $File "damagemodifier.cpp" + $File "$SRCDIR\game\shared\death_pose.cpp" + $File "$SRCDIR\game\shared\debugoverlay_shared.cpp" + $File "$SRCDIR\game\shared\debugoverlay_shared.h" + $File "$SRCDIR\game\shared\decals.cpp" + $File "doors.cpp" + $File "doors.h" + $File "dynamiclight.cpp" + $File "$SRCDIR\public\edict.h" + $File "$SRCDIR\public\editor_sendcommand.h" + $File "$SRCDIR\game\shared\effect_color_tables.h" + $File "$SRCDIR\game\shared\effect_dispatch_data.cpp" + $File "effects.cpp" + $File "effects.h" + $File "EffectsServer.cpp" + $File "$SRCDIR\game\shared\ehandle.cpp" + $File "$SRCDIR\public\eiface.h" + $File "enginecallback.h" + $File "entityapi.h" + $File "entityblocker.cpp" + $File "entityblocker.h" + $File "EntityDissolve.cpp" + $File "EntityDissolve.h" + $File "EntityFlame.cpp" + $File "entityinput.h" + $File "entitylist.cpp" + $File "entitylist.h" + $File "$SRCDIR\game\shared\entitylist_base.cpp" + $File "entityoutput.h" + $File "EntityParticleTrail.cpp" + $File "EntityParticleTrail.h" + $File "$SRCDIR\game\shared\EntityParticleTrail_Shared.cpp" + $File "$SRCDIR\game\shared\entityparticletrail_shared.h" + $File "env_debughistory.cpp" + $File "env_debughistory.h" + $File "$SRCDIR\game\shared\env_detail_controller.cpp" + $File "env_effectsscript.cpp" + $File "env_entity_maker.cpp" + $File "env_particlescript.cpp" + $File "env_player_surface_trigger.cpp" + $File "env_player_surface_trigger.h" + $File "env_projectedtexture.cpp" + $File "env_screenoverlay.cpp" + $File "env_texturetoggle.cpp" + $File "env_tonemap_controller.cpp" + $File "$SRCDIR\game\shared\env_wind_shared.cpp" + $File "$SRCDIR\game\shared\env_wind_shared.h" + $File "env_zoom.cpp" + $File "env_zoom.h" + $File "EnvBeam.cpp" + $File "EnvFade.cpp" + $File "EnvHudHint.cpp" + $File "EnvLaser.cpp" + $File "EnvLaser.h" + $File "EnvMessage.cpp" + $File "EnvMessage.h" + $File "envmicrophone.cpp" + $File "envmicrophone.h" + $File "EnvShake.cpp" + $File "EnvSpark.cpp" + $File "envspark.h" + $File "$SRCDIR\public\event_flags.h" + $File "event_tempentity_tester.h" + $File "$SRCDIR\game\shared\eventlist.cpp" + $File "$SRCDIR\game\shared\eventlist.h" + $File "EventLog.cpp" + $File "eventqueue.h" + $File "explode.cpp" + $File "explode.h" + $File "filters.cpp" + $File "filters.h" + $File "fire.cpp" + $File "fire.h" + $File "fire_smoke.cpp" + $File "fire_smoke.h" + $File "fish.cpp" + $File "fish.h" + $File "fogcontroller.cpp" + $File "fourwheelvehiclephysics.cpp" + $File "fourwheelvehiclephysics.h" + $File "func_areaportal.cpp" + $File "func_areaportalbase.cpp" + $File "func_areaportalbase.h" + $File "func_areaportalwindow.cpp" + $File "func_areaportalwindow.h" + $File "func_break.cpp" + $File "func_break.h" + $File "func_breakablesurf.cpp" + $File "func_breakablesurf.h" + $File "func_dust.cpp" + $File "$SRCDIR\game\shared\func_dust_shared.h" + $File "$SRCDIR\game\shared\func_ladder.cpp" + $File "func_ladder_endpoint.cpp" + $File "func_lod.cpp" + $File "func_movelinear.cpp" + $File "func_movelinear.h" + $File "func_occluder.cpp" + $File "func_reflective_glass.cpp" + $File "func_smokevolume.cpp" + $File "game.cpp" + $File "game.h" + $File "game_ui.cpp" + $File "gameinterface.cpp" + $File "gameinterface.h" + $File "$SRCDIR\game\shared\gamemovement.cpp" + $File "$SRCDIR\game\shared\gamemovement.h" + $File "$SRCDIR\game\shared\gamerules.cpp" + $File "$SRCDIR\game\shared\gamerules.h" + $File "$SRCDIR\game\shared\gamerules_register.cpp" + $File "$SRCDIR\game\shared\GameStats.cpp" + $File "$SRCDIR\game\shared\gamestats.h" + $File "$SRCDIR\game\shared\gamestringpool.cpp" + $File "$SRCDIR\game\shared\gamestringpool.h" + $File "gametrace_dll.cpp" + $File "$SRCDIR\game\shared\gamevars_shared.cpp" + $File "$SRCDIR\game\shared\gamevars_shared.h" + $File "gameweaponmanager.cpp" + $File "gameweaponmanager.h" + $File "genericactor.cpp" + $File "genericmonster.cpp" + $File "gib.cpp" + $File "gib.h" + $File "globals.cpp" + $File "globalstate.cpp" + $File "globalstate.h" + $File "globalstate_private.h" + $File "guntarget.cpp" + $File "h_ai.cpp" + $File "hierarchy.cpp" + $File "hierarchy.h" + $file "$SRCDIR\common\hl2orange.spa.h" + $File "hltvdirector.cpp" + $File "hltvdirector.h" + $File "$SRCDIR\game\shared\hintmessage.cpp" + $File "$SRCDIR\game\shared\hintmessage.h" + $File "$SRCDIR\game\shared\hintsystem.cpp" + $File "$SRCDIR\game\shared\hintsystem.h" + $File "$SRCDIR\game\shared\ichoreoeventcallback.h" + $File "$SRCDIR\game\shared\igamesystem.cpp" + $File "$SRCDIR\game\shared\igamesystem.h" + $File "info_camera_link.cpp" + $File "info_camera_link.h" + $File "info_overlay_accessor.cpp" + $File "init_factory.h" + $File "intermission.cpp" + $File "$SRCDIR\public\interpolatortypes.h" + $File "$SRCDIR\game\shared\interval.h" + $File "$SRCDIR\public\iregistry.h" + $File "$SRCDIR\game\shared\iscenetokenprocessor.h" + $File "iservervehicle.h" + $File "item_world.cpp" + $File "items.h" + $File "$SRCDIR\public\ivoiceserver.h" + $File "$SRCDIR\public\keyframe\keyframe.h" + $File "lightglow.cpp" + $File "lights.cpp" + $File "lights.h" + $File "locksounds.h" + $File "logic_measure_movement.cpp" + $File "logic_navigation.cpp" + $File "logicauto.cpp" + $File "logicentities.cpp" + $File "logicrelay.cpp" + $File "mapentities.cpp" + $File "$SRCDIR\game\shared\mapentities_shared.cpp" + $File "maprules.cpp" + $File "maprules.h" + $File "MaterialModifyControl.cpp" + $File "$SRCDIR\public\mathlib\mathlib.h" + $File "message_entity.cpp" + $File "$SRCDIR\public\model_types.h" + $File "modelentities.cpp" + $File "$SRCDIR\game\shared\ModelSoundsCache.cpp" + $File "movehelper_server.cpp" + $File "movehelper_server.h" + $File "movement.cpp" + $File "$SRCDIR\game\shared\movevars_shared.cpp" + $File "movie_explosion.h" + $File "$SRCDIR\game\shared\multiplay_gamerules.cpp" + $File "$SRCDIR\game\shared\multiplay_gamerules.h" + $File "ndebugoverlay.cpp" + $File "ndebugoverlay.h" + $File "networkstringtable_gamedll.h" + $File "$SRCDIR\public\networkstringtabledefs.h" + $File "npc_vehicledriver.cpp" + $File "$SRCDIR\game\shared\obstacle_pushaway.cpp" + $File "$SRCDIR\game\shared\obstacle_pushaway.h" + $File "particle_fire.h" + $File "particle_light.cpp" + $File "particle_light.h" + $File "$SRCDIR\game\shared\particle_parse.cpp" + $File "$SRCDIR\game\shared\particle_parse.h" + $File "particle_smokegrenade.h" + $File "particle_system.cpp" + $File "$SRCDIR\game\shared\particlesystemquery.cpp" + $File "pathcorner.cpp" + $File "pathtrack.cpp" + $File "pathtrack.h" + $File "$SRCDIR\public\vphysics\performance.h" + $File "phys_controller.cpp" + $File "phys_controller.h" + $File "physconstraint.cpp" + $File "physconstraint.h" + $File "physics.cpp" + $File "physics.h" + $File "physics_bone_follower.cpp" + $File "physics_cannister.cpp" + $File "physics_collisionevent.h" + $File "physics_fx.cpp" + $File "physics_impact_damage.cpp" + $File "pushentity.h" + $File "physics_main.cpp" + $File "$SRCDIR\game\shared\physics_main_shared.cpp" + $File "physics_npc_solver.cpp" + $File "physics_npc_solver.h" + $File "physics_prop_ragdoll.cpp" + $File "physics_prop_ragdoll.h" + $File "$SRCDIR\game\shared\physics_saverestore.cpp" + $File "$SRCDIR\game\shared\physics_saverestore.h" + $File "$SRCDIR\game\shared\physics_shared.cpp" + $File "$SRCDIR\game\shared\physics_shared.h" + $File "physobj.cpp" + $File "physobj.h" + $File "player.cpp" + $File "player.h" + $File "player_command.cpp" + $File "player_command.h" + $File "player_lagcompensation.cpp" + $File "player_pickup.cpp" + $File "player_pickup.h" + $File "player_resource.cpp" + $File "player_resource.h" + $File "playerinfomanager.cpp" + $File "playerlocaldata.cpp" + $File "playerlocaldata.h" + $File "plugin_check.cpp" + $File "$SRCDIR\game\shared\point_bonusmaps_accessor.cpp" + $File "$SRCDIR\game\shared\point_bonusmaps_accessor.h" + $File "point_camera.cpp" + $File "point_camera.h" + $File "point_devshot_camera.cpp" + $File "point_playermoveconstraint.cpp" + $File "$SRCDIR\game\shared\point_posecontroller.cpp" + $File "$SRCDIR\game\shared\point_posecontroller.h" + $File "point_spotlight.cpp" + $File "point_template.cpp" + $File "point_template.h" + $File "pointanglesensor.cpp" + $File "PointAngularVelocitySensor.cpp" + $File "pointhurt.cpp" + $File "pointteleport.cpp" + $File "$SRCDIR\public\mathlib\polyhedron.h" + $File "$SRCDIR\game\shared\positionwatcher.h" + $File "$SRCDIR\game\shared\precache_register.cpp" + $File "$SRCDIR\game\shared\precache_register.h" + $File "$SRCDIR\game\shared\predictableid.cpp" + $File "$SRCDIR\game\shared\predictableid.h" + $File "props.cpp" + $File "props.h" + $File "$SRCDIR\game\shared\props_shared.cpp" + $File "$SRCDIR\game\shared\querycache.cpp" + $File "ragdoll_manager.cpp" + $File "$SRCDIR\game\shared\ragdoll_shared.cpp" + $File "RagdollBoogie.cpp" + $File "RagdollBoogie.h" + $File "recipientfilter.cpp" + $File "recipientfilter.h" + $File "rope.cpp" + $File "rope.h" + $File "$SRCDIR\game\shared\rope_helpers.cpp" + $File "$SRCDIR\public\rope_physics.h" + $File "$SRCDIR\public\rope_shared.h" + $File "$SRCDIR\game\shared\saverestore.cpp" + $File "$SRCDIR\game\shared\saverestore.h" + $File "$SRCDIR\game\shared\saverestore_bitstring.h" + $File "saverestore_gamedll.cpp" + $File "$SRCDIR\game\shared\saverestore_utlsymbol.h" + $File "$SRCDIR\game\shared\saverestore_utlvector.h" + $File "$SRCDIR\game\shared\SceneCache.cpp" + $File "sceneentity.cpp" + $File "sceneentity.h" + $File "$SRCDIR\game\shared\sceneentity_shared.cpp" + $File "scratchpad_gamedll_helpers.cpp" + $File "scripted.cpp" + $File "scripted.h" + $File "scriptedtarget.cpp" + $File "scriptedtarget.h" + $File "$SRCDIR\game\shared\scriptevent.h" + $File "sendproxy.cpp" + $File "$SRCDIR\game\shared\sequence_Transitioner.cpp" + $File "$SRCDIR\game\server\serverbenchmark_base.cpp" + $File "$SRCDIR\game\server\serverbenchmark_base.h" + $File "$SRCDIR\public\server_class.h" + $File "ServerNetworkProperty.cpp" + $File "ServerNetworkProperty.h" + $File "shadowcontrol.cpp" + $File "$SRCDIR\public\shattersurfacetypes.h" + $File "$SRCDIR\game\shared\sheetsimulator.h" + $File "$SRCDIR\public\simple_physics.h" + $File "$SRCDIR\game\shared\simtimer.cpp" + $File "$SRCDIR\game\shared\simtimer.h" + $File "$SRCDIR\game\shared\singleplay_gamerules.cpp" + $File "$SRCDIR\game\shared\singleplay_gamerules.h" + $File "SkyCamera.cpp" + $File "slideshow_display.cpp" + $File "sound.cpp" + $File "$SRCDIR\game\shared\SoundEmitterSystem.cpp" + $File "soundent.cpp" + $File "soundent.h" + $File "$SRCDIR\game\shared\soundenvelope.cpp" + $File "$SRCDIR\public\SoundParametersInternal.cpp" + $File "soundscape.cpp" + $File "soundscape.h" + $File "soundscape_system.cpp" + $File "spark.h" + $File "spotlightend.cpp" + $File "spotlightend.h" + $File "$SRCDIR\game\shared\Sprite.cpp" + $File "$SRCDIR\game\shared\Sprite.h" + $File "sprite_perfmonitor.cpp" + $File "$SRCDIR\game\shared\SpriteTrail.h" + $File "$SRCDIR\public\vphysics\stats.h" + $File "$SRCDIR\public\steam\steam_api.h" + $File "$SRCDIR\public\stringregistry.h" + $File "$SRCDIR\game\shared\studio_shared.cpp" + $File "subs.cpp" + $File "sun.cpp" + $File "tactical_mission.cpp" + $File "tactical_mission.h" + $File "$SRCDIR\game\shared\takedamageinfo.cpp" + $File "tanktrain.cpp" + $File "team.cpp" + $File "team.h" + $File "$SRCDIR\game\shared\teamplay_gamerules.cpp" + $File "$SRCDIR\game\shared\teamplay_gamerules.h" + $File "$SRCDIR\game\shared\tempentity.h" + $File "TemplateEntities.cpp" + $File "TemplateEntities.h" + $File "tempmonster.cpp" + $File "tesla.cpp" + $File "$SRCDIR\game\shared\test_ehandle.cpp" + $File "test_proxytoggle.cpp" + $File "test_stressentities.cpp" + $File "testfunctions.cpp" + $File "testtraceline.cpp" + $File "textstatsmgr.cpp" + $File "timedeventmgr.cpp" + $File "trains.cpp" + $File "trains.h" + $File "triggers.cpp" + $File "triggers.h" + $File "$SRCDIR\game\shared\usercmd.cpp" + $File "util.cpp" + $File "util.h" + $File "$SRCDIR\game\shared\util_shared.cpp" + $File "variant_t.cpp" + $File "vehicle_base.cpp" + $File "vehicle_baseserver.cpp" + $File "vehicle_sounds.h" + $File "$SRCDIR\game\shared\vehicle_viewblend_shared.cpp" + $File "vguiscreen.cpp" + $File "vguiscreen.h" + $File "$SRCDIR\public\mathlib\vmatrix.h" + $File "$SRCDIR\game\shared\voice_common.h" + $File "$SRCDIR\game\shared\voice_gamemgr.cpp" + $File "$SRCDIR\game\shared\voice_gamemgr.h" + $File "vscript_server.cpp" + $File "vscript_server.h" + $File "vscript_server.nut" + $File "$SRCDIR\game\shared\vscript_shared.cpp" + $File "$SRCDIR\game\shared\vscript_shared.h" + $File "waterbullet.cpp" + $File "waterbullet.h" + $File "WaterLODControl.cpp" + $File "wcedit.cpp" + $File "wcedit.h" + $File "$SRCDIR\game\shared\weapon_parse.cpp" + $File "$SRCDIR\game\shared\weapon_parse.h" + $File "$SRCDIR\game\shared\weapon_proficiency.cpp" + $File "$SRCDIR\game\shared\weapon_proficiency.h" + $File "weight_button.cpp" + $File "world.cpp" + $File "world.h" + $File "$SRCDIR\game\shared\mp_shareddefs.cpp" + $File "$SRCDIR\game\shared\SharedFunctorUtils.h" + $File "$SRCDIR\game\server\vote_controller.h" + $File "$SRCDIR\game\server\vote_controller.cpp" + //Haptics + $File "$SRCDIR\public\haptics\haptic_msgs.cpp" + $File "$SRCDIR\public\haptics\haptic_utils.cpp" [$WIN32] + + // Not using precompiled header cbase.h + + $File "$SRCDIR\public\bone_setup.cpp" \ + "$SRCDIR\public\collisionutils.cpp" \ + "$SRCDIR\public\dt_send.cpp" \ + "$SRCDIR\public\dt_utlvector_common.cpp" \ + "$SRCDIR\public\dt_utlvector_send.cpp" \ + "$SRCDIR\public\editor_sendcommand.cpp" \ + "$SRCDIR\public\filesystem_helpers.cpp" \ + "gamehandle.cpp" \ + "h_export.cpp" \ + "init_factory.cpp" \ + "$SRCDIR\public\interpolatortypes.cpp" \ + "$SRCDIR\game\shared\interval.cpp" \ + "$SRCDIR\public\keyframe\keyframe.cpp" \ + "$SRCDIR\common\language.cpp" \ + "$SRCDIR\public\map_utils.cpp" \ + "$SRCDIR\public\networkvar.cpp" \ + "$SRCDIR\common\randoverride.cpp" \ + "$SRCDIR\public\registry.cpp" \ + "$SRCDIR\public\rope_physics.cpp" \ + "$SRCDIR\public\scratchpad3d.cpp" \ + "$SRCDIR\public\ScratchPadUtils.cpp" \ + "$SRCDIR\public\server_class.cpp" \ + "$SRCDIR\game\shared\sheetsimulator.cpp" \ + "$SRCDIR\public\simple_physics.cpp" \ + "$SRCDIR\public\stringregistry.cpp" \ + "$SRCDIR\public\studio.cpp" \ + "GameStats_BasicStatsFunctions.cpp" + { + $Configuration + { + $Compiler + { + $Create/UsePrecompiledHeader "Not Using Precompiled Headers" + } + } + } + + $Folder "Precompiled Header" + { + $File "stdafx.cpp" + { + $Configuration + { + $Compiler + { + $Create/UsePrecompiledHeader "Create Precompiled Header (/Yc)" + } + } + } + } + + $Folder "temporary entities" + { + $File "basetempentity.cpp" + $File "event_tempentity_tester.cpp" + $File "movie_explosion.cpp" + $File "particle_fire.cpp" + $File "particle_smokegrenade.cpp" + $File "plasma.cpp" + $File "plasma.h" + $File "smoke_trail.h" + $File "smokestack.cpp" + $File "smokestack.h" + $File "smoke_trail.cpp" + $File "$SRCDIR\game\shared\SpriteTrail.cpp" + $File "steamjet.cpp" + $File "steamjet.h" + $File "te.cpp" + $File "te.h" + $File "te_armorricochet.cpp" + $File "te_basebeam.cpp" + $File "te_basebeam.h" + $File "te_beamentpoint.cpp" + $File "te_beaments.cpp" + $File "te_beamfollow.cpp" + $File "te_beamlaser.cpp" + $File "te_beampoints.cpp" + $File "te_beamring.cpp" + $File "te_beamringpoint.cpp" + $File "te_beamspline.cpp" + $File "te_bloodsprite.cpp" + $File "te_bloodstream.cpp" + $File "te_breakmodel.cpp" + $File "te_bspdecal.cpp" + $File "te_bubbles.cpp" + $File "te_bubbletrail.cpp" + $File "te_clientprojectile.cpp" + $File "te_decal.cpp" + $File "te_dynamiclight.cpp" + $File "te_effect_dispatch.cpp" + $File "te_energysplash.cpp" + $File "te_explosion.cpp" + $File "te_fizz.cpp" + $File "te_footprintdecal.cpp" + $File "hl2\te_gaussexplosion.cpp" + $File "te_glassshatter.cpp" + $File "te_glowsprite.cpp" + $File "te_impact.cpp" + $File "te_killplayerattachments.cpp" + $File "te_largefunnel.cpp" + $File "te_muzzleflash.cpp" + $File "te_particlesystem.cpp" + $File "te_particlesystem.h" + $File "te_physicsprop.cpp" + $File "te_playerdecal.cpp" + $File "te_projecteddecal.cpp" + $File "te_showline.cpp" + $File "te_smoke.cpp" + $File "te_sparks.cpp" + $File "te_sprite.cpp" + $File "te_spritespray.cpp" + $File "te_worlddecal.cpp" + $File "$SRCDIR\game\shared\usermessages.cpp" + } + + } + + $Folder "Header Files" + { + $File "$SRCDIR\public\mathlib\amd3dx.h" + $File "$SRCDIR\game\shared\ammodef.h" + $File "$SRCDIR\game\shared\base_playeranimstate.h" + $File "base_transmit_proxy.h" + $File "$SRCDIR\public\basehandle.h" + $File "basetempentity.h" + $File "$SRCDIR\public\tier0\basetypes.h" + $File "$SRCDIR\game\shared\beam_flags.h" + $File "$SRCDIR\public\tier1\bitbuf.h" + $File "$SRCDIR\public\bitvec.h" + $File "$SRCDIR\public\bone_accessor.h" + $File "$SRCDIR\public\bspfile.h" + $File "$SRCDIR\public\bspflags.h" + $File "$SRCDIR\public\mathlib\bumpvects.h" + $File "$SRCDIR\public\tier1\characterset.h" + $File "$SRCDIR\public\tier1\checksum_md5.h" + $File "$SRCDIR\public\client_class.h" + $File "$SRCDIR\public\client_textmessage.h" + $File "$SRCDIR\public\cmodel.h" + $File "$SRCDIR\public\vphysics\collision_set.h" + $File "$SRCDIR\public\Color.h" + $File "$SRCDIR\public\tier0\commonmacros.h" + $File "$SRCDIR\public\mathlib\compressed_light_cube.h" + $File "$SRCDIR\public\mathlib\compressed_vector.h" + $File "$SRCDIR\public\const.h" + $File "$SRCDIR\public\vphysics\constraints.h" + $File "$SRCDIR\public\coordsize.h" + $File "cplane.h" + $File "damagemodifier.h" + $File "$SRCDIR\public\datamap.h" + $File "$SRCDIR\public\tier0\dbg.h" + $File "$SRCDIR\game\shared\death_pose.h" + $File "$SRCDIR\game\shared\decals.h" + $File "$SRCDIR\public\dlight.h" + $File "$SRCDIR\public\dt_common.h" + $File "$SRCDIR\public\dt_recv.h" + $File "$SRCDIR\public\dt_send.h" + $File "$SRCDIR\public\dt_utlvector_common.h" + $File "$SRCDIR\public\dt_utlvector_send.h" + $File "$SRCDIR\game\shared\effect_dispatch_data.h" + $File "$SRCDIR\game\shared\ehandle.h" + $File "$SRCDIR\game\shared\entitydatainstantiator.h" + $File "$SRCDIR\game\shared\entitylist_base.h" + $File "$SRCDIR\game\shared\env_detail_controller.h" + $File "EventLog.h" + $File "$SRCDIR\game\shared\expressionsample.h" + $File "$SRCDIR\public\tier0\fasttimer.h" + $File "$SRCDIR\public\filesystem.h" + $File "$SRCDIR\public\filesystem_helpers.h" + $File "$SRCDIR\public\tier1\fmtstr.h" + $File "fogcontroller.h" + $File "$SRCDIR\public\vphysics\friction.h" + $File "$SRCDIR\game\shared\func_ladder.h" + $File "$SRCDIR\game\shared\gameeventdefs.h" + $File "$SRCDIR\game\shared\GameEventListener.h" + $File "$SRCDIR\game\shared\gamerules_register.h" + $File "$SRCDIR\public\gametrace.h" + $File "globals.h" + $File "$SRCDIR\public\globalvars_base.h" + $File "$SRCDIR\game\shared\groundlink.h" + $File "$SRCDIR\game\shared\hl2\hl2_vehicle_radar.h" + $File "$SRCDIR\public\iachievementmgr.h" + $File "$SRCDIR\public\appframework\IAppSystem.h" + $File "$SRCDIR\public\icliententity.h" + $File "$SRCDIR\public\iclientnetworkable.h" + $File "$SRCDIR\public\iclientrenderable.h" + $File "$SRCDIR\public\iclientunknown.h" + $File "$SRCDIR\public\engine\ICollideable.h" + $File "$SRCDIR\public\tier0\icommandline.h" + $File "$SRCDIR\public\icvar.h" + $File "$SRCDIR\game\shared\IEffects.h" + $File "$SRCDIR\public\engine\IEngineSound.h" + $File "$SRCDIR\public\engine\IEngineTrace.h" + $File "$SRCDIR\public\igameevents.h" + $File "$SRCDIR\game\shared\igamemovement.h" + $File "$SRCDIR\public\ihandleentity.h" + $File "$SRCDIR\public\ihltv.h" + $File "$SRCDIR\public\ihltvdirector.h" + $File "$SRCDIR\public\vstdlib\IKeyValuesSystem.h" + $File "ilagcompensationmanager.h" + $File "$SRCDIR\public\vgui\ILocalize.h" + $File "$SRCDIR\public\materialsystem\imaterial.h" + $File "$SRCDIR\public\materialsystem\imaterialsystem.h" + $File "$SRCDIR\public\materialsystem\imaterialvar.h" + $File "$SRCDIR\game\shared\imovehelper.h" + $File "$SRCDIR\game\shared\in_buttons.h" + $File "$SRCDIR\public\inetchannelinfo.h" + $File "$SRCDIR\game\shared\iplayeranimstate.h" + $File "$SRCDIR\game\shared\ipredictionsystem.h" + $File "$SRCDIR\public\irecipientfilter.h" + $File "$SRCDIR\public\isaverestore.h" + $File "$SRCDIR\public\iscratchpad3d.h" + $File "$SRCDIR\public\iserverentity.h" + $File "$SRCDIR\public\iservernetworkable.h" + $File "$SRCDIR\public\iserverunknown.h" + $File "$SRCDIR\public\SoundEmitterSystem\isoundemittersystembase.h" + $File "$SRCDIR\public\ispatialpartition.h" + $File "$SRCDIR\public\engine\IStaticPropMgr.h" + $File "$SRCDIR\game\shared\itempents.h" + $File "$SRCDIR\public\engine\ivdebugoverlay.h" + $File "$SRCDIR\game\shared\IVehicle.h" + $File "$SRCDIR\public\engine\ivmodelinfo.h" + $File "$SRCDIR\public\tier1\KeyValues.h" + $File "$SRCDIR\common\language.h" + $File "$SRCDIR\public\tier0\l2cache.h" + $File "logicrelay.h" + $File "$SRCDIR\public\map_utils.h" + $File "mapentities.h" + $File "$SRCDIR\game\shared\mapentities_shared.h" + $File "$SRCDIR\public\tier0\mem.h" + $File "$SRCDIR\public\tier0\memalloc.h" + $File "$SRCDIR\public\tier0\memdbgoff.h" + $File "$SRCDIR\public\tier0\memdbgon.h" + $File "modelentities.h" + $File "$SRCDIR\game\shared\movevars_shared.h" + $File "$SRCDIR\public\networkvar.h" + $File "npc_vehicledriver.h" + $File "$SRCDIR\game\shared\npcevent.h" + $File "$SRCDIR\public\vphysics\object_hash.h" + $File "particle_system.h" + $File "physics_cannister.h" + $File "physics_fx.h" + $File "physics_impact_damage.h" + $File "$SRCDIR\public\tier0\platform.h" + $File "$SRCDIR\public\vphysics\player_controller.h" + $File "playerinfomanager.h" + $File "$SRCDIR\game\shared\playernet_vars.h" + $File "$SRCDIR\public\PlayerState.h" + $File "$SRCDIR\game\shared\precipitation_shared.h" + $File "$SRCDIR\game\shared\predictable_entity.h" + $File "$SRCDIR\game\shared\predictioncopy.h" + $File "$SRCDIR\public\tier1\processor_detect.h" + $File "$SRCDIR\game\shared\querycache.h" + $File "$SRCDIR\game\shared\props_shared.h" + $File "$SRCDIR\public\tier0\protected_things.h" + $File "$SRCDIR\public\vstdlib\random.h" + $File "$SRCDIR\game\shared\rope_helpers.h" + $File "$SRCDIR\game\shared\saverestore_stringtable.h" + $File "$SRCDIR\game\shared\saverestore_utlclass.h" + $File "$SRCDIR\game\shared\saverestore_utlmap.h" + $File "$SRCDIR\game\shared\saverestore_utlrbtree.h" + $File "$SRCDIR\public\saverestoretypes.h" + $File "$SRCDIR\public\scratchpad3d.h" + $File "scratchpad_gamedll_helpers.h" + $File "$SRCDIR\public\ScratchPadUtils.h" + $File "sendproxy.h" + $File "$SRCDIR\public\shake.h" + $File "$SRCDIR\game\shared\shared_classnames.h" + $File "$SRCDIR\game\shared\shareddefs.h" + $File "$SRCDIR\game\shared\sharedInterface.h" + $File "$SRCDIR\game\shared\shot_manipulator.h" + $File "SkyCamera.h" + $File "$SRCDIR\public\soundchars.h" + $File "$SRCDIR\game\shared\soundenvelope.h" + $File "$SRCDIR\public\soundflags.h" + $File "soundscape_system.h" + $File "$SRCDIR\public\stdstring.h" + $File "$SRCDIR\public\string_t.h" + $File "$SRCDIR\public\tier1\stringpool.h" + $File "$SRCDIR\public\tier1\strtools.h" + $File "$SRCDIR\public\studio.h" + $File "$SRCDIR\game\shared\sun_shared.h" + $File "$SRCDIR\game\shared\takedamageinfo.h" + $File "te_effect_dispatch.h" + $File "tesla.h" + $File "test_stressentities.h" + $File "textstatsmgr.h" + $File "$SRCDIR\public\texture_group_names.h" + $File "timedeventmgr.h" + $File "$SRCDIR\game\shared\usercmd.h" + $File "$SRCDIR\game\shared\usermessages.h" + $File "$SRCDIR\game\shared\util_shared.h" + $File "$SRCDIR\public\UtlCachedFileData.h" + $File "$SRCDIR\public\tier1\utldict.h" + $File "$SRCDIR\public\tier1\utlfixedmemory.h" + $File "$SRCDIR\public\tier1\utlhash.h" + $File "$SRCDIR\public\tier1\utllinkedlist.h" + $File "$SRCDIR\public\tier1\utlmap.h" + $File "$SRCDIR\public\tier1\utlmemory.h" + $File "$SRCDIR\public\tier1\utlmultilist.h" + $File "$SRCDIR\public\tier1\utlpriorityqueue.h" + $File "$SRCDIR\public\tier1\utlrbtree.h" + $File "$SRCDIR\public\tier1\UtlSortVector.h" + $File "$SRCDIR\public\tier1\utlvector.h" + $File "$SRCDIR\public\vallocator.h" + $File "variant_t.h" + $File "$SRCDIR\public\vcollide.h" + $File "$SRCDIR\public\vcollide_parse.h" + $File "$SRCDIR\public\tier0\vcr_shared.h" + $File "$SRCDIR\public\tier0\vcrmode.h" + $File "$SRCDIR\public\mathlib\vector.h" + $File "$SRCDIR\public\mathlib\vector2d.h" + $File "$SRCDIR\public\mathlib\vector4d.h" + $File "vehicle_base.h" + $File "vehicle_baseserver.h" + $File "$SRCDIR\game\shared\vehicle_viewblend_shared.h" + $File "$SRCDIR\public\vphysics\vehicles.h" + $File "$SRCDIR\public\vgui\VGUI.h" + $File "$SRCDIR\public\vphysics_interface.h" + $File "$SRCDIR\game\shared\vphysics_sound.h" + $File "$SRCDIR\public\mathlib\vplane.h" + $File "$SRCDIR\public\tier0\vprof.h" + $File "$SRCDIR\public\vstdlib\vstdlib.h" + $File "$SRCDIR\public\winlite.h" + $File "$SRCDIR\public\worldsize.h" + $File "$SRCDIR\public\zip_uncompressed.h" + $File "$SRCDIR\game\shared\mp_shareddefs.h" + $File "$SRCDIR\game\shared\econ\ihasowner.h" + //Haptics + $File "$SRCDIR\public\haptics\haptic_utils.h" [$WIN32] + } + + $Folder "Tools Framework" + { + $File "entity_tools_server.cpp" + $File "toolframework_server.cpp" + $File "toolframework_server.h" + } + + $Folder "Link Libraries" + { + $Lib choreoobjects + $Lib dmxloader + $Lib mathlib + $Lib particles + $Lib tier2 + $Lib tier3 + $ImpLibexternal steam_api + } +} diff --git a/sp/src/game/server/server_episodic.vpc b/sp/src/game/server/server_episodic.vpc new file mode 100644 index 00000000..7c7ca5da --- /dev/null +++ b/sp/src/game/server/server_episodic.vpc @@ -0,0 +1,303 @@ +//----------------------------------------------------------------------------- +// SERVER_EPISODIC.VPC +// +// Project Script +//----------------------------------------------------------------------------- + +$Macro SRCDIR "..\.." +$Macro GAMENAME "episodic" [!$SOURCESDK] +$Macro GAMENAME "mod_episodic" [$SOURCESDK] + +$Include "$SRCDIR\game\server\server_base.vpc" + +$Configuration +{ + $Compiler + { + $AdditionalIncludeDirectories "$BASE;$SRCDIR\game\shared\hl2;$SRCDIR\game\shared\episodic;.\hl2;.\episodic" + $PreprocessorDefinitions "$BASE;HL2_DLL;HL2_EPISODIC;USES_SAVERESTORE" + } +} + +$Project "Server (Episodic)" +{ + $Folder "Source Files" + { + $File "ai_eventresponse.cpp" + $File "ai_eventresponse.h" + $File "ai_relationship.cpp" + $File "base_gameinterface.cpp" + $File "basegrenade_concussion.cpp" + $File "basegrenade_contact.cpp" + $File "basegrenade_timed.cpp" + $File "grenadethrown.cpp" + $File "grenadethrown.h" + $File "h_cycler.cpp" + $File "h_cycler.h" + $File "logic_achievement.cpp" + $File "monstermaker.cpp" + $File "monstermaker.h" + $File "physics_bone_follower.h" + $File "$SRCDIR\game\shared\ragdoll_shared.h" + $File "$SRCDIR\game\shared\solidsetdefaults.h" + $File "$SRCDIR\game\shared\hl2\survival_gamerules.cpp" + $File "team_spawnpoint.cpp" + $File "team_spawnpoint.h" + $File "$SRCDIR\game\shared\touchlink.h" + $File "vehicle_choreo_generic.cpp" + $File "$SRCDIR\game\shared\vehicle_choreo_generic_shared.h" + $File "$SRCDIR\game\shared\weapon_parse_default.cpp" + + $Folder "HL2 DLL" + { + $File "$SRCDIR\game\shared\episodic\achievements_ep1.cpp" + $File "$SRCDIR\game\shared\episodic\achievements_ep2.cpp" + $File "$SRCDIR\game\shared\episodic\achievements_epx.cpp" + $File "hl2\ai_allymanager.cpp" + $File "hl2\ai_behavior_actbusy.cpp" + $File "hl2\ai_behavior_actbusy.h" + $File "episodic\ai_behavior_alyx_injured.cpp" + $File "episodic\ai_behavior_alyx_injured.h" + $File "hl2\ai_behavior_functank.cpp" + $File "hl2\ai_behavior_functank.h" + $File "hl2\ai_behavior_holster.cpp" + $File "hl2\ai_behavior_holster.h" + $File "hl2\ai_behavior_operator.cpp" + $File "hl2\ai_behavior_operator.h" + $File "ai_behavior_passenger.cpp" + $File "ai_behavior_passenger.h" + $File "episodic\ai_behavior_passenger_companion.cpp" + $File "episodic\ai_behavior_passenger_companion.h" + $File "episodic\ai_behavior_passenger_zombie.cpp" + $File "episodic\ai_behavior_passenger_zombie.h" + $File "hl2\ai_behavior_police.cpp" + $File "hl2\ai_behavior_police.h" + $File "hl2\ai_goal_police.cpp" + $File "hl2\ai_goal_police.h" + $File "hl2\ai_interactions.h" + $File "hl2\ai_spotlight.cpp" + $File "hl2\ai_spotlight.h" + $File "hl2\antlion_dust.cpp" + $File "hl2\antlion_dust.h" + $File "hl2\antlion_maker.cpp" + $File "hl2\antlion_maker.h" + $File "hl2\ar2_explosion.cpp" + $File "hl2\ar2_explosion.h" + $File "basebludgeonweapon.cpp" + $File "basebludgeonweapon.h" + $File "hl2\basehlcombatweapon.cpp" + $File "hl2\basehlcombatweapon.h" + $File "$SRCDIR\game\shared\hl2\basehlcombatweapon_shared.cpp" + $File "$SRCDIR\game\shared\hl2\basehlcombatweapon_shared.h" + $File "hl2\cbasehelicopter.cpp" + $File "hl2\cbasehelicopter.h" + $File "hl2\cbasespriteprojectile.cpp" + $File "hl2\cbasespriteprojectile.h" + $File "hl2\citadel_effects.cpp" + $File "$SRCDIR\game\shared\hl2\citadel_effects_shared.h" + $File "hl2\combine_mine.cpp" + $File "hl2\combine_mine.h" + $File "hl2\energy_wave.h" + $File "hl2\env_alyxemp.cpp" + $File "$SRCDIR\game\shared\hl2\env_alyxemp_shared.h" + $File "hl2\env_headcrabcanister.cpp" + $File "$SRCDIR\game\shared\hl2\env_headcrabcanister_shared.cpp" + $File "$SRCDIR\game\shared\hl2\env_headcrabcanister_shared.h" + $File "hl2\env_speaker.cpp" + $File "hl2\env_starfield.cpp" + $File "hl2\Func_Monitor.cpp" + $File "hl2\func_recharge.cpp" + $File "hl2\func_tank.cpp" + $File "hl2\func_tank.h" + $File "hl2\grenade_ar2.cpp" + $File "hl2\grenade_ar2.h" + $File "hl2\grenade_bugbait.cpp" + $File "hl2\grenade_bugbait.h" + $File "hl2\grenade_frag.cpp" + $File "hl2\grenade_frag.h" + $File "hl2\grenade_spit.cpp" + $File "hl2\grenade_spit.h" + $File "hl2\hl2_ai_network.cpp" + $File "hl2\hl2_client.cpp" + $File "hl2\hl2_eventlog.cpp" + $File "$SRCDIR\game\shared\hl2\hl2_gamerules.cpp" + $File "$SRCDIR\game\shared\hl2\hl2_gamerules.h" + $File "hl2\hl2_player.cpp" + $File "hl2\hl2_player.h" + $File "$SRCDIR\game\shared\hl2\hl2_player_shared.h" + $File "hl2\hl2_playerlocaldata.cpp" + $File "hl2\hl2_playerlocaldata.h" + $File "$SRCDIR\game\shared\hl2\hl2_shareddefs.h" + $File "hl2\hl2_triggers.cpp" + $File "$SRCDIR\game\shared\hl2\hl2_usermessages.cpp" + $File "$SRCDIR\game\shared\hl2\hl_gamemovement.cpp" + $File "$SRCDIR\game\shared\hl2\hl_gamemovement.h" + $File "$SRCDIR\game\shared\hl2\hl_movedata.h" + $File "hl2\hl_playermove.cpp" + $File "hl2\info_darknessmode_lightsource.cpp" + $File "hl2\info_darknessmode_lightsource.h" + $File "hl2\info_teleporter_countdown.cpp" + $File "hl2\item_ammo.cpp" + $File "hl2\item_battery.cpp" + $File "hl2\item_dynamic_resupply.cpp" + $File "hl2\item_dynamic_resupply.h" + $File "hl2\item_healthkit.cpp" + $File "hl2\item_itemcrate.cpp" + $File "hl2\item_suit.cpp" + $File "hl2\look_door.cpp" + $File "hl2\monster_dummy.cpp" + $File "$SRCDIR\game\shared\episodic\npc_advisor_shared.h" + $File "episodic\npc_advisor.cpp" + $File "hl2\npc_alyx_episodic.cpp" + $File "hl2\npc_alyx_episodic.h" + $File "hl2\npc_antlion.cpp" + $File "hl2\npc_antlion.h" + $File "hl2\npc_antlionguard.cpp" + $File "hl2\npc_antliongrub.cpp" + $File "hl2\npc_apcdriver.cpp" + $File "hl2\npc_attackchopper.cpp" + $File "hl2\npc_attackchopper.h" + $File "hl2\npc_barnacle.cpp" + $File "hl2\npc_barnacle.h" + $File "hl2\npc_barney.cpp" + $File "hl2\npc_basescanner.cpp" + $File "hl2\npc_basescanner.h" + $File "hl2\npc_BaseZombie.cpp" + $File "hl2\npc_BaseZombie.h" + $File "hl2\npc_breen.cpp" + $File "hl2\npc_bullseye.cpp" + $File "hl2\npc_bullseye.h" + $File "hl2\npc_citizen17.cpp" + $File "hl2\npc_citizen17.h" + $File "episodic\npc_combine_cannon.cpp" + $File "hl2\npc_combine.cpp" + $File "hl2\npc_combine.h" + $File "hl2\npc_combinecamera.cpp" + $File "hl2\npc_combinedropship.cpp" + $File "hl2\npc_combinegunship.cpp" + $File "hl2\npc_combines.cpp" + $File "hl2\npc_combines.h" + $File "hl2\npc_cranedriver.cpp" + $File "hl2\npc_crow.cpp" + $File "hl2\npc_crow.h" + $File "hl2\npc_dog.cpp" + $File "hl2\npc_eli.cpp" + $File "hl2\npc_enemyfinder.cpp" + $File "hl2\npc_fastzombie.cpp" + $File "hl2\npc_fisherman.cpp" + $File "hl2\npc_gman.cpp" + $File "hl2\npc_headcrab.cpp" + $File "hl2\npc_headcrab.h" + $File "episodic\npc_hunter.cpp" + $File "episodic\npc_hunter.h" + $File "hl2\npc_ichthyosaur.cpp" + $File "hl2\npc_kleiner.cpp" + $File "hl2\npc_launcher.cpp" + $File "episodic\npc_magnusson.cpp" + $File "hl2\npc_manhack.cpp" + $File "hl2\npc_manhack.h" + $File "hl2\npc_metropolice.cpp" + $File "hl2\npc_metropolice.h" + $File "hl2\npc_monk.cpp" + $File "hl2\npc_mossman.cpp" + $File "hl2\npc_playercompanion.cpp" + $File "hl2\npc_playercompanion.h" + $File "hl2\npc_PoisonZombie.cpp" + $File "hl2\npc_rollermine.cpp" + $File "hl2\npc_rollermine.h" + $File "hl2\npc_scanner.cpp" + $File "hl2\npc_scanner.h" + $File "hl2\npc_stalker.cpp" + $File "hl2\npc_stalker.h" + $File "hl2\npc_strider.cpp" + $File "hl2\npc_strider.h" + $File "npc_talker.cpp" + $File "npc_talker.h" + $File "hl2\npc_turret_ceiling.cpp" + $File "hl2\npc_turret_floor.cpp" + $File "hl2\npc_turret_ground.cpp" + $File "hl2\npc_vortigaunt_episodic.cpp" + $File "hl2\npc_vortigaunt_episodic.h" + $File "hl2\npc_zombie.cpp" + $File "hl2\npc_zombine.cpp" + $File "hl2\point_apc_controller.cpp" + $File "hl2\prop_combine_ball.cpp" + $File "hl2\prop_combine_ball.h" + $File "episodic\prop_scalable.cpp" + $File "hl2\prop_thumper.cpp" + $File "hl2\proto_sniper.cpp" + $File "hl2\rotorwash.cpp" + $File "hl2\rotorwash.h" + $File "hl2\script_intro.cpp" + $File "hl2\script_intro.h" + $File "$SRCDIR\game\shared\script_intro_shared.cpp" + $File "hl2\vehicle_airboat.cpp" + $File "hl2\vehicle_apc.h" + $File "hl2\vehicle_cannon.cpp" + $File "hl2\vehicle_crane.cpp" + $File "hl2\vehicle_crane.h" + $File "hl2\vehicle_jeep.cpp" + $File "episodic\vehicle_jeep_episodic.cpp" + $File "episodic\vehicle_jeep_episodic.h" + $File "hl2\vehicle_prisoner_pod.cpp" + $File "hl2\vehicle_viewcontroller.cpp" + $File "hl2\weapon_357.cpp" + $File "hl2\weapon_alyxgun.cpp" + $File "hl2\weapon_alyxgun.h" + $File "hl2\weapon_annabelle.cpp" + $File "hl2\weapon_ar2.cpp" + $File "hl2\weapon_ar2.h" + $File "hl2\weapon_bugbait.cpp" + $File "hl2\weapon_citizenpackage.cpp" + $File "hl2\weapon_citizenpackage.h" + $File "hl2\weapon_crossbow.cpp" + $File "hl2\weapon_crowbar.cpp" + $File "hl2\weapon_crowbar.h" + $File "weapon_cubemap.cpp" + $File "hl2\weapon_frag.cpp" + $File "hl2\weapon_physcannon.cpp" + $File "hl2\weapon_physcannon.h" + $File "hl2\weapon_pistol.cpp" + $File "hl2\weapon_rpg.cpp" + $File "hl2\weapon_rpg.h" + $File "hl2\weapon_shotgun.cpp" + $File "hl2\weapon_smg1.cpp" + $File "episodic\weapon_striderbuster.cpp" + $File "episodic\weapon_striderbuster.h" + $File "hl2\weapon_stunstick.cpp" [!$MAPBASE] // See server_mapbase.vpc + $File "hl2\weapon_stunstick.h" [!$MAPBASE] // See server_mapbase.vpc + $File "episodic\ep1_gamestats.h" + $File "episodic\ep2_gamestats.h" + $File "episodic\ep1_gamestats.cpp" \ + "episodic\ep2_gamestats.cpp" + { + $Configuration + { + $Compiler + { + $Create/UsePrecompiledHeader "Not Using Precompiled Headers" + } + } + } + $File "hl2\func_bulletshield.cpp" + $File "hl2\func_bulletshield.h" + $File "episodic\npc_puppet.cpp" + + + $Folder "unused" + { + $File "hl2\grenade_beam.cpp" + $File "hl2\grenade_beam.h" + $File "hl2\grenade_homer.cpp" + $File "hl2\grenade_homer.h" + $File "hl2\grenade_pathfollower.cpp" + $File "hl2\grenade_pathfollower.h" + $File "hl2\npc_missiledefense.cpp" + $File "hl2\vehicle_apc.cpp" + $File "hl2\weapon_cguard.cpp" + $File "hl2\weapon_flaregun.cpp" + $File "hl2\weapon_flaregun.h" + } + } + } +} diff --git a/sp/src/game/server/server_hl2.vpc b/sp/src/game/server/server_hl2.vpc new file mode 100644 index 00000000..d5b83f45 --- /dev/null +++ b/sp/src/game/server/server_hl2.vpc @@ -0,0 +1,269 @@ +//----------------------------------------------------------------------------- +// SERVER_HL2.VPC +// +// Project Script +//----------------------------------------------------------------------------- + +$Macro SRCDIR "..\.." +$Macro GAMENAME "hl2" [!$SOURCESDK] +$Macro GAMENAME "mod_hl2" [$SOURCESDK] + +$Include "$SRCDIR\game\server\server_base.vpc" + +$Configuration +{ + $Compiler + { + $AdditionalIncludeDirectories "$BASE;$SRCDIR\game\shared\hl2;.\hl2" + $PreprocessorDefinitions "$BASE;HL2_DLL;USES_SAVERESTORE" + } +} + +$Project "Server (HL2)" +{ + $Folder "Source Files" + { + $File "ai_eventresponse.cpp" + $File "ai_eventresponse.h" + $File "ai_relationship.cpp" + $File "base_gameinterface.cpp" + $File "basegrenade_concussion.cpp" + $File "basegrenade_contact.cpp" + $File "basegrenade_timed.cpp" + $File "EntityFlame.h" + $File "hl2\Func_Monitor.cpp" + $File "grenadethrown.cpp" + $File "grenadethrown.h" + $File "h_cycler.cpp" + $File "h_cycler.h" + $File "logic_achievement.cpp" + $File "monstermaker.cpp" + $File "monstermaker.h" + $File "physics_bone_follower.h" + $File "$SRCDIR\game\shared\ragdoll_shared.h" + $File "$SRCDIR\game\shared\solidsetdefaults.h" + $File "$SRCDIR\game\shared\hl2\survival_gamerules.cpp" + $File "team_spawnpoint.cpp" + $File "team_spawnpoint.h" + $File "$SRCDIR\game\shared\touchlink.h" + $File "vehicle_choreo_generic.cpp" + $File "$SRCDIR\game\shared\vehicle_choreo_generic_shared.h" + $File "$SRCDIR\game\shared\weapon_parse_default.cpp" + + $Folder "HL2 DLL" + { + $File "$SRCDIR\game\shared\hl2\achievements_hl2.cpp" + $File "hl2\ai_allymanager.cpp" + $File "hl2\ai_behavior_actbusy.cpp" + $File "hl2\ai_behavior_actbusy.h" + $File "hl2\ai_behavior_functank.cpp" + $File "hl2\ai_behavior_functank.h" + $File "hl2\ai_behavior_holster.cpp" + $File "hl2\ai_behavior_holster.h" + $File "hl2\ai_behavior_operator.cpp" + $File "hl2\ai_behavior_operator.h" + $File "hl2\ai_behavior_police.cpp" + $File "hl2\ai_behavior_police.h" + $File "hl2\ai_goal_police.cpp" + $File "hl2\ai_goal_police.h" + $File "hl2\ai_interactions.h" + $File "hl2\ai_spotlight.cpp" + $File "hl2\ai_spotlight.h" + $File "hl2\antlion_dust.cpp" + $File "hl2\antlion_dust.h" + $File "hl2\antlion_maker.cpp" + $File "hl2\antlion_maker.h" + $File "hl2\ar2_explosion.cpp" + $File "hl2\ar2_explosion.h" + $File "basebludgeonweapon.cpp" + $File "basebludgeonweapon.h" + $File "hl2\basehlcombatweapon.cpp" + $File "hl2\basehlcombatweapon.h" + $File "$SRCDIR\game\shared\hl2\basehlcombatweapon_shared.cpp" + $File "$SRCDIR\game\shared\hl2\basehlcombatweapon_shared.h" + $File "hl2\cbasehelicopter.cpp" + $File "hl2\cbasehelicopter.h" + $File "hl2\cbasespriteprojectile.cpp" + $File "hl2\cbasespriteprojectile.h" + $File "hl2\citadel_effects.cpp" + $File "$SRCDIR\game\shared\hl2\citadel_effects_shared.h" + $File "hl2\combine_mine.cpp" + $File "hl2\combine_mine.h" + $File "hl2\energy_wave.h" + $File "hl2\env_alyxemp.cpp" + $File "$SRCDIR\game\shared\hl2\env_alyxemp_shared.h" + $File "hl2\env_headcrabcanister.cpp" + $File "$SRCDIR\game\shared\hl2\env_headcrabcanister_shared.cpp" + $File "$SRCDIR\game\shared\hl2\env_headcrabcanister_shared.h" + $File "hl2\env_speaker.cpp" + $File "hl2\env_speaker.h" + $File "hl2\env_starfield.cpp" + $File "hl2\func_recharge.cpp" + $File "hl2\func_tank.cpp" + $File "hl2\func_tank.h" + $File "hl2\grenade_ar2.cpp" + $File "hl2\grenade_ar2.h" + $File "hl2\grenade_bugbait.cpp" + $File "hl2\grenade_bugbait.h" + $File "hl2\grenade_frag.cpp" + $File "hl2\grenade_frag.h" + $File "hl2\hl2_ai_network.cpp" + $File "hl2\hl2_client.cpp" + $File "hl2\hl2_eventlog.cpp" + $File "$SRCDIR\game\shared\hl2\hl2_gamerules.cpp" + $File "$SRCDIR\game\shared\hl2\hl2_gamerules.h" + $File "hl2\hl2_gamestats.cpp" + $File "hl2\hl2_gamestats.h" + $File "hl2\hl2_player.cpp" + $File "hl2\hl2_player.h" + $File "$SRCDIR\game\shared\hl2\hl2_player_shared.h" + $File "hl2\hl2_playerlocaldata.cpp" + $File "hl2\hl2_playerlocaldata.h" + $File "$SRCDIR\game\shared\hl2\hl2_shareddefs.h" + $File "hl2\hl2_triggers.cpp" + $File "$SRCDIR\game\shared\hl2\hl2_usermessages.cpp" + $File "$SRCDIR\game\shared\hl2\hl_gamemovement.cpp" + $File "$SRCDIR\game\shared\hl2\hl_gamemovement.h" + $File "$SRCDIR\game\shared\hl2\hl_movedata.h" + $File "hl2\hl_playermove.cpp" + $File "hl2\info_darknessmode_lightsource.cpp" + $File "hl2\info_darknessmode_lightsource.h" + $File "hl2\info_teleporter_countdown.cpp" + $File "hl2\item_ammo.cpp" + $File "hl2\item_battery.cpp" + $File "hl2\item_dynamic_resupply.cpp" + $File "hl2\item_dynamic_resupply.h" + $File "hl2\item_healthkit.cpp" + $File "hl2\item_itemcrate.cpp" + $File "hl2\item_suit.cpp" + $File "hl2\look_door.cpp" + $File "hl2\monster_dummy.cpp" + $File "hl2\npc_alyx.cpp" + $File "hl2\npc_alyx.h" + $File "hl2\npc_alyx_episodic.h" + $File "hl2\npc_antlion.cpp" + $File "hl2\npc_antlion.h" + $File "hl2\npc_antlionguard.cpp" + $File "hl2\npc_apcdriver.cpp" + $File "hl2\npc_attackchopper.cpp" + $File "hl2\npc_attackchopper.h" + $File "hl2\npc_barnacle.cpp" + $File "hl2\npc_barnacle.h" + $File "hl2\npc_barney.cpp" + $File "hl2\npc_basescanner.cpp" + $File "hl2\npc_basescanner.h" + $File "hl2\npc_BaseZombie.cpp" + $File "hl2\npc_BaseZombie.h" + $File "hl2\npc_blob.cpp" + $File "hl2\npc_breen.cpp" + $File "hl2\npc_bullseye.cpp" + $File "hl2\npc_bullseye.h" + $File "hl2\npc_citizen17.cpp" + $File "hl2\npc_citizen17.h" + $File "hl2\npc_combine.cpp" + $File "hl2\npc_combine.h" + $File "hl2\npc_combinecamera.cpp" + $File "hl2\npc_combinedropship.cpp" + $File "hl2\npc_combinegunship.cpp" + $File "hl2\npc_combines.cpp" + $File "hl2\npc_combines.h" + $File "hl2\npc_cranedriver.cpp" + $File "hl2\npc_crow.cpp" + $File "hl2\npc_crow.h" + $File "hl2\npc_dog.cpp" + $File "hl2\npc_eli.cpp" + $File "hl2\npc_enemyfinder.cpp" + $File "hl2\npc_fastzombie.cpp" + $File "hl2\npc_fisherman.cpp" + $File "hl2\npc_gman.cpp" + $File "hl2\npc_headcrab.cpp" + $File "hl2\npc_headcrab.h" + $File "hl2\npc_ichthyosaur.cpp" + $File "hl2\npc_kleiner.cpp" + $File "hl2\npc_launcher.cpp" + $File "hl2\npc_manhack.cpp" + $File "hl2\npc_manhack.h" + $File "hl2\npc_metropolice.cpp" + $File "hl2\npc_metropolice.h" + $File "hl2\npc_monk.cpp" + $File "hl2\npc_mossman.cpp" + $File "hl2\npc_playercompanion.cpp" + $File "hl2\npc_playercompanion.h" + $File "hl2\npc_PoisonZombie.cpp" + $File "hl2\npc_rollermine.cpp" + $File "hl2\npc_rollermine.h" + $File "hl2\npc_scanner.cpp" + $File "hl2\npc_scanner.h" + $File "hl2\npc_stalker.cpp" + $File "hl2\npc_stalker.h" + $File "hl2\npc_strider.cpp" + $File "hl2\npc_strider.h" + $File "npc_talker.cpp" + $File "npc_talker.h" + $File "hl2\npc_turret_ceiling.cpp" + $File "hl2\npc_turret_floor.cpp" + $File "hl2\npc_turret_floor.h" + $File "hl2\npc_turret_ground.cpp" + $File "hl2\npc_turret_ground.h" + $File "hl2\npc_vortigaunt_episodic.cpp" + $File "hl2\npc_vortigaunt_episodic.h" + $File "hl2\npc_zombie.cpp" + $File "hl2\point_apc_controller.cpp" + $File "hl2\prop_combine_ball.cpp" + $File "hl2\prop_combine_ball.h" + $File "hl2\prop_thumper.cpp" + $File "hl2\proto_sniper.cpp" + $File "hl2\rotorwash.cpp" + $File "hl2\rotorwash.h" + $File "hl2\script_intro.cpp" + $File "hl2\script_intro.h" + $File "$SRCDIR\game\shared\script_intro_shared.cpp" + $File "hl2\vehicle_airboat.cpp" + $File "hl2\vehicle_apc.h" + $File "hl2\vehicle_cannon.cpp" + $File "hl2\vehicle_crane.cpp" + $File "hl2\vehicle_crane.h" + $File "hl2\vehicle_jeep.cpp" + $File "hl2\vehicle_prisoner_pod.cpp" + $File "hl2\vehicle_viewcontroller.cpp" + $File "hl2\weapon_357.cpp" + $File "hl2\weapon_alyxgun.cpp" + $File "hl2\weapon_alyxgun.h" + $File "hl2\weapon_annabelle.cpp" + $File "hl2\weapon_ar2.cpp" + $File "hl2\weapon_ar2.h" + $File "hl2\weapon_bugbait.cpp" + $File "hl2\weapon_citizenpackage.cpp" + $File "hl2\weapon_citizenpackage.h" + $File "hl2\weapon_crossbow.cpp" + $File "hl2\weapon_crowbar.cpp" + $File "hl2\weapon_crowbar.h" + $File "weapon_cubemap.cpp" + $File "hl2\weapon_frag.cpp" + $File "hl2\weapon_physcannon.cpp" + $File "hl2\weapon_physcannon.h" + $File "hl2\weapon_pistol.cpp" + $File "hl2\weapon_rpg.cpp" + $File "hl2\weapon_rpg.h" + $File "hl2\weapon_shotgun.cpp" + $File "hl2\weapon_smg1.cpp" + $File "hl2\weapon_stunstick.cpp" [!$MAPBASE] // See server_mapbase.vpc + $File "hl2\weapon_stunstick.h" [!$MAPBASE] // See server_mapbase.vpc + + $Folder "Unused" + { + $File "hl2\grenade_beam.cpp" + $File "hl2\grenade_beam.h" + $File "hl2\grenade_homer.cpp" + $File "hl2\grenade_homer.h" + $File "hl2\grenade_pathfollower.cpp" + $File "hl2\grenade_pathfollower.h" + $File "hl2\npc_missiledefense.cpp" + $File "hl2\vehicle_apc.cpp" + $File "hl2\weapon_cguard.cpp" + $File "hl2\weapon_flaregun.cpp" + $File "hl2\weapon_flaregun.h" + } + } + } +} diff --git a/sp/src/game/server/server_mapbase.vpc b/sp/src/game/server/server_mapbase.vpc new file mode 100644 index 00000000..6b2ec779 --- /dev/null +++ b/sp/src/game/server/server_mapbase.vpc @@ -0,0 +1,111 @@ +//----------------------------------------------------------------------------- +// SERVER_MAPBASE.VPC +// +// Project Base Script +//----------------------------------------------------------------------------- + +$Configuration +{ + $Compiler + { + $PreprocessorDefinitions "$BASE;ASW_PROJECTED_TEXTURES;DYNAMIC_RTT_SHADOWS;GLOWS_ENABLE" + $PreprocessorDefinitions "$BASE;MAPBASE_VSCRIPT" [$MAPBASE_VSCRIPT] + } +} + +$Project +{ + $Folder "Source Files" + { + $File "logic_random_outputs.cpp" + $File "point_entity_finder.cpp" + $File "env_projectedtexture.h" + $File "env_global_light.cpp" + $File "skyboxswapper.cpp" + $File "env_instructor_hint.cpp" + $File "postprocesscontroller.cpp" + $File "postprocesscontroller.h" + $File "env_dof_controller.cpp" + $File "env_dof_controller.h" + $File "logic_playmovie.cpp" + $File "movie_display.cpp" + + $Folder "Mapbase" + { + $File "$SRCDIR\game\shared\mapbase\mapbase_shared.cpp" + $File "$SRCDIR\game\shared\mapbase\mapbase_usermessages.cpp" + $File "$SRCDIR\game\shared\mapbase\mapbase_rpc.cpp" + $File "$SRCDIR\game\shared\mapbase\mapbase_game_log.cpp" + $File "$SRCDIR\game\shared\mapbase\MapEdit.cpp" + $File "$SRCDIR\game\shared\mapbase\MapEdit.h" + $File "$SRCDIR\game\shared\mapbase\matchers.cpp" + $File "$SRCDIR\game\shared\mapbase\matchers.h" + $File "$SRCDIR\game\shared\mapbase\vscript_funcs_shared.cpp" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\vscript_funcs_shared.h" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\vscript_singletons.cpp" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\vscript_singletons.h" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\vscript_funcs_hl2.cpp" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\vscript_consts_shared.cpp" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\vscript_consts_weapons.cpp" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\weapon_custom_scripted.cpp" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\weapon_custom_scripted.h" [$MAPBASE_VSCRIPT] + $File "$SRCDIR\game\shared\mapbase\logic_script_client.cpp" [$MAPBASE_VSCRIPT] + + $File "mapbase\ai_grenade.cpp" + $File "mapbase\ai_grenade.h" + $File "mapbase\ai_monitor.cpp" + $File "mapbase\ai_weaponmodifier.cpp" + $File "mapbase\closecaption_entity.cpp" + $File "mapbase\datadesc_mod.cpp" + $File "mapbase\datadesc_mod.h" + $File "mapbase\expandedrs_combine.h" + $File "mapbase\func_clientclip.cpp" + $File "mapbase\func_fake_worldportal.cpp" + $File "mapbase\GlobalStrings.cpp" + $File "mapbase\GlobalStrings.h" + $File "mapbase\logic_externaldata.cpp" + $File "mapbase\logic_skill.cpp" + $File "mapbase\point_advanced_finder.cpp" + $File "mapbase\point_copy_size.cpp" + $File "mapbase\point_damageinfo.cpp" + $File "mapbase\point_entity_replace.cpp" + //$File "mapbase\point_physics_control.cpp" // Backlogged + $File "mapbase\point_projectile.cpp" + $File "mapbase\point_radiation_source.cpp" + $File "mapbase\point_glow.cpp" + $File "mapbase\SystemConvarMod.cpp" + $File "mapbase\SystemConvarMod.h" + $File "mapbase\variant_tools.h" + $File "mapbase\vgui_text_display.cpp" + + $File "mapbase\logic_eventlistener.cpp" + $File "mapbase\logic_register_activator.cpp" + } + + $Folder "HL2 DLL" + { + // Original stunstick files are conditional'd out in the HL2 VPCs + $File "$SRCDIR\game\shared\hl2mp\weapon_stunstick.cpp" + $File "$SRCDIR\game\shared\hl2mp\weapon_stunstick.h" + } + + $Folder "HL2MP" + { + $Folder "Weapons" + { + $File "hl2mp\grenade_satchel.cpp" + $File "hl2mp\grenade_satchel.h" + $File "hl2mp\grenade_tripmine.cpp" + $File "hl2mp\grenade_tripmine.h" + + $File "$SRCDIR\game\shared\hl2mp\weapon_slam.cpp" + $File "$SRCDIR\game\shared\hl2mp\weapon_slam.h" + } + } + } + + $Folder "Link Libraries" + { + $Lib "vscript" [$MAPBASE_VSCRIPT] + } +} diff --git a/sp/src/game/server/serverbenchmark_base.cpp b/sp/src/game/server/serverbenchmark_base.cpp new file mode 100644 index 00000000..dbe7269a --- /dev/null +++ b/sp/src/game/server/serverbenchmark_base.cpp @@ -0,0 +1,424 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "serverbenchmark_base.h" +#include "props.h" +#include "filesystem.h" +#include "tier0/icommandline.h" + + +// Server benchmark. Only works on specified maps. +// Lasts for N ticks. +// Enable sv_stressbots. +// Create 20 players and move them around and have them shoot. +// At the end, report the # seconds it took to complete the test. +// Don't start measuring for the first N ticks to account for HD load. + +static ConVar sv_benchmark_numticks( "sv_benchmark_numticks", "3300", 0, "If > 0, then it only runs the benchmark for this # of ticks." ); +static ConVar sv_benchmark_autovprofrecord( "sv_benchmark_autovprofrecord", "0", 0, "If running a benchmark and this is set, it will record a vprof file over the duration of the benchmark with filename benchmark.vprof." ); + +static float s_flBenchmarkStartWaitSeconds = 3; // Wait this many seconds after level load before starting the benchmark. + +static int s_nBenchmarkBotsToCreate = 22; // Create this many bots. +static int s_nBenchmarkBotCreateInterval = 50; // Create a bot every N ticks. + +static int s_nBenchmarkPhysicsObjects = 100; // Create this many physics objects. + + +static double Benchmark_ValidTime() +{ + bool bOld = Plat_IsInBenchmarkMode(); + Plat_SetBenchmarkMode( false ); + double flRet = Plat_FloatTime(); + Plat_SetBenchmarkMode( bOld ); + + return flRet; +} + + +// ---------------------------------------------------------------------------------------------- // +// CServerBenchmark implementation. +// ---------------------------------------------------------------------------------------------- // +class CServerBenchmark : public IServerBenchmark +{ +public: + CServerBenchmark() + { + m_BenchmarkState = BENCHMARKSTATE_NOT_RUNNING; + + // The benchmark should always have the same seed and do exactly the same thing on the same ticks. + m_RandomStream.SetSeed( 1111 ); + } + + virtual bool StartBenchmark() + { + bool bBenchmark = (CommandLine()->FindParm( "-sv_benchmark" ) != 0); + + return InternalStartBenchmark( bBenchmark, s_flBenchmarkStartWaitSeconds ); + } + + // nBenchmarkMode: 0 = no benchmark + // 1 = benchmark + // 2 = exit out afterwards and write sv_benchmark.txt + bool InternalStartBenchmark( int nBenchmarkMode, float flCountdown ) + { + bool bWasRunningBenchmark = (m_BenchmarkState != BENCHMARKSTATE_NOT_RUNNING); + + if ( nBenchmarkMode == 0 ) + { + // Tear down the previous benchmark environment if necessary. + if ( bWasRunningBenchmark ) + EndBenchmark(); + return false; + } + + m_nBenchmarkMode = nBenchmarkMode; + + if ( !CServerBenchmarkHook::s_pBenchmarkHook ) + Error( "This game doesn't support server benchmarks (no CServerBenchmarkHook found)." ); + + m_BenchmarkState = BENCHMARKSTATE_START_WAIT; + m_flBenchmarkStartTime = Plat_FloatTime(); + m_flBenchmarkStartWaitTime = flCountdown; + + m_nBotsCreated = 0; + m_nStartWaitCounter = -1; + + // Setup the benchmark environment. + engine->SetDedicatedServerBenchmarkMode( true ); // Run 1 tick per frame and ignore all timing stuff. + + // Tell the game-specific hook that we're starting. + CServerBenchmarkHook::s_pBenchmarkHook->StartBenchmark(); + CServerBenchmarkHook::s_pBenchmarkHook->GetPhysicsModelNames( m_PhysicsModelNames ); + + return true; + } + + virtual void UpdateBenchmark() + { + // No benchmark running? + if ( m_BenchmarkState == BENCHMARKSTATE_NOT_RUNNING ) + return; + + // Wait a certain number of ticks to start the benchmark. + if ( m_BenchmarkState == BENCHMARKSTATE_START_WAIT ) + { + if ( (Plat_FloatTime() - m_flBenchmarkStartTime) < m_flBenchmarkStartWaitTime ) + { + UpdateStartWaitCounter(); + return; + } + else + { + // Ok, now we're officially starting it. + Msg( "Starting benchmark!\n" ); + m_flLastBenchmarkCounterUpdate = m_flBenchmarkStartTime = Plat_FloatTime(); + m_fl_ValidTime_BenchmarkStartTime = Benchmark_ValidTime(); + m_nBenchmarkStartTick = gpGlobals->tickcount; + m_nLastPhysicsObjectTick = m_nLastPhysicsForceTick = 0; + m_BenchmarkState = BENCHMARKSTATE_RUNNING; + + StartVProfRecord(); + + RandomSeed( 0 ); + m_RandomStream.SetSeed( 0 ); + } + } + + int nTicksRunSoFar = gpGlobals->tickcount - m_nBenchmarkStartTick; + UpdateBenchmarkCounter(); + + // Are we finished with the benchmark? + if ( nTicksRunSoFar >= sv_benchmark_numticks.GetInt() ) + { + EndVProfRecord(); + OutputResults(); + EndBenchmark(); + return; + } + + // Ok, update whatever we're doing in the benchmark. + UpdatePlayerCreation(); + UpdateVPhysicsObjects(); + CServerBenchmarkHook::s_pBenchmarkHook->UpdateBenchmark(); + } + + void StartVProfRecord() + { + if ( sv_benchmark_autovprofrecord.GetInt() ) + { + engine->ServerCommand( "vprof_record_start benchmark\n" ); + engine->ServerExecute(); + } + } + + void EndVProfRecord() + { + if ( sv_benchmark_autovprofrecord.GetInt() ) + { + engine->ServerCommand( "vprof_record_stop\n" ); + engine->ServerExecute(); + } + } + + virtual void EndBenchmark( void ) + { + // Write out the results if we're running the build scripts. + float flRunTime = Benchmark_ValidTime() - m_fl_ValidTime_BenchmarkStartTime; + if ( m_nBenchmarkMode == 2 ) + { + FileHandle_t fh = filesystem->Open( "sv_benchmark_results.txt", "wt", "DEFAULT_WRITE_PATH" ); + + // If this file doesn't get written out, then the build script will generate an email that there's a problem somewhere. + if ( fh ) + { + filesystem->FPrintf( fh, "sv_benchmark := %.2f\n", flRunTime ); + } + filesystem->Close( fh ); + + // Quit out. + engine->ServerCommand( "quit\n" ); + } + + m_BenchmarkState = BENCHMARKSTATE_NOT_RUNNING; + engine->SetDedicatedServerBenchmarkMode( false ); + } + + virtual bool IsLocalBenchmarkPlayer( CBasePlayer *pPlayer ) + { + if ( m_BenchmarkState != BENCHMARKSTATE_NOT_RUNNING ) + { + if( !engine->IsDedicatedServer() && pPlayer->entindex() == 1 ) + return true; + } + + return false; + } + + void UpdateVPhysicsObjects() + { + int nPhysicsObjectInterval = sv_benchmark_numticks.GetInt() / s_nBenchmarkPhysicsObjects; + + int nNextSpawnTick = m_nLastPhysicsObjectTick + nPhysicsObjectInterval; + if ( GetTickOffset() >= nNextSpawnTick ) + { + m_nLastPhysicsObjectTick = nNextSpawnTick; + + if ( m_PhysicsObjects.Count() < s_nBenchmarkPhysicsObjects ) + { + // Find a bot to spawn it from. + CUtlVector curPlayers; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer && (pPlayer->GetFlags() & FL_FAKECLIENT) ) + { + curPlayers.AddToTail( pPlayer ); + } + } + + if ( curPlayers.Count() > 0 && m_PhysicsModelNames.Count() > 0 ) + { + int iModelName = this->RandomInt( 0, m_PhysicsModelNames.Count() - 1 ); + const char *pModelName = m_PhysicsModelNames[iModelName]; + + int iPlayer = this->RandomInt( 0, curPlayers.Count() - 1 ); + + Vector vSpawnPos = curPlayers[iPlayer]->EyePosition() + Vector( 0, 0, 50 ); + + // We'll try 15 locations around the player to spawn this thing. + for ( int i=0; i < 15; i++ ) + { + Vector vOffset( this->RandomFloat( -2000, 2000 ), this->RandomFloat( -2000, 2000 ), 0 ); + CPhysicsProp *pProp = CreatePhysicsProp( pModelName, vSpawnPos, vSpawnPos+vOffset, curPlayers[iPlayer], false, "prop_physics_multiplayer" ); + if ( pProp ) + { + m_PhysicsObjects.AddToTail( pProp ); + pProp->SetAbsVelocity( Vector( this->RandomFloat(-500,500), this->RandomFloat(-500,500), this->RandomFloat(-500,500) ) ); + break; + } + } + } + } + } + + // Give them all a boost periodically. + int nPhysicsForceInterval = sv_benchmark_numticks.GetInt() / 20; + + int nNextForceTick = m_nLastPhysicsForceTick + nPhysicsForceInterval; + if ( GetTickOffset() >= nNextForceTick ) + { + m_nLastPhysicsForceTick = nNextForceTick; + + for ( int i=0; i < m_PhysicsObjects.Count(); i++ ) + { + CBaseEntity *pEnt = m_PhysicsObjects[i]; + if ( pEnt ) + { + IPhysicsObject *pPhysicsObject = pEnt->VPhysicsGetObject(); + if ( pPhysicsObject ) + { + float flAngImpulse = 300000; + float flForce = 500000; + AngularImpulse vAngularImpulse( this->RandomFloat(-flAngImpulse,flAngImpulse), this->RandomFloat(-flAngImpulse,flAngImpulse), this->RandomFloat(flAngImpulse,flAngImpulse) ); + pPhysicsObject->ApplyForceCenter( Vector( this->RandomFloat(-flForce,flForce), this->RandomFloat(-flForce,flForce), this->RandomFloat(0,flForce) ) ); + } + } + } + } + } + + void UpdateStartWaitCounter() + { + int nSecondsLeft = (int)ceil( m_flBenchmarkStartWaitTime - (Plat_FloatTime() - m_flBenchmarkStartTime) ); + if ( m_nStartWaitCounter != nSecondsLeft ) + { + Msg( "Starting benchmark in %d seconds...\n", nSecondsLeft ); + m_nStartWaitCounter = nSecondsLeft; + } + } + + void UpdateBenchmarkCounter() + { + float flCurTime = Plat_FloatTime(); + if ( (flCurTime - m_flLastBenchmarkCounterUpdate) > 3.0f ) + { + m_flLastBenchmarkCounterUpdate = flCurTime; + Msg( "Benchmark: %d%% complete.\n", ((gpGlobals->tickcount - m_nBenchmarkStartTick) * 100) / sv_benchmark_numticks.GetInt() ); + } + } + + virtual bool IsBenchmarkRunning() + { + return (m_BenchmarkState == BENCHMARKSTATE_RUNNING); + } + + virtual int GetTickOffset() + { + if ( m_BenchmarkState == BENCHMARKSTATE_RUNNING ) + { + Assert( gpGlobals->tickcount >= m_nBenchmarkStartTick ); + return gpGlobals->tickcount - m_nBenchmarkStartTick; + } + else + { + return gpGlobals->tickcount; + } + } + + + void UpdatePlayerCreation() + { + if ( m_nBotsCreated >= s_nBenchmarkBotsToCreate ) + return; + + // Spawn the player. + int nTicksRunSoFar = gpGlobals->tickcount - m_nBenchmarkStartTick; + + if ( (nTicksRunSoFar % s_nBenchmarkBotCreateInterval) == 0 ) + { + CServerBenchmarkHook::s_pBenchmarkHook->CreateBot(); + ++m_nBotsCreated; + } + } + + void OutputResults() + { + float flRunTime = Benchmark_ValidTime() - m_fl_ValidTime_BenchmarkStartTime; + + Warning( "------------------ SERVER BENCHMARK RESULTS ------------------\n" ); + Warning( "Total time : %.2f seconds\n", flRunTime ); + Warning( "Num ticks simulated : %d\n", sv_benchmark_numticks.GetInt() ); + Warning( "Ticks per second : %.2f\n", sv_benchmark_numticks.GetInt() / flRunTime ); + Warning( "Benchmark CRC : %d\n", CalculateBenchmarkCRC() ); + Warning( "--------------------------------------------------------------\n" ); + } + + int CalculateBenchmarkCRC() + { + int crc = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer && (pPlayer->GetFlags() & FL_FAKECLIENT) ) + { + crc += pPlayer->GetTeamNumber(); + crc += (int)pPlayer->GetAbsOrigin().x; + crc += (int)pPlayer->GetAbsOrigin().y; + } + } + + return crc; + } + + virtual int RandomInt( int nMin, int nMax ) + { + return m_RandomStream.RandomInt( nMin, nMax ); + } + + virtual float RandomFloat( float nMin, float nMax ) + { + return m_RandomStream.RandomInt( nMin, nMax ); + } + + +private: + + enum EBenchmarkState + { + BENCHMARKSTATE_NOT_RUNNING, + BENCHMARKSTATE_START_WAIT, + BENCHMARKSTATE_RUNNING + }; + EBenchmarkState m_BenchmarkState; + + float m_fl_ValidTime_BenchmarkStartTime; + + float m_flBenchmarkStartTime; + float m_flLastBenchmarkCounterUpdate; + float m_flBenchmarkStartWaitTime; + + int m_nBenchmarkStartTick; + int m_nStartWaitCounter; + int m_nLastPhysicsObjectTick; + int m_nLastPhysicsForceTick; + + int m_nBotsCreated; + CUtlVector< EHANDLE > m_PhysicsObjects; + + CUtlVector m_PhysicsModelNames; + int m_nBenchmarkMode; + + CUniformRandomStream m_RandomStream; +}; + +static CServerBenchmark g_ServerBenchmark; +IServerBenchmark *g_pServerBenchmark = &g_ServerBenchmark; + +CON_COMMAND( sv_benchmark_force_start, "Force start the benchmark. This is only for debugging. It's better to set sv_benchmark to 1 and restart the level." ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + g_ServerBenchmark.InternalStartBenchmark( 1, 1 ); +} + + +// ---------------------------------------------------------------------------------------------- // +// CServerBenchmarkHook implementation. +// ---------------------------------------------------------------------------------------------- // + +CServerBenchmarkHook *CServerBenchmarkHook::s_pBenchmarkHook = NULL; + +CServerBenchmarkHook::CServerBenchmarkHook() +{ + if ( s_pBenchmarkHook ) + Error( "There can only be one CServerBenchmarkHook" ); + + s_pBenchmarkHook = this; +} diff --git a/sp/src/game/server/serverbenchmark_base.h b/sp/src/game/server/serverbenchmark_base.h new file mode 100644 index 00000000..c4cb005c --- /dev/null +++ b/sp/src/game/server/serverbenchmark_base.h @@ -0,0 +1,65 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef SERVERBENCHMARK_BASE_H +#define SERVERBENCHMARK_BASE_H +#ifdef _WIN32 +#pragma once +#endif + + +// The base server code calls into this. +class IServerBenchmark +{ +public: + virtual bool StartBenchmark() = 0; + virtual void UpdateBenchmark() = 0; + virtual void EndBenchmark() = 0; + + virtual bool IsBenchmarkRunning() = 0; + virtual bool IsLocalBenchmarkPlayer( CBasePlayer *pPlayer ) = 0; + + // Game-specific benchmark code should use this. + virtual int RandomInt( int nMin, int nMax ) = 0; + virtual float RandomFloat( float flMin, float flMax ) = 0; + virtual int GetTickOffset() = 0; +}; + +extern IServerBenchmark *g_pServerBenchmark; + + +// +// Each game can derive from this to hook into the server benchmark. +// +// Hooks should always use g_pServerBenchmark->RandomInt/Float to get random numbers +// so the benchmark is deterministic. +// +// If they use an absolute tick number for anything, then they should also call g_pServerBenchmark->GetTickOffset() +// to get a tick count since the start of the benchmark instead of looking at gpGlobals->tickcount. +// +class CServerBenchmarkHook +{ +public: + CServerBenchmarkHook(); + + virtual void StartBenchmark() {} + virtual void UpdateBenchmark() {} + virtual void EndBenchmark() {} + + // Give a list of model names that can be spawned in for physics props during the simulation. + virtual void GetPhysicsModelNames( CUtlVector &modelNames ) = 0; + + // The benchmark will call this to create a bot each time it wants to create a player. + // If you want to manage the bots yourself, you can return NULL here. + virtual CBasePlayer* CreateBot() = 0; + +private: + friend class CServerBenchmark; + static CServerBenchmarkHook *s_pBenchmarkHook; // There can be only one!! +}; + + +#endif // SERVERBENCHMARK_BASE_H diff --git a/sp/src/game/server/shadowcontrol.cpp b/sp/src/game/server/shadowcontrol.cpp new file mode 100644 index 00000000..8db9665d --- /dev/null +++ b/sp/src/game/server/shadowcontrol.cpp @@ -0,0 +1,172 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Shadow control entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//------------------------------------------------------------------------------ +// FIXME: This really should inherit from something more lightweight +//------------------------------------------------------------------------------ + + +//------------------------------------------------------------------------------ +// Purpose : Shadow control entity +//------------------------------------------------------------------------------ +class CShadowControl : public CBaseEntity +{ +public: + DECLARE_CLASS( CShadowControl, CBaseEntity ); + + CShadowControl(); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + int UpdateTransmitState(); + void InputSetAngles( inputdata_t &inputdata ); + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + CNetworkVector( m_shadowDirection ); + CNetworkColor32( m_shadowColor ); + CNetworkVar( float, m_flShadowMaxDist ); + CNetworkVar( bool, m_bDisableShadows ); +#ifdef MAPBASE + CNetworkVar( bool, m_bEnableLocalLightShadows ); +#endif +}; + +LINK_ENTITY_TO_CLASS(shadow_control, CShadowControl); + +BEGIN_DATADESC( CShadowControl ) + + DEFINE_KEYFIELD( m_flShadowMaxDist, FIELD_FLOAT, "distance" ), + DEFINE_KEYFIELD( m_bDisableShadows, FIELD_BOOLEAN, "disableallshadows" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bEnableLocalLightShadows, FIELD_BOOLEAN, "enableshadowsfromlocallights" ), +#endif + + // Inputs + DEFINE_INPUT( m_shadowColor, FIELD_COLOR32, "color" ), + DEFINE_INPUT( m_shadowDirection, FIELD_VECTOR, "direction" ), + DEFINE_INPUT( m_flShadowMaxDist, FIELD_FLOAT, "SetDistance" ), + DEFINE_INPUT( m_bDisableShadows, FIELD_BOOLEAN, "SetShadowsDisabled" ), +#ifdef MAPBASE + DEFINE_INPUT( m_bEnableLocalLightShadows, FIELD_BOOLEAN, "SetShadowsFromLocalLightsEnabled" ), +#endif + + DEFINE_INPUTFUNC( FIELD_STRING, "SetAngles", InputSetAngles ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CShadowControl, DT_ShadowControl) + SendPropVector(SENDINFO(m_shadowDirection), -1, SPROP_NOSCALE ), +#ifdef MAPBASE + /*SendPropInt(SENDINFO(m_shadowColor), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt32 ),*/ + SendPropInt(SENDINFO(m_shadowColor), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), +#else + SendPropInt(SENDINFO(m_shadowColor), 32, SPROP_UNSIGNED), +#endif + SendPropFloat(SENDINFO(m_flShadowMaxDist), 0, SPROP_NOSCALE ), + SendPropBool(SENDINFO(m_bDisableShadows)), +#ifdef MAPBASE + SendPropBool(SENDINFO(m_bEnableLocalLightShadows)), +#endif +END_SEND_TABLE() + + +CShadowControl::CShadowControl() +{ + m_shadowDirection.Init( 0.2, 0.2, -2 ); + m_flShadowMaxDist = 50.0f; + m_shadowColor.Init( 64, 64, 64, 0 ); + m_bDisableShadows = false; +#ifdef MAPBASE + m_bEnableLocalLightShadows = false; +#endif +} + + +//------------------------------------------------------------------------------ +// Purpose : Send even though we don't have a model +//------------------------------------------------------------------------------ +int CShadowControl::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + +bool CShadowControl::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "color" ) ) + { + color32 tmp; + UTIL_StringToColor32( &tmp, szValue ); + m_shadowColor = tmp; + return true; + } + + if ( FStrEq( szKeyName, "angles" ) ) + { + QAngle angles; + UTIL_StringToVector( angles.Base(), szValue ); + if (angles == vec3_angle) + { + angles.Init( 80, 30, 0 ); + } + Vector vForward; + AngleVectors( angles, &vForward ); + m_shadowDirection = vForward; + return true; + } + + // For backward compatibility... + if ( FStrEq( szKeyName, "direction" ) ) + { + // Only use this if angles haven't been set... + if ( fabs(m_shadowDirection->LengthSqr() - 1.0f) > 1e-3 ) + { + Vector vTemp; + UTIL_StringToVector( vTemp.Base(), szValue ); + m_shadowDirection = vTemp; + } + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CShadowControl::Spawn( void ) +{ + Precache(); + SetSolid( SOLID_NONE ); +} + +//------------------------------------------------------------------------------ +// Input values +//------------------------------------------------------------------------------ +void CShadowControl::InputSetAngles( inputdata_t &inputdata ) +{ + const char *pAngles = inputdata.value.String(); + + QAngle angles; + UTIL_StringToVector( angles.Base(), pAngles ); + + Vector vTemp; + AngleVectors( angles, &vTemp ); + m_shadowDirection = vTemp; +} diff --git a/sp/src/game/server/skyboxswapper.cpp b/sp/src/game/server/skyboxswapper.cpp new file mode 100644 index 00000000..40d1f2ba --- /dev/null +++ b/sp/src/game/server/skyboxswapper.cpp @@ -0,0 +1,80 @@ +//=========== Copyright (c) Valve Corporation, All rights reserved. =========== +// +// Simple entity to switch the map's 2D skybox texture when triggered. +// +//============================================================================= + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//----------------------------------------------------------------------------- +// CSkyboxSwapper +//----------------------------------------------------------------------------- +class CSkyboxSwapper : public CServerOnlyPointEntity +{ + DECLARE_CLASS( CSkyboxSwapper, CServerOnlyPointEntity ); +public: + DECLARE_DATADESC(); + + virtual void Spawn( void ); + virtual void Precache( void ); + + void InputTrigger( inputdata_t &inputdata ); + +protected: + string_t m_iszSkyboxName; +}; + +LINK_ENTITY_TO_CLASS(skybox_swapper, CSkyboxSwapper); + +BEGIN_DATADESC( CSkyboxSwapper ) + DEFINE_KEYFIELD( m_iszSkyboxName, FIELD_STRING, "SkyboxName" ), + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "Trigger", InputTrigger), +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: not much +//----------------------------------------------------------------------------- +void CSkyboxSwapper::Spawn( void ) +{ + Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Precache the skybox materials. +//----------------------------------------------------------------------------- +void CSkyboxSwapper::Precache( void ) +{ + if ( Q_strlen( m_iszSkyboxName.ToCStr() ) == 0 ) + { + Warning( "skybox_swapper (%s) has no skybox specified!\n", STRING(GetEntityName()) ); + return; + } + + char name[ MAX_PATH ]; + char *skyboxsuffix[ 6 ] = { "rt", "bk", "lf", "ft", "up", "dn" }; + for ( int i = 0; i < 6; i++ ) + { + Q_snprintf( name, sizeof( name ), "skybox/%s%s", m_iszSkyboxName.ToCStr(), skyboxsuffix[i] ); + PrecacheMaterial( name ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler that triggers the skybox swap. +//----------------------------------------------------------------------------- +void CSkyboxSwapper::InputTrigger( inputdata_t &inputdata ) +{ + static ConVarRef skyname( "sv_skyname", false ); + if ( !skyname.IsValid() ) + { + Warning( "skybox_swapper (%s) trigger input failed - cannot find 'sv_skyname' convar!\n", STRING(GetEntityName()) ); + return; + } + skyname.SetValue( m_iszSkyboxName.ToCStr() ); +} diff --git a/sp/src/game/server/slideshow_display.cpp b/sp/src/game/server/slideshow_display.cpp new file mode 100644 index 00000000..79e78841 --- /dev/null +++ b/sp/src/game/server/slideshow_display.cpp @@ -0,0 +1,625 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements the big scary boom-boom machine Antlions fear. +// +//=============================================================================// + +#include "cbase.h" +#include "EnvMessage.h" +#include "fmtstr.h" +#include "vguiscreen.h" +#include "filesystem.h" + + +#define SLIDESHOW_LIST_BUFFER_MAX 8192 + + +struct SlideKeywordList_t +{ + char szSlideKeyword[64]; +}; + + +class CSlideshowDisplay : public CBaseEntity +{ +public: + + DECLARE_CLASS( CSlideshowDisplay, CBaseEntity ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + virtual ~CSlideshowDisplay(); + + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + + virtual int UpdateTransmitState(); + virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + + virtual void Spawn( void ); + virtual void Precache( void ); + virtual void OnRestore( void ); + + void ScreenVisible( bool bVisible ); + + void Disable( void ); + void Enable( void ); + + void InputDisable( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + + void InputSetDisplayText( inputdata_t &inputdata ); + void InputRemoveAllSlides( inputdata_t &inputdata ); + void InputAddSlides( inputdata_t &inputdata ); + + void InputSetMinSlideTime( inputdata_t &inputdata ); + void InputSetMaxSlideTime( inputdata_t &inputdata ); + + void InputSetCycleType( inputdata_t &inputdata ); + void InputSetNoListRepeats( inputdata_t &inputdata ); + +private: + + // Control panel + void GetControlPanelInfo( int nPanelIndex, const char *&pPanelName ); + void GetControlPanelClassName( int nPanelIndex, const char *&pPanelName ); + void SpawnControlPanels( void ); + void RestoreControlPanels( void ); + void BuildSlideShowImagesList( void ); + +private: + + CNetworkVar( bool, m_bEnabled ); + + CNetworkString( m_szDisplayText, 128 ); + + CNetworkString( m_szSlideshowDirectory, 128 ); + string_t m_String_tSlideshowDirectory; + + CUtlVector m_SlideKeywordList; + CNetworkArray( unsigned char, m_chCurrentSlideLists, 16 ); + + CNetworkVar( float, m_fMinSlideTime ); + CNetworkVar( float, m_fMaxSlideTime ); + + CNetworkVar( int, m_iCycleType ); + CNetworkVar( bool, m_bNoListRepeats ); + + int m_iScreenWidth; + int m_iScreenHeight; + + bool m_bDoFullTransmit; + + typedef CHandle ScreenHandle_t; + CUtlVector m_hScreens; +}; + + +LINK_ENTITY_TO_CLASS( vgui_slideshow_display, CSlideshowDisplay ); + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CSlideshowDisplay ) + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + + DEFINE_AUTO_ARRAY_KEYFIELD( m_szDisplayText, FIELD_CHARACTER, "displaytext" ), + + DEFINE_AUTO_ARRAY( m_szSlideshowDirectory, FIELD_CHARACTER ), + DEFINE_KEYFIELD( m_String_tSlideshowDirectory, FIELD_STRING, "directory" ), + + // DEFINE_FIELD( m_SlideKeywordList, CUtlVector < SlideKeywordList_t* > ), + DEFINE_AUTO_ARRAY( m_chCurrentSlideLists, FIELD_CHARACTER ), + + DEFINE_KEYFIELD( m_fMinSlideTime, FIELD_FLOAT, "minslidetime" ), + DEFINE_KEYFIELD( m_fMaxSlideTime, FIELD_FLOAT, "maxslidetime" ), + + DEFINE_KEYFIELD( m_iCycleType, FIELD_INTEGER, "cycletype" ), + DEFINE_KEYFIELD( m_bNoListRepeats, FIELD_BOOLEAN, "nolistrepeats" ), + + DEFINE_KEYFIELD( m_iScreenWidth, FIELD_INTEGER, "width" ), + DEFINE_KEYFIELD( m_iScreenHeight, FIELD_INTEGER, "height" ), + + //DEFINE_FIELD( m_bDoFullTransmit, FIELD_BOOLEAN ), + + //DEFINE_UTLVECTOR( m_hScreens, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetDisplayText", InputSetDisplayText ), + + DEFINE_INPUTFUNC( FIELD_VOID, "RemoveAllSlides", InputRemoveAllSlides ), + DEFINE_INPUTFUNC( FIELD_STRING, "AddSlides", InputAddSlides ), + + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMinSlideTime", InputSetMinSlideTime ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMaxSlideTime", InputSetMaxSlideTime ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetCycleType", InputSetCycleType ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetNoListRepeats", InputSetNoListRepeats ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CSlideshowDisplay, DT_SlideshowDisplay ) + SendPropBool( SENDINFO(m_bEnabled) ), + SendPropString( SENDINFO( m_szDisplayText ) ), + SendPropString( SENDINFO( m_szSlideshowDirectory ) ), + SendPropArray3( SENDINFO_ARRAY3(m_chCurrentSlideLists), SendPropInt( SENDINFO_ARRAY(m_chCurrentSlideLists), 8, SPROP_UNSIGNED ) ), + SendPropFloat( SENDINFO(m_fMinSlideTime), 11, 0, 0.0f, 20.0f ), + SendPropFloat( SENDINFO(m_fMaxSlideTime), 11, 0, 0.0f, 20.0f ), + SendPropInt( SENDINFO(m_iCycleType), 2, SPROP_UNSIGNED ), + SendPropBool( SENDINFO(m_bNoListRepeats) ), +END_SEND_TABLE() + + +CSlideshowDisplay::~CSlideshowDisplay() +{ + int i; + // Kill the control panels + for ( i = m_hScreens.Count(); --i >= 0; ) + { + DestroyVGuiScreen( m_hScreens[i].Get() ); + } + m_hScreens.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Read in worldcraft data... +//----------------------------------------------------------------------------- +bool CSlideshowDisplay::KeyValue( const char *szKeyName, const char *szValue ) +{ + //!! temp hack, until worldcraft is fixed + // strip the # tokens from (duplicate) key names + char *s = (char *)strchr( szKeyName, '#' ); + if ( s ) + { + *s = '\0'; + } + + // NOTE: Have to do these separate because they set two values instead of one + if( FStrEq( szKeyName, "angles" ) ) + { + Assert( GetMoveParent() == NULL ); + QAngle angles; + UTIL_StringToVector( angles.Base(), szValue ); + + // Because the vgui screen basis is strange (z is front, y is up, x is right) + // we need to rotate the typical basis before applying it + VMatrix mat, rotation, tmp; + MatrixFromAngles( angles, mat ); + MatrixBuildRotationAboutAxis( rotation, Vector( 0, 1, 0 ), 90 ); + MatrixMultiply( mat, rotation, tmp ); + MatrixBuildRotateZ( rotation, 90 ); + MatrixMultiply( tmp, rotation, mat ); + MatrixToAngles( mat, angles ); + SetAbsAngles( angles ); + + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + +int CSlideshowDisplay::UpdateTransmitState() +{ + if ( m_bDoFullTransmit ) + { + m_bDoFullTransmit = false; + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + return SetTransmitState( FL_EDICT_FULLCHECK ); +} + +void CSlideshowDisplay::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + // Are we already marked for transmission? + if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) + return; + + BaseClass::SetTransmit( pInfo, bAlways ); + + // Force our screens to be sent too. + for ( int i=0; i < m_hScreens.Count(); i++ ) + { + CVGuiScreen *pScreen = m_hScreens[i].Get(); + pScreen->SetTransmit( pInfo, bAlways ); + } +} + +void CSlideshowDisplay::Spawn( void ) +{ + Q_strcpy( m_szSlideshowDirectory.GetForModify(), m_String_tSlideshowDirectory.ToCStr() ); + Precache(); + + BaseClass::Spawn(); + + m_bEnabled = false; + + // Clear out selected list + m_chCurrentSlideLists.GetForModify( 0 ) = 0; // Select all slides to begin with + for ( int i = 1; i < 16; ++i ) + m_chCurrentSlideLists.GetForModify( i ) = (unsigned char)-1; + + SpawnControlPanels(); + + ScreenVisible( m_bEnabled ); + + m_bDoFullTransmit = true; +} + +void CSlideshowDisplay::Precache( void ) +{ + BaseClass::Precache(); + + BuildSlideShowImagesList(); + + PrecacheVGuiScreen( "slideshow_display_screen" ); +} + +void CSlideshowDisplay::OnRestore( void ) +{ + BaseClass::OnRestore(); + + BuildSlideShowImagesList(); + + RestoreControlPanels(); + + ScreenVisible( m_bEnabled ); +} + +void CSlideshowDisplay::ScreenVisible( bool bVisible ) +{ + for ( int iScreen = 0; iScreen < m_hScreens.Count(); ++iScreen ) + { + CVGuiScreen *pScreen = m_hScreens[ iScreen ].Get(); + if ( bVisible ) + pScreen->RemoveEffects( EF_NODRAW ); + else + pScreen->AddEffects( EF_NODRAW ); + } +} + +void CSlideshowDisplay::Disable( void ) +{ + if ( !m_bEnabled ) + return; + + m_bEnabled = false; + + ScreenVisible( false ); +} + +void CSlideshowDisplay::Enable( void ) +{ + if ( m_bEnabled ) + return; + + m_bEnabled = true; + + ScreenVisible( true ); +} + + +void CSlideshowDisplay::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +void CSlideshowDisplay::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + + +void CSlideshowDisplay::InputSetDisplayText( inputdata_t &inputdata ) +{ + Q_strcpy( m_szDisplayText.GetForModify(), inputdata.value.String() ); +} + +void CSlideshowDisplay::InputRemoveAllSlides( inputdata_t &inputdata ) +{ + // Clear out selected list + for ( int i = 0; i < 16; ++i ) + m_chCurrentSlideLists.GetForModify( i ) = (unsigned char)-1; +} + +void CSlideshowDisplay::InputAddSlides( inputdata_t &inputdata ) +{ + // Find the list with the current keyword + int iList; + for ( iList = 0; iList < m_SlideKeywordList.Count(); ++iList ) + { + if ( Q_strcmp( m_SlideKeywordList[ iList ]->szSlideKeyword, inputdata.value.String() ) == 0 ) + break; + } + + if ( iList < m_SlideKeywordList.Count() ) + { + // Found the keyword list, so add this index to the selected lists + int iNumCurrentSlideLists; + for ( iNumCurrentSlideLists = 0; iNumCurrentSlideLists < 16; ++iNumCurrentSlideLists ) + { + if ( m_chCurrentSlideLists[ iNumCurrentSlideLists ] == (unsigned char)-1 ) + break; + } + + if ( iNumCurrentSlideLists >= 16 ) + return; + + m_chCurrentSlideLists.GetForModify( iNumCurrentSlideLists ) = iList; + } +} + + +void CSlideshowDisplay::InputSetMinSlideTime( inputdata_t &inputdata ) +{ + m_fMinSlideTime = inputdata.value.Float(); +} + +void CSlideshowDisplay::InputSetMaxSlideTime( inputdata_t &inputdata ) +{ + m_fMaxSlideTime = inputdata.value.Float(); +} + + +void CSlideshowDisplay::InputSetCycleType( inputdata_t &inputdata ) +{ + m_iCycleType = inputdata.value.Int(); +} + +void CSlideshowDisplay::InputSetNoListRepeats( inputdata_t &inputdata ) +{ + m_bNoListRepeats = inputdata.value.Bool(); +} + + +void CSlideshowDisplay::GetControlPanelInfo( int nPanelIndex, const char *&pPanelName ) +{ + pPanelName = "slideshow_display_screen"; +} + +void CSlideshowDisplay::GetControlPanelClassName( int nPanelIndex, const char *&pPanelName ) +{ + pPanelName = "vgui_screen"; +} + +//----------------------------------------------------------------------------- +// This is called by the base object when it's time to spawn the control panels +//----------------------------------------------------------------------------- +void CSlideshowDisplay::SpawnControlPanels() +{ + int nPanel; + for ( nPanel = 0; true; ++nPanel ) + { + const char *pScreenName; + GetControlPanelInfo( nPanel, pScreenName ); + if (!pScreenName) + continue; + + const char *pScreenClassname; + GetControlPanelClassName( nPanel, pScreenClassname ); + if ( !pScreenClassname ) + continue; + + float flWidth = m_iScreenWidth; + float flHeight = m_iScreenHeight; + + CVGuiScreen *pScreen = CreateVGuiScreen( pScreenClassname, pScreenName, this, this, -1 ); + pScreen->ChangeTeam( GetTeamNumber() ); + pScreen->SetActualSize( flWidth, flHeight ); + pScreen->SetActive( true ); + pScreen->MakeVisibleOnlyToTeammates( false ); + pScreen->SetTransparency( true ); + int nScreen = m_hScreens.AddToTail( ); + m_hScreens[nScreen].Set( pScreen ); + + return; + } +} + +void CSlideshowDisplay::RestoreControlPanels( void ) +{ + int nPanel; + for ( nPanel = 0; true; ++nPanel ) + { + const char *pScreenName; + GetControlPanelInfo( nPanel, pScreenName ); + if (!pScreenName) + continue; + + const char *pScreenClassname; + GetControlPanelClassName( nPanel, pScreenClassname ); + if ( !pScreenClassname ) + continue; + + CVGuiScreen *pScreen = (CVGuiScreen *)gEntList.FindEntityByClassname( NULL, pScreenClassname ); + + while ( ( pScreen && pScreen->GetOwnerEntity() != this ) || Q_strcmp( pScreen->GetPanelName(), pScreenName ) != 0 ) + { + pScreen = (CVGuiScreen *)gEntList.FindEntityByClassname( pScreen, pScreenClassname ); + } + + if ( pScreen ) + { + int nScreen = m_hScreens.AddToTail( ); + m_hScreens[nScreen].Set( pScreen ); + pScreen->SetActive( true ); + } + + return; + } +} + +void CSlideshowDisplay::BuildSlideShowImagesList( void ) +{ + FileFindHandle_t matHandle; + char szDirectory[_MAX_PATH]; + char szMatFileName[_MAX_PATH] = {'\0'}; + char szFileBuffer[ SLIDESHOW_LIST_BUFFER_MAX ]; + char *pchCurrentLine = NULL; + + if ( IsX360() ) + { + Q_snprintf( szDirectory, sizeof( szDirectory ), "materials/vgui/%s/slides.txt", m_szSlideshowDirectory.Get() ); + + FileHandle_t fh = g_pFullFileSystem->Open( szDirectory, "rt" ); + if ( !fh ) + { + DevWarning( "Couldn't read slideshow image file %s!", szDirectory ); + return; + } + + int iFileSize = MIN( g_pFullFileSystem->Size( fh ), SLIDESHOW_LIST_BUFFER_MAX ); + + int iBytesRead = g_pFullFileSystem->Read( szFileBuffer, iFileSize, fh ); + g_pFullFileSystem->Close( fh ); + + // Ensure we don't write outside of our buffer + if ( iBytesRead > iFileSize ) + iBytesRead = iFileSize; + szFileBuffer[ iBytesRead ] = '\0'; + + pchCurrentLine = szFileBuffer; + + // Seek to end of first line + char *pchNextLine = pchCurrentLine; + while ( *pchNextLine != '\0' && *pchNextLine != '\n' && *pchNextLine != ' ' ) + ++pchNextLine; + + if ( *pchNextLine != '\0' ) + { + // Mark end of string + *pchNextLine = '\0'; + + // Seek to start of next string + ++pchNextLine; + while ( *pchNextLine != '\0' && ( *pchNextLine == '\n' || *pchNextLine == ' ' ) ) + ++pchNextLine; + } + + Q_strncpy( szMatFileName, pchCurrentLine, sizeof(szMatFileName) ); + pchCurrentLine = pchNextLine; + } + else + { + Q_snprintf( szDirectory, sizeof( szDirectory ), "materials/vgui/%s/*.vmt", m_szSlideshowDirectory.Get() ); + const char *pMatFileName = g_pFullFileSystem->FindFirst( szDirectory, &matHandle ); + + if ( pMatFileName ) + Q_strncpy( szMatFileName, pMatFileName, sizeof(szMatFileName) ); + } + + int iSlideIndex = 0; + + while ( szMatFileName[ 0 ] ) + { + char szFileName[_MAX_PATH]; + Q_snprintf( szFileName, sizeof( szFileName ), "vgui/%s/%s", m_szSlideshowDirectory.Get(), szMatFileName ); + szFileName[ Q_strlen( szFileName ) - 4 ] = '\0'; + + PrecacheMaterial( szFileName ); + + // Get material keywords + char szFullFileName[_MAX_PATH]; + Q_snprintf( szFullFileName, sizeof( szFullFileName ), "materials/vgui/%s/%s", m_szSlideshowDirectory.Get(), szMatFileName ); + + KeyValues *pMaterialKeys = new KeyValues( "material" ); + bool bLoaded = pMaterialKeys->LoadFromFile( g_pFullFileSystem, szFullFileName, NULL ); + + if ( bLoaded ) + { + char szKeywords[ 256 ]; + Q_strcpy( szKeywords, pMaterialKeys->GetString( "%keywords", "" ) ); + + char *pchKeyword = szKeywords; + + while ( pchKeyword[ 0 ] != '\0' ) + { + char *pNextKeyword = pchKeyword; + + // Skip commas and spaces + while ( pNextKeyword[ 0 ] != '\0' && pNextKeyword[ 0 ] != ',' ) + ++pNextKeyword; + + if ( pNextKeyword[ 0 ] != '\0' ) + { + pNextKeyword[ 0 ] = '\0'; + ++pNextKeyword; + + while ( pNextKeyword[ 0 ] != '\0' && ( pNextKeyword[ 0 ] == ',' || pNextKeyword[ 0 ] == ' ' ) ) + ++pNextKeyword; + } + + // Find the list with the current keyword + int iList; + for ( iList = 0; iList < m_SlideKeywordList.Count(); ++iList ) + { + if ( Q_strcmp( m_SlideKeywordList[ iList ]->szSlideKeyword, pchKeyword ) == 0 ) + break; + } + + if ( iList >= m_SlideKeywordList.Count() ) + { + // Couldn't find the list, so create it + iList = m_SlideKeywordList.AddToTail( new SlideKeywordList_t ); + Q_strcpy( m_SlideKeywordList[ iList ]->szSlideKeyword, pchKeyword ); + } + + pchKeyword = pNextKeyword; + } + } + + // Find the generic list + int iList; + for ( iList = 0; iList < m_SlideKeywordList.Count(); ++iList ) + { + if ( Q_strcmp( m_SlideKeywordList[ iList ]->szSlideKeyword, "" ) == 0 ) + break; + } + + if ( iList >= m_SlideKeywordList.Count() ) + { + // Couldn't find the generic list, so create it + iList = m_SlideKeywordList.AddToHead( new SlideKeywordList_t ); + Q_strcpy( m_SlideKeywordList[ iList ]->szSlideKeyword, "" ); + } + + if ( IsX360() ) + { + // Seek to end of first line + char *pchNextLine = pchCurrentLine; + while ( *pchNextLine != '\0' && *pchNextLine != '\n' && *pchNextLine != ' ' ) + ++pchNextLine; + + if ( *pchNextLine != '\0' ) + { + // Mark end of string + *pchNextLine = '\0'; + + // Seek to start of next string + ++pchNextLine; + while ( *pchNextLine != '\0' && ( *pchNextLine == '\n' || *pchNextLine == ' ' ) ) + ++pchNextLine; + } + + Q_strncpy( szMatFileName, pchCurrentLine, sizeof(szMatFileName) ); + pchCurrentLine = pchNextLine; + } + else + { + const char *pMatFileName = g_pFullFileSystem->FindNext( matHandle ); + + if ( pMatFileName ) + Q_strncpy( szMatFileName, pMatFileName, sizeof(szMatFileName) ); + else + szMatFileName[ 0 ] = '\0'; + } + + ++iSlideIndex; + } + + if ( !IsX360() ) + { + g_pFullFileSystem->FindClose( matHandle ); + } +} \ No newline at end of file diff --git a/sp/src/game/server/smoke_trail.cpp b/sp/src/game/server/smoke_trail.cpp new file mode 100644 index 00000000..f467e60b --- /dev/null +++ b/sp/src/game/server/smoke_trail.cpp @@ -0,0 +1,645 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "smoke_trail.h" +#include "dt_send.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SMOKETRAIL_ENTITYNAME "env_smoketrail" +#define SPORETRAIL_ENTITYNAME "env_sporetrail" +#define SPOREEXPLOSION_ENTITYNAME "env_sporeexplosion" +#define DUSTTRAIL_ENTITYNAME "env_dusttrail" + +//----------------------------------------------------------------------------- +//Data table +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST(SmokeTrail, DT_SmokeTrail) + SendPropFloat(SENDINFO(m_SpawnRate), 8, 0, 1, 1024), + SendPropVector(SENDINFO(m_StartColor), 8, 0, 0, 1), + SendPropVector(SENDINFO(m_EndColor), 8, 0, 0, 1), + SendPropFloat(SENDINFO(m_ParticleLifetime), 16, SPROP_ROUNDUP, 0.1, 100), + SendPropFloat(SENDINFO(m_StopEmitTime), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MinSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MaxSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MinDirectedSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MaxDirectedSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_StartSize), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_EndSize), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_SpawnRadius), -1, SPROP_NOSCALE), + SendPropBool(SENDINFO(m_bEmit) ), + SendPropInt(SENDINFO(m_nAttachment), 32 ), + SendPropFloat(SENDINFO(m_Opacity), -1, SPROP_NOSCALE), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS(env_smoketrail, SmokeTrail); + +BEGIN_DATADESC( SmokeTrail ) + + DEFINE_FIELD( m_StartColor, FIELD_VECTOR ), + DEFINE_FIELD( m_EndColor, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_Opacity, FIELD_FLOAT, "opacity" ), + DEFINE_KEYFIELD( m_SpawnRate, FIELD_FLOAT, "spawnrate" ), + DEFINE_KEYFIELD( m_ParticleLifetime, FIELD_FLOAT, "lifetime" ), + DEFINE_FIELD( m_StopEmitTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_MinSpeed, FIELD_FLOAT, "minspeed" ), + DEFINE_KEYFIELD( m_MaxSpeed, FIELD_FLOAT, "maxspeed" ), + DEFINE_KEYFIELD( m_MinDirectedSpeed, FIELD_FLOAT, "mindirectedspeed" ), + DEFINE_KEYFIELD( m_MaxDirectedSpeed, FIELD_FLOAT, "maxdirectedspeed" ), + DEFINE_KEYFIELD( m_StartSize, FIELD_FLOAT, "startsize" ), + DEFINE_KEYFIELD( m_EndSize, FIELD_FLOAT, "endsize" ), + DEFINE_KEYFIELD( m_SpawnRadius, FIELD_FLOAT, "spawnradius" ), + DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nAttachment, FIELD_INTEGER ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : +//----------------------------------------------------------------------------- +SmokeTrail::SmokeTrail() +{ + m_SpawnRate = 10; + m_StartColor.GetForModify().Init(0.5, 0.5, 0.5); + m_EndColor.GetForModify().Init(0,0,0); + m_ParticleLifetime = 5; + m_StopEmitTime = 0; // Don't stop emitting particles + m_MinSpeed = 2; + m_MaxSpeed = 4; + m_MinDirectedSpeed = m_MaxDirectedSpeed = 0; + m_StartSize = 35; + m_EndSize = 55; + m_SpawnRadius = 2; + m_bEmit = true; + m_nAttachment = 0; + m_Opacity = 0.5f; +} + + +//----------------------------------------------------------------------------- +// Parse data from a map file +//----------------------------------------------------------------------------- +bool SmokeTrail::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "startcolor" ) ) + { + color32 tmp; + UTIL_StringToColor32( &tmp, szValue ); + m_StartColor.GetForModify().Init( tmp.r / 255.0f, tmp.g / 255.0f, tmp.b / 255.0f ); + return true; + } + + if ( FStrEq( szKeyName, "endcolor" ) ) + { + color32 tmp; + UTIL_StringToColor32( &tmp, szValue ); + m_EndColor.GetForModify().Init( tmp.r / 255.0f, tmp.g / 255.0f, tmp.b / 255.0f ); + return true; + } + + if ( FStrEq( szKeyName, "emittime" ) ) + { + m_StopEmitTime = gpGlobals->curtime + atof( szValue ); + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose : +// Input : +// Output : +//----------------------------------------------------------------------------- +void SmokeTrail::SetEmit(bool bVal) +{ + m_bEmit = bVal; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : SmokeTrail* +//----------------------------------------------------------------------------- +SmokeTrail* SmokeTrail::CreateSmokeTrail() +{ + CBaseEntity *pEnt = CreateEntityByName(SMOKETRAIL_ENTITYNAME); + if(pEnt) + { + SmokeTrail *pSmoke = dynamic_cast(pEnt); + if(pSmoke) + { + pSmoke->Activate(); + return pSmoke; + } + else + { + UTIL_Remove(pEnt); + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Attach the smoke trail to an entity or point +// Input : index - entity that has the attachment +// attachment - point to attach to +//----------------------------------------------------------------------------- +void SmokeTrail::FollowEntity( CBaseEntity *pEntity, const char *pAttachmentName ) +{ + // For attachments + if ( pAttachmentName && pEntity && pEntity->GetBaseAnimating() ) + { + m_nAttachment = pEntity->GetBaseAnimating()->LookupAttachment( pAttachmentName ); + } + else + { + m_nAttachment = 0; + } + + BaseClass::FollowEntity( pEntity ); +} + + +//================================================== +// RocketTrail +//================================================== + +//Data table +IMPLEMENT_SERVERCLASS_ST(RocketTrail, DT_RocketTrail) + SendPropFloat(SENDINFO(m_SpawnRate), 8, 0, 1, 1024), + SendPropVector(SENDINFO(m_StartColor), 8, 0, 0, 1), + SendPropVector(SENDINFO(m_EndColor), 8, 0, 0, 1), + SendPropFloat(SENDINFO(m_ParticleLifetime), 16, SPROP_ROUNDUP, 0.1, 100), + SendPropFloat(SENDINFO(m_StopEmitTime), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MinSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MaxSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_StartSize), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_EndSize), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_SpawnRadius), -1, SPROP_NOSCALE), + SendPropBool(SENDINFO(m_bEmit)), + SendPropInt(SENDINFO(m_nAttachment), 32 ), + SendPropFloat(SENDINFO(m_Opacity), -1, SPROP_NOSCALE), + SendPropInt (SENDINFO(m_bDamaged), 1, SPROP_UNSIGNED), + SendPropFloat(SENDINFO(m_flFlareScale), -1, SPROP_NOSCALE), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_rockettrail, RocketTrail ); + +BEGIN_DATADESC( RocketTrail ) + + DEFINE_FIELD( m_StartColor, FIELD_VECTOR ), + DEFINE_FIELD( m_EndColor, FIELD_VECTOR ), + DEFINE_FIELD( m_Opacity, FIELD_FLOAT ), + DEFINE_FIELD( m_SpawnRate, FIELD_FLOAT ), + DEFINE_FIELD( m_ParticleLifetime, FIELD_FLOAT ), + DEFINE_FIELD( m_StopEmitTime, FIELD_TIME ), + DEFINE_FIELD( m_MinSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_MaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_StartSize, FIELD_FLOAT ), + DEFINE_FIELD( m_EndSize, FIELD_FLOAT ), + DEFINE_FIELD( m_SpawnRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nAttachment, FIELD_INTEGER ), + DEFINE_FIELD( m_bDamaged, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flFlareScale, FIELD_FLOAT ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: +// Output : +//----------------------------------------------------------------------------- +RocketTrail::RocketTrail() +{ + m_SpawnRate = 10; + m_StartColor.GetForModify().Init(0.5, 0.5, 0.5); + m_EndColor.GetForModify().Init(0,0,0); + m_ParticleLifetime = 5; + m_StopEmitTime = 0; // Don't stop emitting particles + m_MinSpeed = 2; + m_MaxSpeed = 4; + m_StartSize = 35; + m_EndSize = 55; + m_SpawnRadius = 2; + m_bEmit = true; + m_nAttachment = 0; + m_Opacity = 0.5f; + m_flFlareScale = 1.5; +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void RocketTrail::SetEmit(bool bVal) +{ + m_bEmit = bVal; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : SmokeTrail* +//----------------------------------------------------------------------------- +RocketTrail* RocketTrail::CreateRocketTrail() +{ + CBaseEntity *pEnt = CreateEntityByName( "env_rockettrail" ); + + if( pEnt != NULL ) + { + RocketTrail *pTrail = dynamic_cast(pEnt); + + if( pTrail != NULL ) + { + pTrail->Activate(); + return pTrail; + } + else + { + UTIL_Remove( pEnt ); + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Attach the smoke trail to an entity or point +// Input : index - entity that has the attachment +// attachment - point to attach to +//----------------------------------------------------------------------------- +void RocketTrail::FollowEntity( CBaseEntity *pEntity, const char *pAttachmentName ) +{ + // For attachments + if ( pAttachmentName && pEntity && pEntity->GetBaseAnimating() ) + { + m_nAttachment = pEntity->GetBaseAnimating()->LookupAttachment( pAttachmentName ); + } + else + { + m_nAttachment = 0; + } + + BaseClass::FollowEntity( pEntity ); +} + +//================================================== +// SporeTrail +//================================================== + +IMPLEMENT_SERVERCLASS_ST( SporeTrail, DT_SporeTrail ) + SendPropFloat (SENDINFO(m_flSpawnRate), 8, 0, 1, 1024), + SendPropVector (SENDINFO(m_vecEndColor), 8, 0, 0, 1), + SendPropFloat (SENDINFO(m_flParticleLifetime), 16, SPROP_ROUNDUP, 0.1, 100), + SendPropFloat (SENDINFO(m_flStartSize), -1, SPROP_NOSCALE), + SendPropFloat (SENDINFO(m_flEndSize), -1, SPROP_NOSCALE), + SendPropFloat (SENDINFO(m_flSpawnRadius), -1, SPROP_NOSCALE), + SendPropBool (SENDINFO(m_bEmit)), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS(env_sporetrail, SporeTrail); + +BEGIN_DATADESC( SporeTrail ) + + DEFINE_FIELD( m_vecEndColor, FIELD_VECTOR ), + DEFINE_FIELD( m_flSpawnRate, FIELD_FLOAT ), + DEFINE_FIELD( m_flParticleLifetime, FIELD_FLOAT ), + DEFINE_FIELD( m_flStartSize, FIELD_FLOAT ), + DEFINE_FIELD( m_flEndSize, FIELD_FLOAT ), + DEFINE_FIELD( m_flSpawnRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), + +END_DATADESC() + +SporeTrail::SporeTrail( void ) +{ + m_vecEndColor.GetForModify().Init(); + + m_flSpawnRate = 100.0f; + m_flParticleLifetime = 1.0f; + m_flStartSize = 1.0f; + m_flEndSize = 0.0f; + m_flSpawnRadius = 16.0f; + SetRenderColor( 255, 255, 255, 255 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : SporeTrail* +//----------------------------------------------------------------------------- +SporeTrail* SporeTrail::CreateSporeTrail() +{ + CBaseEntity *pEnt = CreateEntityByName( SPORETRAIL_ENTITYNAME ); + + if(pEnt) + { + SporeTrail *pSpore = dynamic_cast(pEnt); + + if ( pSpore ) + { + pSpore->Activate(); + return pSpore; + } + else + { + UTIL_Remove( pEnt ); + } + } + + return NULL; +} + +//================================================== +// SporeExplosion +//================================================== + +IMPLEMENT_SERVERCLASS_ST( SporeExplosion, DT_SporeExplosion ) + SendPropFloat (SENDINFO(m_flSpawnRate), 8, 0, 1, 1024), + SendPropFloat (SENDINFO(m_flParticleLifetime), 16, SPROP_ROUNDUP, 0.1, 100), + SendPropFloat (SENDINFO(m_flStartSize), -1, SPROP_NOSCALE), + SendPropFloat (SENDINFO(m_flEndSize), -1, SPROP_NOSCALE), + SendPropFloat (SENDINFO(m_flSpawnRadius), -1, SPROP_NOSCALE), + SendPropBool (SENDINFO(m_bEmit) ), + SendPropBool (SENDINFO(m_bDontRemove) ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_sporeexplosion, SporeExplosion ); + +BEGIN_DATADESC( SporeExplosion ) + + DEFINE_KEYFIELD( m_flSpawnRate, FIELD_FLOAT, "spawnrate" ), + DEFINE_FIELD( m_flParticleLifetime, FIELD_FLOAT ), + DEFINE_FIELD( m_flStartSize, FIELD_FLOAT ), + DEFINE_FIELD( m_flEndSize, FIELD_FLOAT ), + DEFINE_FIELD( m_flSpawnRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "startdisabled" ), + DEFINE_FIELD( m_bDontRemove, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + +END_DATADESC() + +SporeExplosion::SporeExplosion( void ) +{ + m_flSpawnRate = 100.0f; + m_flParticleLifetime = 1.0f; + m_flStartSize = 1.0f; + m_flEndSize = 0.0f; + m_flSpawnRadius = 16.0f; + SetRenderColor( 255, 255, 255, 255 ); + m_bEmit = true; + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void SporeExplosion::Spawn( void ) +{ + BaseClass::Spawn(); + + m_bEmit = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : SporeExplosion* +//----------------------------------------------------------------------------- +SporeExplosion *SporeExplosion::CreateSporeExplosion() +{ + CBaseEntity *pEnt = CreateEntityByName( SPOREEXPLOSION_ENTITYNAME ); + + if ( pEnt ) + { + SporeExplosion *pSpore = dynamic_cast(pEnt); + + if ( pSpore ) + { + pSpore->Activate(); + return pSpore; + } + else + { + UTIL_Remove( pEnt ); + } + } + + return NULL; +} + +void SporeExplosion::InputEnable( inputdata_t &inputdata ) +{ + m_bDontRemove = true; + m_bDisabled = false; + m_bEmit = true; +} + +void SporeExplosion::InputDisable( inputdata_t &inputdata ) +{ + m_bDontRemove = true; + m_bDisabled = true; + m_bEmit = false; +} + +BEGIN_DATADESC( CFireTrail ) + + DEFINE_FIELD( m_flLifetime, FIELD_FLOAT ), + DEFINE_FIELD( m_nAttachment, FIELD_INTEGER ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CFireTrail, DT_FireTrail ) + SendPropInt( SENDINFO( m_nAttachment ), 32 ), + SendPropFloat( SENDINFO( m_flLifetime ), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_fire_trail, CFireTrail ); + +void CFireTrail::Precache( void ) +{ + PrecacheMaterial( "sprites/flamelet1" ); + PrecacheMaterial( "sprites/flamelet2" ); + PrecacheMaterial( "sprites/flamelet3" ); + PrecacheMaterial( "sprites/flamelet4" ); + PrecacheMaterial( "sprites/flamelet5" ); + PrecacheMaterial( "particle/particle_smokegrenade" ); + PrecacheMaterial( "particle/particle_noisesphere" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Attach the smoke trail to an entity or point +// Input : index - entity that has the attachment +// attachment - point to attach to +//----------------------------------------------------------------------------- +void CFireTrail::FollowEntity( CBaseEntity *pEntity, const char *pAttachmentName ) +{ + // For attachments + if ( pAttachmentName && pEntity && pEntity->GetBaseAnimating() ) + { + m_nAttachment = pEntity->GetBaseAnimating()->LookupAttachment( pAttachmentName ); + } + else + { + m_nAttachment = 0; + } + + BaseClass::FollowEntity( pEntity ); +} + +//----------------------------------------------------------------------------- +// Purpose: Create and return a new fire trail entity +//----------------------------------------------------------------------------- +CFireTrail *CFireTrail::CreateFireTrail( void ) +{ + CBaseEntity *pEnt = CreateEntityByName( "env_fire_trail" ); + + if ( pEnt ) + { + CFireTrail *pTrail = dynamic_cast(pEnt); + + if ( pTrail ) + { + pTrail->Activate(); + return pTrail; + } + else + { + UTIL_Remove( pEnt ); + } + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +//Data table +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST(DustTrail, DT_DustTrail) + SendPropFloat(SENDINFO(m_SpawnRate), 8, 0, 1, 1024), + SendPropVector(SENDINFO(m_Color), 8, 0, 0, 1), + SendPropFloat(SENDINFO(m_ParticleLifetime), 16, SPROP_ROUNDUP, 0.1, 100), + SendPropFloat(SENDINFO(m_StopEmitTime), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MinSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MaxSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MinDirectedSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_MaxDirectedSpeed), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_StartSize), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_EndSize), -1, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_SpawnRadius), -1, SPROP_NOSCALE), + SendPropBool(SENDINFO(m_bEmit) ), + SendPropFloat(SENDINFO(m_Opacity), -1, SPROP_NOSCALE), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_dusttrail, DustTrail); + +BEGIN_DATADESC( DustTrail ) + + DEFINE_FIELD( m_Color, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_Opacity, FIELD_FLOAT, "opacity" ), + DEFINE_KEYFIELD( m_SpawnRate, FIELD_FLOAT, "spawnrate" ), + DEFINE_KEYFIELD( m_ParticleLifetime, FIELD_FLOAT, "lifetime" ), + DEFINE_FIELD( m_StopEmitTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_MinSpeed, FIELD_FLOAT, "minspeed" ), + DEFINE_KEYFIELD( m_MaxSpeed, FIELD_FLOAT, "maxspeed" ), + DEFINE_KEYFIELD( m_MinDirectedSpeed, FIELD_FLOAT, "mindirectedspeed" ), + DEFINE_KEYFIELD( m_MaxDirectedSpeed, FIELD_FLOAT, "maxdirectedspeed" ), + DEFINE_KEYFIELD( m_StartSize, FIELD_FLOAT, "startsize" ), + DEFINE_KEYFIELD( m_EndSize, FIELD_FLOAT, "endsize" ), + DEFINE_KEYFIELD( m_SpawnRadius, FIELD_FLOAT, "spawnradius" ), + DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nAttachment, FIELD_INTEGER ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : +//----------------------------------------------------------------------------- +DustTrail::DustTrail() +{ + m_SpawnRate = 10; + m_Color.GetForModify().Init(0.5, 0.5, 0.5); + m_ParticleLifetime = 5; + m_StopEmitTime = 0; // Don't stop emitting particles + m_MinSpeed = 2; + m_MaxSpeed = 4; + m_MinDirectedSpeed = m_MaxDirectedSpeed = 0; + m_StartSize = 35; + m_EndSize = 55; + m_SpawnRadius = 2; + m_bEmit = true; + m_Opacity = 0.5f; +} + + +//----------------------------------------------------------------------------- +// Parse data from a map file +//----------------------------------------------------------------------------- +bool DustTrail::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq( szKeyName, "color" ) ) + { + color32 tmp; + UTIL_StringToColor32( &tmp, szValue ); + m_Color.GetForModify().Init( tmp.r / 255.0f, tmp.g / 255.0f, tmp.b / 255.0f ); + return true; + } + + if ( FStrEq( szKeyName, "emittime" ) ) + { + m_StopEmitTime = gpGlobals->curtime + atof( szValue ); + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + + +//----------------------------------------------------------------------------- +// Purpose : +// Input : +// Output : +//----------------------------------------------------------------------------- +void DustTrail::SetEmit(bool bVal) +{ + m_bEmit = bVal; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : DustTrail* +//----------------------------------------------------------------------------- +DustTrail* DustTrail::CreateDustTrail() +{ + CBaseEntity *pEnt = CreateEntityByName(DUSTTRAIL_ENTITYNAME); + if(pEnt) + { + DustTrail *pDust = dynamic_cast(pEnt); + if(pDust) + { + pDust->Activate(); + return pDust; + } + else + { + UTIL_Remove(pEnt); + } + } + + return NULL; +} diff --git a/sp/src/game/server/smoke_trail.h b/sp/src/game/server/smoke_trail.h new file mode 100644 index 00000000..34398c10 --- /dev/null +++ b/sp/src/game/server/smoke_trail.h @@ -0,0 +1,210 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#ifndef SMOKE_TRAIL_H +#define SMOKE_TRAIL_H + +#include "baseparticleentity.h" + +//================================================== +// SmokeTrail +//================================================== + +class SmokeTrail : public CBaseParticleEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( SmokeTrail, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + SmokeTrail(); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + void SetEmit(bool bVal); + void FollowEntity( CBaseEntity *pEntity, const char *pAttachmentName = NULL); + static SmokeTrail* CreateSmokeTrail(); + +public: + // Effect parameters. These will assume default values but you can change them. + CNetworkVector( m_StartColor ); // Fade between these colors. + CNetworkVector( m_EndColor ); + CNetworkVar( float, m_Opacity ); + + CNetworkVar( float, m_SpawnRate ); // How many particles per second. + CNetworkVar( float, m_ParticleLifetime ); // How long do the particles live? + CNetworkVar( float, m_StopEmitTime ); // When do I stop emitting particles? + CNetworkVar( float, m_MinSpeed ); // Speed range. + CNetworkVar( float, m_MaxSpeed ); + CNetworkVar( float, m_StartSize ); // Size ramp. + CNetworkVar( float, m_EndSize ); + CNetworkVar( float, m_SpawnRadius ); + CNetworkVar( float, m_MinDirectedSpeed ); // Speed range. + CNetworkVar( float, m_MaxDirectedSpeed ); + CNetworkVar( bool, m_bEmit ); + + CNetworkVar( int, m_nAttachment ); +}; + +//================================================== +// RocketTrail +//================================================== + +class RocketTrail : public CBaseParticleEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( RocketTrail, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + RocketTrail(); + void SetEmit(bool bVal); + void FollowEntity( CBaseEntity *pEntity, const char *pAttachmentName = NULL); + static RocketTrail *CreateRocketTrail(); + +public: + // Effect parameters. These will assume default values but you can change them. + CNetworkVector( m_StartColor ); // Fade between these colors. + CNetworkVector( m_EndColor ); + CNetworkVar( float, m_Opacity ); + + CNetworkVar( float, m_SpawnRate ); // How many particles per second. + CNetworkVar( float, m_ParticleLifetime ); // How long do the particles live? + CNetworkVar( float, m_StopEmitTime ); // When do I stop emitting particles? + CNetworkVar( float, m_MinSpeed ); // Speed range. + CNetworkVar( float, m_MaxSpeed ); + CNetworkVar( float, m_StartSize ); // Size ramp. + CNetworkVar( float, m_EndSize ); + CNetworkVar( float, m_SpawnRadius ); + + CNetworkVar( bool, m_bEmit ); + + CNetworkVar( int, m_nAttachment ); + + CNetworkVar( bool, m_bDamaged ); + + CNetworkVar( float, m_flFlareScale ); // Size of the flare +}; + +//================================================== +// SporeTrail +//================================================== + +class SporeTrail : public CBaseParticleEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( SporeTrail, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + SporeTrail( void ); + + static SporeTrail* CreateSporeTrail(); + +//Data members +public: + + CNetworkVector( m_vecEndColor ); + + CNetworkVar( float, m_flSpawnRate ); + CNetworkVar( float, m_flParticleLifetime ); + CNetworkVar( float, m_flStartSize ); + CNetworkVar( float, m_flEndSize ); + CNetworkVar( float, m_flSpawnRadius ); + + CNetworkVar( bool, m_bEmit ); +}; + +//================================================== +// SporeExplosion +//================================================== + +class SporeExplosion : public CBaseParticleEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( SporeExplosion, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + SporeExplosion( void ); + void Spawn( void ); + + static SporeExplosion* CreateSporeExplosion(); + + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +//Data members +public: + + bool m_bDisabled; + + CNetworkVar( float, m_flSpawnRate ); + CNetworkVar( float, m_flParticleLifetime ); + CNetworkVar( float, m_flStartSize ); + CNetworkVar( float, m_flEndSize ); + CNetworkVar( float, m_flSpawnRadius ); + + CNetworkVar( bool, m_bEmit ); + CNetworkVar( bool, m_bDontRemove ); +}; + +//================================================== +// CFireTrail +//================================================== + +class CFireTrail : public CBaseParticleEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( CFireTrail, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + static CFireTrail *CreateFireTrail( void ); + void FollowEntity( CBaseEntity *pEntity, const char *pAttachmentName ); + void Precache( void ); + + CNetworkVar( int, m_nAttachment ); + CNetworkVar( float, m_flLifetime ); +}; + +//================================================== +// DustTrail +//================================================== + +class DustTrail : public CBaseParticleEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( DustTrail, CBaseParticleEntity ); + DECLARE_SERVERCLASS(); + + DustTrail(); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + void SetEmit(bool bVal); + static DustTrail* CreateDustTrail(); + +public: + // Effect parameters. These will assume default values but you can change them. + CNetworkVector( m_Color ); + CNetworkVar( float, m_Opacity ); + + CNetworkVar( float, m_SpawnRate ); // How many particles per second. + CNetworkVar( float, m_ParticleLifetime ); // How long do the particles live? + CNetworkVar( float, m_StopEmitTime ); // When do I stop emitting particles? + CNetworkVar( float, m_MinSpeed ); // Speed range. + CNetworkVar( float, m_MaxSpeed ); + CNetworkVar( float, m_StartSize ); // Size ramp. + CNetworkVar( float, m_EndSize ); + CNetworkVar( float, m_SpawnRadius ); + CNetworkVar( float, m_MinDirectedSpeed ); // Speed range. + CNetworkVar( float, m_MaxDirectedSpeed ); + CNetworkVar( bool, m_bEmit ); + + CNetworkVar( int, m_nAttachment ); +}; + + +#endif diff --git a/sp/src/game/server/smokestack.cpp b/sp/src/game/server/smokestack.cpp new file mode 100644 index 00000000..570971ab --- /dev/null +++ b/sp/src/game/server/smokestack.cpp @@ -0,0 +1,276 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements the server side of a steam jet particle system entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "smokestack.h" +#include "particle_light.h" +#include "filesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//Networking +IMPLEMENT_SERVERCLASS_ST(CSmokeStack, DT_SmokeStack) + SendPropFloat(SENDINFO(m_SpreadSpeed), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_Speed), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_StartSize), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_EndSize), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_Rate), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_JetLength), 0, SPROP_NOSCALE), + SendPropInt(SENDINFO(m_bEmit), 1, SPROP_UNSIGNED), + SendPropFloat(SENDINFO(m_flBaseSpread), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO( m_flRollSpeed ), 0, SPROP_NOSCALE ), + + // Note: the base color is specified in the smokestack entity, but the directional + // and ambient light must come from env_particlelight entities. + SendPropVector( SENDINFO_NOCHECK(m_DirLight.m_vPos), 0, SPROP_NOSCALE ), + SendPropVector( SENDINFO_NOCHECK(m_DirLight.m_vColor), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_NOCHECK(m_DirLight.m_flIntensity), 0, SPROP_NOSCALE ), + + SendPropVector( SENDINFO_NOCHECK(m_AmbientLight.m_vPos), 0, SPROP_NOSCALE ), + SendPropVector( SENDINFO_NOCHECK(m_AmbientLight.m_vColor), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO_NOCHECK(m_AmbientLight.m_flIntensity), 0, SPROP_NOSCALE ), + + SendPropVector(SENDINFO(m_vWind), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_flTwist), 0, SPROP_NOSCALE), + SendPropIntWithMinusOneFlag( SENDINFO(m_iMaterialModel), 16 ) + +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_smokestack, CSmokeStack ); + + +//Save/restore + +BEGIN_SIMPLE_DATADESC( CSmokeStackLightInfo ) + DEFINE_FIELD( m_vPos, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vColor, FIELD_VECTOR ), + DEFINE_FIELD( m_flIntensity, FIELD_FLOAT ), +END_DATADESC() + +BEGIN_DATADESC( CSmokeStack ) + + //Keyvalue fields + DEFINE_KEYFIELD( m_StartSize, FIELD_FLOAT, "StartSize" ), + DEFINE_KEYFIELD( m_EndSize, FIELD_FLOAT, "EndSize" ), + DEFINE_KEYFIELD( m_InitialState, FIELD_BOOLEAN, "InitialState" ), + DEFINE_KEYFIELD( m_flBaseSpread, FIELD_FLOAT, "BaseSpread" ), + DEFINE_KEYFIELD( m_flTwist, FIELD_FLOAT, "Twist" ), + DEFINE_KEYFIELD( m_flRollSpeed, FIELD_FLOAT, "Roll" ), + + DEFINE_FIELD( m_strMaterialModel, FIELD_STRING ), + DEFINE_FIELD( m_iMaterialModel,FIELD_INTEGER ), + + DEFINE_EMBEDDED( m_AmbientLight ), + DEFINE_EMBEDDED( m_DirLight ), + + DEFINE_KEYFIELD( m_WindAngle, FIELD_INTEGER, "WindAngle" ), + DEFINE_KEYFIELD( m_WindSpeed, FIELD_INTEGER, "WindSpeed" ), + + //Regular fields + DEFINE_FIELD( m_vWind, FIELD_VECTOR ), + DEFINE_FIELD( m_bEmit, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUT( m_JetLength, FIELD_FLOAT, "JetLength" ), + DEFINE_INPUT( m_SpreadSpeed, FIELD_FLOAT, "SpreadSpeed" ), + DEFINE_INPUT( m_Speed, FIELD_FLOAT, "Speed" ), + DEFINE_INPUT( m_Rate, FIELD_FLOAT, "Rate" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after key values have been set. +//----------------------------------------------------------------------------- +CSmokeStack::CSmokeStack() +{ + memset( &m_AmbientLight, 0, sizeof(m_AmbientLight) ); + memset( &m_DirLight, 0, sizeof(m_DirLight) ); + + IMPLEMENT_NETWORKVAR_CHAIN( &m_AmbientLight ); + IMPLEMENT_NETWORKVAR_CHAIN( &m_DirLight ); + + m_flTwist = 0; + SetRenderColor( 0, 0, 0, 255 ); + m_vWind.GetForModify().Init(); + m_WindAngle = m_WindSpeed = 0; + m_iMaterialModel = -1; + m_flRollSpeed = 0.0f; +} + + +CSmokeStack::~CSmokeStack() +{ +} + + +void CSmokeStack::Spawn( void ) +{ + if ( m_InitialState ) + { + m_bEmit = true; + } +} + + +void CSmokeStack::Activate() +{ + DetectInSkybox(); + + bool bGotDirLight = false; + + // Find local lights. + CBaseEntity *pTestEnt = NULL; + while ( 1 ) + { + pTestEnt = gEntList.FindEntityByClassname( pTestEnt, PARTICLELIGHT_ENTNAME ); + if ( !pTestEnt ) + break; + + CParticleLight *pLight = (CParticleLight*)pTestEnt; + if( !FStrEq( STRING(GetEntityName()), STRING(pLight->m_PSName) ) ) + continue; + + CSmokeStackLightInfo *pInfo = &m_AmbientLight; + if ( pLight->m_bDirectional ) + { + bGotDirLight = true; + pInfo = &m_DirLight; + } + + pInfo->m_flIntensity = pLight->m_flIntensity; + pInfo->m_vColor = pLight->m_vColor; + pInfo->m_vPos = pLight->GetAbsOrigin(); + } + + // Put our light colors in 0-1 space. + m_AmbientLight.m_vColor.GetForModify() /= 255.0f; + m_DirLight.m_vColor.GetForModify() /= 255.0f; + + BaseClass::Activate(); + + // Legacy support.. + if ( m_iMaterialModel == -1 ) + m_iMaterialModel = PrecacheModel( "particle/SmokeStack.vmt" ); +} + + +bool CSmokeStack::KeyValue( const char *szKeyName, const char *szValue ) +{ + if( stricmp( szKeyName, "Wind" ) == 0 ) + { + sscanf( szValue, "%f %f %f", &m_vWind.GetForModify().x, &m_vWind.GetForModify().y, &m_vWind.GetForModify().z ); + return true; + } + else if( stricmp( szKeyName, "WindAngle" ) == 0 ) + { + m_WindAngle = atoi( szValue ); + RecalcWindVector(); + return true; + } + else if( stricmp( szKeyName, "WindSpeed" ) == 0 ) + { + m_WindSpeed = atoi( szValue ); + RecalcWindVector(); + return true; + } + else if ( stricmp( szKeyName, "SmokeMaterial" ) == 0 ) + { + // Make sure we have a vmt extension. + if ( Q_stristr( szValue, ".vmt" ) ) + { + m_strMaterialModel = AllocPooledString( szValue ); + } + else + { + char str[512]; + Q_snprintf( str, sizeof( str ), "%s.vmt", szValue ); + m_strMaterialModel = AllocPooledString( str ); + } + + const char *pName = STRING( m_strMaterialModel ); + char szStrippedName[512]; + + m_iMaterialModel = PrecacheModel( pName ); + Q_StripExtension( pName, szStrippedName, Q_strlen(pName)+1 ); + + int iLength = Q_strlen( szStrippedName ); + szStrippedName[iLength-1] = '\0'; + + int iCount = 1; + char str[512]; + Q_snprintf( str, sizeof( str ), "%s%d.vmt", szStrippedName, iCount ); + + while ( filesystem->FileExists( UTIL_VarArgs( "materials/%s", str ) ) ) + { + PrecacheModel( str ); + iCount++; + + Q_snprintf( str, sizeof( str ), "%s%d.vmt", szStrippedName, iCount ); + } + + return true; + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } +} + + +void CSmokeStack::Precache() +{ + m_iMaterialModel = PrecacheModel( STRING( m_strMaterialModel ) ); + + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for toggling the steam jet on/off. +//----------------------------------------------------------------------------- +void CSmokeStack::InputToggle( inputdata_t &inputdata ) +{ + m_bEmit = !m_bEmit; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for turning on the steam jet. +//----------------------------------------------------------------------------- +void CSmokeStack::InputTurnOn( inputdata_t &inputdata ) +{ + m_bEmit = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for turning off the steam jet. +//----------------------------------------------------------------------------- +void CSmokeStack::InputTurnOff( inputdata_t &inputdata ) +{ + m_bEmit = false; +} + + +void CSmokeStack::RecalcWindVector() +{ + m_vWind = Vector( + cos( DEG2RAD( (float)m_WindAngle ) ) * m_WindSpeed, + sin( DEG2RAD( (float)m_WindAngle ) ) * m_WindSpeed, + 0 ); + +} + + diff --git a/sp/src/game/server/smokestack.h b/sp/src/game/server/smokestack.h new file mode 100644 index 00000000..e8d4986c --- /dev/null +++ b/sp/src/game/server/smokestack.h @@ -0,0 +1,85 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Defines the server side of a steam jet particle system entity. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SMOKESTACK_H +#define SMOKESTACK_H +#pragma once + +#include "baseparticleentity.h" + +//================================================== +// CSmokeStack +//================================================== + +class CSmokeStackLightInfo +{ +public: + DECLARE_CLASS_NOBASE( CSmokeStackLightInfo ); + DECLARE_SIMPLE_DATADESC(); + DECLARE_NETWORKVAR_CHAIN(); + + CNetworkVector( m_vPos ); + CNetworkVector( m_vColor ); + CNetworkVar( float, m_flIntensity ); +}; + +class CSmokeStack : public CBaseParticleEntity +{ +public: + DECLARE_CLASS( CSmokeStack, CBaseParticleEntity ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CSmokeStack(); + ~CSmokeStack(); + + virtual void Spawn( void ); + virtual void Activate(); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual void Precache(); + + +protected: + + // Input handlers. + void InputTurnOn(inputdata_t &data); + void InputTurnOff(inputdata_t &data); + void InputToggle(inputdata_t &data); + + void RecalcWindVector(); + + +// Stuff from the datatable. +public: + CNetworkVar( float, m_SpreadSpeed ); + CNetworkVar( float, m_Speed ); + CNetworkVar( float, m_StartSize ); + CNetworkVar( float, m_EndSize ); + CNetworkVar( float, m_Rate ); + CNetworkVar( float, m_JetLength ); // Length of the jet. Lifetime is derived from this. + CNetworkVar( float, m_flRollSpeed ); + + CNetworkVar( int, m_bEmit ); // Emit particles? + CNetworkVar( float, m_flBaseSpread ); + + CSmokeStackLightInfo m_AmbientLight; + CSmokeStackLightInfo m_DirLight; + + CNetworkVar( float, m_flTwist ); + + string_t m_strMaterialModel; + CNetworkVar( int, m_iMaterialModel ); + + int m_WindAngle; + int m_WindSpeed; + CNetworkVector( m_vWind ); // m_vWind is just calculated from m_WindAngle and m_WindSpeed. + + bool m_InitialState; +}; + +#endif // SMOKESTACK_H + diff --git a/sp/src/game/server/sound.cpp b/sp/src/game/server/sound.cpp new file mode 100644 index 00000000..cb7df905 --- /dev/null +++ b/sp/src/game/server/sound.cpp @@ -0,0 +1,1587 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entities relating to in-level sound effects. +// +// ambient_generic: a sound emitter used for one-shot and looping sounds. +// +// env_speaker: used for public address announcements over loudspeakers. +// This tries not to drown out talking NPCs. +// +// env_soundscape: controls what sound script an area uses. +// +//=============================================================================// + +#include "cbase.h" +#include "player.h" +#include "mathlib/mathlib.h" +#include "ai_speech.h" +#include "stringregistry.h" +#include "gamerules.h" +#include "game.h" +#include +#include "entitylist.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "ndebugoverlay.h" +#include "soundscape.h" +#include "igamesystem.h" +#include "KeyValues.h" +#include "filesystem.h" + +#ifdef PORTAL +#include "portal_gamerules.h" +#endif // PORTAL + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Compute a suitable attenuation value given an audible radius +// Input : radius - +// playEverywhere - (disable attenuation) +//----------------------------------------------------------------------------- +#define REFERENCE_dB 60.0 + +#define AMBIENT_GENERIC_UPDATE_RATE 5 // update at 5hz +#define AMBIENT_GENERIC_THINK_DELAY ( 1.0f / float( AMBIENT_GENERIC_UPDATE_RATE ) ) + +#ifdef HL1_DLL +ConVar hl1_ref_db_distance( "hl1_ref_db_distance", "18.0" ); +#define REFERENCE_dB_DISTANCE hl1_ref_db_distance.GetFloat() +#else +#define REFERENCE_dB_DISTANCE 36.0 +#endif//HL1_DLL + +static soundlevel_t ComputeSoundlevel( float radius, bool playEverywhere ) +{ + soundlevel_t soundlevel = SNDLVL_NONE; + + if ( radius > 0 && !playEverywhere ) + { + // attenuation is set to a distance, compute falloff + + float dB_loss = 20 * log10( radius / REFERENCE_dB_DISTANCE ); + + soundlevel = (soundlevel_t)(int)(40 + dB_loss); // sound at 40dB at reference distance + } + + return soundlevel; +} + +// ==================== GENERIC AMBIENT SOUND ====================================== + +// runtime pitch shift and volume fadein/out structure + +// NOTE: IF YOU CHANGE THIS STRUCT YOU MUST CHANGE THE SAVE/RESTORE VERSION NUMBER +// SEE BELOW (in the typedescription for the class) +typedef struct dynpitchvol +{ + // NOTE: do not change the order of these parameters + // NOTE: unless you also change order of rgdpvpreset array elements! + int preset; + + int pitchrun; // pitch shift % when sound is running 0 - 255 + int pitchstart; // pitch shift % when sound stops or starts 0 - 255 + int spinup; // spinup time 0 - 100 + int spindown; // spindown time 0 - 100 + + int volrun; // volume change % when sound is running 0 - 10 + int volstart; // volume change % when sound stops or starts 0 - 10 + int fadein; // volume fade in time 0 - 100 + int fadeout; // volume fade out time 0 - 100 + + // Low Frequency Oscillator + int lfotype; // 0) off 1) square 2) triangle 3) random + int lforate; // 0 - 1000, how fast lfo osciallates + + int lfomodpitch; // 0-100 mod of current pitch. 0 is off. + int lfomodvol; // 0-100 mod of current volume. 0 is off. + + int cspinup; // each trigger hit increments counter and spinup pitch + + + int cspincount; + + int pitch; + int spinupsav; + int spindownsav; + int pitchfrac; + + int vol; + int fadeinsav; + int fadeoutsav; + int volfrac; + + int lfofrac; + int lfomult; + + +} dynpitchvol_t; + +#define CDPVPRESETMAX 27 + +// presets for runtime pitch and vol modulation of ambient sounds + +dynpitchvol_t rgdpvpreset[CDPVPRESETMAX] = +{ +// pitch pstart spinup spindwn volrun volstrt fadein fadeout lfotype lforate modptch modvol cspnup +{1, 255, 75, 95, 95, 10, 1, 50, 95, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{2, 255, 85, 70, 88, 10, 1, 20, 88, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{3, 255, 100, 50, 75, 10, 1, 10, 75, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{4, 100, 100, 0, 0, 10, 1, 90, 90, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{5, 100, 100, 0, 0, 10, 1, 80, 80, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{6, 100, 100, 0, 0, 10, 1, 50, 70, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{7, 100, 100, 0, 0, 5, 1, 40, 50, 1, 50, 0, 10, 0, 0,0,0,0,0,0,0,0,0,0}, +{8, 100, 100, 0, 0, 5, 1, 40, 50, 1, 150, 0, 10, 0, 0,0,0,0,0,0,0,0,0,0}, +{9, 100, 100, 0, 0, 5, 1, 40, 50, 1, 750, 0, 10, 0, 0,0,0,0,0,0,0,0,0,0}, +{10,128, 100, 50, 75, 10, 1, 30, 40, 2, 8, 20, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{11,128, 100, 50, 75, 10, 1, 30, 40, 2, 25, 20, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{12,128, 100, 50, 75, 10, 1, 30, 40, 2, 70, 20, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{13,50, 50, 0, 0, 10, 1, 20, 50, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{14,70, 70, 0, 0, 10, 1, 20, 50, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{15,90, 90, 0, 0, 10, 1, 20, 50, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{16,120, 120, 0, 0, 10, 1, 20, 50, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{17,180, 180, 0, 0, 10, 1, 20, 50, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{18,255, 255, 0, 0, 10, 1, 20, 50, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{19,200, 75, 90, 90, 10, 1, 50, 90, 2, 100, 20, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{20,255, 75, 97, 90, 10, 1, 50, 90, 1, 40, 50, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{21,100, 100, 0, 0, 10, 1, 30, 50, 3, 15, 20, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{22,160, 160, 0, 0, 10, 1, 50, 50, 3, 500, 25, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{23,255, 75, 88, 0, 10, 1, 40, 0, 0, 0, 0, 0, 5, 0,0,0,0,0,0,0,0,0,0}, +{24,200, 20, 95, 70, 10, 1, 70, 70, 3, 20, 50, 0, 0, 0,0,0,0,0,0,0,0,0,0}, +{25,180, 100, 50, 60, 10, 1, 40, 60, 2, 90, 100, 100, 0, 0,0,0,0,0,0,0,0,0,0}, +{26,60, 60, 0, 0, 10, 1, 40, 70, 3, 80, 20, 50, 0, 0,0,0,0,0,0,0,0,0,0}, +{27,128, 90, 10, 10, 10, 1, 20, 40, 1, 5, 10, 20, 0, 0,0,0,0,0,0,0,0,0,0} +}; + +class CAmbientGeneric : public CPointEntity +{ +public: + DECLARE_CLASS( CAmbientGeneric, CPointEntity ); + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Spawn( void ); + void Precache( void ); + void Activate( void ); + void RampThink( void ); + void InitModulationParms(void); + void ComputeMaxAudibleDistance( ); + + // Rules about which entities need to transmit along with me + virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); + virtual void UpdateOnRemove( void ); + + void ToggleSound(); + void SendSound( SoundFlags_t flags ); +#ifdef MAPBASE + void SoundEnd(); +#endif + + // Input handlers + void InputPlaySound( inputdata_t &inputdata ); + void InputStopSound( inputdata_t &inputdata ); + void InputToggleSound( inputdata_t &inputdata ); + void InputPitch( inputdata_t &inputdata ); + void InputVolume( inputdata_t &inputdata ); + void InputFadeIn( inputdata_t &inputdata ); + void InputFadeOut( inputdata_t &inputdata ); +#ifdef MAPBASE + void InputSetSound( inputdata_t &inputdata ); +#endif + + DECLARE_DATADESC(); + + float m_radius; + float m_flMaxRadius; + soundlevel_t m_iSoundLevel; // dB value + dynpitchvol_t m_dpv; + + bool m_fActive; // only true when the entity is playing a looping sound + bool m_fLooping; // true when the sound played will loop + + string_t m_iszSound; // Path/filename of WAV file to play. + string_t m_sSourceEntName; + EHANDLE m_hSoundSource; // entity from which the sound comes + int m_nSoundSourceEntIndex; // In case the entity goes away before we finish stopping the sound... + +#ifdef MAPBASE + int m_iSoundFlags; + + COutputEvent m_OnSoundFinished; +#endif +}; + +LINK_ENTITY_TO_CLASS( ambient_generic, CAmbientGeneric ); + +BEGIN_DATADESC( CAmbientGeneric ) + + DEFINE_KEYFIELD( m_iszSound, FIELD_SOUNDNAME, "message" ), + DEFINE_KEYFIELD( m_radius, FIELD_FLOAT, "radius" ), + DEFINE_KEYFIELD( m_sSourceEntName, FIELD_STRING, "SourceEntityName" ), + // recomputed in Activate() + // DEFINE_FIELD( m_hSoundSource, EHANDLE ), + // DEFINE_FIELD( m_nSoundSourceEntIndex, FIELD_INTERGER ), + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_iSoundFlags, FIELD_INTEGER, "soundflags" ), +#endif + + DEFINE_FIELD( m_flMaxRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_fActive, FIELD_BOOLEAN ), + DEFINE_FIELD( m_fLooping, FIELD_BOOLEAN ), + DEFINE_FIELD( m_iSoundLevel, FIELD_INTEGER ), + + // HACKHACK - This is not really in the spirit of the save/restore design, but save this + // out as a binary data block. If the dynpitchvol_t is changed, old saved games will NOT + // load these correctly, so bump the save/restore version if you change the size of the struct + // The right way to do this is to split the input parms (read in keyvalue) into members and re-init this + // struct in Precache(), but it's unlikely that the struct will change, so it's not worth the time right now. + DEFINE_ARRAY( m_dpv, FIELD_CHARACTER, sizeof(dynpitchvol_t) ), + + // Function Pointers + DEFINE_FUNCTION( RampThink ), +#ifdef MAPBASE + DEFINE_THINKFUNC( SoundEnd ), +#endif + + // Inputs + DEFINE_INPUTFUNC(FIELD_VOID, "PlaySound", InputPlaySound ), + DEFINE_INPUTFUNC(FIELD_VOID, "StopSound", InputStopSound ), + DEFINE_INPUTFUNC(FIELD_VOID, "ToggleSound", InputToggleSound ), + DEFINE_INPUTFUNC(FIELD_FLOAT, "Pitch", InputPitch ), + DEFINE_INPUTFUNC(FIELD_FLOAT, "Volume", InputVolume ), + DEFINE_INPUTFUNC(FIELD_FLOAT, "FadeIn", InputFadeIn ), + DEFINE_INPUTFUNC(FIELD_FLOAT, "FadeOut", InputFadeOut ), +#ifdef MAPBASE + DEFINE_INPUTFUNC(FIELD_STRING, "SetSound", InputSetSound ), + + DEFINE_OUTPUT( m_OnSoundFinished, "OnSoundFinished" ), +#endif + +END_DATADESC() + + +#define SF_AMBIENT_SOUND_EVERYWHERE 1 +#define SF_AMBIENT_SOUND_START_SILENT 16 +#define SF_AMBIENT_SOUND_NOT_LOOPING 32 + +#ifdef MAPBASE +static const char *g_SoundEndContext = "SoundEnd"; +#endif + + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CAmbientGeneric::Spawn( void ) +{ + m_iSoundLevel = ComputeSoundlevel( m_radius, FBitSet( m_spawnflags, SF_AMBIENT_SOUND_EVERYWHERE )?true:false ); + ComputeMaxAudibleDistance( ); + + char *szSoundFile = (char *)STRING( m_iszSound ); + if ( !m_iszSound || strlen( szSoundFile ) < 1 ) + { + Warning( "Empty %s (%s) at %.2f, %.2f, %.2f\n", GetClassname(), GetDebugName(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + UTIL_Remove(this); + return; + } + + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + + // Set up think function for dynamic modification + // of ambient sound's pitch or volume. Don't + // start thinking yet. + + SetThink(&CAmbientGeneric::RampThink); + SetNextThink( TICK_NEVER_THINK ); + + m_fActive = false; + + if ( FBitSet ( m_spawnflags, SF_AMBIENT_SOUND_NOT_LOOPING ) ) + { + m_fLooping = false; + } + else + { + m_fLooping = true; + } + + m_hSoundSource = NULL; + m_nSoundSourceEntIndex = -1; + + Precache( ); + + // init all dynamic modulation parms + InitModulationParms(); +} + + +//----------------------------------------------------------------------------- +// Computes the max audible radius for a given sound level +//----------------------------------------------------------------------------- +#define MIN_AUDIBLE_VOLUME 1.01e-3 + +void CAmbientGeneric::ComputeMaxAudibleDistance( ) +{ + if (( m_iSoundLevel == SNDLVL_NONE ) || ( m_radius == 0.0f )) + { + m_flMaxRadius = -1.0f; + return; + } + + // Sadly, there's no direct way of getting at this. + // We have to do an interative computation. + float flGain = enginesound->GetDistGainFromSoundLevel( m_iSoundLevel, m_radius ); + if ( flGain <= MIN_AUDIBLE_VOLUME ) + { + m_flMaxRadius = m_radius; + return; + } + + float flMinRadius = m_radius; + float flMaxRadius = m_radius * 2; + while ( true ) + { + // First, find a min + max range surrounding the desired distance gain + float flGain = enginesound->GetDistGainFromSoundLevel( m_iSoundLevel, flMaxRadius ); + if ( flGain <= MIN_AUDIBLE_VOLUME ) + break; + + // Always audible. + if ( flMaxRadius > 1e5 ) + { + m_flMaxRadius = -1.0f; + return; + } + + flMinRadius = flMaxRadius; + flMaxRadius *= 2.0f; + } + + // Now home in a little bit + int nInterations = 4; + while ( --nInterations >= 0 ) + { + float flTestRadius = (flMinRadius + flMaxRadius) * 0.5f; + float flGain = enginesound->GetDistGainFromSoundLevel( m_iSoundLevel, flTestRadius ); + if ( flGain <= MIN_AUDIBLE_VOLUME ) + { + flMaxRadius = flTestRadius; + } + else + { + flMinRadius = flTestRadius; + } + } + + m_flMaxRadius = flMaxRadius; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for changing pitch. +// Input : Float new pitch from 0 - 255 (100 = as recorded). +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputPitch( inputdata_t &inputdata ) +{ + m_dpv.pitch = clamp( FastFloatToSmallInt( inputdata.value.Float() ), 0, 255 ); + + SendSound( SND_CHANGE_PITCH ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for changing volume. +// Input : Float new volume, from 0 - 10. +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputVolume( inputdata_t &inputdata ) +{ + // + // Multiply the input value by ten since volumes are expected to be from 0 - 100. + // + m_dpv.vol = clamp( RoundFloatToInt( inputdata.value.Float() * 10.f ), 0, 100 ); + m_dpv.volfrac = m_dpv.vol << 8; + + SendSound( SND_CHANGE_VOL ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for fading in volume over time. +// Input : Float volume fade in time 0 - 100 seconds +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputFadeIn( inputdata_t &inputdata ) +{ + // cancel any fade out that might be happening + m_dpv.fadeout = 0; + + m_dpv.fadein = inputdata.value.Float(); + if (m_dpv.fadein > 100) m_dpv.fadein = 100; + if (m_dpv.fadein < 0) m_dpv.fadein = 0; + + if (m_dpv.fadein > 0) + m_dpv.fadein = ( 100 << 8 ) / ( m_dpv.fadein * AMBIENT_GENERIC_UPDATE_RATE ); + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for fading out volume over time. +// Input : Float volume fade out time 0 - 100 seconds +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputFadeOut( inputdata_t &inputdata ) +{ + // cancel any fade in that might be happening + m_dpv.fadein = 0; + + m_dpv.fadeout = inputdata.value.Float(); + + if (m_dpv.fadeout > 100) m_dpv.fadeout = 100; + if (m_dpv.fadeout < 0) m_dpv.fadeout = 0; + + if (m_dpv.fadeout > 0) + m_dpv.fadeout = ( 100 << 8 ) / ( m_dpv.fadeout * AMBIENT_GENERIC_UPDATE_RATE ); + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputSetSound( inputdata_t &inputdata ) +{ + m_iszSound = inputdata.value.StringID(); + if (STRING(m_iszSound)[0] != '!') + { + PrecacheScriptSound(STRING(m_iszSound)); + } + + // Try to refresh the sound + if (m_fActive) + SendSound(SND_NOFLAGS); +} +#endif + + +void CAmbientGeneric::Precache( void ) +{ + char *szSoundFile = (char *)STRING( m_iszSound ); + if ( m_iszSound != NULL_STRING && strlen( szSoundFile ) > 1 ) + { + if (*szSoundFile != '!') + { + PrecacheScriptSound(szSoundFile); + } + } + + if ( !FBitSet (m_spawnflags, SF_AMBIENT_SOUND_START_SILENT ) ) + { + // start the sound ASAP + if (m_fLooping) + m_fActive = true; + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CAmbientGeneric::Activate( void ) +{ + BaseClass::Activate(); + + // Initialize sound source. If no source was given, or source can't be found + // then this is the source + if (m_hSoundSource == NULL) + { + if (m_sSourceEntName != NULL_STRING) + { + m_hSoundSource = gEntList.FindEntityByName( NULL, m_sSourceEntName ); + if ( m_hSoundSource != NULL ) + { + m_nSoundSourceEntIndex = m_hSoundSource->entindex(); + } + } + + if (m_hSoundSource == NULL) + { + m_hSoundSource = this; + m_nSoundSourceEntIndex = entindex(); + } + else + { + if ( !FBitSet( m_spawnflags, SF_AMBIENT_SOUND_EVERYWHERE ) ) + { + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + } + } + } + +#ifdef PORTAL + // This is the only way we can silence the radio sound from the first room without touching them map -- jdw + if ( PortalGameRules() && PortalGameRules()->ShouldRemoveRadio() ) + { + if ( V_strcmp( STRING( gpGlobals->mapname ), "testchmb_a_00" ) == 0 || + V_strcmp( STRING( gpGlobals->mapname ), "testchmb_a_11" ) == 0 || + V_strcmp( STRING( gpGlobals->mapname ), "testchmb_a_14" ) == 0 ) + { + if ( V_strcmp( STRING( GetEntityName() ), "radio_sound" ) == 0 ) + { + UTIL_Remove( this ); + return; + } + } + } +#endif // PORTAL + + // If active start the sound + if ( m_fActive ) + { + int flags = SND_SPAWNING; + // If we are loading a saved game, we can't write into the init/signon buffer here, so just issue + // as a regular sound message... + if ( gpGlobals->eLoadType == MapLoad_Transition || + gpGlobals->eLoadType == MapLoad_LoadGame || + g_pGameRules->InRoundRestart() ) + { + flags = SND_NOFLAGS; + } + + // Tracker 76119: 8/12/07 ywb: + // Make sure pitch and volume are set up to the correct value (especially after restoring a .sav file) + flags |= ( SND_CHANGE_PITCH | SND_CHANGE_VOL ); + + // Don't bother sending over to client if volume is zero, though + if ( m_dpv.vol > 0 ) + { + SendSound( (SoundFlags_t)flags ); + } + + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + + +//----------------------------------------------------------------------------- +// Rules about which entities need to transmit along with me +//----------------------------------------------------------------------------- +void CAmbientGeneric::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) +{ + // Ambient generics never transmit; this is just a way for us to ensure + // the sound source gets transmitted; that's why we don't call pInfo->m_pTransmitEdict->Set + if ( !m_hSoundSource || m_hSoundSource == this || !m_fActive ) + return; + + // Don't bother sending the position of the source if we have to play everywhere + if ( FBitSet( m_spawnflags, SF_AMBIENT_SOUND_EVERYWHERE ) ) + return; + + Assert( pInfo->m_pClientEnt ); + CBaseEntity *pClient = (CBaseEntity*)(pInfo->m_pClientEnt->GetUnknown()); + if ( !pClient ) + return; + + // Send the sound source if he's close enough + if ( ( m_flMaxRadius < 0 ) || ( pClient->GetAbsOrigin().DistToSqr( m_hSoundSource->GetAbsOrigin() ) <= m_flMaxRadius * m_flMaxRadius ) ) + { + m_hSoundSource->SetTransmit( pInfo, false ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAmbientGeneric::UpdateOnRemove( void ) +{ + if ( m_fActive ) + { + // Stop the sound we're generating + SendSound( SND_STOP ); + } + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +// Purpose: Think at 5hz if we are dynamically modifying pitch or volume of the +// playing sound. This function will ramp pitch and/or volume up or +// down, modify pitch/volume with lfo if active. +//----------------------------------------------------------------------------- +void CAmbientGeneric::RampThink( void ) +{ + int pitch = m_dpv.pitch; + int vol = m_dpv.vol; + int flags = 0; + int fChanged = 0; // false if pitch and vol remain unchanged this round + int prev; + + if (!m_dpv.spinup && !m_dpv.spindown && !m_dpv.fadein && !m_dpv.fadeout && !m_dpv.lfotype) + return; // no ramps or lfo, stop thinking + + // ============== + // pitch envelope + // ============== + if (m_dpv.spinup || m_dpv.spindown) + { + prev = m_dpv.pitchfrac >> 8; + + if (m_dpv.spinup > 0) + m_dpv.pitchfrac += m_dpv.spinup; + else if (m_dpv.spindown > 0) + m_dpv.pitchfrac -= m_dpv.spindown; + + pitch = m_dpv.pitchfrac >> 8; + + if (pitch > m_dpv.pitchrun) + { + pitch = m_dpv.pitchrun; + m_dpv.spinup = 0; // done with ramp up + } + + if (pitch < m_dpv.pitchstart) + { + pitch = m_dpv.pitchstart; + m_dpv.spindown = 0; // done with ramp down + + // shut sound off + SendSound( SND_STOP ); + + // return without setting m_flNextThink + return; + } + + if (pitch > 255) pitch = 255; + if (pitch < 1) pitch = 1; + + m_dpv.pitch = pitch; + + fChanged |= (prev != pitch); + flags |= SND_CHANGE_PITCH; + } + + // ================== + // amplitude envelope + // ================== + if (m_dpv.fadein || m_dpv.fadeout) + { + prev = m_dpv.volfrac >> 8; + + if (m_dpv.fadein > 0) + m_dpv.volfrac += m_dpv.fadein; + else if (m_dpv.fadeout > 0) + m_dpv.volfrac -= m_dpv.fadeout; + + vol = m_dpv.volfrac >> 8; + + if (vol > m_dpv.volrun) + { + vol = m_dpv.volrun; + m_dpv.volfrac = vol << 8; + m_dpv.fadein = 0; // done with ramp up + } + + if (vol < m_dpv.volstart) + { + vol = m_dpv.volstart; + m_dpv.vol = vol; + m_dpv.volfrac = vol << 8; + m_dpv.fadeout = 0; // done with ramp down + + // shut sound off + SendSound( SND_STOP ); + + // return without setting m_flNextThink + return; + } + + if (vol > 100) + { + vol = 100; + m_dpv.volfrac = vol << 8; + } + if (vol < 1) + { + vol = 1; + m_dpv.volfrac = vol << 8; + } + + m_dpv.vol = vol; + + fChanged |= (prev != vol); + flags |= SND_CHANGE_VOL; + } + + // =================== + // pitch/amplitude LFO + // =================== + if (m_dpv.lfotype) + { + int pos; + + if (m_dpv.lfofrac > 0x6fffffff) + m_dpv.lfofrac = 0; + + // update lfo, lfofrac/255 makes a triangle wave 0-255 + m_dpv.lfofrac += m_dpv.lforate; + pos = m_dpv.lfofrac >> 8; + + if (m_dpv.lfofrac < 0) + { + m_dpv.lfofrac = 0; + m_dpv.lforate = abs(m_dpv.lforate); + pos = 0; + } + else if (pos > 255) + { + pos = 255; + m_dpv.lfofrac = (255 << 8); + m_dpv.lforate = -abs(m_dpv.lforate); + } + + switch(m_dpv.lfotype) + { + case LFO_SQUARE: + if (pos < 128) + m_dpv.lfomult = 255; + else + m_dpv.lfomult = 0; + + break; + case LFO_RANDOM: + if (pos == 255) + m_dpv.lfomult = random->RandomInt(0, 255); + break; + case LFO_TRIANGLE: + default: + m_dpv.lfomult = pos; + break; + } + + if (m_dpv.lfomodpitch) + { + prev = pitch; + + // pitch 0-255 + pitch += ((m_dpv.lfomult - 128) * m_dpv.lfomodpitch) / 100; + + if (pitch > 255) pitch = 255; + if (pitch < 1) pitch = 1; + + + fChanged |= (prev != pitch); + flags |= SND_CHANGE_PITCH; + } + + if (m_dpv.lfomodvol) + { + // vol 0-100 + prev = vol; + + vol += ((m_dpv.lfomult - 128) * m_dpv.lfomodvol) / 100; + + if (vol > 100) vol = 100; + if (vol < 0) vol = 0; + + fChanged |= (prev != vol); + flags |= SND_CHANGE_VOL; + } + + } + + // Send update to playing sound only if we actually changed + // pitch or volume in this routine. + + if (flags && fChanged) + { + if (pitch == PITCH_NORM) + pitch = PITCH_NORM + 1; // don't send 'no pitch' ! + + CBaseEntity* pSoundSource = m_hSoundSource; + if (pSoundSource) + { + UTIL_EmitAmbientSound(pSoundSource->GetSoundSourceIndex(), pSoundSource->GetAbsOrigin(), + STRING( m_iszSound ), (vol * 0.01), m_iSoundLevel, flags, pitch); + } + } + + // update ramps at 5hz + SetNextThink( gpGlobals->curtime + AMBIENT_GENERIC_THINK_DELAY ); + return; +} + + +//----------------------------------------------------------------------------- +// Purpose: Init all ramp params in preparation to play a new sound. +//----------------------------------------------------------------------------- +void CAmbientGeneric::InitModulationParms(void) +{ + int pitchinc; + + m_dpv.volrun = m_iHealth * 10; // 0 - 100 + if (m_dpv.volrun > 100) m_dpv.volrun = 100; + if (m_dpv.volrun < 0) m_dpv.volrun = 0; + + // get presets + if (m_dpv.preset != 0 && m_dpv.preset <= CDPVPRESETMAX) + { + // load preset values + m_dpv = rgdpvpreset[m_dpv.preset - 1]; + + // fixup preset values, just like + // fixups in KeyValue routine. + if (m_dpv.spindown > 0) + m_dpv.spindown = (101 - m_dpv.spindown) * 64; + if (m_dpv.spinup > 0) + m_dpv.spinup = (101 - m_dpv.spinup) * 64; + + m_dpv.volstart *= 10; + m_dpv.volrun *= 10; + + if (m_dpv.fadein > 0) + m_dpv.fadein = (101 - m_dpv.fadein) * 64; + if (m_dpv.fadeout > 0) + m_dpv.fadeout = (101 - m_dpv.fadeout) * 64; + + m_dpv.lforate *= 256; + + m_dpv.fadeinsav = m_dpv.fadein; + m_dpv.fadeoutsav = m_dpv.fadeout; + m_dpv.spinupsav = m_dpv.spinup; + m_dpv.spindownsav = m_dpv.spindown; + } + + m_dpv.fadein = m_dpv.fadeinsav; + m_dpv.fadeout = 0; + + if (m_dpv.fadein) + m_dpv.vol = m_dpv.volstart; + else + m_dpv.vol = m_dpv.volrun; + + m_dpv.spinup = m_dpv.spinupsav; + m_dpv.spindown = 0; + + if (m_dpv.spinup) + m_dpv.pitch = m_dpv.pitchstart; + else + m_dpv.pitch = m_dpv.pitchrun; + + if (m_dpv.pitch == 0) + m_dpv.pitch = PITCH_NORM; + + m_dpv.pitchfrac = m_dpv.pitch << 8; + m_dpv.volfrac = m_dpv.vol << 8; + + m_dpv.lfofrac = 0; + m_dpv.lforate = abs(m_dpv.lforate); + + m_dpv.cspincount = 1; + + if (m_dpv.cspinup) + { + pitchinc = (255 - m_dpv.pitchstart) / m_dpv.cspinup; + + m_dpv.pitchrun = m_dpv.pitchstart + pitchinc; + if (m_dpv.pitchrun > 255) m_dpv.pitchrun = 255; + } + + if ((m_dpv.spinupsav || m_dpv.spindownsav || (m_dpv.lfotype && m_dpv.lfomodpitch)) + && (m_dpv.pitch == PITCH_NORM)) + m_dpv.pitch = PITCH_NORM + 1; // must never send 'no pitch' as first pitch + // if we intend to pitch shift later! +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that begins playing the sound. +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputPlaySound( inputdata_t &inputdata ) +{ + if (!m_fActive) + { + //Adrian: Stop our current sound before starting a new one! + SendSound( SND_STOP ); + +#ifdef MAPBASE + // Procedural handling like !activator + if (STRING(m_sSourceEntName)[0] == '!') + { + CBaseEntity *pEntity = gEntList.FindEntityProcedural(STRING(m_sSourceEntName), this, inputdata.pActivator, inputdata.pCaller); + if (pEntity) + { + m_hSoundSource = pEntity; + m_nSoundSourceEntIndex = pEntity->entindex(); + } + } +#endif + + ToggleSound(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that stops playing the sound. +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputStopSound( inputdata_t &inputdata ) +{ + if (m_fActive) + { + ToggleSound(); + } +} + +void CAmbientGeneric::SendSound( SoundFlags_t flags) +{ +#ifdef MAPBASE + int iFlags = flags != SND_STOP ? ((int)flags | m_iSoundFlags) : flags; + char *szSoundFile = (char *)STRING( m_iszSound ); + CBaseEntity* pSoundSource = m_hSoundSource; + if ( pSoundSource ) + { + if ( iFlags & SND_STOP ) + { + UTIL_EmitAmbientSound(pSoundSource->GetSoundSourceIndex(), pSoundSource->GetAbsOrigin(), szSoundFile, + 0, SNDLVL_NONE, iFlags, 0); + + SetContextThink( NULL, TICK_NEVER_THINK, g_SoundEndContext ); + + m_fActive = false; + } + else + { + float duration = 0.0f; + UTIL_EmitAmbientSound(pSoundSource->GetSoundSourceIndex(), pSoundSource->GetAbsOrigin(), szSoundFile, + (m_dpv.vol * 0.01), m_iSoundLevel, iFlags, m_dpv.pitch, 0.0f, &duration); + + SetContextThink( &CAmbientGeneric::SoundEnd, gpGlobals->curtime + duration, g_SoundEndContext ); + + // Only mark active if this is a looping sound. If not looping, each + // trigger will cause the sound to play. If the sound is still + // playing from a previous trigger press, it will be shut off + // and then restarted. + + if (m_fLooping) + m_fActive = true; + } + } + else + { + if ( ( iFlags & SND_STOP ) && + ( m_nSoundSourceEntIndex != -1 ) ) + { + UTIL_EmitAmbientSound(m_nSoundSourceEntIndex, GetAbsOrigin(), szSoundFile, + 0, SNDLVL_NONE, iFlags, 0); + + SetContextThink( NULL, TICK_NEVER_THINK, g_SoundEndContext ); + + m_fActive = false; + } + } +#else + char *szSoundFile = (char *)STRING( m_iszSound ); + CBaseEntity* pSoundSource = m_hSoundSource; + if ( pSoundSource ) + { + if ( flags == SND_STOP ) + { + UTIL_EmitAmbientSound(pSoundSource->GetSoundSourceIndex(), pSoundSource->GetAbsOrigin(), szSoundFile, + 0, SNDLVL_NONE, flags, 0); + } + else + { + UTIL_EmitAmbientSound(pSoundSource->GetSoundSourceIndex(), pSoundSource->GetAbsOrigin(), szSoundFile, + (m_dpv.vol * 0.01), m_iSoundLevel, flags, m_dpv.pitch); + } + } + else + { + if ( ( flags == SND_STOP ) && + ( m_nSoundSourceEntIndex != -1 ) ) + { + UTIL_EmitAmbientSound(m_nSoundSourceEntIndex, GetAbsOrigin(), szSoundFile, + 0, SNDLVL_NONE, flags, 0); + } + } +#endif +} + +#ifdef MAPBASE +void CAmbientGeneric::SoundEnd() +{ + m_OnSoundFinished.FireOutput(this, this); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that stops playing the sound. +//----------------------------------------------------------------------------- +void CAmbientGeneric::InputToggleSound( inputdata_t &inputdata ) +{ +#ifdef MAPBASE + // Procedural handling like !activator + if (STRING(m_sSourceEntName)[0] == '!') + { + CBaseEntity *pEntity = gEntList.FindEntityProcedural(STRING(m_sSourceEntName), this, inputdata.pActivator, inputdata.pCaller); + if (pEntity) + { + m_hSoundSource = pEntity; + m_nSoundSourceEntIndex = pEntity->entindex(); + } + } +#endif + + ToggleSound(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Turns an ambient sound on or off. If the ambient is a looping sound, +// mark sound as active (m_fActive) if it's playing, innactive if not. +// If the sound is not a looping sound, never mark it as active. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CAmbientGeneric::ToggleSound() +{ + // m_fActive is true only if a looping sound is playing. + + if ( m_fActive ) + {// turn sound off + + if (m_dpv.cspinup) + { + // Don't actually shut off. Each toggle causes + // incremental spinup to max pitch + + if (m_dpv.cspincount <= m_dpv.cspinup) + { + int pitchinc; + + // start a new spinup + m_dpv.cspincount++; + + pitchinc = (255 - m_dpv.pitchstart) / m_dpv.cspinup; + + m_dpv.spinup = m_dpv.spinupsav; + m_dpv.spindown = 0; + + m_dpv.pitchrun = m_dpv.pitchstart + pitchinc * m_dpv.cspincount; + if (m_dpv.pitchrun > 255) m_dpv.pitchrun = 255; + + SetNextThink( gpGlobals->curtime + 0.1f ); + } + + } + else + { + m_fActive = false; + + // HACKHACK - this makes the code in Precache() work properly after a save/restore + m_spawnflags |= SF_AMBIENT_SOUND_START_SILENT; + + if (m_dpv.spindownsav || m_dpv.fadeoutsav) + { + // spin it down (or fade it) before shutoff if spindown is set + m_dpv.spindown = m_dpv.spindownsav; + m_dpv.spinup = 0; + + m_dpv.fadeout = m_dpv.fadeoutsav; + m_dpv.fadein = 0; + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else + { + SendSound( SND_STOP ); // stop sound + } + } + } + else + {// turn sound on + + // only toggle if this is a looping sound. If not looping, each + // trigger will cause the sound to play. If the sound is still + // playing from a previous trigger press, it will be shut off + // and then restarted. + + if (m_fLooping) + m_fActive = true; + else + { + // shut sound off now - may be interrupting a long non-looping sound + SendSound( SND_STOP ); // stop sound + } + + // init all ramp params for startup + + InitModulationParms(); + + SendSound( SND_NOFLAGS ); // send sound + + SetNextThink( gpGlobals->curtime + 0.1f ); + + } +} + + +// KeyValue - load keyvalue pairs into member data of the +// ambient generic. NOTE: called BEFORE spawn! +bool CAmbientGeneric::KeyValue( const char *szKeyName, const char *szValue ) +{ + // NOTE: changing any of the modifiers in this code + // NOTE: also requires changing InitModulationParms code. + + // preset + if (FStrEq(szKeyName, "preset")) + { + m_dpv.preset = atoi(szValue); + } + // pitchrun + else if (FStrEq(szKeyName, "pitch")) + { + m_dpv.pitchrun = atoi(szValue); + + if (m_dpv.pitchrun > 255) m_dpv.pitchrun = 255; + if (m_dpv.pitchrun < 0) m_dpv.pitchrun = 0; + } + // pitchstart + else if (FStrEq(szKeyName, "pitchstart")) + { + m_dpv.pitchstart = atoi(szValue); + + if (m_dpv.pitchstart > 255) m_dpv.pitchstart = 255; + if (m_dpv.pitchstart < 0) m_dpv.pitchstart = 0; + } + // spinup + else if (FStrEq(szKeyName, "spinup")) + { + m_dpv.spinup = atoi(szValue); + + if (m_dpv.spinup > 100) m_dpv.spinup = 100; + if (m_dpv.spinup < 0) m_dpv.spinup = 0; + + if (m_dpv.spinup > 0) + m_dpv.spinup = (101 - m_dpv.spinup) * 64; + m_dpv.spinupsav = m_dpv.spinup; + } + // spindown + else if (FStrEq(szKeyName, "spindown")) + { + m_dpv.spindown = atoi(szValue); + + if (m_dpv.spindown > 100) m_dpv.spindown = 100; + if (m_dpv.spindown < 0) m_dpv.spindown = 0; + + if (m_dpv.spindown > 0) + m_dpv.spindown = (101 - m_dpv.spindown) * 64; + m_dpv.spindownsav = m_dpv.spindown; + } + // volstart + else if (FStrEq(szKeyName, "volstart")) + { + m_dpv.volstart = atoi(szValue); + + if (m_dpv.volstart > 10) m_dpv.volstart = 10; + if (m_dpv.volstart < 0) m_dpv.volstart = 0; + + m_dpv.volstart *= 10; // 0 - 100 + } + // legacy fadein + else if (FStrEq(szKeyName, "fadein")) + { + m_dpv.fadein = atoi(szValue); + + if (m_dpv.fadein > 100) m_dpv.fadein = 100; + if (m_dpv.fadein < 0) m_dpv.fadein = 0; + + if (m_dpv.fadein > 0) + m_dpv.fadein = (101 - m_dpv.fadein) * 64; + m_dpv.fadeinsav = m_dpv.fadein; + } + // legacy fadeout + else if (FStrEq(szKeyName, "fadeout")) + { + m_dpv.fadeout = atoi(szValue); + + if (m_dpv.fadeout > 100) m_dpv.fadeout = 100; + if (m_dpv.fadeout < 0) m_dpv.fadeout = 0; + + if (m_dpv.fadeout > 0) + m_dpv.fadeout = (101 - m_dpv.fadeout) * 64; + m_dpv.fadeoutsav = m_dpv.fadeout; + } + // fadeinsecs + else if (FStrEq(szKeyName, "fadeinsecs")) + { + m_dpv.fadein = atoi(szValue); + + if (m_dpv.fadein > 100) m_dpv.fadein = 100; + if (m_dpv.fadein < 0) m_dpv.fadein = 0; + + if (m_dpv.fadein > 0) + m_dpv.fadein = ( 100 << 8 ) / ( m_dpv.fadein * AMBIENT_GENERIC_UPDATE_RATE ); + m_dpv.fadeinsav = m_dpv.fadein; + } + // fadeoutsecs + else if (FStrEq(szKeyName, "fadeoutsecs")) + { + m_dpv.fadeout = atoi(szValue); + + if (m_dpv.fadeout > 100) m_dpv.fadeout = 100; + if (m_dpv.fadeout < 0) m_dpv.fadeout = 0; + + if (m_dpv.fadeout > 0) + m_dpv.fadeout = ( 100 << 8 ) / ( m_dpv.fadeout * AMBIENT_GENERIC_UPDATE_RATE ); + m_dpv.fadeoutsav = m_dpv.fadeout; + } + // lfotype + else if (FStrEq(szKeyName, "lfotype")) + { + m_dpv.lfotype = atoi(szValue); + if (m_dpv.lfotype > 4) m_dpv.lfotype = LFO_TRIANGLE; + } + // lforate + else if (FStrEq(szKeyName, "lforate")) + { + m_dpv.lforate = atoi(szValue); + + if (m_dpv.lforate > 1000) m_dpv.lforate = 1000; + if (m_dpv.lforate < 0) m_dpv.lforate = 0; + + m_dpv.lforate *= 256; + } + // lfomodpitch + else if (FStrEq(szKeyName, "lfomodpitch")) + { + m_dpv.lfomodpitch = atoi(szValue); + if (m_dpv.lfomodpitch > 100) m_dpv.lfomodpitch = 100; + if (m_dpv.lfomodpitch < 0) m_dpv.lfomodpitch = 0; + } + + // lfomodvol + else if (FStrEq(szKeyName, "lfomodvol")) + { + m_dpv.lfomodvol = atoi(szValue); + if (m_dpv.lfomodvol > 100) m_dpv.lfomodvol = 100; + if (m_dpv.lfomodvol < 0) m_dpv.lfomodvol = 0; + } + // cspinup + else if (FStrEq(szKeyName, "cspinup")) + { + m_dpv.cspinup = atoi(szValue); + if (m_dpv.cspinup > 100) m_dpv.cspinup = 100; + if (m_dpv.cspinup < 0) m_dpv.cspinup = 0; + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + +// =================== ROOM SOUND FX ========================================== + + + + +// ==================== SENTENCE GROUPS, UTILITY FUNCTIONS ====================================== + +int fSentencesInit = false; + +// ===================== SENTENCE GROUPS, MAIN ROUTINES ======================== + +// given sentence group index, play random sentence for given entity. +// returns sentenceIndex - which sentence was picked +// Ipick is only needed if you plan on stopping the sound before playback is done (see SENTENCEG_Stop). +// sentenceIndex can be used to find the name/length of the sentence + +int SENTENCEG_PlayRndI(edict_t *entity, int isentenceg, + float volume, soundlevel_t soundlevel, int flags, int pitch) +{ + char name[64]; + int ipick; + + if (!fSentencesInit) + return -1; + + name[0] = 0; + + ipick = engine->SentenceGroupPick( isentenceg, name, sizeof( name ) ); + if (ipick > 0 && name) + { + int sentenceIndex = SENTENCEG_Lookup( name ); + CPASAttenuationFilter filter( GetContainingEntity( entity ), soundlevel ); + CBaseEntity::EmitSentenceByIndex( filter, ENTINDEX(entity), CHAN_VOICE, sentenceIndex, volume, soundlevel, flags, pitch ); + return sentenceIndex; + } + + return -1; +} + + +//----------------------------------------------------------------------------- +// Picks a sentence, but doesn't play it +//----------------------------------------------------------------------------- +int SENTENCEG_PickRndSz(const char *szgroupname) +{ + char name[64]; + int ipick; + int isentenceg; + + if (!fSentencesInit) + return -1; + + name[0] = 0; + + isentenceg = engine->SentenceGroupIndexFromName(szgroupname); + if (isentenceg < 0) + { + Warning( "No such sentence group %s\n", szgroupname ); + return -1; + } + + ipick = engine->SentenceGroupPick(isentenceg, name, sizeof( name )); + if (ipick >= 0 && name[0]) + { + return SENTENCEG_Lookup( name ); + } + return -1; +} + +//----------------------------------------------------------------------------- +// Plays a sentence by sentence index +//----------------------------------------------------------------------------- +void SENTENCEG_PlaySentenceIndex( edict_t *entity, int iSentenceIndex, float volume, soundlevel_t soundlevel, int flags, int pitch ) +{ + if ( iSentenceIndex >= 0 ) + { + CPASAttenuationFilter filter( GetContainingEntity( entity ), soundlevel ); + CBaseEntity::EmitSentenceByIndex( filter, ENTINDEX(entity), CHAN_VOICE, iSentenceIndex, volume, soundlevel, flags, pitch ); + } +} + + +int SENTENCEG_PlayRndSz(edict_t *entity, const char *szgroupname, + float volume, soundlevel_t soundlevel, int flags, int pitch) +{ + char name[64]; + int ipick; + int isentenceg; + + if (!fSentencesInit) + return -1; + + name[0] = 0; + + isentenceg = engine->SentenceGroupIndexFromName(szgroupname); + if (isentenceg < 0) + { + Warning( "No such sentence group %s\n", szgroupname ); + return -1; + } + + ipick = engine->SentenceGroupPick(isentenceg, name, sizeof( name )); + if (ipick >= 0 && name[0]) + { + int sentenceIndex = SENTENCEG_Lookup( name ); + CPASAttenuationFilter filter( GetContainingEntity( entity ), soundlevel ); + CBaseEntity::EmitSentenceByIndex( filter, ENTINDEX(entity), CHAN_VOICE, sentenceIndex, volume, soundlevel, flags, pitch ); + return sentenceIndex; + } + + return -1; +} + +// play sentences in sequential order from sentence group. Reset after last sentence. + +int SENTENCEG_PlaySequentialSz(edict_t *entity, const char *szgroupname, + float volume, soundlevel_t soundlevel, int flags, int pitch, int ipick, int freset) +{ + char name[64]; + int ipicknext; + int isentenceg; + + if (!fSentencesInit) + return -1; + + name[0] = 0; + + isentenceg = engine->SentenceGroupIndexFromName(szgroupname); + if (isentenceg < 0) + return -1; + + ipicknext = engine->SentenceGroupPickSequential(isentenceg, name, sizeof( name ), ipick, freset); + if (ipicknext >= 0 && name[0]) + { + int sentenceIndex = SENTENCEG_Lookup( name ); + CPASAttenuationFilter filter( GetContainingEntity( entity ), soundlevel ); + CBaseEntity::EmitSentenceByIndex( filter, ENTINDEX(entity), CHAN_VOICE, sentenceIndex, volume, soundlevel, flags, pitch ); + return sentenceIndex; + } + + return -1; +} + + +#if 0 +// for this entity, for the given sentence within the sentence group, stop +// the sentence. + +void SENTENCEG_Stop(edict_t *entity, int isentenceg, int ipick) +{ + char buffer[64]; + char sznum[8]; + + if (!fSentencesInit) + return; + + if (isentenceg < 0 || ipick < 0) + return; + + Q_snprintf(buffer,sizeof(buffer),"!%s%d", engine->SentenceGroupNameFromIndex( isentenceg ), ipick ); + + UTIL_StopSound(entity, CHAN_VOICE, buffer); +} +#endif + +// open sentences.txt, scan for groups, build rgsentenceg +// Should be called from world spawn, only works on the +// first call and is ignored subsequently. +void SENTENCEG_Init() +{ + if (fSentencesInit) + return; + + engine->PrecacheSentenceFile( "scripts/sentences.txt" ); + fSentencesInit = true; +} + +// convert sentence (sample) name to !sentencenum, return !sentencenum + +int SENTENCEG_Lookup(const char *sample) +{ + return engine->SentenceIndexFromName( sample + 1 ); +} + + +int SENTENCEG_GetIndex(const char *szrootname) +{ + return engine->SentenceGroupIndexFromName( szrootname ); +} + +void UTIL_RestartAmbientSounds( void ) +{ + CAmbientGeneric *pAmbient = NULL; + while ( ( pAmbient = (CAmbientGeneric*) gEntList.FindEntityByClassname( pAmbient, "ambient_generic" ) ) != NULL ) + { + if (pAmbient->m_fActive ) + { + if ( strstr( STRING( pAmbient->m_iszSound ), "mp3" ) ) + { + pAmbient->SendSound( SND_CHANGE_VOL ); // fake a change, so we don't create 2 sounds + } + pAmbient->SendSound( SND_CHANGE_VOL ); // fake a change, so we don't create 2 sounds + } + } +} + + +// play a specific sentence over the HEV suit speaker - just pass player entity, and !sentencename + +void UTIL_EmitSoundSuit(edict_t *entity, const char *sample) +{ + float fvol; + int pitch = PITCH_NORM; + + fvol = suitvolume.GetFloat(); + if (random->RandomInt(0,1)) + pitch = random->RandomInt(0,6) + 98; + + // If friendlies are talking, reduce the volume of the suit + if ( !g_AIFriendliesTalkSemaphore.IsAvailable( GetContainingEntity( entity ) ) ) + { + fvol *= 0.3; + } + + if (fvol > 0.05) + { + CPASAttenuationFilter filter( GetContainingEntity( entity ) ); + filter.MakeReliable(); + + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = sample; + ep.m_flVolume = fvol; + ep.m_SoundLevel = SNDLVL_NORM; + ep.m_nPitch = pitch; + + CBaseEntity::EmitSound( filter, ENTINDEX(entity), ep ); + } +} + +// play a sentence, randomly selected from the passed in group id, over the HEV suit speaker + +int UTIL_EmitGroupIDSuit(edict_t *entity, int isentenceg) +{ + float fvol; + int pitch = PITCH_NORM; + int sentenceIndex = -1; + + fvol = suitvolume.GetFloat(); + if (random->RandomInt(0,1)) + pitch = random->RandomInt(0,6) + 98; + + // If friendlies are talking, reduce the volume of the suit + if ( !g_AIFriendliesTalkSemaphore.IsAvailable( GetContainingEntity( entity ) ) ) + { + fvol *= 0.3; + } + + if (fvol > 0.05) + sentenceIndex = SENTENCEG_PlayRndI(entity, isentenceg, fvol, SNDLVL_NORM, 0, pitch); + + return sentenceIndex; +} + +// play a sentence, randomly selected from the passed in groupname + +int UTIL_EmitGroupnameSuit(edict_t *entity, const char *groupname) +{ + float fvol; + int pitch = PITCH_NORM; + int sentenceIndex = -1; + + fvol = suitvolume.GetFloat(); + if (random->RandomInt(0,1)) + pitch = random->RandomInt(0,6) + 98; + + // If friendlies are talking, reduce the volume of the suit + if ( !g_AIFriendliesTalkSemaphore.IsAvailable( GetContainingEntity( entity ) ) ) + { + fvol *= 0.3; + } + + if (fvol > 0.05) + sentenceIndex = SENTENCEG_PlayRndSz(entity, groupname, fvol, SNDLVL_NORM, 0, pitch); + + return sentenceIndex; +} + +// ===================== MATERIAL TYPE DETECTION, MAIN ROUTINES ======================== +// +// Used to detect the texture the player is standing on, map the +// texture name to a material type. Play footstep sound based +// on material type. + +char TEXTURETYPE_Find( trace_t *ptr ) +{ + const surfacedata_t *psurfaceData = physprops->GetSurfaceData( ptr->surface.surfaceProps ); + + return psurfaceData->game.material; +} diff --git a/sp/src/game/server/soundent.cpp b/sp/src/game/server/soundent.cpp new file mode 100644 index 00000000..59273206 --- /dev/null +++ b/sp/src/game/server/soundent.cpp @@ -0,0 +1,879 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "soundent.h" +#include "game.h" +#include "world.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Some enumerations needed by CSoundEnt +//----------------------------------------------------------------------------- + +// identifiers passed to functions that can operate on either list, to indicate which list to operate on. +#define SOUNDLISTTYPE_FREE 1 +#define SOUNDLISTTYPE_ACTIVE 2 + + + +LINK_ENTITY_TO_CLASS( soundent, CSoundEnt ); + +static CSoundEnt *g_pSoundEnt = NULL; + +BEGIN_SIMPLE_DATADESC( CSound ) + + DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ), + DEFINE_FIELD( m_iVolume, FIELD_INTEGER ), + DEFINE_FIELD( m_flOcclusionScale, FIELD_FLOAT ), + DEFINE_FIELD( m_iType, FIELD_INTEGER ), +// DEFINE_FIELD( m_iNextAudible, FIELD_INTEGER ), + DEFINE_FIELD( m_bNoExpirationTime, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flExpireTime, FIELD_TIME ), + DEFINE_FIELD( m_iNext, FIELD_SHORT ), + DEFINE_FIELD( m_ownerChannelIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_vecOrigin, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_bHasOwner, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_iMyIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +BEGIN_SCRIPTDESC_ROOT( CSound, "A sound NPCs can hear." ) + + DEFINE_SCRIPTFUNC( DoesSoundExpire, "Returns true if the sound expires." ) + DEFINE_SCRIPTFUNC( SoundExpirationTime, "Gets the sound's expiration time." ) + DEFINE_SCRIPTFUNC( SetSoundOrigin, "Sets the sound's origin." ) + DEFINE_SCRIPTFUNC( GetSoundOrigin, "Gets the sound's origin." ) + DEFINE_SCRIPTFUNC( GetSoundReactOrigin, "Gets the sound's react origin." ) + DEFINE_SCRIPTFUNC_NAMED( FIsSound, "IsSound", "Returns true if this is a type of sound (as opposed to a scent)." ) + DEFINE_SCRIPTFUNC_NAMED( FIsScent, "IsScent", "Returns true if this is a type of scent (as opposed to a sound)." ) + DEFINE_SCRIPTFUNC( IsSoundType, "Returns true if the sound type is the specified type." ) + DEFINE_SCRIPTFUNC( SoundType, "Gets the raw sound type." ) + DEFINE_SCRIPTFUNC( SoundContext, "Gets the sound type with contexts only." ) + DEFINE_SCRIPTFUNC( SoundTypeNoContext, "Gets the sound type with contexts excluded." ) + DEFINE_SCRIPTFUNC( Volume, "Gets the sound's volume." ) + DEFINE_SCRIPTFUNC( OccludedVolume, "Gets the sound's occluded volume." ) + DEFINE_SCRIPTFUNC( Reset, "Clears the volume, type, and origin for the sound without actually removing it." ) + DEFINE_SCRIPTFUNC( SoundChannel, "Gets the sound's channel." ) + DEFINE_SCRIPTFUNC( ValidateOwner, "Returns true if the sound's owner is still valid or if the sound never had an owner in the first place." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetOwner, "GetOwner", "Gets the sound's owner." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetTarget, "GetTarget", "Gets the sound's target." ) + +END_SCRIPTDESC(); +#endif + + +//========================================================= +// CSound - Clear - zeros all fields for a sound +//========================================================= +void CSound::Clear ( void ) +{ + m_vecOrigin = vec3_origin; + m_iType = 0; + m_iVolume = 0; + m_flOcclusionScale = 0; + m_flExpireTime = 0; + m_bNoExpirationTime = false; + m_iNext = SOUNDLIST_EMPTY; + m_iNextAudible = 0; +} + +//========================================================= +// Reset - clears the volume, origin, and type for a sound, +// but doesn't expire or unlink it. +//========================================================= +void CSound::Reset ( void ) +{ + m_vecOrigin = vec3_origin; + m_iType = 0; + m_iVolume = 0; + m_iNext = SOUNDLIST_EMPTY; +} + +//========================================================= +// FIsSound - returns true if the sound is an Audible sound +//========================================================= +bool CSound::FIsSound ( void ) +{ + switch( SoundTypeNoContext() ) + { + case SOUND_COMBAT: + case SOUND_WORLD: + case SOUND_PLAYER: + case SOUND_DANGER: + case SOUND_DANGER_SNIPERONLY: + case SOUND_THUMPER: + case SOUND_BULLET_IMPACT: + case SOUND_BUGBAIT: + case SOUND_PHYSICS_DANGER: + case SOUND_MOVE_AWAY: + case SOUND_PLAYER_VEHICLE: + return true; + + default: + return false; + } +} + +//========================================================= +// FIsScent - returns true if the sound is actually a scent +// do we really need this function? If a sound isn't a sound, +// it must be a scent. (sjb) +//========================================================= +bool CSound::FIsScent ( void ) +{ + switch( m_iType ) + { + case SOUND_CARCASS: + case SOUND_MEAT: + case SOUND_GARBAGE: + return true; + + default: + return false; + } +} + + +//--------------------------------------------------------- +// This function returns the spot the listener should be +// interested in if he hears the sound. MOST of the time, +// this spot is the same as the sound's origin. But sometimes +// (like with bullet impacts) the entity that owns the +// sound is more interesting than the actual location of the +// sound effect. +//--------------------------------------------------------- +const Vector &CSound::GetSoundReactOrigin( void ) +{ + + // Check pure types. + switch( m_iType ) + { + case SOUND_BULLET_IMPACT: + case SOUND_PHYSICS_DANGER: + if( m_hOwner.Get() != NULL ) + { + // We really want the origin of this sound's + // owner. + return m_hOwner->GetAbsOrigin(); + } + else + { + // If the owner is somehow invalid, we'll settle + // for the sound's origin rather than a crash. + return GetSoundOrigin(); + } + break; + } + + if( m_iType & SOUND_CONTEXT_REACT_TO_SOURCE ) + { + if( m_hOwner.Get() != NULL ) + { + return m_hOwner->GetAbsOrigin(); + } + } + + // Check for types with additional context. + if( m_iType & SOUND_DANGER ) + { + if( (m_iType & SOUND_CONTEXT_FROM_SNIPER) ) + { + if( m_hOwner.Get() != NULL ) + { + // Be afraid of the sniper's location, not where the bullet will hit. + return m_hOwner->GetAbsOrigin(); + } + else + { + return GetSoundOrigin(); + } + } + } + + + return GetSoundOrigin(); +} + + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CSoundEnt ) + + DEFINE_FIELD( m_iFreeSound, FIELD_INTEGER ), + DEFINE_FIELD( m_iActiveSound, FIELD_INTEGER ), + DEFINE_FIELD( m_cLastActiveSounds, FIELD_INTEGER ), + DEFINE_EMBEDDED_ARRAY( m_SoundPool, MAX_WORLD_SOUNDS_SP ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Class factory methods +//----------------------------------------------------------------------------- +bool CSoundEnt::InitSoundEnt() +{ + ///!!!LATER - do we want a sound ent in deathmatch? (sjb) + g_pSoundEnt = (CSoundEnt*)CBaseEntity::Create( "soundent", vec3_origin, vec3_angle, GetWorldEntity() ); + if ( !g_pSoundEnt ) + { + Warning( "**COULD NOT CREATE SOUNDENT**\n" ); + return false; + } + g_pSoundEnt->AddEFlags( EFL_KEEP_ON_RECREATE_ENTITIES ); + return true; +} + +void CSoundEnt::ShutdownSoundEnt() +{ + if ( g_pSoundEnt ) + { + g_pSoundEnt->FreeList(); + g_pSoundEnt = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Construction, destruction +//----------------------------------------------------------------------------- +CSoundEnt::CSoundEnt() +{ +} + +CSoundEnt::~CSoundEnt() +{ +} + + +//========================================================= +// Spawn +//========================================================= +void CSoundEnt::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + Initialize(); + + SetNextThink( gpGlobals->curtime + 1 ); +} + +void CSoundEnt::OnRestore() +{ + BaseClass::OnRestore(); + + // Make sure the singleton points to the restored version of this. + if ( g_pSoundEnt ) + { + Assert( g_pSoundEnt != this ); + UTIL_Remove( g_pSoundEnt ); + } + g_pSoundEnt = this; +} + + +//========================================================= +// Think - at interval, the entire active sound list is checked +// for sounds that have ExpireTimes less than or equal +// to the current world time, and these sounds are deallocated. +//========================================================= +void CSoundEnt::Think ( void ) +{ + int iSound; + int iPreviousSound; + + SetNextThink( gpGlobals->curtime + 0.1 );// how often to check the sound list. + + iPreviousSound = SOUNDLIST_EMPTY; + iSound = m_iActiveSound; + + while ( iSound != SOUNDLIST_EMPTY ) + { + if ( (m_SoundPool[ iSound ].m_flExpireTime <= gpGlobals->curtime && (!m_SoundPool[ iSound ].m_bNoExpirationTime)) || !m_SoundPool[iSound].ValidateOwner() ) + { + int iNext = m_SoundPool[ iSound ].m_iNext; + + if( displaysoundlist.GetInt() == 1 ) + { + Msg(" Removed Sound: %d (Time:%f)\n", m_SoundPool[ iSound ].SoundType(), gpGlobals->curtime ); + } + if( displaysoundlist.GetInt() == 2 && m_SoundPool[ iSound ].IsSoundType( SOUND_DANGER ) ) + { + Msg(" Removed Danger Sound: %d (time:%f)\n", m_SoundPool[ iSound ].SoundType(), gpGlobals->curtime ); + } + + // move this sound back into the free list + FreeSound( iSound, iPreviousSound ); + + iSound = iNext; + } + else + { + if( displaysoundlist.GetBool() ) + { + Vector forward, right, up; + GetVectors( &forward, &right, &up ); + byte r, g, b; + + // Default to yellow. + r = 255; + g = 255; + b = 0; + + CSound *pSound = &m_SoundPool[ iSound ]; + + if( pSound->IsSoundType( SOUND_DANGER ) ) + { + r = 255; + g = 0; + b = 0; + } + + if( displaysoundlist.GetInt() == 1 || (displaysoundlist.GetInt() == 2 && pSound->IsSoundType( SOUND_DANGER ) ) ) + { + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + forward * pSound->Volume(), r,g,b, false, 0.1 ); + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - forward * pSound->Volume(), r,g,b, false, 0.1 ); + + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + right * pSound->Volume(), r,g,b, false, 0.1 ); + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - right * pSound->Volume(), r,g,b, false, 0.1 ); + + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + up * pSound->Volume(), r,g,b, false, 0.1 ); + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - up * pSound->Volume(), r,g,b, false, 0.1 ); + + if( pSound->m_flOcclusionScale != 1.0 ) + { + // Draw the occluded radius, too. + r = 0; g = 150; b = 255; + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + forward * pSound->OccludedVolume(), r,g,b, false, 0.1 ); + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - forward * pSound->OccludedVolume(), r,g,b, false, 0.1 ); + + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + right * pSound->OccludedVolume(), r,g,b, false, 0.1 ); + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - right * pSound->OccludedVolume(), r,g,b, false, 0.1 ); + + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + up * pSound->OccludedVolume(), r,g,b, false, 0.1 ); + NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - up * pSound->OccludedVolume(), r,g,b, false, 0.1 ); + } + } + + DevMsg( 2, "Soundlist: %d / %d (%d)\n", ISoundsInList( SOUNDLISTTYPE_ACTIVE ),ISoundsInList( SOUNDLISTTYPE_FREE ), ISoundsInList( SOUNDLISTTYPE_ACTIVE ) - m_cLastActiveSounds ); + m_cLastActiveSounds = ISoundsInList ( SOUNDLISTTYPE_ACTIVE ); + } + + iPreviousSound = iSound; + iSound = m_SoundPool[ iSound ].m_iNext; + } + } + +} + +//========================================================= +// Precache - dummy function +//========================================================= +void CSoundEnt::Precache ( void ) +{ +} + +//========================================================= +// FreeSound - clears the passed active sound and moves it +// to the top of the free list. TAKE CARE to only call this +// function for sounds in the Active list!! +//========================================================= +void CSoundEnt::FreeSound ( int iSound, int iPrevious ) +{ + if ( !g_pSoundEnt ) + { + // no sound ent! + return; + } + + if ( iPrevious != SOUNDLIST_EMPTY ) + { + // iSound is not the head of the active list, so + // must fix the index for the Previous sound + g_pSoundEnt->m_SoundPool[ iPrevious ].m_iNext = g_pSoundEnt->m_SoundPool[ iSound ].m_iNext; + } + else + { + // the sound we're freeing IS the head of the active list. + g_pSoundEnt->m_iActiveSound = g_pSoundEnt->m_SoundPool [ iSound ].m_iNext; + } + + // make iSound the head of the Free list. + g_pSoundEnt->m_SoundPool[ iSound ].m_iNext = g_pSoundEnt->m_iFreeSound; + g_pSoundEnt->m_iFreeSound = iSound; +} + +//========================================================= +// IAllocSound - moves a sound from the Free list to the +// Active list returns the index of the alloc'd sound +//========================================================= +int CSoundEnt::IAllocSound( void ) +{ + int iNewSound; + + if ( m_iFreeSound == SOUNDLIST_EMPTY ) + { + // no free sound! + if ( developer.GetInt() >= 2 ) + Msg( "Free Sound List is full!\n" ); + + return SOUNDLIST_EMPTY; + } + + // there is at least one sound available, so move it to the + // Active sound list, and return its SoundPool index. + + iNewSound = m_iFreeSound;// copy the index of the next free sound + + m_iFreeSound = m_SoundPool[ m_iFreeSound ].m_iNext;// move the index down into the free list. + + m_SoundPool[ iNewSound ].m_iNext = m_iActiveSound;// point the new sound at the top of the active list. + + m_iActiveSound = iNewSound;// now make the new sound the top of the active list. You're done. + +#ifdef DEBUG + m_SoundPool[ iNewSound ].m_iMyIndex = iNewSound; +#endif // DEBUG + + return iNewSound; +} + +//========================================================= +// InsertSound - Allocates a free sound and fills it with +// sound info. +//========================================================= +void CSoundEnt::InsertSound ( int iType, const Vector &vecOrigin, int iVolume, float flDuration, CBaseEntity *pOwner, int soundChannelIndex, CBaseEntity *pSoundTarget ) +{ + int iThisSound; + + if ( !g_pSoundEnt ) + return; + + if( soundChannelIndex == SOUNDENT_CHANNEL_UNSPECIFIED ) + { + // No sound channel specified. So just make a new sound. + iThisSound = g_pSoundEnt->IAllocSound(); + } + else + { + // If this entity has already got a sound in the soundlist that's on this + // channel, update that sound. Otherwise add a new one. + iThisSound = g_pSoundEnt->FindOrAllocateSound( pOwner, soundChannelIndex ); + } + + if ( iThisSound == SOUNDLIST_EMPTY ) + { + DevMsg( "Could not AllocSound() for InsertSound() (Game DLL)\n" ); + return; + } + + CSound *pSound; + + pSound = &g_pSoundEnt->m_SoundPool[ iThisSound ]; + + pSound->SetSoundOrigin( vecOrigin ); + pSound->m_iType = iType; + pSound->m_iVolume = iVolume; + pSound->m_flOcclusionScale = 0.5; + pSound->m_flExpireTime = gpGlobals->curtime + flDuration; + pSound->m_bNoExpirationTime = false; + pSound->m_hOwner.Set( pOwner ); + pSound->m_hTarget.Set( pSoundTarget ); + pSound->m_ownerChannelIndex = soundChannelIndex; + + // Keep track of whether this sound had an owner when it was made. If the sound has a long duration, + // the owner could disappear by the time someone hears this sound, so we have to look at this boolean + // and throw out sounds who have a NULL owner but this field set to true. (sjb) 12/2/2005 + if( pOwner ) + { + pSound->m_bHasOwner = true; + } + else + { + pSound->m_bHasOwner = false; + } + + if( displaysoundlist.GetInt() == 1 ) + { + Msg(" Added Sound! Type:%d Duration:%f (Time:%f)\n", pSound->SoundType(), flDuration, gpGlobals->curtime ); + } + if( displaysoundlist.GetInt() == 2 && (iType & SOUND_DANGER) ) + { + Msg(" Added Danger Sound! Duration:%f (Time:%f)\n", flDuration, gpGlobals->curtime ); + } +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +int CSoundEnt::FindOrAllocateSound( CBaseEntity *pOwner, int soundChannelIndex ) +{ + int iSound = m_iActiveSound; + + while ( iSound != SOUNDLIST_EMPTY ) + { + CSound &sound = m_SoundPool[iSound]; + + if ( sound.m_ownerChannelIndex == soundChannelIndex && sound.m_hOwner == pOwner ) + { + return iSound; + } + + iSound = sound.m_iNext; + } + + return IAllocSound(); +} + +//========================================================= +// Initialize - clears all sounds and moves them into the +// free sound list. +//========================================================= +void CSoundEnt::Initialize ( void ) +{ + int i; + int iSound; + + m_cLastActiveSounds; + m_iFreeSound = 0; + m_iActiveSound = SOUNDLIST_EMPTY; + + // In SP, we should only use the first 64 slots so save/load works right. + // In MP, have one for each player and 32 extras. + int nTotalSoundsInPool = MAX_WORLD_SOUNDS_SP; + if ( gpGlobals->maxClients > 1 ) + nTotalSoundsInPool = MIN( MAX_WORLD_SOUNDS_MP, gpGlobals->maxClients + 32 ); + + if ( gpGlobals->maxClients+16 > nTotalSoundsInPool ) + { + Warning( "CSoundEnt pool is low on sounds due to high number of clients.\n" ); + } + + for ( i = 0 ; i < nTotalSoundsInPool ; i++ ) + { + // clear all sounds, and link them into the free sound list. + m_SoundPool[ i ].Clear(); + m_SoundPool[ i ].m_iNext = i + 1; + } + + m_SoundPool[ i - 1 ].m_iNext = SOUNDLIST_EMPTY;// terminate the list here. + + + // now reserve enough sounds for each client + for ( i = 0 ; i < gpGlobals->maxClients ; i++ ) + { + iSound = IAllocSound(); + + if ( iSound == SOUNDLIST_EMPTY ) + { + DevMsg( "Could not AllocSound() for Client Reserve! (DLL)\n" ); + return; + } + + m_SoundPool[ iSound ].m_bNoExpirationTime = true; + } +} + +//========================================================= +// ISoundsInList - returns the number of sounds in the desired +// sound list. +//========================================================= +int CSoundEnt::ISoundsInList ( int iListType ) +{ + int i; + int iThisSound = SOUNDLIST_EMPTY; + + if ( iListType == SOUNDLISTTYPE_FREE ) + { + iThisSound = m_iFreeSound; + } + else if ( iListType == SOUNDLISTTYPE_ACTIVE ) + { + iThisSound = m_iActiveSound; + } + else + { + Msg( "Unknown Sound List Type!\n" ); + } + + if ( iThisSound == SOUNDLIST_EMPTY ) + { + return 0; + } + + i = 0; + + while ( iThisSound != SOUNDLIST_EMPTY ) + { + i++; + + iThisSound = m_SoundPool[ iThisSound ].m_iNext; + } + + return i; +} + +//========================================================= +// ActiveList - returns the head of the active sound list +//========================================================= +int CSoundEnt::ActiveList ( void ) +{ + if ( !g_pSoundEnt ) + { + return SOUNDLIST_EMPTY; + } + + return g_pSoundEnt->m_iActiveSound; +} + +//========================================================= +// FreeList - returns the head of the free sound list +//========================================================= +int CSoundEnt::FreeList ( void ) +{ + if ( !g_pSoundEnt ) + { + return SOUNDLIST_EMPTY; + } + + return g_pSoundEnt->m_iFreeSound; +} + +//========================================================= +// SoundPointerForIndex - returns a pointer to the instance +// of CSound at index's position in the sound pool. +//========================================================= +CSound* CSoundEnt::SoundPointerForIndex( int iIndex ) +{ + if ( !g_pSoundEnt ) + { + return NULL; + } + + if ( iIndex > ( MAX_WORLD_SOUNDS_MP - 1 ) ) + { + Msg( "SoundPointerForIndex() - Index too large!\n" ); + return NULL; + } + + if ( iIndex < 0 ) + { + Msg( "SoundPointerForIndex() - Index < 0!\n" ); + return NULL; + } + + return &g_pSoundEnt->m_SoundPool[ iIndex ]; +} + +//========================================================= +// Clients are numbered from 1 to MAXCLIENTS, but the client +// reserved sounds in the soundlist are from 0 to MAXCLIENTS - 1, +// so this function ensures that a client gets the proper index +// to his reserved sound in the soundlist. +//========================================================= +int CSoundEnt::ClientSoundIndex ( edict_t *pClient ) +{ + int iReturn = ENTINDEX( pClient ) - 1; + +#ifdef _DEBUG + if ( iReturn < 0 || iReturn >= gpGlobals->maxClients ) + { + Msg( "** ClientSoundIndex returning a bogus value! **\n" ); + } +#endif // _DEBUG + + return iReturn; +} + +//----------------------------------------------------------------------------- +// Purpose: Return the loudest sound of the specified type at "earposition" +//----------------------------------------------------------------------------- +CSound* CSoundEnt::GetLoudestSoundOfType( int iType, const Vector &vecEarPosition ) +{ + CSound *pLoudestSound = NULL; + + int iThisSound; + int iBestSound = SOUNDLIST_EMPTY; + float flBestDist = MAX_COORD_RANGE*MAX_COORD_RANGE;// so first nearby sound will become best so far. + float flDist; + CSound *pSound; + + iThisSound = ActiveList(); + + while ( iThisSound != SOUNDLIST_EMPTY ) + { + pSound = SoundPointerForIndex( iThisSound ); + + if ( pSound && pSound->m_iType == iType && pSound->ValidateOwner() ) + { + flDist = ( pSound->GetSoundOrigin() - vecEarPosition ).Length(); + + //FIXME: This doesn't match what's in Listen() + //flDist = UTIL_DistApprox( pSound->GetSoundOrigin(), vecEarPosition ); + + if ( flDist <= pSound->m_iVolume && flDist < flBestDist ) + { + pLoudestSound = pSound; + + iBestSound = iThisSound; + flBestDist = flDist; + } + } + + iThisSound = pSound->m_iNext; + } + + return pLoudestSound; +} + + +//----------------------------------------------------------------------------- +// Purpose: Inserts an AI sound into the world sound list. +//----------------------------------------------------------------------------- +class CAISound : public CPointEntity +{ +public: + CAISound() + { + // Initialize these new keyvalues appropriately + // in order to support legacy instances of ai_sound. + m_iSoundContext = 0x00000000; + m_iVolume = 0; + m_flDuration = 0.3; + } + + DECLARE_CLASS( CAISound, CPointEntity ); + + DECLARE_DATADESC(); + + // data + int m_iSoundType; + int m_iSoundContext; + int m_iVolume; + float m_flDuration; + string_t m_iszProxyEntityName; + + // Input handlers + void InputInsertSound( inputdata_t &inputdata ); + void InputEmitAISound( inputdata_t &inputdata ); +}; + +LINK_ENTITY_TO_CLASS( ai_sound, CAISound ); + +BEGIN_DATADESC( CAISound ) + + DEFINE_KEYFIELD( m_iSoundType, FIELD_INTEGER, "soundtype" ), + DEFINE_KEYFIELD( m_iSoundContext, FIELD_INTEGER, "soundcontext" ), + DEFINE_KEYFIELD( m_iVolume, FIELD_INTEGER, "volume" ), + DEFINE_KEYFIELD( m_flDuration, FIELD_FLOAT, "duration" ), + DEFINE_KEYFIELD( m_iszProxyEntityName, FIELD_STRING, "locationproxy" ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "InsertSound", InputInsertSound ), + DEFINE_INPUTFUNC( FIELD_VOID, "EmitAISound", InputEmitAISound ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: *** OBSOLETE **** Here for legacy support only! +//----------------------------------------------------------------------------- +void CAISound::InputInsertSound( inputdata_t &inputdata ) +{ + int iVolume; + + iVolume = inputdata.value.Int(); + + Vector vecLocation = GetAbsOrigin(); + + if( m_iszProxyEntityName != NULL_STRING ) + { +#ifdef MAPBASE + CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_iszProxyEntityName, this, inputdata.pActivator, inputdata.pCaller ); +#else + CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_iszProxyEntityName ); +#endif + + if( pProxy ) + { + vecLocation = pProxy->GetAbsOrigin(); + } + else + { + DevWarning("Warning- ai_sound cannot find proxy entity named '%s'. Using self.\n", STRING(m_iszProxyEntityName) ); + } + } + +#ifdef MAPBASE + EHANDLE hOwner = this; + if (m_target != NULL_STRING) + { + CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_target, this, inputdata.pActivator, inputdata.pCaller ); + + if( pProxy ) + { + hOwner = pProxy; + } + else + { + DevWarning("Warning- ai_sound cannot find owner entity named '%s'. Using self.\n", STRING(m_target) ); + } + } + + g_pSoundEnt->InsertSound( m_iSoundType | m_iSoundContext, vecLocation, iVolume, m_flDuration, hOwner ); +#else + g_pSoundEnt->InsertSound( m_iSoundType, vecLocation, iVolume, m_flDuration, this ); +#endif +} + +void CAISound::InputEmitAISound( inputdata_t &inputdata ) +{ + Vector vecLocation = GetAbsOrigin(); + + if( m_iszProxyEntityName != NULL_STRING ) + { +#ifdef MAPBASE + CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_iszProxyEntityName, this, inputdata.pActivator, inputdata.pCaller ); +#else + CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_iszProxyEntityName ); +#endif + + if( pProxy ) + { + vecLocation = pProxy->GetAbsOrigin(); + } + else + { + DevWarning("Warning- ai_sound cannot find proxy entity named '%s'. Using self.\n", STRING(m_iszProxyEntityName) ); + } + } + +#ifdef MAPBASE + EHANDLE hOwner = this; + if (m_target != NULL_STRING) + { + CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_target, this, inputdata.pActivator, inputdata.pCaller ); + + if( pProxy ) + { + hOwner = pProxy; + } + else + { + DevWarning("Warning- ai_sound cannot find owner entity named '%s'. Using self.\n", STRING(m_target) ); + } + } + + g_pSoundEnt->InsertSound( m_iSoundType | m_iSoundContext, vecLocation, m_iVolume, m_flDuration, hOwner ); +#else + g_pSoundEnt->InsertSound( m_iSoundType | m_iSoundContext, vecLocation, m_iVolume, m_flDuration, this ); +#endif +} + + diff --git a/sp/src/game/server/soundent.h b/sp/src/game/server/soundent.h new file mode 100644 index 00000000..7ce3d401 --- /dev/null +++ b/sp/src/game/server/soundent.h @@ -0,0 +1,280 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// Soundent.h - the entity that spawns when the world +// spawns, and handles the world's active and free sound +// lists. + +#ifndef SOUNDENT_H +#define SOUNDENT_H + +#ifdef _WIN32 +#pragma once +#endif + +enum +{ + MAX_WORLD_SOUNDS_SP = 64, // Maximum number of sounds handled by the world at one time in single player. + // This is also the number of entries saved in a savegame file (for b/w compatibility). + + MAX_WORLD_SOUNDS_MP = 128 // The sound array size is set this large but we'll only use gpGlobals->maxPlayers+32 entries in mp. +}; + +enum +{ + SOUND_NONE = 0, + SOUND_COMBAT = 0x00000001, + SOUND_WORLD = 0x00000002, + SOUND_PLAYER = 0x00000004, + SOUND_DANGER = 0x00000008, + SOUND_BULLET_IMPACT = 0x00000010, + SOUND_CARCASS = 0x00000020, + SOUND_MEAT = 0x00000040, + SOUND_GARBAGE = 0x00000080, + SOUND_THUMPER = 0x00000100, // keeps certain creatures at bay + SOUND_BUGBAIT = 0x00000200, // gets the antlion's attention + SOUND_PHYSICS_DANGER = 0x00000400, + SOUND_DANGER_SNIPERONLY = 0x00000800, // only scares the sniper NPC. + SOUND_MOVE_AWAY = 0x00001000, + SOUND_PLAYER_VEHICLE = 0x00002000, + SOUND_READINESS_LOW = 0x00004000, // Changes listener's readiness (Player Companion only) + SOUND_READINESS_MEDIUM = 0x00008000, + SOUND_READINESS_HIGH = 0x00010000, + + // Contexts begin here. + SOUND_CONTEXT_FROM_SNIPER = 0x00100000, // additional context for SOUND_DANGER + SOUND_CONTEXT_GUNFIRE = 0x00200000, // Added to SOUND_COMBAT + SOUND_CONTEXT_MORTAR = 0x00400000, // Explosion going to happen here. + SOUND_CONTEXT_COMBINE_ONLY = 0x00800000, // Only combine can hear sounds marked this way + SOUND_CONTEXT_REACT_TO_SOURCE = 0x01000000, // React to sound source's origin, not sound's location + SOUND_CONTEXT_EXPLOSION = 0x02000000, // Context added to SOUND_COMBAT, usually. + SOUND_CONTEXT_EXCLUDE_COMBINE = 0x04000000, // Combine do NOT hear this + SOUND_CONTEXT_DANGER_APPROACH = 0x08000000, // Treat as a normal danger sound if you see the source, otherwise turn to face source. + SOUND_CONTEXT_ALLIES_ONLY = 0x10000000, // Only player allies can hear this sound + SOUND_CONTEXT_PLAYER_VEHICLE = 0x20000000, // HACK: need this because we're not treating the SOUND_xxx values as true bit values! See switch in OnListened. + +#ifdef MAPBASE + // You know, I wouldn't mind this approach of leaving types and contexts on the same int + // since it was important in the GoldSrc era with how many CSounds there can be at any given time. + // I'm just frustrated that this system was retained in Source with very specific and/or useless contexts with very little room to expand. + // If this doesn't work, replace SOUND_CONTEXT_PLAYER_VEHICLE with owner server vehicle checks. + + // Only heard by NPCs the owner likes. Needed for shared grenade code. + SOUND_CONTEXT_OWNER_ALLIES = 0x40000000, +#endif + + ALL_CONTEXTS = 0xFFF00000, + + ALL_SCENTS = SOUND_CARCASS | SOUND_MEAT | SOUND_GARBAGE, + + ALL_SOUNDS = 0x000FFFFF & ~ALL_SCENTS, + +}; + +// Make as many of these as you want. +enum +{ + SOUNDENT_CHANNEL_UNSPECIFIED = 0, + SOUNDENT_CHANNEL_REPEATING, + SOUNDENT_CHANNEL_REPEATED_DANGER, // for things that make danger sounds frequently. + SOUNDENT_CHANNEL_REPEATED_PHYSICS_DANGER, + SOUNDENT_CHANNEL_WEAPON, + SOUNDENT_CHANNEL_INJURY, + SOUNDENT_CHANNEL_BULLET_IMPACT, + SOUNDENT_CHANNEL_NPC_FOOTSTEP, + SOUNDENT_CHANNEL_SPOOKY_NOISE, // made by zombies in darkness + SOUNDENT_CHANNEL_ZOMBINE_GRENADE, +}; + +enum +{ + SOUNDLIST_EMPTY = -1 +}; + +#define SOUNDENT_VOLUME_MACHINEGUN 1500.0 +#define SOUNDENT_VOLUME_SHOTGUN 1500.0 +#define SOUNDENT_VOLUME_PISTOL 1500.0 +#define SOUNDENT_VOLUME_EMPTY 500.0 // volume of the "CLICK" when you have no bullets + +enum +{ + SOUND_PRIORITY_VERY_LOW = -2, + SOUND_PRIORITY_LOW, + SOUND_PRIORITY_NORMAL = 0, + SOUND_PRIORITY_HIGH, + SOUND_PRIORITY_VERY_HIGH, + SOUND_PRIORITY_HIGHEST, +}; + +//========================================================= +// CSound - an instance of a sound in the world. +//========================================================= +class CSound +{ + DECLARE_SIMPLE_DATADESC(); + +public: + bool DoesSoundExpire() const; + float SoundExpirationTime() const; + void SetSoundOrigin( const Vector &vecOrigin ) { m_vecOrigin = vecOrigin; } + const Vector& GetSoundOrigin( void ) { return m_vecOrigin; } + const Vector& GetSoundReactOrigin( void ); + bool FIsSound( void ); + bool FIsScent( void ); + bool IsSoundType( int nSoundFlags ) const; + int SoundType( ) const; + int SoundContext() const; + int SoundTypeNoContext( ) const; + int Volume( ) const; + float OccludedVolume() { return m_iVolume * m_flOcclusionScale; } + int NextSound() const; + void Reset ( void ); + int SoundChannel( void ) const; + bool ValidateOwner() const; + +#ifdef MAPBASE_VSCRIPT + // For VScript functions + HSCRIPT ScriptGetOwner() const { return ToHScript( m_hOwner ); } + HSCRIPT ScriptGetTarget() const { return ToHScript( m_hTarget ); } +#endif + + EHANDLE m_hOwner; // sound's owner + EHANDLE m_hTarget; // Sounds's target - an odd concept. For a gunfire sound, the target is the entity being fired at + int m_iVolume; // how loud the sound is + float m_flOcclusionScale; // How loud the sound is when occluded by the world. (volume * occlusionscale) + int m_iType; // what type of sound this is + int m_iNextAudible; // temporary link that NPCs use to build a list of audible sounds + +private: + void Clear ( void ); + + float m_flExpireTime; // when the sound should be purged from the list + short m_iNext; // index of next sound in this list ( Active or Free ) + bool m_bNoExpirationTime; + int m_ownerChannelIndex; + + Vector m_vecOrigin; // sound's location in space + + bool m_bHasOwner; // Lets us know if this sound was created with an owner. In case the owner goes null. + +#ifdef DEBUG + int m_iMyIndex; // debugging +#endif + + friend class CSoundEnt; +}; + +inline bool CSound::DoesSoundExpire() const +{ + return m_bNoExpirationTime == false; +} + +inline float CSound::SoundExpirationTime() const +{ + return m_bNoExpirationTime ? FLT_MAX : m_flExpireTime; +} + +inline bool CSound::IsSoundType( int nSoundFlags ) const +{ + return (m_iType & nSoundFlags) != 0; +} + +inline int CSound::SoundType( ) const +{ + return m_iType; +} + +inline int CSound::SoundContext( ) const +{ + return m_iType & ALL_CONTEXTS; +} + +inline int CSound::SoundTypeNoContext( ) const +{ + return m_iType & ~ALL_CONTEXTS; +} + +inline int CSound::Volume( ) const +{ + return m_iVolume; +} + +inline int CSound::NextSound() const +{ + return m_iNext; +} + +inline int CSound::SoundChannel( void ) const +{ + return m_ownerChannelIndex; +} + +// The owner is considered valid if: +// -The sound never had an assigned owner (quite common) +// -The sound was assigned an owner and that owner still exists +inline bool CSound::ValidateOwner( void ) const +{ + return ( !m_bHasOwner || (m_hOwner.Get() != NULL) ); +} + +//========================================================= +// CSoundEnt - a single instance of this entity spawns when +// the world spawns. The SoundEnt's job is to update the +// world's Free and Active sound lists. +//========================================================= +class CSoundEnt : public CPointEntity +{ + DECLARE_DATADESC(); + +public: + DECLARE_CLASS( CSoundEnt, CPointEntity ); + + // Construction, destruction + static bool InitSoundEnt(); + static void ShutdownSoundEnt(); + + CSoundEnt(); + virtual ~CSoundEnt(); + + virtual void OnRestore(); + void Precache ( void ); + void Spawn( void ); + void Think( void ); + void Initialize ( void ); + int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + static void InsertSound ( int iType, const Vector &vecOrigin, int iVolume, float flDuration, CBaseEntity *pOwner = NULL, int soundChannelIndex = SOUNDENT_CHANNEL_UNSPECIFIED, CBaseEntity *pSoundTarget = NULL ); + static void FreeSound ( int iSound, int iPrevious ); + static int ActiveList( void );// return the head of the active list + static int FreeList( void );// return the head of the free list + static CSound* SoundPointerForIndex( int iIndex );// return a pointer for this index in the sound list + static CSound* GetLoudestSoundOfType( int iType, const Vector &vecEarPosition ); + static int ClientSoundIndex ( edict_t *pClient ); + + bool IsEmpty( void ); + int ISoundsInList ( int iListType ); + int IAllocSound ( void ); + int FindOrAllocateSound( CBaseEntity *pOwner, int soundChannelIndex ); + +private: + int m_iFreeSound; // index of the first sound in the free sound list + int m_iActiveSound; // indes of the first sound in the active sound list + int m_cLastActiveSounds; // keeps track of the number of active sounds at the last update. (for diagnostic work) + CSound m_SoundPool[ MAX_WORLD_SOUNDS_MP ]; +}; + + +//----------------------------------------------------------------------------- +// Inline methods +//----------------------------------------------------------------------------- +inline bool CSoundEnt::IsEmpty( void ) +{ + return m_iActiveSound == SOUNDLIST_EMPTY; +} + + +#endif //SOUNDENT_H diff --git a/sp/src/game/server/soundscape.cpp b/sp/src/game/server/soundscape.cpp new file mode 100644 index 00000000..3c37cc59 --- /dev/null +++ b/sp/src/game/server/soundscape.cpp @@ -0,0 +1,598 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "cbase.h" +#include "soundscape.h" +#include "datamap.h" +#include "soundscape_system.h" +#include "triggers.h" +#include "saverestore_utlvector.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar soundscape_debug( "soundscape_debug", "0", FCVAR_CHEAT, "When on, draws lines to all env_soundscape entities. Green lines show the active soundscape, red lines show soundscapes that aren't in range, and white lines show soundscapes that are in range, but not the active soundscape." ); + +// ----------------------------------------------------------------------------- // +// CEnvSoundscapeProxy stuff. +// ----------------------------------------------------------------------------- // + +LINK_ENTITY_TO_CLASS( env_soundscape_proxy, CEnvSoundscapeProxy ); + +BEGIN_DATADESC( CEnvSoundscapeProxy ) + + DEFINE_KEYFIELD( m_MainSoundscapeName, FIELD_STRING, "MainSoundscapeName" ) + +END_DATADESC() + + +CEnvSoundscapeProxy::CEnvSoundscapeProxy() +{ + m_MainSoundscapeName = NULL_STRING; +} + + +void CEnvSoundscapeProxy::Activate() +{ + if ( m_MainSoundscapeName != NULL_STRING ) + { + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_MainSoundscapeName ); + if ( pEntity ) + { + m_hProxySoundscape = dynamic_cast< CEnvSoundscape* >( pEntity ); + } + } + + if ( m_hProxySoundscape ) + { + // Copy the relevant parameters from our main soundscape. + m_soundscapeIndex = m_hProxySoundscape->m_soundscapeIndex; + for ( int i=0; i < ARRAYSIZE( m_positionNames ); i++ ) + m_positionNames[i] = m_hProxySoundscape->m_positionNames[i]; + } + else + { + Warning( "env_soundscape_proxy can't find target soundscape: '%s'\n", STRING( m_MainSoundscapeName ) ); + } + + BaseClass::Activate(); +} + + +// ----------------------------------------------------------------------------- // +// CEnvSoundscape stuff. +// ----------------------------------------------------------------------------- // + +LINK_ENTITY_TO_CLASS( env_soundscape, CEnvSoundscape ); + +BEGIN_DATADESC( CEnvSoundscape ) + + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + // don't save, recomputed on load + //DEFINE_FIELD( m_soundscapeIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_soundscapeName, FIELD_STRING ), + DEFINE_FIELD( m_hProxySoundscape, FIELD_EHANDLE ), + +// Silence, Classcheck! +// DEFINE_ARRAY( m_positionNames, FIELD_STRING, 4 ), + + DEFINE_KEYFIELD( m_positionNames[0], FIELD_STRING, "position0" ), + DEFINE_KEYFIELD( m_positionNames[1], FIELD_STRING, "position1" ), + DEFINE_KEYFIELD( m_positionNames[2], FIELD_STRING, "position2" ), + DEFINE_KEYFIELD( m_positionNames[3], FIELD_STRING, "position3" ), + DEFINE_KEYFIELD( m_positionNames[4], FIELD_STRING, "position4" ), + DEFINE_KEYFIELD( m_positionNames[5], FIELD_STRING, "position5" ), + DEFINE_KEYFIELD( m_positionNames[6], FIELD_STRING, "position6" ), + DEFINE_KEYFIELD( m_positionNames[7], FIELD_STRING, "position7" ), + + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "ToggleEnabled", InputToggleEnabled ), + + DEFINE_OUTPUT( m_OnPlay, "OnPlay" ), + + +END_DATADESC() + +CEnvSoundscape::CEnvSoundscape() +{ + m_soundscapeName = NULL_STRING; + m_soundscapeIndex = -1; + m_soundscapeEntityId = -1; + m_bDisabled = false; + g_SoundscapeSystem.AddSoundscapeEntity( this ); +} + +CEnvSoundscape::~CEnvSoundscape() +{ + g_SoundscapeSystem.RemoveSoundscapeEntity( this ); +} + +void CEnvSoundscape::InputEnable( inputdata_t &inputdata ) +{ + if (!IsEnabled()) + { + Enable(); + } +} + +void CEnvSoundscape::InputDisable( inputdata_t &inputdata ) +{ + if (IsEnabled()) + { + Disable(); + } +} + +void CEnvSoundscape::InputToggleEnabled( inputdata_t &inputdata ) +{ + if ( IsEnabled() ) + { + Disable(); + } + else + { + Enable(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Returns whether the laser is currently active. +//----------------------------------------------------------------------------- +bool CEnvSoundscape::IsEnabled( void ) const +{ + return !m_bDisabled; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvSoundscape::Disable( void ) +{ + m_bDisabled = true; + + // Reset if we are the currently active soundscape +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEnvSoundscape::Enable( void ) +{ + m_bDisabled = false; + + // Force the player to recheck soundscapes +} + +bool CEnvSoundscape::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "soundscape")) + { + m_soundscapeName = AllocPooledString( szValue ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +// returns true if the given sound entity is in range +// and can see the given player entity (pTarget) + +bool CEnvSoundscape::InRangeOfPlayer( CBasePlayer *pTarget ) +{ + Vector vecSpot1 = EarPosition(); + Vector vecSpot2 = pTarget->EarPosition(); + + // calc range from sound entity to player + Vector vecRange = vecSpot2 - vecSpot1; + float range = vecRange.Length(); + if ( m_flRadius > range || m_flRadius == -1 ) + { + trace_t tr; + + UTIL_TraceLine( vecSpot1, vecSpot2, MASK_SOLID_BRUSHONLY|MASK_WATER, pTarget, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction == 1 && !tr.startsolid ) + { + return true; + } + } + + return false; +} + +int CEnvSoundscape::UpdateTransmitState() +{ + // Always transmit all soundscapes to the player. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +void CEnvSoundscape::WriteAudioParamsTo( audioparams_t &audio ) +{ + audio.ent.Set( this ); + audio.soundscapeIndex = m_soundscapeIndex; + audio.localBits = 0; + for ( int i = 0; i < ARRAYSIZE(m_positionNames); i++ ) + { + if ( m_positionNames[i] != NULL_STRING ) + { + // We are a valid entity for a sound position + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_positionNames[i], this, this ); + if ( pEntity ) + { + audio.localBits |= 1<GetAbsOrigin() ); + } + } + } + + m_OnPlay.FireOutput( this, this ); +} + + +// +// A client that is visible and in range of a sound entity will +// have its soundscape set by that sound entity. If two or more +// sound entities are contending for a client, then the nearest +// sound entity to the client will set the client's soundscape. +// A client's soundscape will remain set to its prior value until +// a new in-range, visible sound entity resets a new soundscape. +// + +// CONSIDER: if player in water state, autoset and underwater soundscape? +void CEnvSoundscape::UpdateForPlayer( ss_update_t &update ) +{ + if ( !IsEnabled() ) + { + if ( update.pCurrentSoundscape == this ) + { + update.pCurrentSoundscape = NULL; + update.currentDistance = 0; + update.bInRange = false; + } + return; + } + + // calc range from sound entity to player + Vector target = EarPosition(); + float range = (update.playerPosition - target).Length(); + + if ( update.pCurrentSoundscape == this ) + { + update.currentDistance = range; + update.bInRange = false; + if ( m_flRadius > range || m_flRadius == -1 ) + { + trace_t tr; + + update.traceCount++; + UTIL_TraceLine( target, update.playerPosition, MASK_SOLID_BRUSHONLY|MASK_WATER, update.pPlayer, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction == 1 && !tr.startsolid ) + { + update.bInRange = true; + } + } + } + else + { + if ( (!update.bInRange || range < update.currentDistance ) && (m_flRadius > range || m_flRadius == -1) ) + { + trace_t tr; + + update.traceCount++; + UTIL_TraceLine( target, update.playerPosition, MASK_SOLID_BRUSHONLY|MASK_WATER, update.pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction == 1 && !tr.startsolid ) + { + audioparams_t &audio = update.pPlayer->GetAudioParams(); + WriteAudioParamsTo( audio ); + update.pCurrentSoundscape = this; + update.bInRange = true; + update.currentDistance = range; + } + } + } + + + if ( soundscape_debug.GetBool() ) + { + // draw myself + NDebugOverlay::Box(GetAbsOrigin(), Vector(-10,-10,-10), Vector(10,10,10), 255, 0, 255, 64, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + + if ( update.pPlayer ) + { + audioparams_t &audio = update.pPlayer->GetAudioParams(); + if ( audio.ent.Get() != this ) + { + if ( InRangeOfPlayer( update.pPlayer ) ) + { + NDebugOverlay::Line( GetAbsOrigin(), update.pPlayer->WorldSpaceCenter(), 255, 255, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + NDebugOverlay::Line( GetAbsOrigin(), update.pPlayer->WorldSpaceCenter(), 255, 0, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + else + { + if ( InRangeOfPlayer( update.pPlayer ) ) + { + NDebugOverlay::Line( GetAbsOrigin(), update.pPlayer->WorldSpaceCenter(), 0, 255, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + NDebugOverlay::Line( GetAbsOrigin(), update.pPlayer->WorldSpaceCenter(), 255, 170, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + + // also draw lines to each sound position. + // we don't store the number of local sound positions, just a bitvector of which ones are on. + unsigned int soundbits = audio.localBits.Get(); + float periodic = 2.0f * sin((fmod(gpGlobals->curtime,2.0f) - 1.0f) * M_PI); // = -4f .. 4f + for (int ii = 0 ; ii < NUM_AUDIO_LOCAL_SOUNDS ; ++ii ) + { + if ( soundbits & (1 << ii) ) + { + const Vector &soundLoc = audio.localSound.Get(ii); + NDebugOverlay::Line( GetAbsOrigin(), soundLoc, 0, 32 , 255 , false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + NDebugOverlay::Cross3D( soundLoc, 16.0f + periodic, 0, 0, 255, false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + } + + NDebugOverlay::EntityTextAtPosition( GetAbsOrigin(), 0, STRING(m_soundscapeName), NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } +} + +// +// env_soundscape - spawn a sound entity that will set player soundscape +// when player moves in range and sight. +// +// +void CEnvSoundscape::Spawn( ) +{ + Precache(); + // Because the soundscape has no model, need to make sure it doesn't get culled from the PVS for this reason and therefore + // never exist on the client, etc. + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + +} + +void CEnvSoundscape::Precache() +{ + if ( m_soundscapeName == NULL_STRING ) + { + DevMsg("Found soundscape entity with no soundscape name.\n" ); + return; + } + + m_soundscapeIndex = g_SoundscapeSystem.GetSoundscapeIndex( STRING(m_soundscapeName) ); + if ( IsX360()) + { + g_SoundscapeSystem.PrecacheSounds( m_soundscapeIndex ); + } + if ( !g_SoundscapeSystem.IsValidIndex( m_soundscapeIndex ) ) + { + DevWarning("Can't find soundscape: %s\n", STRING(m_soundscapeName) ); + } +} + +void CEnvSoundscape::DrawDebugGeometryOverlays( void ) +{ + if ( m_debugOverlays & (OVERLAY_BBOX_BIT|OVERLAY_PIVOT_BIT|OVERLAY_ABSBOX_BIT) ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); + if ( pPlayer ) + { + audioparams_t &audio = pPlayer->GetAudioParams(); + if ( audio.ent.Get() != this ) + { + CBaseEntity *pEnt = pPlayer; // ->GetSoundscapeListener(); + if ( pEnt ) + { + NDebugOverlay::Line(GetAbsOrigin(), pEnt->WorldSpaceCenter(), 255, 0, 255, false, 0 ); + } + } + } + } + + BaseClass::DrawDebugGeometryOverlays(); +} + + +// ---------------------------------------------------------------------------------------------------- // +// CEnvSoundscapeTriggerable +// ---------------------------------------------------------------------------------------------------- // + +LINK_ENTITY_TO_CLASS( env_soundscape_triggerable, CEnvSoundscapeTriggerable ); + +BEGIN_DATADESC( CEnvSoundscapeTriggerable ) +END_DATADESC() + + +CEnvSoundscapeTriggerable::CEnvSoundscapeTriggerable() +{ +} + + +void CEnvSoundscapeTriggerable::DelegateStartTouch( CBaseEntity *pEnt ) +{ + CBasePlayer *pPlayer = dynamic_cast< CBasePlayer* >( pEnt ); + if ( !pPlayer ) + return; + + // Just in case.. we shouldn't already be in the player's list because it should have + // called DelegateEndTouch, but this seems to happen when they're noclipping. + pPlayer->m_hTriggerSoundscapeList.FindAndRemove( this ); + + // Add us to the player's list of soundscapes and + pPlayer->m_hTriggerSoundscapeList.AddToHead( this ); + WriteAudioParamsTo( pPlayer->GetAudioParams() ); +} + + +void CEnvSoundscapeTriggerable::DelegateEndTouch( CBaseEntity *pEnt ) +{ + CBasePlayer *pPlayer = dynamic_cast< CBasePlayer* >( pEnt ); + if ( !pPlayer ) + return; + + // Remove us from the ent's list of soundscapes. + pPlayer->m_hTriggerSoundscapeList.FindAndRemove( this ); + while ( pPlayer->m_hTriggerSoundscapeList.Count() > 0 ) + { + CEnvSoundscapeTriggerable *pSS = dynamic_cast< CEnvSoundscapeTriggerable* >( pPlayer->m_hTriggerSoundscapeList[0].Get() ); + if ( pSS ) + { + // Make this one current. + pSS->WriteAudioParamsTo( pPlayer->GetAudioParams() ); + return; + } + else + { + pPlayer->m_hTriggerSoundscapeList.Remove( 0 ); + } + } + + // No soundscapes left. + pPlayer->GetAudioParams().ent = NULL; +} + + +void CEnvSoundscapeTriggerable::Think() +{ + // Overrides the base class's think and prevents it from running at all. +} + + +// ---------------------------------------------------------------------------------------------------- // +// CTriggerSoundscape +// ---------------------------------------------------------------------------------------------------- // + +class CTriggerSoundscape : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerSoundscape, CBaseTrigger ); + DECLARE_DATADESC(); + + CTriggerSoundscape(); + + virtual void StartTouch( CBaseEntity *pOther ); + virtual void EndTouch( CBaseEntity *pOther ); + + virtual void Spawn(); + virtual void Activate(); + + void PlayerUpdateThink(); + +private: + CHandle m_hSoundscape; + string_t m_SoundscapeName; + + CUtlVector m_spectators; // spectators in our volume +}; + + +LINK_ENTITY_TO_CLASS( trigger_soundscape, CTriggerSoundscape ); + +BEGIN_DATADESC( CTriggerSoundscape ) + DEFINE_THINKFUNC( PlayerUpdateThink ), + DEFINE_KEYFIELD( m_SoundscapeName, FIELD_STRING, "soundscape" ), + DEFINE_FIELD( m_hSoundscape, FIELD_EHANDLE ), + DEFINE_UTLVECTOR( m_spectators, FIELD_EHANDLE ), +END_DATADESC() + + +CTriggerSoundscape::CTriggerSoundscape() +{ +} + + +void CTriggerSoundscape::StartTouch( CBaseEntity *pOther ) +{ + if ( m_hSoundscape ) + m_hSoundscape->DelegateStartTouch( pOther ); + + BaseClass::StartTouch( pOther ); +} + + +void CTriggerSoundscape::EndTouch( CBaseEntity *pOther ) +{ + if ( m_hSoundscape ) + m_hSoundscape->DelegateEndTouch( pOther ); + + BaseClass::EndTouch( pOther ); +} + + +void CTriggerSoundscape::Spawn() +{ + BaseClass::Spawn(); + InitTrigger(); + + SetThink( &CTriggerSoundscape::PlayerUpdateThink ); + SetNextThink( gpGlobals->curtime + 0.2 ); +} + + +void CTriggerSoundscape::Activate() +{ + m_hSoundscape = dynamic_cast< CEnvSoundscapeTriggerable* >( gEntList.FindEntityByName( NULL, m_SoundscapeName ) ); + BaseClass::Activate(); +} + + +// look for dead/spectating players in our volume, to call touch on +void CTriggerSoundscape::PlayerUpdateThink() +{ + int i; + SetNextThink( gpGlobals->curtime + 0.2 ); + + CUtlVector oldSpectators; + oldSpectators = m_spectators; + m_spectators.RemoveAll(); + + for ( i=1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = UTIL_PlayerByIndex( i ); + + if ( !player ) + continue; + + if ( player->IsAlive() ) + continue; + + // if the spectator is intersecting the trigger, track it, and start a touch if it is just starting to touch + if ( Intersects( player ) ) + { + if ( !oldSpectators.HasElement( player ) ) + { + StartTouch( player ); + } + m_spectators.AddToTail( player ); + } + } + + // check for spectators who are no longer intersecting + for ( i=0; i m_hProxySoundscape; + + +private: + + bool m_bDisabled; +}; + + +class CEnvSoundscapeProxy : public CEnvSoundscape +{ +public: + DECLARE_CLASS( CEnvSoundscapeProxy, CEnvSoundscape ); + DECLARE_DATADESC(); + + CEnvSoundscapeProxy(); + virtual void Activate(); + + // Here just to stop it falling back to CEnvSoundscape's, and + // printing bogus errors about missing soundscapes. + virtual void Precache() { return; } + +private: + string_t m_MainSoundscapeName; +}; + + +class CEnvSoundscapeTriggerable : public CEnvSoundscape +{ +friend class CTriggerSoundscape; + +public: + DECLARE_CLASS( CEnvSoundscapeTriggerable, CEnvSoundscape ); + DECLARE_DATADESC(); + + CEnvSoundscapeTriggerable(); + + // Overrides the base class's think and prevents it from running at all. + virtual void Think(); + + +private: + + // Passed through from CTriggerSoundscape. + void DelegateStartTouch( CBaseEntity *pEnt ); + void DelegateEndTouch( CBaseEntity *pEnt ); +}; + + +#endif // SOUNDSCAPE_H diff --git a/sp/src/game/server/soundscape_system.cpp b/sp/src/game/server/soundscape_system.cpp new file mode 100644 index 00000000..fc81579f --- /dev/null +++ b/sp/src/game/server/soundscape_system.cpp @@ -0,0 +1,468 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + + +#include "cbase.h" +#include "soundscape_system.h" +#include "soundscape.h" +#include "KeyValues.h" +#include "filesystem.h" +#include "game.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SOUNDSCAPE_MANIFEST_FILE "scripts/soundscapes_manifest.txt" + +CON_COMMAND(soundscape_flush, "Flushes the server & client side soundscapes") +{ + CBasePlayer *pPlayer = ToBasePlayer( UTIL_GetCommandClient() ); + if ( engine->IsDedicatedServer() ) + { + // If it's a dedicated server, only the server console can run this. + if ( pPlayer ) + return; + } + else + { + // If it's a listen server, only the listen server host can run this. + if ( !pPlayer || pPlayer != UTIL_GetListenServerHost() ) + return; + } + + g_SoundscapeSystem.FlushSoundscapes(); // don't bother forgetting about the entities + g_SoundscapeSystem.Init(); + + + if ( engine->IsDedicatedServer() ) + { + // If the ds console typed it, send it to everyone. + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pSendToPlayer = UTIL_PlayerByIndex( i ); + if ( pSendToPlayer ) + engine->ClientCommand( pSendToPlayer->edict(), "cl_soundscape_flush\n" ); + } + } + else + { + engine->ClientCommand( pPlayer->edict(), "cl_soundscape_flush\n" ); + } +} + +CSoundscapeSystem g_SoundscapeSystem( "CSoundscapeSystem" ); + +extern ConVar soundscape_debug; + +void CSoundscapeSystem::AddSoundscapeFile( const char *filename ) +{ + MEM_ALLOC_CREDIT(); + // Open the soundscape data file, and abort if we can't + KeyValues *pKeyValuesData = new KeyValues( filename ); + if ( filesystem->LoadKeyValues( *pKeyValuesData, IFileSystem::TYPE_SOUNDSCAPE, filename, "GAME" ) ) + { + // parse out all of the top level sections and save their names + KeyValues *pKeys = pKeyValuesData; + while ( pKeys ) + { + if ( pKeys->GetFirstSubKey() ) + { + if ( g_pDeveloper->GetBool() ) + { + if ( strstr( pKeys->GetName(), "{" ) ) + { + Msg("Error parsing soundscape file %s after %s\n", filename, m_soundscapeCount>0 ?m_soundscapes.GetStringText( m_soundscapeCount-1 ) : "FIRST" ); + } + } + m_soundscapes.AddString( pKeys->GetName(), m_soundscapeCount ); + + if ( IsX360() ) + { + AddSoundscapeSounds( pKeys, m_soundscapeCount ); + } + m_soundscapeCount++; + } + pKeys = pKeys->GetNextKey(); + } + } + pKeyValuesData->deleteThis(); +} + +CON_COMMAND_F( sv_soundscape_printdebuginfo, "print soundscapes", FCVAR_DEVELOPMENTONLY ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + g_SoundscapeSystem.PrintDebugInfo(); +} + + +void CSoundscapeSystem::PrintDebugInfo() +{ + Msg( "\n------- SERVER SOUNDSCAPES -------\n" ); + for ( int key=m_soundscapes.First(); key != m_soundscapes.InvalidIndex(); key = m_soundscapes.Next( key ) ) + { + int id = m_soundscapes.GetIDForKey( key ); + const char *pName = m_soundscapes.GetStringForKey( key ); + + Msg( "- %d: %s\n", id, pName ); + } + Msg( "-------- SOUNDSCAPE ENTITIES -----\n" ); + for( int entityIndex = 0; entityIndex < m_soundscapeEntities.Size(); ++entityIndex ) + { + CEnvSoundscape *currentSoundscape = m_soundscapeEntities[entityIndex]; + Msg("- %d: %s x:%.4f y:%.4f z:%.4f\n", + entityIndex, + STRING(currentSoundscape->GetSoundscapeName()), + currentSoundscape->GetAbsOrigin().x, + currentSoundscape->GetAbsOrigin().y, + currentSoundscape->GetAbsOrigin().z + ); + } + Msg( "----------------------------------\n\n" ); +} + +bool CSoundscapeSystem::Init() +{ + m_soundscapeCount = 0; + + const char *mapname = STRING( gpGlobals->mapname ); + const char *mapSoundscapeFilename = NULL; + if ( mapname && *mapname ) + { + mapSoundscapeFilename = UTIL_VarArgs( "scripts/soundscapes_%s.txt", mapname ); + } + +#ifdef MAPBASE + if (filesystem->FileExists(UTIL_VarArgs("maps/%s_soundscapes.txt", mapname))) + { + // A Mapbase-specific file exists. Load that instead. + // Any additional soundscape files, like the original scripts/soundscapes version, + // could be loaded through #include and/or #base. + mapSoundscapeFilename = UTIL_VarArgs("maps/%s_soundscapes.txt", mapname); + } +#endif + + KeyValues *manifest = new KeyValues( SOUNDSCAPE_MANIFEST_FILE ); + if ( filesystem->LoadKeyValues( *manifest, IFileSystem::TYPE_SOUNDSCAPE, SOUNDSCAPE_MANIFEST_FILE, "GAME" ) ) + { + for ( KeyValues *sub = manifest->GetFirstSubKey(); sub != NULL; sub = sub->GetNextKey() ) + { + if ( !Q_stricmp( sub->GetName(), "file" ) ) + { + // Add + AddSoundscapeFile( sub->GetString() ); + if ( mapSoundscapeFilename && FStrEq( sub->GetString(), mapSoundscapeFilename ) ) + { + mapSoundscapeFilename = NULL; // we've already loaded the map's soundscape + } + continue; + } + + Warning( "CSoundscapeSystem::Init: Manifest '%s' with bogus file type '%s', expecting 'file'\n", + SOUNDSCAPE_MANIFEST_FILE, sub->GetName() ); + } + + if ( mapSoundscapeFilename && filesystem->FileExists( mapSoundscapeFilename ) ) + { + AddSoundscapeFile( mapSoundscapeFilename ); + } + } + else + { + Error( "Unable to load manifest file '%s'\n", SOUNDSCAPE_MANIFEST_FILE ); + } + manifest->deleteThis(); + m_activeIndex = 0; + + return true; +} + +void CSoundscapeSystem::FlushSoundscapes( void ) +{ + m_soundscapeCount = 0; + m_soundscapes.ClearStrings(); +} + +void CSoundscapeSystem::Shutdown() +{ + FlushSoundscapes(); + m_soundscapeEntities.RemoveAll(); + m_activeIndex = 0; + + if ( IsX360() ) + { + m_soundscapeSounds.Purge(); + } +} + +void CSoundscapeSystem::LevelInitPreEntity() +{ + g_SoundscapeSystem.Shutdown(); + g_SoundscapeSystem.Init(); +} + +void CSoundscapeSystem::LevelInitPostEntity() +{ + if ( IsX360() ) + { + m_soundscapeSounds.Purge(); + } + CUtlVector clusterbounds; + int clusterCount = engine->GetClusterCount(); + clusterbounds.SetCount( clusterCount ); + engine->GetAllClusterBounds( clusterbounds.Base(), clusterCount ); + m_soundscapesInCluster.SetCount(clusterCount); + for ( int i = 0; i < clusterCount; i++ ) + { + m_soundscapesInCluster[i].soundscapeCount = 0; + m_soundscapesInCluster[i].firstSoundscape = 0; + } + unsigned char myPVS[16 * 1024]; + CUtlVector clusterIndexList; + CUtlVector soundscapeIndexList; + + // find the clusters visible from each soundscape + // add this soundscape to the list of soundscapes for that cluster, clip cluster bounds to radius + for ( int i = 0; i < m_soundscapeEntities.Count(); i++ ) + { + Vector position = m_soundscapeEntities[i]->GetAbsOrigin(); + float radius = m_soundscapeEntities[i]->m_flRadius; + float radiusSq = radius * radius; + engine->GetPVSForCluster( engine->GetClusterForOrigin( position ), sizeof( myPVS ), myPVS ); + for ( int j = 0; j < clusterCount; j++ ) + { + if ( myPVS[ j >> 3 ] & (1<<(j&7)) ) + { + float distSq = CalcSqrDistanceToAABB( clusterbounds[j].mins, clusterbounds[j].maxs, position ); + if ( distSq < radiusSq || radius < 0 ) + { + m_soundscapesInCluster[j].soundscapeCount++; + clusterIndexList.AddToTail(j); + // UNDONE: Technically you just need a soundscape index and a count for this list. + soundscapeIndexList.AddToTail(i); + } + } + } + } + + // basically this part is like a radix sort + // this is how many entries we need in the soundscape index list + m_soundscapeIndexList.SetCount(soundscapeIndexList.Count()); + + // now compute the starting index of each cluster + int firstSoundscape = 0; + for ( int i = 0; i < clusterCount; i++ ) + { + m_soundscapesInCluster[i].firstSoundscape = firstSoundscape; + firstSoundscape += m_soundscapesInCluster[i].soundscapeCount; + m_soundscapesInCluster[i].soundscapeCount = 0; + } + // now add each soundscape index to the appropriate cluster's list + // The resulting list is precomputing all soundscapes that need to be checked for a player + // in each cluster. This is used to accelerate the per-frame operations + for ( int i = 0; i < soundscapeIndexList.Count(); i++ ) + { + int cluster = clusterIndexList[i]; + int outIndex = m_soundscapesInCluster[cluster].soundscapeCount + m_soundscapesInCluster[cluster].firstSoundscape; + m_soundscapesInCluster[cluster].soundscapeCount++; + m_soundscapeIndexList[outIndex] = soundscapeIndexList[i]; + } +} + +int CSoundscapeSystem::GetSoundscapeIndex( const char *pName ) +{ + return m_soundscapes.GetStringID( pName ); +} + +bool CSoundscapeSystem::IsValidIndex( int index ) +{ + if ( index >= 0 && index < m_soundscapeCount ) + return true; + return false; +} + +void CSoundscapeSystem::AddSoundscapeEntity( CEnvSoundscape *pSoundscape ) +{ + if ( m_soundscapeEntities.Find( pSoundscape ) == -1 ) + { + int index = m_soundscapeEntities.AddToTail( pSoundscape ); + pSoundscape->m_soundscapeEntityId = index + 1; + } +} + +void CSoundscapeSystem::RemoveSoundscapeEntity( CEnvSoundscape *pSoundscape ) +{ + m_soundscapeEntities.FindAndRemove( pSoundscape ); + pSoundscape->m_soundscapeEntityId = -1; +} + +void CSoundscapeSystem::FrameUpdatePostEntityThink() +{ + int total = m_soundscapeEntities.Count(); + if ( total > 0 ) + { + int traceCount = 0; + int playerCount = 0; + // budget tuned for TF. Do a max of 20 traces. That's going to happen anyway because a bunch of the maps + // use radius -1 for all soundscapes. So to trace one player you'll often need that many and this code must + // always trace one player's soundscapes. + // If the map has been optimized, then allow more players to update per frame. + int maxPlayers = gpGlobals->maxClients / 2; + // maxPlayers has to be at least 1 + maxPlayers = MAX( 1, maxPlayers ); + int maxTraces = 20; + if ( soundscape_debug.GetBool() ) + { + maxTraces = 9999; + maxPlayers = MAX_PLAYERS; + } + + // load balance across server ticks a bit by limiting the numbers of players (get cluster for origin) + // and traces processed in a single tick. In single player this will update the player every tick + // because it always does at least one player's full load of work + for ( int i = 0; i < gpGlobals->maxClients && traceCount <= maxTraces && playerCount <= maxPlayers; i++ ) + { + m_activeIndex = (m_activeIndex+1) % gpGlobals->maxClients; + CBasePlayer *pPlayer = UTIL_PlayerByIndex( m_activeIndex + 1 ); + if ( pPlayer && pPlayer->IsNetClient() ) + { + // check to see if this is the sound entity that is + // currently affecting this player + audioparams_t &audio = pPlayer->GetAudioParams(); + + // if we got this far, we're looking at an entity that is contending + // for current player sound. the closest entity to player wins. + CEnvSoundscape *pCurrent = (CEnvSoundscape *)( audio.ent.Get() ); + if ( pCurrent ) + { + int nEntIndex = pCurrent->m_soundscapeEntityId - 1; + NOTE_UNUSED( nEntIndex ); + Assert( m_soundscapeEntities[nEntIndex] == pCurrent ); + } + ss_update_t update; + update.pPlayer = pPlayer; + update.pCurrentSoundscape = pCurrent; + update.playerPosition = pPlayer->EarPosition(); + update.bInRange = false; + update.currentDistance = 0; + update.traceCount = 0; + if ( pCurrent ) + { + pCurrent->UpdateForPlayer(update); + } + + int clusterIndex = engine->GetClusterForOrigin( update.playerPosition ); + + if ( clusterIndex >= 0 && clusterIndex < m_soundscapesInCluster.Count() ) + { + // find all soundscapes that could possibly attach to this player and update them + for ( int j = 0; j < m_soundscapesInCluster[clusterIndex].soundscapeCount; j++ ) + { + int ssIndex = m_soundscapeIndexList[m_soundscapesInCluster[clusterIndex].firstSoundscape + j]; + if ( m_soundscapeEntities[ssIndex] == update.pCurrentSoundscape ) + continue; + m_soundscapeEntities[ssIndex]->UpdateForPlayer( update ); + } + } + playerCount++; + traceCount += update.traceCount; + } + } + } +} + +void CSoundscapeSystem::AddSoundscapeSounds( KeyValues *pSoundscape, int soundscapeIndex ) +{ + if ( !IsX360() ) + { + return; + } + + int i = m_soundscapeSounds.AddToTail(); + Assert( i == soundscapeIndex ); + + KeyValues *pKey = pSoundscape->GetFirstSubKey(); + while ( pKey ) + { + if ( !Q_strcasecmp( pKey->GetName(), "playlooping" ) ) + { + KeyValues *pAmbientKey = pKey->GetFirstSubKey(); + while ( pAmbientKey ) + { + if ( !Q_strcasecmp( pAmbientKey->GetName(), "wave" ) ) + { + char const *pSoundName = pAmbientKey->GetString(); + m_soundscapeSounds[i].AddToTail( pSoundName ); + } + pAmbientKey = pAmbientKey->GetNextKey(); + } + } + else if ( !Q_strcasecmp( pKey->GetName(), "playrandom" ) ) + { + KeyValues *pRandomKey = pKey->GetFirstSubKey(); + while ( pRandomKey ) + { + if ( !Q_strcasecmp( pRandomKey->GetName(), "rndwave" ) ) + { + KeyValues *pRndWaveKey = pRandomKey->GetFirstSubKey(); + while ( pRndWaveKey ) + { + if ( !Q_strcasecmp( pRndWaveKey->GetName(), "wave" ) ) + { + char const *pSoundName = pRndWaveKey->GetString(); + m_soundscapeSounds[i].AddToTail( pSoundName ); + } + pRndWaveKey = pRndWaveKey->GetNextKey(); + } + } + pRandomKey = pRandomKey->GetNextKey(); + } + } + else if ( !Q_strcasecmp( pKey->GetName(), "playsoundscape" ) ) + { + KeyValues *pPlayKey = pKey->GetFirstSubKey(); + while ( pPlayKey ) + { + if ( !Q_strcasecmp( pPlayKey->GetName(), "name" ) ) + { + char const *pSoundName = pPlayKey->GetString(); + m_soundscapeSounds[i].AddToTail( pSoundName ); + } + pPlayKey = pPlayKey->GetNextKey(); + } + } + pKey = pKey->GetNextKey(); + } +} + +void CSoundscapeSystem::PrecacheSounds( int soundscapeIndex ) +{ + if ( !IsX360() ) + { + return; + } + + if ( !IsValidIndex( soundscapeIndex ) ) + { + return; + } + + int count = m_soundscapeSounds[soundscapeIndex].Count(); + for ( int i=0; i m_soundscapeEntities; + CUtlVector m_soundscapesInCluster; + CUtlVector m_soundscapeIndexList; + int m_activeIndex; + CUtlVector< CUtlVector< CUtlString > > m_soundscapeSounds; +}; + +extern CSoundscapeSystem g_SoundscapeSystem; + + +#endif // SOUNDSCAPE_SYSTEM_H diff --git a/sp/src/game/server/spark.h b/sp/src/game/server/spark.h new file mode 100644 index 00000000..c79fa341 --- /dev/null +++ b/sp/src/game/server/spark.h @@ -0,0 +1,15 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SPARK_H +#define SPARK_H +#ifdef _WIN32 +#pragma once +#endif + +void DoSpark( CBaseEntity *ent, const Vector &location, int nMagnitude, int nTrailLength, bool bPlaySound, const Vector &vecDir ); +#endif // SPARK_H diff --git a/sp/src/game/server/spotlightend.cpp b/sp/src/game/server/spotlightend.cpp new file mode 100644 index 00000000..57c08561 --- /dev/null +++ b/sp/src/game/server/spotlightend.cpp @@ -0,0 +1,52 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Dynamic light at the end of a spotlight +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "spotlightend.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS(spotlight_end, CSpotlightEnd); + +IMPLEMENT_SERVERCLASS_ST(CSpotlightEnd, DT_SpotlightEnd) + SendPropFloat(SENDINFO(m_flLightScale), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_Radius), 0, SPROP_NOSCALE), +// SendPropVector(SENDINFO(m_vSpotlightDir), -1, SPROP_NORMAL), +// SendPropVector(SENDINFO(m_vSpotlightOrg), -1, SPROP_COORD), +END_SEND_TABLE() + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CSpotlightEnd ) + + DEFINE_FIELD( m_flLightScale, FIELD_FLOAT ), + DEFINE_FIELD( m_Radius, FIELD_FLOAT ), + DEFINE_FIELD( m_vSpotlightDir, FIELD_VECTOR ), + DEFINE_FIELD( m_vSpotlightOrg, FIELD_POSITION_VECTOR ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CSpotlightEnd::Spawn( void ) +{ + Precache(); + m_flLightScale = 100; + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_FLY ); + UTIL_SetSize( this, vec3_origin, vec3_origin ); + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); +} diff --git a/sp/src/game/server/spotlightend.h b/sp/src/game/server/spotlightend.h new file mode 100644 index 00000000..c6b8ae03 --- /dev/null +++ b/sp/src/game/server/spotlightend.h @@ -0,0 +1,47 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Dynamic light at the end of a spotlight +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#ifndef SPOTLIGHTEND_H +#define SPOTLIGHTEND_H + +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" + +class CSpotlightEnd : public CBaseEntity +{ + DECLARE_DATADESC(); +public: + DECLARE_CLASS( CSpotlightEnd, CBaseEntity ); + + void Spawn( void ); + + int ObjectCaps( void ) + { + // Don't save and don't go across transitions + return (BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION) | FCAP_DONT_SAVE; + } + + DECLARE_SERVERCLASS(); + +public: + CNetworkVar( float, m_flLightScale ); + CNetworkVar( float, m_Radius ); +// CNetworkVector( m_vSpotlightDir ); +// CNetworkVector( m_vSpotlightOrg ); + Vector m_vSpotlightDir; + Vector m_vSpotlightOrg; +}; + +#endif //SPOTLIGHTEND_H + + diff --git a/sp/src/game/server/sprite_perfmonitor.cpp b/sp/src/game/server/sprite_perfmonitor.cpp new file mode 100644 index 00000000..0e42b04e --- /dev/null +++ b/sp/src/game/server/sprite_perfmonitor.cpp @@ -0,0 +1,95 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: an entity which turns on and off counting and display of the particle +// performance metric +// +//============================================================================= + +#include "cbase.h" +#include "baseentity.h" +#include "entityoutput.h" +#include "convar.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Entity that particle performance measuring +//----------------------------------------------------------------------------- +class CParticlePerformanceMonitor : public CPointEntity +{ + DECLARE_CLASS( CParticlePerformanceMonitor, CPointEntity ); +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + void Spawn( void ); + int UpdateTransmitState( void ); + + // Inputs + void InputTurnOnDisplay( inputdata_t &inputdata ); + void InputTurnOffDisplay( inputdata_t &inputdata ); + void InputStartMeasuring( inputdata_t &inputdata ); + void InputStopMeasuring( inputdata_t &inputdata ); + +private: + CNetworkVar( bool, m_bDisplayPerf ); + CNetworkVar( bool, m_bMeasurePerf ); +}; + +LINK_ENTITY_TO_CLASS( env_particle_performance_monitor, CParticlePerformanceMonitor ); + +BEGIN_DATADESC( CParticlePerformanceMonitor ) + DEFINE_FIELD( m_bDisplayPerf, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bMeasurePerf, FIELD_BOOLEAN ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOnDisplay", InputTurnOnDisplay ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOffDisplay", InputTurnOffDisplay ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartMeasuring", InputStartMeasuring ), + DEFINE_INPUTFUNC( FIELD_VOID, "StopMeasuring", InputStopMeasuring ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CParticlePerformanceMonitor, DT_ParticlePerformanceMonitor ) + SendPropInt( SENDINFO(m_bDisplayPerf), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_bMeasurePerf), 1, SPROP_UNSIGNED ), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CParticlePerformanceMonitor::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + m_bDisplayPerf = false; + m_bMeasurePerf = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CParticlePerformanceMonitor::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +void CParticlePerformanceMonitor::InputTurnOnDisplay( inputdata_t &inputdata ) +{ + m_bDisplayPerf = true; +} + +void CParticlePerformanceMonitor::InputTurnOffDisplay( inputdata_t &inputdata ) +{ + m_bDisplayPerf = false; +} + +void CParticlePerformanceMonitor::InputStartMeasuring( inputdata_t &inputdata ) +{ + m_bMeasurePerf = true; +} + +void CParticlePerformanceMonitor::InputStopMeasuring( inputdata_t &inputdata ) +{ + m_bMeasurePerf = false; +} + diff --git a/sp/src/game/server/stdafx.cpp b/sp/src/game/server/stdafx.cpp new file mode 100644 index 00000000..1ec8dcd7 --- /dev/null +++ b/sp/src/game/server/stdafx.cpp @@ -0,0 +1,11 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Builds the precompiled header for the game DLL +// +// $NoKeywords: $ +//=============================================================================// + + +#include "cbase.h" + +// NOTE: DO NOT ADD ANY CODE OR HEADERS TO THIS FILE!!! diff --git a/sp/src/game/server/steamjet.cpp b/sp/src/game/server/steamjet.cpp new file mode 100644 index 00000000..f8c3107b --- /dev/null +++ b/sp/src/game/server/steamjet.cpp @@ -0,0 +1,130 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements the server side of a steam jet particle system entity. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "steamjet.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//Networking +IMPLEMENT_SERVERCLASS_ST(CSteamJet, DT_SteamJet) + SendPropFloat(SENDINFO(m_SpreadSpeed), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_Speed), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_StartSize), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_EndSize), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_Rate), 0, SPROP_NOSCALE), + SendPropFloat(SENDINFO(m_JetLength), 0, SPROP_NOSCALE), + SendPropInt(SENDINFO(m_bEmit), 1, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_bFaceLeft), 1, SPROP_UNSIGNED), // For support of legacy env_steamjet, which faced left instead of forward. + SendPropInt(SENDINFO(m_nType), 32, SPROP_UNSIGNED), + SendPropInt( SENDINFO(m_spawnflags), 8, SPROP_UNSIGNED ), + SendPropFloat(SENDINFO(m_flRollSpeed), 0, SPROP_NOSCALE), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( env_steam, CSteamJet ); +LINK_ENTITY_TO_CLASS( env_steamjet, CSteamJet ); // For support of legacy env_steamjet, which faced left instead of forward. + +//Save/restore +BEGIN_DATADESC( CSteamJet ) + + //Keyvalue fields + DEFINE_KEYFIELD( m_StartSize, FIELD_FLOAT, "StartSize" ), + DEFINE_KEYFIELD( m_EndSize, FIELD_FLOAT, "EndSize" ), + DEFINE_KEYFIELD( m_InitialState, FIELD_BOOLEAN, "InitialState" ), + DEFINE_KEYFIELD( m_nType, FIELD_INTEGER, "Type" ), + DEFINE_KEYFIELD( m_flRollSpeed, FIELD_FLOAT, "RollSpeed" ), + + //Regular fields + DEFINE_FIELD( m_bEmit, FIELD_INTEGER ), + DEFINE_FIELD( m_bFaceLeft, FIELD_BOOLEAN ), + + // Inputs + DEFINE_INPUT( m_JetLength, FIELD_FLOAT, "JetLength" ), + DEFINE_INPUT( m_SpreadSpeed, FIELD_FLOAT, "SpreadSpeed" ), + DEFINE_INPUT( m_Speed, FIELD_FLOAT, "Speed" ), + DEFINE_INPUT( m_Rate, FIELD_FLOAT, "Rate" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + +END_DATADESC() + + +CSteamJet::CSteamJet( void ) +{ + m_flRollSpeed = 8.0f; +} +//----------------------------------------------------------------------------- +// Purpose: Called before spawning, after key values have been set. +//----------------------------------------------------------------------------- +void CSteamJet::Spawn( void ) +{ + Precache(); + + // + // Legacy env_steamjet pointed left instead of forward. + // + if ( FClassnameIs( this, "env_steamjet" )) + { + m_bFaceLeft = true; + } + + if ( m_InitialState ) + { + m_bEmit = true; + } +} + +void CSteamJet::Precache( void ) +{ + PrecacheMaterial( "particle/particle_smokegrenade" ); + PrecacheMaterial( "sprites/heatwave" ); +} + + void CSteamJet::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) + { + if (!pActivator->IsPlayer()) + { + if (useType == USE_ON) + { + m_bEmit = true; + } + else if (useType == USE_OFF) + { + m_bEmit = false; + } + } + } + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for toggling the steam jet on/off. +//----------------------------------------------------------------------------- +void CSteamJet::InputToggle(inputdata_t &data) +{ + m_bEmit = !m_bEmit; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for turning on the steam jet. +//----------------------------------------------------------------------------- +void CSteamJet::InputTurnOn(inputdata_t &data) +{ + m_bEmit = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for turning off the steam jet. +//----------------------------------------------------------------------------- +void CSteamJet::InputTurnOff(inputdata_t &data) +{ + m_bEmit = false; +} diff --git a/sp/src/game/server/steamjet.h b/sp/src/game/server/steamjet.h new file mode 100644 index 00000000..2d784d53 --- /dev/null +++ b/sp/src/game/server/steamjet.h @@ -0,0 +1,60 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Defines the server side of a steam jet particle system entity. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef STEAMJET_H +#define STEAMJET_H +#pragma once + +#include "baseparticleentity.h" + +//NOTENOTE: Mirrored in cl_dlls\c_steamjet.cpp +#define STEAM_NORMAL 0 +#define STEAM_HEATWAVE 1 + +//================================================== +// CSteamJet +//================================================== + +class CSteamJet : public CBaseParticleEntity +{ +public: + CSteamJet(); + DECLARE_CLASS( CSteamJet, CBaseParticleEntity ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + virtual void Spawn( void ); + virtual void Precache( void ); + +protected: + + // Input handlers. + void InputTurnOn(inputdata_t &data); + void InputTurnOff(inputdata_t &data); + void InputToggle(inputdata_t &data); + +// Stuff from the datatable. +public: + CNetworkVar( float, m_SpreadSpeed ); + CNetworkVar( float, m_Speed ); + CNetworkVar( float, m_StartSize ); + CNetworkVar( float, m_EndSize ); + CNetworkVar( float, m_Rate ); + CNetworkVar( float, m_JetLength ); // Length of the jet. Lifetime is derived from this. + + CNetworkVar( int, m_bEmit ); // Emit particles? + CNetworkVar( bool, m_bFaceLeft ); // For support of legacy env_steamjet, which faced left instead of forward. + bool m_InitialState; + + CNetworkVar( int, m_nType ); // Type of steam (normal, heatwave) + CNetworkVar( float, m_flRollSpeed ); + + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); +}; + +#endif // STEAMJET_H + diff --git a/sp/src/game/server/subs.cpp b/sp/src/game/server/subs.cpp new file mode 100644 index 00000000..0a37e4c1 --- /dev/null +++ b/sp/src/game/server/subs.cpp @@ -0,0 +1,354 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Frequently used global functions. +// +// $NoKeywords: $ +//=============================================================================// + + +#include "cbase.h" +#include "doors.h" +#include "entitylist.h" +#include "globals.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Landmark class +void CPointEntity::Spawn( void ) +{ + SetSolid( SOLID_NONE ); +// UTIL_SetSize(this, vec3_origin, vec3_origin); +} + + +class CNullEntity : public CBaseEntity +{ +public: + DECLARE_CLASS( CNullEntity, CBaseEntity ); + + void Spawn( void ); +}; + + +// Null Entity, remove on startup +void CNullEntity::Spawn( void ) +{ + UTIL_Remove( this ); +} +LINK_ENTITY_TO_CLASS(info_null,CNullEntity); + +#ifdef MAPBASE +// Eh, good enough. +LINK_ENTITY_TO_CLASS(func_null,CNullEntity); +#endif + +class CBaseDMStart : public CPointEntity +{ +public: + DECLARE_CLASS( CBaseDMStart, CPointEntity ); + + bool IsTriggered( CBaseEntity *pEntity ); + + DECLARE_DATADESC(); + + string_t m_Master; + +private: +}; + +BEGIN_DATADESC( CBaseDMStart ) + + DEFINE_KEYFIELD( m_Master, FIELD_STRING, "master" ), + +END_DATADESC() + + +// These are the new entry points to entities. +LINK_ENTITY_TO_CLASS(info_player_deathmatch,CBaseDMStart); +LINK_ENTITY_TO_CLASS(info_player_start,CPointEntity); +LINK_ENTITY_TO_CLASS(info_landmark,CPointEntity); + +bool CBaseDMStart::IsTriggered( CBaseEntity *pEntity ) +{ + bool master = UTIL_IsMasterTriggered( m_Master, pEntity ); + + return master; +} + + +// Convenient way to delay removing oneself +void CBaseEntity::SUB_Remove( void ) +{ + if (m_iHealth > 0) + { + // this situation can screw up NPCs who can't tell their entity pointers are invalid. + m_iHealth = 0; + DevWarning( 2, "SUB_Remove called on entity with health > 0\n"); + } + + UTIL_Remove( this ); +} + + +// Convenient way to explicitly do nothing (passed to functions that require a method) +void CBaseEntity::SUB_DoNothing( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Finds all active entities with the given targetname and calls their +// 'Use' function. +// Input : targetName - Target name to search for. +// pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void FireTargets( const char *targetName, CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBaseEntity *pTarget = NULL; + if ( !targetName || !targetName[0] ) + return; + + DevMsg( 2, "Firing: (%s)\n", targetName ); + + for (;;) + { + CBaseEntity *pSearchingEntity = pActivator; + pTarget = gEntList.FindEntityByName( pTarget, targetName, pSearchingEntity, pActivator, pCaller ); + if ( !pTarget ) + break; + + if (!pTarget->IsMarkedForDeletion() ) // Don't use dying ents + { + DevMsg( 2, "[%03d] Found: %s, firing (%s)\n", gpGlobals->tickcount%1000, pTarget->GetDebugName(), targetName ); + pTarget->Use( pActivator, pCaller, useType, value ); + } + } +} + +enum togglemovetypes_t +{ + MOVE_TOGGLE_NONE = 0, + MOVE_TOGGLE_LINEAR = 1, + MOVE_TOGGLE_ANGULAR = 2, +}; + +// Global Savedata for Toggle +BEGIN_DATADESC( CBaseToggle ) + + DEFINE_FIELD( m_toggle_state, FIELD_INTEGER ), + DEFINE_FIELD( m_flMoveDistance, FIELD_FLOAT ), + DEFINE_FIELD( m_flWait, FIELD_FLOAT ), + DEFINE_FIELD( m_flLip, FIELD_FLOAT ), + DEFINE_FIELD( m_vecPosition1, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecPosition2, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecMoveAng, FIELD_VECTOR ), // UNDONE: Position could go through transition, but also angle? + DEFINE_FIELD( m_vecAngle1, FIELD_VECTOR ), // UNDONE: Position could go through transition, but also angle? + DEFINE_FIELD( m_vecAngle2, FIELD_VECTOR ), // UNDONE: Position could go through transition, but also angle? + DEFINE_FIELD( m_flHeight, FIELD_FLOAT ), + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), + DEFINE_FIELD( m_vecFinalDest, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecFinalAngle, FIELD_VECTOR ), + DEFINE_FIELD( m_sMaster, FIELD_STRING), + DEFINE_FIELD( m_movementType, FIELD_INTEGER ), // Linear or angular movement? (togglemovetypes_t) + +END_DATADESC() + + +CBaseToggle::CBaseToggle() +{ +#ifdef _DEBUG + // necessary since in debug, we initialize vectors to NAN for debugging + m_vecPosition1.Init(); + m_vecPosition2.Init(); + m_vecAngle1.Init(); + m_vecAngle2.Init(); + m_vecFinalDest.Init(); + m_vecFinalAngle.Init(); +#endif +} + +bool CBaseToggle::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "lip")) + { + m_flLip = atof(szValue); + } + else if (FStrEq(szKeyName, "wait")) + { + m_flWait = atof(szValue); + } + else if (FStrEq(szKeyName, "master")) + { + m_sMaster = AllocPooledString(szValue); + } + else if (FStrEq(szKeyName, "distance")) + { + m_flMoveDistance = atof(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculate m_vecVelocity and m_flNextThink to reach vecDest from +// GetOrigin() traveling at flSpeed. +// Input : Vector vecDest - +// flSpeed - +//----------------------------------------------------------------------------- +void CBaseToggle::LinearMove( const Vector &vecDest, float flSpeed ) +{ + ASSERTSZ(flSpeed != 0, "LinearMove: no speed is defined!"); + + m_vecFinalDest = vecDest; + + m_movementType = MOVE_TOGGLE_LINEAR; + // Already there? + if (vecDest == GetLocalOrigin()) + { + MoveDone(); + return; + } + + // set destdelta to the vector needed to move + Vector vecDestDelta = vecDest - GetLocalOrigin(); + + // divide vector length by speed to get time to reach dest + float flTravelTime = vecDestDelta.Length() / flSpeed; + + // set m_flNextThink to trigger a call to LinearMoveDone when dest is reached + SetMoveDoneTime( flTravelTime ); + + // scale the destdelta vector by the time spent traveling to get velocity + SetLocalVelocity( vecDestDelta / flTravelTime ); +} + + +void CBaseToggle::MoveDone( void ) +{ + switch ( m_movementType ) + { + case MOVE_TOGGLE_LINEAR: + LinearMoveDone(); + break; + case MOVE_TOGGLE_ANGULAR: + AngularMoveDone(); + break; + } + m_movementType = MOVE_TOGGLE_NONE; + BaseClass::MoveDone(); +} +//----------------------------------------------------------------------------- +// Purpose: After moving, set origin to exact final destination, call "move done" function. +//----------------------------------------------------------------------------- +void CBaseToggle::LinearMoveDone( void ) +{ + UTIL_SetOrigin( this, m_vecFinalDest); + SetAbsVelocity( vec3_origin ); + SetMoveDoneTime( -1 ); +} + + +// DVS TODO: obselete, remove? +bool CBaseToggle::IsLockedByMaster( void ) +{ + if (m_sMaster != NULL_STRING && !UTIL_IsMasterTriggered(m_sMaster, m_hActivator)) + return true; + else + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculate m_vecVelocity and m_flNextThink to reach vecDest from +// GetLocalOrigin() traveling at flSpeed. Just like LinearMove, but rotational. +// Input : vecDestAngle - +// flSpeed - +//----------------------------------------------------------------------------- +void CBaseToggle::AngularMove( const QAngle &vecDestAngle, float flSpeed ) +{ + ASSERTSZ(flSpeed != 0, "AngularMove: no speed is defined!"); + + m_vecFinalAngle = vecDestAngle; + + m_movementType = MOVE_TOGGLE_ANGULAR; + // Already there? + if (vecDestAngle == GetLocalAngles()) + { + MoveDone(); + return; + } + + // set destdelta to the vector needed to move + QAngle vecDestDelta = vecDestAngle - GetLocalAngles(); + + // divide by speed to get time to reach dest + float flTravelTime = vecDestDelta.Length() / flSpeed; + + const float MinTravelTime = 0.01f; + if ( flTravelTime < MinTravelTime ) + { + // If we only travel for a short time, we can fail WillSimulateGamePhysics() + flTravelTime = MinTravelTime; + flSpeed = vecDestDelta.Length() / flTravelTime; + } + + // set m_flNextThink to trigger a call to AngularMoveDone when dest is reached + SetMoveDoneTime( flTravelTime ); + + // scale the destdelta vector by the time spent traveling to get velocity + SetLocalAngularVelocity( vecDestDelta * (1.0 / flTravelTime) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: After rotating, set angle to exact final angle, call "move done" function. +//----------------------------------------------------------------------------- +void CBaseToggle::AngularMoveDone( void ) +{ + SetLocalAngles( m_vecFinalAngle ); + SetLocalAngularVelocity( vec3_angle ); + SetMoveDoneTime( -1 ); +} + + +float CBaseToggle::AxisValue( int flags, const QAngle &angles ) +{ + if ( FBitSet(flags, SF_DOOR_ROTATE_ROLL) ) + return angles.z; + if ( FBitSet(flags, SF_DOOR_ROTATE_PITCH) ) + return angles.x; + + return angles.y; +} + + +void CBaseToggle::AxisDir( void ) +{ + if ( m_spawnflags & SF_DOOR_ROTATE_ROLL ) + m_vecMoveAng = QAngle( 0, 0, 1 ); // angles are roll + else if ( m_spawnflags & SF_DOOR_ROTATE_PITCH ) + m_vecMoveAng = QAngle( 1, 0, 0 ); // angles are pitch + else + m_vecMoveAng = QAngle( 0, 1, 0 ); // angles are yaw +} + + +float CBaseToggle::AxisDelta( int flags, const QAngle &angle1, const QAngle &angle2 ) +{ + // UNDONE: Use AngleDistance() here? + if ( FBitSet (flags, SF_DOOR_ROTATE_ROLL) ) + return angle1.z - angle2.z; + + if ( FBitSet (flags, SF_DOOR_ROTATE_PITCH) ) + return angle1.x - angle2.x; + + return angle1.y - angle2.y; +} + + diff --git a/sp/src/game/server/sun.cpp b/sp/src/game/server/sun.cpp new file mode 100644 index 00000000..90649d74 --- /dev/null +++ b/sp/src/game/server/sun.cpp @@ -0,0 +1,203 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" +#include "sendproxy.h" +#include "sun_shared.h" +#include "map_utils.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CSun : public CBaseEntity +{ +public: + DECLARE_CLASS( CSun, CBaseEntity ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CSun(); + + virtual void Activate(); + + // Input handlers + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputSetColor( inputdata_t &inputdata ); + + virtual int UpdateTransmitState(); + +public: + CNetworkVector( m_vDirection ); + + string_t m_strMaterial; + string_t m_strOverlayMaterial; + + int m_bUseAngles; + float m_flPitch; + float m_flYaw; + + CNetworkVar( int, m_nSize ); // Size of the main core image + CNetworkVar( int, m_nOverlaySize ); // Size for the glow overlay + CNetworkVar( color32, m_clrOverlay ); + CNetworkVar( bool, m_bOn ); + CNetworkVar( int, m_nMaterial ); + CNetworkVar( int, m_nOverlayMaterial ); + CNetworkVar( float, m_flHDRColorScale ); +}; + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CSun, DT_Sun ) + SendPropInt( SENDINFO(m_clrRender), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), + SendPropInt( SENDINFO(m_clrOverlay), 32, SPROP_UNSIGNED, SendProxy_Color32ToInt ), + SendPropVector( SENDINFO(m_vDirection), 0, SPROP_NORMAL ), + SendPropInt( SENDINFO(m_bOn), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nSize), 10, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nOverlaySize), 10, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nMaterial), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nOverlayMaterial), 32, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO_NAME( m_flHDRColorScale, HDRColorScale ), 0, SPROP_NOSCALE, 0.0f, 100.0f ), +END_SEND_TABLE() + + +LINK_ENTITY_TO_CLASS( env_sun, CSun ); + + +BEGIN_DATADESC( CSun ) + + DEFINE_FIELD( m_vDirection, FIELD_VECTOR ), + + DEFINE_KEYFIELD( m_bUseAngles, FIELD_INTEGER, "use_angles" ), + DEFINE_KEYFIELD( m_flPitch, FIELD_FLOAT, "pitch" ), + DEFINE_KEYFIELD( m_flYaw, FIELD_FLOAT, "angle" ), + DEFINE_KEYFIELD( m_nSize, FIELD_INTEGER, "size" ), + DEFINE_KEYFIELD( m_clrOverlay, FIELD_COLOR32, "overlaycolor" ), + DEFINE_KEYFIELD( m_nOverlaySize, FIELD_INTEGER, "overlaysize" ), + DEFINE_KEYFIELD( m_strMaterial, FIELD_STRING, "material" ), + DEFINE_KEYFIELD( m_strOverlayMaterial, FIELD_STRING, "overlaymaterial" ), + + // NOT SAVED + // m_nOverlayMaterial + // m_nMaterial + + DEFINE_FIELD( m_bOn, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_COLOR32, "SetColor", InputSetColor ), + + DEFINE_KEYFIELD( m_flHDRColorScale, FIELD_FLOAT, "HDRColorScale" ), +END_DATADESC() + +CSun::CSun() +{ + m_vDirection.Init( 0, 0, 1 ); + + m_bUseAngles = false; + m_flPitch = 0; + m_flYaw = 0; + m_nSize = 16; + + m_bOn = true; + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + + m_strMaterial = NULL_STRING; + m_strOverlayMaterial = NULL_STRING; + m_nOverlaySize = -1; +} + +void CSun::Activate() +{ + BaseClass::Activate(); + + // Find our target. + if ( m_bUseAngles ) + { + SetupLightNormalFromProps( GetAbsAngles(), m_flYaw, m_flPitch, m_vDirection.GetForModify() ); + m_vDirection = -m_vDirection.Get(); + } + else + { + CBaseEntity *pEnt = gEntList.FindEntityByName( 0, m_target ); + if( pEnt ) + { + Vector vDirection = GetAbsOrigin() - pEnt->GetAbsOrigin(); + VectorNormalize( vDirection ); + m_vDirection = vDirection; + } + } + + // Default behavior + if ( m_nOverlaySize == -1 ) + { + m_nOverlaySize = m_nSize; + } + + // Cache off our image indices + if ( m_strMaterial == NULL_STRING ) + { + m_strMaterial = AllocPooledString( "sprites/light_glow02_add_noz.vmt" ); + } + else + { + const char *pExtension = V_GetFileExtension( STRING( m_strMaterial ) ); + if ( !pExtension ) + { + char szFixedString[MAX_PATH]; + V_strncpy( szFixedString, STRING( m_strMaterial ), sizeof( szFixedString ) ); + V_strncat( szFixedString, ".vmt", sizeof( szFixedString ) ); + m_strMaterial = AllocPooledString( szFixedString ); + } + } + + if ( m_strOverlayMaterial == NULL_STRING ) + { + m_strOverlayMaterial = AllocPooledString( "sprites/light_glow02_add_noz.vmt" ); + } + else + { + const char *pExtension = V_GetFileExtension( STRING( m_strOverlayMaterial ) ); + if ( !pExtension ) + { + char szFixedString[MAX_PATH]; + V_strncpy( szFixedString, STRING( m_strOverlayMaterial ), sizeof( szFixedString ) ); + V_strncat( szFixedString, ".vmt", sizeof( szFixedString ) ); + m_strOverlayMaterial = AllocPooledString( szFixedString ); + } + } + + m_nMaterial = PrecacheModel( STRING( m_strMaterial ) ); + m_nOverlayMaterial = PrecacheModel( STRING( m_strOverlayMaterial ) ); +} + +void CSun::InputTurnOn( inputdata_t &inputdata ) +{ + if( !m_bOn ) + { + m_bOn = true; + } +} + +void CSun::InputTurnOff( inputdata_t &inputdata ) +{ + if ( m_bOn ) + { + m_bOn = false; + } +} + +void CSun::InputSetColor( inputdata_t &inputdata ) +{ + m_clrRender = inputdata.value.Color32(); +} + +int CSun::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + + diff --git a/sp/src/game/server/tactical_mission.cpp b/sp/src/game/server/tactical_mission.cpp new file mode 100644 index 00000000..6cac72c7 --- /dev/null +++ b/sp/src/game/server/tactical_mission.cpp @@ -0,0 +1,186 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tactical_mission.cpp +// Interface for managing player "missions" +// Michael Booth, June 2009 + +#include "cbase.h" +#include "tactical_mission.h" + +/** + * Global singleton accessor. + */ +CTacticalMissionManager &TheTacticalMissions( void ) +{ + static CTacticalMissionManager *manager = g_pGameRules->TacticalMissionManagerFactory(); + + return *manager; +} + + +//--------------------------------------------------------------------------------------------- +class CListMissions : public CTacticalMissionManager::IForEachMission +{ +public: + virtual bool Inspect( const CTacticalMission &mission ) + { + Msg( "%s\n", mission.GetName() ); + return true; + } +}; + +CON_COMMAND_F( mission_list, "List all available tactical missions", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CListMissions list; + TheTacticalMissions().ForEachMission( list ); +} + + + +//--------------------------------------------------------------------------------------------- +class CShowZone : public IForEachNavArea +{ +public: + virtual bool Inspect( const CNavArea *area ) + { + area->DrawFilled( 255, 255, 0, 255, 9999.9f ); + return true; + } +}; + + +CON_COMMAND_F( mission_show, "Show the given mission", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() < 2 ) + { + Msg( "%s \n", args.Arg(0) ); + return; + } + + const CTacticalMission *mission = TheTacticalMissions().GetMission( args.Arg(1) ); + if ( mission ) + { + const CTacticalMissionZone *zone = mission->GetDeployZone( NULL ); + if ( zone ) + { + CShowZone show; + zone->ForEachArea( show ); + } + else + { + Msg( "No deploy zone\n" ); + } + } + else + { + Msg( "Unknown mission '%s'\n", args.Arg(1) ); + } +} + + +//--------------------------------------------------------------------------------------------- +CNavArea *CTacticalMissionZone::SelectArea( CBasePlayer *who ) const +{ + if ( m_areaVector.Count() == 0 ) + return NULL; + + int which = RandomInt( 0, m_areaVector.Count()-1 ); + return m_areaVector[ which ]; +} + + +//--------------------------------------------------------------------------------------------- +/** + * Iterate each area in this zone. + * If functor returns false, stop iterating and return false. + */ +bool CTacticalMissionZone::ForEachArea( IForEachNavArea &func ) const +{ + int i; + + for( i=0; iGetName(), "round_start" ) || FStrEq( gameEvent->GetName(), "teamplay_round_start" ) ) + { + OnRoundRestart(); + } +} + + +//--------------------------------------------------------------------------------------------- +void CTacticalMissionManager::Register( CTacticalMission *mission ) +{ + if ( m_missionVector.Find( mission ) == m_missionVector.InvalidIndex() ) + { + m_missionVector.AddToTail( mission ); + } +} + + +//--------------------------------------------------------------------------------------------- +void CTacticalMissionManager::Unregister( CTacticalMission *mission ) +{ + m_missionVector.FindAndRemove( mission ); +} + + +//--------------------------------------------------------------------------------------------- +/** + * Given a mission name, return the mission (or NULL) + */ +const CTacticalMission *CTacticalMissionManager::GetMission( const char *name ) +{ + FOR_EACH_VEC( m_missionVector, it ) + { + if ( FStrEq( m_missionVector[it]->GetName(), name ) ) + return m_missionVector[it]; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +/** + * Iterate each mission. + * If functor returns false, stop iterating and return false. + */ +bool CTacticalMissionManager::ForEachMission( CTacticalMissionManager::IForEachMission &func ) +{ + FOR_EACH_VEC( m_missionVector, it ) + { + if ( !func.Inspect( *m_missionVector[it] ) ) + return false; + } + + return true; +} diff --git a/sp/src/game/server/tactical_mission.h b/sp/src/game/server/tactical_mission.h new file mode 100644 index 00000000..697d09dd --- /dev/null +++ b/sp/src/game/server/tactical_mission.h @@ -0,0 +1,109 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tactical_mission.h +// Interface for managing player "missions" +// Michael Booth, June 2009 + +#ifndef TACTICAL_MISSION_H +#define TACTICAL_MISSION_H + +#include "nav_area.h" +#include "GameEventListener.h" + +class CBasePlayer; + +//--------------------------------------------------------------------------------------------- +/** + * A mission zone defines a region of space where something tactically interesting occurs. + */ +class CTacticalMissionZone +{ +public: + virtual CNavArea *SelectArea( CBasePlayer *who ) const; + + /** + * Iterate each area in this zone. + * If functor returns false, stop iterating and return false. + */ + virtual bool ForEachArea( IForEachNavArea &func ) const; + +protected: + CUtlVector< CNavArea * > m_areaVector; +}; + + +//--------------------------------------------------------------------------------------------- +/** + * A mission encapsulates an important task or set of tasks, such as capturing an enemy point + */ +class CTacticalMission +{ +public: + virtual ~CTacticalMission() { } + + virtual const CTacticalMissionZone *GetDeployZone( CBasePlayer *who ) const; // where give player should be during this mission + virtual const CTacticalMissionZone *GetObjectiveZone( void ) const; // control points, setup gates, sections of cart path, etc. + virtual const CTacticalMissionZone *GetEnemyZone( void ) const; // where we expect enemies to be during this mission + + virtual const char *GetName( void ) const = 0; // return name of this mission +}; + +inline const CTacticalMissionZone *CTacticalMission::GetDeployZone( CBasePlayer *who ) const +{ + return NULL; +} + +inline const CTacticalMissionZone *CTacticalMission::GetObjectiveZone( void ) const +{ + return NULL; +} + +inline const CTacticalMissionZone *CTacticalMission::GetEnemyZone( void ) const +{ + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +/** + * The mission manager provides access to all available missions + */ +class CTacticalMissionManager : public CGameEventListener +{ +public: + CTacticalMissionManager( void ); + virtual ~CTacticalMissionManager() { } + + virtual void FireGameEvent( IGameEvent *event ); // incoming event processing + + virtual void OnServerActivate( void ) { } // invoked when server loads a new map, after everything has been created/spawned + virtual void OnRoundRestart( void ) { } // invoked when a game round restarts + + virtual void Register( CTacticalMission *mission ); + virtual void Unregister( CTacticalMission *mission ); + + virtual const CTacticalMission *GetMission( const char *name ); // given a mission name, return the mission (or NULL) + + /** + * Iterate each mission. + * If functor returns false, stop iterating and return false. + */ + class IForEachMission + { + public: + virtual bool Inspect( const CTacticalMission &mission ) = 0; + }; + virtual bool ForEachMission( IForEachMission &func ); + +protected: + CUtlVector< CTacticalMission * > m_missionVector; +}; + + +// global singleton +extern CTacticalMissionManager &TheTacticalMissions( void ); + +// factory for instantiating the global singleton +extern CTacticalMissionManager *TacticalMissionFactory( void ); + + +#endif // TACTICAL_MISSION_H diff --git a/sp/src/game/server/tanktrain.cpp b/sp/src/game/server/tanktrain.cpp new file mode 100644 index 00000000..62a5acd6 --- /dev/null +++ b/sp/src/game/server/tanktrain.cpp @@ -0,0 +1,535 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "trains.h" +#include "entitylist.h" +#include "soundenvelope.h" +#include "engine/IEngineSound.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexFireball; +#define SPRITE_FIREBALL "sprites/zerogxplode.vmt" +#define SPRITE_SMOKE "sprites/steam1.vmt" + +void UTIL_RemoveHierarchy( CBaseEntity *pDead ) +{ + if ( !pDead ) + return; + + if ( pDead->edict() ) + { + CBaseEntity *pChild = pDead->FirstMoveChild(); + while ( pChild ) + { + CBaseEntity *pEntity = pChild; + pChild = pChild->NextMovePeer(); + + UTIL_RemoveHierarchy( pEntity ); + } + } + UTIL_Remove( pDead ); +} + +class CFuncTankTrain : public CFuncTrackTrain +{ +public: + DECLARE_CLASS( CFuncTankTrain, CFuncTrackTrain ); + + void Spawn( void ); + + // Filter out damage messages that don't contain blast damage (impervious to other forms of attack) + int OnTakeDamage( const CTakeDamageInfo &info ); + void Event_Killed( const CTakeDamageInfo &info ); + void Blocked( CBaseEntity *pOther ) + { + // FIxme, set speed to zero? + } + DECLARE_DATADESC(); + +private: + + COutputEvent m_OnDeath; +}; + +LINK_ENTITY_TO_CLASS( func_tanktrain, CFuncTankTrain ); + +BEGIN_DATADESC( CFuncTankTrain ) + + // Outputs + DEFINE_OUTPUT(m_OnDeath, "OnDeath"), + +END_DATADESC() + + +void CFuncTankTrain::Spawn( void ) +{ + m_takedamage = true; + BaseClass::Spawn(); +} + +// Filter out damage messages that don't contain blast damage (impervious to other forms of attack) +int CFuncTankTrain::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( ! (info.GetDamageType() & DMG_BLAST) ) + return 0; + + return BaseClass::OnTakeDamage( info ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when the train is killed. +// Input : pInflictor - What killed us. +// pAttacker - Who killed us. +// flDamage - The damage that the killing blow inflicted. +// bitsDamageType - Bitfield of damage types that were inflicted. +//----------------------------------------------------------------------------- +void CFuncTankTrain::Event_Killed( const CTakeDamageInfo &info ) +{ + m_takedamage = DAMAGE_NO; + m_lifeState = LIFE_DEAD; + + m_OnDeath.FireOutput( info.GetInflictor(), this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Changes the target entity for a func_tank or tanktrain_ai +//----------------------------------------------------------------------------- +class CTankTargetChange : public CPointEntity +{ +public: + DECLARE_CLASS( CTankTargetChange, CPointEntity ); + + void Precache( void ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + DECLARE_DATADESC(); + +private: + variant_t m_newTarget; + string_t m_newTargetName; +}; + +LINK_ENTITY_TO_CLASS( tanktrain_aitarget, CTankTargetChange ); + +BEGIN_DATADESC( CTankTargetChange ) + + // DEFINE_FIELD( m_newTarget, variant_t ), + DEFINE_KEYFIELD( m_newTargetName, FIELD_STRING, "newtarget" ), + +END_DATADESC() + + +void CTankTargetChange::Precache( void ) +{ + BaseClass::Precache(); + + // This needs to be in Precache so save/load works + m_newTarget.SetString( m_newTargetName ); +} + +void CTankTargetChange::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_target, NULL, pActivator, pCaller ); + + // UNDONE: This should use more of the event system + while ( pTarget ) + { + // Change the target over + pTarget->AcceptInput( "TargetEntity", this, this, m_newTarget, 0 ); + pTarget = gEntList.FindEntityByName( pTarget, m_target, NULL, pActivator, pCaller ); + } +} + + +// UNDONE: Should be just a logical entity, but we act as another static sound channel for the train +class CTankTrainAI : public CPointEntity +{ +public: + DECLARE_CLASS( CTankTrainAI, CPointEntity ); + + virtual ~CTankTrainAI( void ); + + void Precache( void ); + void Spawn( void ); + void Activate( void ); + void Think( void ); + + int SoundEnginePitch( void ); + void SoundEngineStart( void ); + void SoundEngineStop( void ); + void SoundShutdown( void ); + + CBaseEntity *FindTarget( string_t target, CBaseEntity *pActivator ); + + DECLARE_DATADESC(); + + // INPUTS + void InputTargetEntity( inputdata_t &inputdata ); + +private: + CHandle m_hTrain; + EHANDLE m_hTargetEntity; + int m_soundPlaying; + + CSoundPatch *m_soundTreads; + CSoundPatch *m_soundEngine; + + string_t m_startSoundName; + string_t m_engineSoundName; + string_t m_movementSoundName; + string_t m_targetEntityName; +}; + +LINK_ENTITY_TO_CLASS( tanktrain_ai, CTankTrainAI ); + +BEGIN_DATADESC( CTankTrainAI ) + + DEFINE_FIELD( m_hTrain, FIELD_EHANDLE), + DEFINE_FIELD( m_hTargetEntity, FIELD_EHANDLE), + DEFINE_FIELD( m_soundPlaying, FIELD_INTEGER), + DEFINE_SOUNDPATCH( m_soundTreads ), + DEFINE_SOUNDPATCH( m_soundEngine ), + + DEFINE_KEYFIELD( m_startSoundName, FIELD_STRING, "startsound" ), + DEFINE_KEYFIELD( m_engineSoundName, FIELD_STRING, "enginesound" ), + DEFINE_KEYFIELD( m_movementSoundName, FIELD_STRING, "movementsound" ), + DEFINE_FIELD( m_targetEntityName, FIELD_STRING), + + // Inputs + DEFINE_INPUTFUNC( FIELD_STRING, "TargetEntity", InputTargetEntity ), + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Input handler for setting the target entity by name. +//----------------------------------------------------------------------------- +void CTankTrainAI::InputTargetEntity( inputdata_t &inputdata ) +{ + m_targetEntityName = inputdata.value.StringID(); + m_hTargetEntity = FindTarget( m_targetEntityName, inputdata.pActivator ); + SetNextThink( gpGlobals->curtime ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the first entity in the entity list with the given name. +// Input : target - String ID of the entity to find. +// pActivator - The activating entity if this is called from an input +// or Use handler, NULL otherwise. +//----------------------------------------------------------------------------- +CBaseEntity *CTankTrainAI::FindTarget( string_t target, CBaseEntity *pActivator ) +{ + return gEntList.FindEntityGeneric( NULL, STRING( target ), this, pActivator ); +} + + +CTankTrainAI::~CTankTrainAI( void ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + if ( m_soundTreads ) + { + controller.SoundDestroy( m_soundTreads ); + } + + if ( m_soundEngine ) + { + controller.SoundDestroy( m_soundEngine ); + } +} + +void CTankTrainAI::Precache( void ) +{ + PrecacheScriptSound( STRING( m_startSoundName ) ); + PrecacheScriptSound( STRING( m_engineSoundName ) ); + PrecacheScriptSound( STRING( m_movementSoundName ) ); +} + +int CTankTrainAI::SoundEnginePitch( void ) +{ + CFuncTrackTrain *pTrain = m_hTrain; + + // we know this isn't NULL here + if ( pTrain->GetMaxSpeed() ) + { + return 90 + (fabs(pTrain->GetCurrentSpeed()) * (20) / pTrain->GetMaxSpeed()); + } + return 100; +} + + +void CTankTrainAI::SoundEngineStart( void ) +{ + CFuncTrackTrain *pTrain = m_hTrain; + + SoundEngineStop(); + // play startup sound for train + if ( m_startSoundName != NULL_STRING ) + { + CPASAttenuationFilter filter( pTrain ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = STRING(m_startSoundName); + ep.m_flVolume = 1.0f; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, pTrain->entindex(), ep ); + } + + // play the looping sounds using the envelope controller + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + if ( m_soundTreads ) + { + controller.Play( m_soundTreads, 1.0, 100 ); + } + + if ( m_soundEngine ) + { + controller.Play( m_soundEngine, 0.5, 90 ); + controller.CommandClear( m_soundEngine ); + controller.CommandAdd( m_soundEngine, 0, SOUNDCTRL_CHANGE_PITCH, 1.5, random->RandomInt(130, 145) ); + controller.CommandAdd( m_soundEngine, 1.5, SOUNDCTRL_CHANGE_PITCH, 2, random->RandomInt(105, 115) ); + } + + m_soundPlaying = true; +} + + +void CTankTrainAI::SoundEngineStop( void ) +{ + if ( !m_soundPlaying ) + return; + + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + if ( m_soundTreads ) + { + controller.SoundFadeOut( m_soundTreads, 0.25 ); + } + + if ( m_soundEngine ) + { + controller.CommandClear( m_soundEngine ); + controller.SoundChangePitch( m_soundEngine, 70, 3.0 ); + } + m_soundPlaying = false; +} + + +void CTankTrainAI::SoundShutdown( void ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + if ( m_soundTreads ) + { + controller.Shutdown( m_soundTreads ); + } + + if ( m_soundEngine ) + { + controller.Shutdown( m_soundEngine ); + } + m_soundPlaying = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Set up think and AI +//----------------------------------------------------------------------------- +void CTankTrainAI::Spawn( void ) +{ + Precache(); + m_soundPlaying = false; + m_hTargetEntity = NULL; +} + +void CTankTrainAI::Activate( void ) +{ + BaseClass::Activate(); + + CBaseEntity *pTarget = NULL; + + CFuncTrackTrain *pTrain = NULL; + + if ( m_target != NULL_STRING ) + { + do + { + pTarget = gEntList.FindEntityByName( pTarget, m_target ); + pTrain = dynamic_cast(pTarget); + } while (!pTrain && pTarget); + } + + m_hTrain = pTrain; + + if ( pTrain ) + { + SetNextThink( gpGlobals->curtime + 0.5f ); + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + if ( m_movementSoundName != NULL_STRING ) + { + CPASAttenuationFilter filter( this, ATTN_NORM * 0.5 ); + m_soundTreads = controller.SoundCreate( filter, pTrain->entindex(), CHAN_STATIC, STRING(m_movementSoundName), ATTN_NORM*0.5 ); + } + if ( m_engineSoundName != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + m_soundEngine = controller.SoundCreate( filter, pTrain->entindex(), CHAN_STATIC, STRING(m_engineSoundName), ATTN_NORM ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Dumb linear serach of the path +// Input : *pStart - starting path node +// &startPosition - starting position +// &destination - position to move close to +// Output : int move direction 1 = forward, -1 = reverse, 0 = stop +//----------------------------------------------------------------------------- +int PathFindDirection( CPathTrack *pStart, const Vector &startPosition, const Vector &destination ) +{ + if ( !pStart ) + return 0; // no path, don't move + + CPathTrack *pPath = pStart->m_pnext; + CPathTrack *pNearest = pStart; + + float nearestDist = (pNearest->GetLocalOrigin() - destination).LengthSqr(); + float length = 0; + float nearestForward = 0, nearestReverse = 0; + + do + { + float dist = (pPath->GetLocalOrigin() - destination).LengthSqr(); + + // This is closer than our current estimate + if ( dist < nearestDist ) + { + nearestDist = dist; + pNearest = pPath; + nearestForward = length; // current path length forward + nearestReverse = 0; // count until we hit the start again + } + CPathTrack *pNext = pPath->m_pnext; + if ( pNext ) + { + // UNDONE: Cache delta in path? + float delta = (pNext->GetLocalOrigin() - pPath->GetLocalOrigin()).LengthSqr(); + length += delta; + // add to current reverse estimate + nearestReverse += delta; + pPath = pNext; + } + else + { + // not a looping path + // traverse back to other end of the path + int fail = 0; + while ( pPath->m_pprevious ) + { + fail++; + // HACKHACK: Don't infinite loop + if ( fail > 256 ) + break; + pPath = pPath->m_pprevious; + } + // don't take the reverse path to old node + nearestReverse = nearestForward + 1; + // dont' take forward path to new node (if we find one) + length = (float)COORD_EXTENT * (float)COORD_EXTENT; // HACKHACK: Max quad length + } + + } while ( pPath != pStart ); + + // UNDONE: Fix this fudge factor + // if you are already at the path, or <100 units away, don't move + if ( pNearest == pStart || (pNearest->GetLocalOrigin() - startPosition).LengthSqr() < 100 ) + return 0; + + if ( nearestForward <= nearestReverse ) + return 1; + + return -1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Find a point on my path near to the target and move toward it +//----------------------------------------------------------------------------- +void CTankTrainAI::Think( void ) +{ + CFuncTrackTrain *pTrain = m_hTrain; + + if ( !pTrain || pTrain->m_lifeState != LIFE_ALIVE ) + { + SoundShutdown(); + if ( pTrain ) + UTIL_RemoveHierarchy( pTrain ); + UTIL_Remove( this ); + return; + } + + int desired = 0; + CBaseEntity *pTarget = m_hTargetEntity; + if ( pTarget ) + { + desired = PathFindDirection( pTrain->m_ppath, pTrain->GetLocalOrigin(), pTarget->GetLocalOrigin() ); + } + + // If the train wants to stop, figure out throttle + // otherwise, just throttle in the indicated direction and let the train logic + // clip the speed + if ( !desired ) + { + if ( pTrain->m_flSpeed > 0 ) + { + desired = -1; + } + else if ( pTrain->m_flSpeed < 0 ) + { + desired = 1; + } + } + + // UNDONE: Align the think time with arrival, and bump this up to a few seconds + SetNextThink( gpGlobals->curtime + 0.5f ); + + if ( desired != 0 ) + { + int wasMoving = (pTrain->m_flSpeed == 0) ? false : true; + // chaser wants train to move, send message + pTrain->SetSpeed( desired ); + int isMoving = (pTrain->m_flSpeed == 0) ? false : true; + + if ( !isMoving && wasMoving ) + { + SoundEngineStop(); + } + else if ( isMoving ) + { + if ( !wasMoving ) + { + SoundEngineStart(); + } + } + } + else + { + SoundEngineStop(); + // UNDONE: Align the think time with arrival, and bump this up to a few seconds + SetNextThink( gpGlobals->curtime + 1.0f ); + } +} diff --git a/sp/src/game/server/te.cpp b/sp/src/game/server/te.cpp new file mode 100644 index 00000000..f5638b5c --- /dev/null +++ b/sp/src/game/server/te.cpp @@ -0,0 +1,526 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "te.h" +#include "effect_dispatch_data.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// External definitions +void TE_ArmorRicochet( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir ); +void TE_BeamEntPoint( IRecipientFilter& filter, float delay, + int nStartEntity, const Vector *start, int nEndEntity, const Vector* end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, + int r, int g, int b, int a, int speed ); +void TE_BeamEnts( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, + int r, int g, int b, int a, int speed ); +void TE_BeamFollow( IRecipientFilter& filter, float delay, + int iEntIndex, int modelIndex, int haloIndex, float life, float width, float endWidth, + float fadeLength,float r, float g, float b, float a ); +void TE_BeamPoints( IRecipientFilter& filter, float delay, + const Vector* start, const Vector* end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, + int r, int g, int b, int a, int speed ); +void TE_BeamLaser( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, int r, int g, int b, int a, int speed ); +void TE_BeamRing( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, int spread, float amplitude, int r, int g, int b, int a, int speed, int flags = 0 ); +void TE_BeamRingPoint( IRecipientFilter& filter, float delay, + const Vector& center, float start_radius, float end_radius, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, int spread, float amplitude, int r, int g, int b, int a, int speed, int flags = 0 ); +void TE_BeamSpline( IRecipientFilter& filter, float delay, + int points, Vector* rgPoints ); +void TE_BloodStream( IRecipientFilter& filter, float delay, + const Vector* org, const Vector* dir, int r, int g, int b, int a, int amount ); +void TE_BloodSprite( IRecipientFilter& filter, float delay, + const Vector* org, const Vector *dir, int r, int g, int b, int a, int size ); +void TE_BreakModel( IRecipientFilter& filter, float delay, + const Vector& pos, const QAngle &angles, const Vector& size, const Vector& vel, + int modelindex, int randomization, int count, float time, int flags ); +void TE_BSPDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int entity, int index ); +void TE_ProjectDecal( IRecipientFilter& filer, float delay, + const Vector* pos, const QAngle *angles, float distance, int index ); +void TE_Bubbles( IRecipientFilter& filter, float delay, + const Vector* mins, const Vector* maxs, float height, int modelindex, int count, float speed ); +void TE_BubbleTrail( IRecipientFilter& filter, float delay, + const Vector* mins, const Vector* maxs, float height, int modelindex, int count, float speed ); +void TE_Decal( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* start, int entity, int hitbox, int index ); +void TE_DynamicLight( IRecipientFilter& filter, float delay, + const Vector* org, int r, int g, int b, int exponent, float radius, float time, float decay ); +void TE_Explosion( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float scale, int framerate, int flags, int radius, int magnitude, const Vector* normal = NULL, unsigned char materialType = 'C' ); +void TE_ShatterSurface( IRecipientFilter& filter, float delay, + const Vector* pos, const QAngle* angle, const Vector* vForce, const Vector* vForcePos, + float width, float height, float shardsize, ShatterSurface_t surfacetype, + int front_r, int front_g, int front_b, int back_r, int back_g, int back_b); +void TE_GlowSprite( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float life, float size, int brightness ); +void TE_FootprintDecal( IRecipientFilter& filter, float delay, const Vector *origin, const Vector* right, + int entity, int index, unsigned char materialType ); +void TE_Fizz( IRecipientFilter& filter, float delay, + const CBaseEntity *ed, int modelindex, int density, int current ); +void TE_KillPlayerAttachments( IRecipientFilter& filter, float delay, + int player ); +void TE_LargeFunnel( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, int reversed ); +void TE_MetalSparks( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir ); +void TE_EnergySplash( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir, bool bExplosive ); +void TE_PlayerDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int player, int entity ); +void TE_ShowLine( IRecipientFilter& filter, float delay, + const Vector* start, const Vector* end ); +void TE_Smoke( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float scale, int framerate ); +void TE_Sparks( IRecipientFilter& filter, float delay, + const Vector* pos, int nMagnitude, int nTrailLength, const Vector *pDir ); +void TE_Sprite( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float size, int brightness ); +void TE_SpriteSpray( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir, int modelindex, int speed, float noise, int count ); +void TE_WorldDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int index ); +void TE_MuzzleFlash( IRecipientFilter& filter, float delayt, + const Vector &start, const QAngle &angles, float scale, int type ); +void TE_Dust( IRecipientFilter& filter, float delayt, + const Vector &pos, const Vector &dir, float size, float speed ); +void TE_DispatchEffect( IRecipientFilter& filter, float delay, + const Vector &pos, const char *pName, const CEffectData &data ); +void TE_PhysicsProp( IRecipientFilter& filter, float delay, + int modelindex, int skin, const Vector& pos, const QAngle &angles, const Vector& vel, int flags, int effects ); +void TE_ClientProjectile( IRecipientFilter& filter, float delay, + const Vector* vecOrigin, const Vector* vecVelocity, int modelindex, int lifetime, CBaseEntity *pOwner ); + +#ifdef HL2_DLL +void TE_GaussExplosion( IRecipientFilter& filter, float delayt, + const Vector &pos, const Vector &dir, int type ); +#endif + +class CTempEntsSystem : public ITempEntsSystem +{ +private: + //----------------------------------------------------------------------------- + // Purpose: Returning true means don't even call TE func + // Input : filter - + // *suppress_host - + // Output : static bool + //----------------------------------------------------------------------------- + bool SuppressTE( IRecipientFilter& filter ) + { + if ( GetSuppressHost() ) + { + CRecipientFilter& _filter = (( CRecipientFilter & )filter); + + if ( !_filter.IgnorePredictionCull() ) + { + _filter.RemoveRecipient( (CBasePlayer *)GetSuppressHost() ); + } + + if ( !_filter.GetRecipientCount() ) + { + // Suppress it + return true; + } + } + + // There's at least one recipient + return false; + } +public: + + virtual void ArmorRicochet( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir ) + { + if ( !SuppressTE( filter ) ) + { + TE_ArmorRicochet( filter, delay, pos, dir ); + } + } + + virtual void BeamEntPoint( IRecipientFilter& filter, float delay, + int nStartEntity, const Vector *pStart, int nEndEntity, const Vector* pEnd, + int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, + int r, int g, int b, int a, int speed ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamEntPoint( filter, delay, nStartEntity, pStart, nEndEntity, pEnd, modelindex, haloindex, startframe, framerate, + life, width, endWidth, fadeLength, amplitude, r, g, b, a, speed ); + } + } + + virtual void BeamEnts( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, + int r, int g, int b, int a, int speed ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamEnts( filter, delay, + start, end, modelindex, haloindex, startframe, framerate, + life, width, endWidth, fadeLength, amplitude, + r, g, b, a, speed ); + } + } + virtual void BeamFollow( IRecipientFilter& filter, float delay, + int iEntIndex, int modelIndex, int haloIndex, float life, float width, float endWidth, + float fadeLength, float r, float g, float b, float a ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamFollow( filter, delay, + iEntIndex, modelIndex, haloIndex, life, width, endWidth, fadeLength, + r, g, b, a ); + } + } + virtual void BeamPoints( IRecipientFilter& filter, float delay, + const Vector* start, const Vector* end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, + int r, int g, int b, int a, int speed ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamPoints( filter, delay, + start, end, modelindex, haloindex, startframe, framerate, + life, width, endWidth, fadeLength, amplitude, + r, g, b, a, speed ); + } + } + virtual void BeamLaser( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, int r, int g, int b, int a, int speed ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamLaser( filter, delay, + start, end, modelindex, haloindex, startframe, framerate, + life, width, endWidth, fadeLength, amplitude, r, g, b, a, speed ); + } + } + virtual void BeamRing( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, int spread, float amplitude, int r, int g, int b, int a, int speed, int flags ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamRing( filter, delay, + start, end, modelindex, haloindex, startframe, framerate, + life, width, spread, amplitude, r, g, b, a, speed, flags ); + } + } + virtual void BeamRingPoint( IRecipientFilter& filter, float delay, + const Vector& center, float start_radius, float end_radius, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, int spread, float amplitude, int r, int g, int b, int a, int speed, int flags ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamRingPoint( filter, delay, + center, start_radius, end_radius, modelindex, haloindex, startframe, framerate, + life, width, spread, amplitude, r, g, b, a, speed, flags ); + } + } + virtual void BeamSpline( IRecipientFilter& filter, float delay, + int points, Vector* rgPoints ) + { + if ( !SuppressTE( filter ) ) + { + TE_BeamSpline( filter, delay, points, rgPoints ); + } + } + virtual void BloodStream( IRecipientFilter& filter, float delay, + const Vector* org, const Vector* dir, int r, int g, int b, int a, int amount ) + { + if ( !SuppressTE( filter ) ) + { + TE_BloodStream( filter, delay, org, dir, r, g, b, a, amount ); + } + } + virtual void BloodSprite( IRecipientFilter& filter, float delay, + const Vector* org, const Vector *dir, int r, int g, int b, int a, int size ) + { + if ( !SuppressTE( filter ) ) + { + TE_BloodSprite( filter, delay, org, dir, r, g, b, a, size ); + } + } + virtual void BreakModel( IRecipientFilter& filter, float delay, + const Vector& pos, const QAngle &angle, const Vector& size, const Vector& vel, + int modelindex, int randomization, int count, float time, int flags ) + { + if ( !SuppressTE( filter ) ) + { + TE_BreakModel( filter, delay, pos, angle, size, vel, modelindex, randomization, count, time, flags ); + } + } + virtual void BSPDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int entity, int index ) + { + if ( !SuppressTE( filter ) ) + { + TE_BSPDecal( filter, delay, pos, entity, index ); + } + } + + virtual void ProjectDecal( IRecipientFilter& filter, float delay, + const Vector* pos, const QAngle *angles, float distance, int index ) + { + if ( !SuppressTE( filter ) ) + { + TE_ProjectDecal( filter, delay, pos, angles, distance, index ); + } + } + + virtual void Bubbles( IRecipientFilter& filter, float delay, + const Vector* mins, const Vector* maxs, float height, int modelindex, int count, float speed ) + { + if ( !SuppressTE( filter ) ) + { + TE_Bubbles( filter, delay, mins, maxs, height, modelindex, count, speed ); + } + } + virtual void BubbleTrail( IRecipientFilter& filter, float delay, + const Vector* mins, const Vector* maxs, float flWaterZ, int modelindex, int count, float speed ) + { + if ( !SuppressTE( filter ) ) + { + TE_BubbleTrail( filter, delay, mins, maxs, flWaterZ, modelindex, count, speed ); + } + } + virtual void Decal( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* start, int entity, int hitbox, int index ) + { + if ( !SuppressTE( filter ) ) + { + TE_Decal( filter, delay, pos, start, entity, hitbox, index ); + } + } + virtual void DynamicLight( IRecipientFilter& filter, float delay, + const Vector* org, int r, int g, int b, int exponent, float radius, float time, float decay ) + { + if ( !SuppressTE( filter ) ) + { + TE_DynamicLight( filter, delay, org, r, g, b, exponent, radius, time, decay ); + } + } + virtual void Explosion( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float scale, int framerate, int flags, int radius, int magnitude, const Vector* normal = NULL, unsigned char materialType = 'C' ) + { + if ( !SuppressTE( filter ) ) + { + TE_Explosion( filter, delay, pos, modelindex, scale, framerate, flags, radius, magnitude, normal, materialType ); + } + } + virtual void ShatterSurface( IRecipientFilter& filter, float delay, + const Vector* pos, const QAngle* angle, const Vector* vForce, const Vector* vForcePos, + float width, float height, float shardsize, ShatterSurface_t surfacetype, + int front_r, int front_g, int front_b, int back_r, int back_g, int back_b) + { + if ( !SuppressTE( filter ) ) + { + TE_ShatterSurface( filter, delay, pos, angle, vForce, vForcePos, width, height, shardsize, surfacetype, + front_r, front_g, front_b, back_r, back_g, back_b ); + } + } + virtual void GlowSprite( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float life, float size, int brightness ) + { + if ( !SuppressTE( filter ) ) + { + TE_GlowSprite( filter, delay, pos, modelindex, life, size, brightness ); + } + } + virtual void FootprintDecal( IRecipientFilter& filter, float delay, const Vector *origin, const Vector* right, + int entity, int index, unsigned char materialType ) + { + if ( !SuppressTE( filter ) ) + { + TE_FootprintDecal( filter, delay, origin, right, + entity, index, materialType ); + } + } + virtual void Fizz( IRecipientFilter& filter, float delay, + const CBaseEntity *ed, int modelindex, int density, int current ) + { + if ( !SuppressTE( filter ) ) + { + TE_Fizz( filter, delay, + ed, modelindex, density, current ); + } + } + virtual void KillPlayerAttachments( IRecipientFilter& filter, float delay, + int player ) + { + if ( !SuppressTE( filter ) ) + { + TE_KillPlayerAttachments( filter, delay, player ); + } + } + virtual void LargeFunnel( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, int reversed ) + { + if ( !SuppressTE( filter ) ) + { + TE_LargeFunnel( filter, delay, pos, modelindex, reversed ); + } + } + virtual void MetalSparks( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir ) + { + if ( !SuppressTE( filter ) ) + { + TE_MetalSparks( filter, delay, pos, dir ); + } + } + virtual void EnergySplash( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir, bool bExplosive ) + { + if ( !SuppressTE( filter ) ) + { + TE_EnergySplash( filter, delay, + pos, dir, bExplosive ); + } + } + virtual void PlayerDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int player, int entity ) + { + if ( !SuppressTE( filter ) ) + { + TE_PlayerDecal( filter, delay, + pos, player, entity ); + } + } + virtual void ShowLine( IRecipientFilter& filter, float delay, + const Vector* start, const Vector* end ) + { + if ( !SuppressTE( filter ) ) + { + TE_ShowLine( filter, delay, + start, end ); + } + } + virtual void Smoke( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float scale, int framerate ) + { + if ( !SuppressTE( filter ) ) + { + TE_Smoke( filter, delay, + pos, modelindex, scale, framerate ); + } + } + virtual void Sparks( IRecipientFilter& filter, float delay, + const Vector* pos, int nMagnitude, int nTrailLength, const Vector *pDir ) + { + if ( !SuppressTE( filter ) ) + { + TE_Sparks( filter, delay, + pos, nMagnitude, nTrailLength, pDir ); + } + } + virtual void Sprite( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float size, int brightness ) + { + if ( !SuppressTE( filter ) ) + { + TE_Sprite( filter, delay, + pos, modelindex, size, brightness ); + } + } + virtual void SpriteSpray( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir, int modelindex, int speed, float noise, int count ) + { + if ( !SuppressTE( filter ) ) + { + TE_SpriteSpray( filter, delay, + pos, dir, modelindex, speed, noise, count ); + } + } + virtual void WorldDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int index ) + { + if ( !SuppressTE( filter ) ) + { + TE_WorldDecal( filter, delay, + pos, index ); + } + } + virtual void MuzzleFlash( IRecipientFilter& filter, float delay, + const Vector &start, const QAngle &angles, float scale, int type ) + { + if ( !SuppressTE( filter ) ) + { + TE_MuzzleFlash( filter, delay, + start, angles, scale, type ); + } + } + virtual void Dust( IRecipientFilter& filter, float delay, + const Vector &pos, const Vector &dir, float size, float speed ) + { + if ( !SuppressTE( filter ) ) + { + TE_Dust( filter, delay, + pos, dir, size, speed ); + } + } + virtual void GaussExplosion( IRecipientFilter& filter, float delay, + const Vector &pos, const Vector &dir, int type ) + { +#ifdef HL2_DLL + if ( !SuppressTE( filter ) ) + { + TE_GaussExplosion( filter, delay, pos, dir, type ); + } +#endif + } + + virtual void DispatchEffect( IRecipientFilter& filter, float delay, + const Vector &pos, const char *pName, const CEffectData &data ) + { + if ( !SuppressTE( filter ) ) + { + TE_DispatchEffect( filter, delay, pos, pName, data ); + } + } + + virtual void PhysicsProp( IRecipientFilter& filter, float delay, int modelindex, int skin, + const Vector& pos, const QAngle &angles, const Vector& vel, int flags, int effects ) + { + if ( !SuppressTE( filter ) ) + { + TE_PhysicsProp( filter, delay, modelindex, skin, pos, angles, vel, flags, effects ); + } + } + + // For playback from external tools + virtual void TriggerTempEntity( KeyValues *pKeyValues ) + { + Assert(0); + } + + virtual void ClientProjectile( IRecipientFilter& filter, float delay, + const Vector* vecOrigin, const Vector* vecVelocity, int modelindex, int lifetime, CBaseEntity *pOwner ) + { + if ( !SuppressTE( filter ) ) + { + TE_ClientProjectile( filter, delay, vecOrigin, vecVelocity, modelindex, lifetime, pOwner ); + } + } +}; + +static CTempEntsSystem g_TESystem; +// Expose to rest of engine +ITempEntsSystem *te = &g_TESystem; diff --git a/sp/src/game/server/te.h b/sp/src/game/server/te.h new file mode 100644 index 00000000..6e97baab --- /dev/null +++ b/sp/src/game/server/te.h @@ -0,0 +1,17 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $NoKeywords: $ +//=============================================================================// + +#if !defined( TE_H ) +#define TE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "itempents.h" + +#endif // TE_H diff --git a/sp/src/game/server/te_armorricochet.cpp b/sp/src/game/server/te_armorricochet.cpp new file mode 100644 index 00000000..e3edc76e --- /dev/null +++ b/sp/src/game/server/te_armorricochet.cpp @@ -0,0 +1,142 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches sparks +//----------------------------------------------------------------------------- +class CTEMetalSparks : public CBaseTempEntity +{ +DECLARE_CLASS( CTEMetalSparks, CBaseTempEntity ); + +public: + CTEMetalSparks( const char *name ); + virtual ~CTEMetalSparks( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecPos ); + CNetworkVector( m_vecDir ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEMetalSparks::CTEMetalSparks( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecPos.Init(); + m_vecDir.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEMetalSparks::~CTEMetalSparks( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEMetalSparks::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecPos = current_origin; + + AngleVectors( current_angles, &m_vecDir.GetForModify() ); + + Vector forward; + + m_vecPos += Vector( 0, 0, 24 ); + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecPos, 100.0, forward, m_vecPos.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CTEMetalSparks, DT_TEMetalSparks) + SendPropVector( SENDINFO(m_vecPos), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecDir), -1, SPROP_COORD), +END_SEND_TABLE() + +// Singleton to fire TEMetalSparks objects +static CTEMetalSparks g_TEMetalSparks( "Metal Sparks" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// scale - +//----------------------------------------------------------------------------- +void TE_MetalSparks( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir ) +{ + g_TEMetalSparks.m_vecPos = *pos; + g_TEMetalSparks.m_vecDir = *dir; + + Assert( dir->Length() < 1.01 ); // make sure it's a normal + + // Send it over the wire + g_TEMetalSparks.Create( filter, delay ); +} + +class CTEArmorRicochet : public CTEMetalSparks +{ +DECLARE_CLASS( CTEArmorRicochet, CTEMetalSparks ); + +public: + CTEArmorRicochet( const char *name ) : CTEMetalSparks(name) {} + DECLARE_SERVERCLASS(); +}; + +IMPLEMENT_SERVERCLASS_ST( CTEArmorRicochet, DT_TEArmorRicochet) +END_SEND_TABLE() + +static CTEArmorRicochet g_TEArmorRicochet( "Armor Ricochet" ); +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// scale - +//----------------------------------------------------------------------------- +void TE_ArmorRicochet( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir ) +{ + g_TEArmorRicochet.m_vecPos = *pos; + g_TEArmorRicochet.m_vecDir = *dir; + + // Send it over the wire + g_TEArmorRicochet.Create( filter, delay ); +} diff --git a/sp/src/game/server/te_basebeam.cpp b/sp/src/game/server/te_basebeam.cpp new file mode 100644 index 00000000..11b4c0db --- /dev/null +++ b/sp/src/game/server/te_basebeam.cpp @@ -0,0 +1,66 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBaseBeam::CTEBaseBeam( const char *name ) : + CBaseTempEntity( name ) +{ + m_nModelIndex = 0; + m_nHaloIndex = 0; + m_nStartFrame = 0; + m_nFrameRate = 0; + m_fLife = 0.0; + m_fWidth = 0; + m_fEndWidth = 0; + m_nFadeLength = 0; + m_fAmplitude = 0; + r = g = b = a = 0; + m_nSpeed = 0; + m_nFlags = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBaseBeam::~CTEBaseBeam( void ) +{ +} + + + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CTEBaseBeam, DT_BaseBeam ) + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropModelIndex( SENDINFO(m_nHaloIndex) ), + SendPropInt( SENDINFO(m_nStartFrame), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nFrameRate), 8, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_fLife), 8, 0, 0.0, 25.6 ), + SendPropFloat( SENDINFO(m_fWidth), 10, 0, 0.0, 128.0 ), + SendPropFloat( SENDINFO(m_fEndWidth), 10, 0, 0.0, 128.0 ), + SendPropInt( SENDINFO(m_nFadeLength), 8, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_fAmplitude), 8, 0, 0.0, 64.0 ), + SendPropInt( SENDINFO(m_nSpeed), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(r), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(g), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(b), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(a), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nFlags), 32, SPROP_UNSIGNED ), +END_SEND_TABLE() + diff --git a/sp/src/game/server/te_basebeam.h b/sp/src/game/server/te_basebeam.h new file mode 100644 index 00000000..b0be4c30 --- /dev/null +++ b/sp/src/game/server/te_basebeam.h @@ -0,0 +1,59 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +//----------------------------------------------------------------------------- +// Purpose: Dispatches a beam ring between two entities +//----------------------------------------------------------------------------- +#if !defined( TE_BASEBEAM_H ) +#define TE_BASEBEAM_H +#ifdef _WIN32 +#pragma once +#endif + +#include "basetempentity.h" + +abstract_class CTEBaseBeam : public CBaseTempEntity +{ +public: + + DECLARE_CLASS( CTEBaseBeam, CBaseTempEntity ); + DECLARE_SERVERCLASS(); + + +public: + CTEBaseBeam( const char *name ); + virtual ~CTEBaseBeam( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ) = 0; + +public: + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nHaloIndex ); + CNetworkVar( int, m_nStartFrame ); + CNetworkVar( int, m_nFrameRate ); + CNetworkVar( float, m_fLife ); + CNetworkVar( float, m_fWidth ); + CNetworkVar( float, m_fEndWidth ); + CNetworkVar( int, m_nFadeLength ); + CNetworkVar( float, m_fAmplitude ); + CNetworkVar( int, r ); + CNetworkVar( int, g ); + CNetworkVar( int, b ); + CNetworkVar( int, a ); + CNetworkVar( int, m_nSpeed ); + CNetworkVar( int, m_nFlags ); +}; + +EXTERN_SEND_TABLE(DT_BaseBeam); + +#endif // TE_BASEBEAM_H \ No newline at end of file diff --git a/sp/src/game/server/te_beamentpoint.cpp b/sp/src/game/server/te_beamentpoint.cpp new file mode 100644 index 00000000..6d799045 --- /dev/null +++ b/sp/src/game/server/te_beamentpoint.cpp @@ -0,0 +1,154 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches a beam ring between two entities +//----------------------------------------------------------------------------- +class CTEBeamEntPoint : public CTEBaseBeam +{ +public: + DECLARE_CLASS( CTEBeamEntPoint, CTEBaseBeam ); + DECLARE_SERVERCLASS(); + + CTEBeamEntPoint( const char *name ); + virtual ~CTEBeamEntPoint( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + +public: + CNetworkVar( int, m_nStartEntity ); + CNetworkVector( m_vecStartPoint ); + CNetworkVar( int, m_nEndEntity ); + CNetworkVector( m_vecEndPoint ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamEntPoint::CTEBeamEntPoint( const char *name ) : + CTEBaseBeam( name ) +{ + m_nStartEntity = 0; + m_nEndEntity = 0; + m_vecStartPoint.Init(); + m_vecEndPoint.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamEntPoint::~CTEBeamEntPoint( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamEntPoint::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_nStartEntity = 1; + + m_nModelIndex = g_sModelIndexSmoke; + m_nStartFrame = 0; + m_nFrameRate = 10; + m_fLife = 2.0; + m_fWidth = 1.0; + m_fAmplitude = 1.0; + r = 0; + g = 63; + b = 127; + a = 150; + m_nSpeed = 1; + + m_vecEndPoint = current_origin; + + Vector forward, right; + + m_vecEndPoint += Vector( 0, 0, 24 ); + + AngleVectors( current_angles, &forward, &right, 0 ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecEndPoint, 50.0, forward, m_vecEndPoint.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEBeamEntPoint, DT_TEBeamEntPoint) + SendPropInt( SENDINFO(m_nStartEntity), 24, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nEndEntity), 24, SPROP_UNSIGNED ), + SendPropVector( SENDINFO(m_vecStartPoint), -1, SPROP_COORD ), + SendPropVector( SENDINFO(m_vecEndPoint), -1, SPROP_COORD ), +END_SEND_TABLE() + + +// Singleton to fire TEBeamEntPoint objects +static CTEBeamEntPoint g_TEBeamEntPoint( "BeamEntPoint" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// int start - +// *end - +// modelindex - +// startframe - +// framerate - +// msg_dest - +// delay - +// origin - +// recipient - +//----------------------------------------------------------------------------- +void TE_BeamEntPoint( IRecipientFilter& filter, float delay, + int nStartEntity, const Vector *start, int nEndEntity, const Vector* end, + int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, int r, int g, int b, int a, int speed ) +{ + g_TEBeamEntPoint.m_nStartEntity = (nStartEntity > 0) ? (nStartEntity & 0x0FFF) | ((1 & 0xF)<<12) : 0; + g_TEBeamEntPoint.m_nEndEntity = (nEndEntity > 0) ? (nEndEntity & 0x0FFF) | ((1 & 0xF)<<12) : 0; + g_TEBeamEntPoint.m_vecStartPoint = start ? *start : vec3_origin; + g_TEBeamEntPoint.m_vecEndPoint = end ? *end : vec3_origin; + g_TEBeamEntPoint.m_nModelIndex = modelindex; + g_TEBeamEntPoint.m_nHaloIndex = haloindex; + g_TEBeamEntPoint.m_nStartFrame = startframe; + g_TEBeamEntPoint.m_nFrameRate = framerate; + g_TEBeamEntPoint.m_fLife = life; + g_TEBeamEntPoint.m_fWidth = width; + g_TEBeamEntPoint.m_fEndWidth = endWidth; + g_TEBeamEntPoint.m_nFadeLength = fadeLength; + g_TEBeamEntPoint.m_fAmplitude = amplitude; + g_TEBeamEntPoint.m_nSpeed = speed; + g_TEBeamEntPoint.r = r; + g_TEBeamEntPoint.g = g; + g_TEBeamEntPoint.b = b; + g_TEBeamEntPoint.a = a; + + // Send it over the wire + g_TEBeamEntPoint.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_beaments.cpp b/sp/src/game/server/te_beaments.cpp new file mode 100644 index 00000000..2f06c307 --- /dev/null +++ b/sp/src/game/server/te_beaments.cpp @@ -0,0 +1,134 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches a beam between two entities +//----------------------------------------------------------------------------- +class CTEBeamEnts : public CTEBaseBeam +{ +public: + DECLARE_CLASS( CTEBeamEnts, CTEBaseBeam ); + DECLARE_SERVERCLASS(); + + CTEBeamEnts( const char *name ); + virtual ~CTEBeamEnts( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + +public: + CNetworkVar( int, m_nStartEntity ); + CNetworkVar( int, m_nEndEntity ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamEnts::CTEBeamEnts( const char *name ) : + CTEBaseBeam( name ) +{ + m_nStartEntity = 0; + m_nEndEntity = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamEnts::~CTEBeamEnts( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamEnts::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_nStartEntity = 1; + m_nEndEntity = 0; + + m_nModelIndex = g_sModelIndexSmoke; + m_nStartFrame = 0; + m_nFrameRate = 10; + m_fLife = 2.0; + m_fWidth = 1.0; + m_fAmplitude = 1; + r = 127; + g = 63; + b = 0; + a = 150; + m_nSpeed = 1; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEBeamEnts, DT_TEBeamEnts) + SendPropInt( SENDINFO(m_nStartEntity), 24, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nEndEntity), 24, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEBeamEnts objects +static CTEBeamEnts g_TEBeamEnts( "BeamEnts" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// int start - +// end - +// modelindex - +// startframe - +// framerate - +// msg_dest - +// delay - +// origin - +// recipient - +//----------------------------------------------------------------------------- +void TE_BeamEnts( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, int r, int g, int b, int a, int speed ) +{ + g_TEBeamEnts.m_nStartEntity = (start & 0x0FFF) | ((1 & 0xF)<<12); + g_TEBeamEnts.m_nEndEntity = (end & 0x0FFF) | ((1 & 0xF)<<12); + g_TEBeamEnts.m_nModelIndex = modelindex; + g_TEBeamEnts.m_nHaloIndex = haloindex; + g_TEBeamEnts.m_nStartFrame = startframe; + g_TEBeamEnts.m_nFrameRate = framerate; + g_TEBeamEnts.m_fLife = life; + g_TEBeamEnts.m_fWidth = width; + g_TEBeamEnts.m_fEndWidth = endWidth; + g_TEBeamEnts.m_nFadeLength = fadeLength; + g_TEBeamEnts.m_fAmplitude = amplitude; + g_TEBeamEnts.m_nSpeed = speed; + g_TEBeamEnts.r = r; + g_TEBeamEnts.g = g; + g_TEBeamEnts.b = b; + g_TEBeamEnts.a = a; + + // Send it over the wire + g_TEBeamEnts.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_beamfollow.cpp b/sp/src/game/server/te_beamfollow.cpp new file mode 100644 index 00000000..6a0cf759 --- /dev/null +++ b/sp/src/game/server/te_beamfollow.cpp @@ -0,0 +1,112 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches a beam ring between two entities +//----------------------------------------------------------------------------- +class CTEBeamFollow : public CTEBaseBeam +{ + DECLARE_CLASS( CTEBeamFollow, CTEBaseBeam ); +public: + + DECLARE_SERVERCLASS(); + + CTEBeamFollow( const char *name ); + virtual ~CTEBeamFollow( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + +public: + + CNetworkVar( int, m_iEntIndex ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamFollow::CTEBeamFollow( const char *name ) : + CTEBaseBeam( name ) +{ + m_iEntIndex = -1; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamFollow::~CTEBeamFollow( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamFollow::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_iEntIndex = 1; +} + +IMPLEMENT_SERVERCLASS_ST(CTEBeamFollow, DT_TEBeamFollow) + SendPropInt( SENDINFO(m_iEntIndex), 24, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEBeamEntPoint objects +static CTEBeamFollow g_TEBeamFollow( "BeamFollow" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : filter - +// delay - +// iEntIndex - +// modelIndex - +// modelindex - +// haloIndex - +// life - +// width - +// endWidth - +// fadeLength - +// r - +// g - +// b - +// a - +//----------------------------------------------------------------------------- +void TE_BeamFollow( IRecipientFilter& filter, float delay, + int iEntIndex, int modelIndex, int haloIndex, float life, float width, float endWidth, + float fadeLength,float r, float g, float b, float a ) +{ + g_TEBeamFollow.m_iEntIndex = (iEntIndex & 0x0FFF) | ((1 & 0xF)<<12); + g_TEBeamFollow.m_nModelIndex = modelIndex; + g_TEBeamFollow.m_nHaloIndex = haloIndex; + g_TEBeamFollow.m_nStartFrame = 0; + g_TEBeamFollow.m_nFrameRate = 0; + g_TEBeamFollow.m_fLife = life; + g_TEBeamFollow.m_fWidth = width; + g_TEBeamFollow.m_fEndWidth = endWidth; + g_TEBeamFollow.m_nFadeLength = fadeLength; + g_TEBeamFollow.r = r; + g_TEBeamFollow.g = g; + g_TEBeamFollow.b = b; + g_TEBeamFollow.a = a; + + // Send it over the wire + g_TEBeamFollow.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_beamlaser.cpp b/sp/src/game/server/te_beamlaser.cpp new file mode 100644 index 00000000..3eb40c41 --- /dev/null +++ b/sp/src/game/server/te_beamlaser.cpp @@ -0,0 +1,127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Beam used for Laser sights. Fades out when it's perpendicular to the viewpoint. +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Beam used for Laser sights. Fades out when it's perpendicular to the viewpoint. +//----------------------------------------------------------------------------- +class CTEBeamLaser : public CTEBaseBeam +{ + DECLARE_CLASS( CTEBeamLaser, CTEBaseBeam ); +public: + DECLARE_SERVERCLASS(); + + CTEBeamLaser( const char *name ); + virtual ~CTEBeamLaser( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + +public: + CNetworkVar( int, m_nStartEntity ); + CNetworkVar( int, m_nEndEntity ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamLaser::CTEBeamLaser( const char *name ) : + CTEBaseBeam( name ) +{ + m_nStartEntity = 0; + m_nEndEntity = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamLaser::~CTEBeamLaser( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamLaser::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_nStartEntity = 1; + m_nEndEntity = 0; + + m_nModelIndex = g_sModelIndexSmoke; + m_nStartFrame = 0; + m_nFrameRate = 10; + m_fLife = 2.0; + m_fWidth = 1.0; + m_fAmplitude = 1.0; + r = 127; + g = 63; + b = 0; + a = 150; + m_nSpeed = 1; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST( CTEBeamLaser, DT_TEBeamLaser) + SendPropInt( SENDINFO(m_nStartEntity), 24, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nEndEntity), 24, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEBeamLaser objects +static CTEBeamLaser g_TEBeamLaser( "BeamLaser" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *start - +// *end - +// modelindex - +// startframe - +// framerate - +// msg_dest - +// delay - +// origin - +// recipient - +//----------------------------------------------------------------------------- +void TE_BeamLaser( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, int r, int g, int b, int a, int speed ) +{ + g_TEBeamLaser.m_nStartEntity = (start & 0x0FFF) | ((1 & 0xF)<<12); + g_TEBeamLaser.m_nEndEntity = (end & 0x0FFF) | ((1 & 0xF)<<12); + g_TEBeamLaser.m_nModelIndex = modelindex; + g_TEBeamLaser.m_nHaloIndex = haloindex; + g_TEBeamLaser.m_nStartFrame = startframe; + g_TEBeamLaser.m_nFrameRate = framerate; + g_TEBeamLaser.m_fLife = life; + g_TEBeamLaser.m_fWidth = width; + g_TEBeamLaser.m_fEndWidth = endWidth; + g_TEBeamLaser.m_nFadeLength = fadeLength; + g_TEBeamLaser.m_fAmplitude = amplitude; + g_TEBeamLaser.m_nSpeed = speed; + g_TEBeamLaser.r = r; + g_TEBeamLaser.g = g; + g_TEBeamLaser.b = b; + g_TEBeamLaser.a = a; + + // Send it over the wire + g_TEBeamLaser.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_beampoints.cpp b/sp/src/game/server/te_beampoints.cpp new file mode 100644 index 00000000..81e911fe --- /dev/null +++ b/sp/src/game/server/te_beampoints.cpp @@ -0,0 +1,144 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches a beam ring between two entities +//----------------------------------------------------------------------------- +class CTEBeamPoints : public CTEBaseBeam +{ +public: + DECLARE_CLASS( CTEBeamPoints, CTEBaseBeam ); + DECLARE_SERVERCLASS(); + + CTEBeamPoints( const char *name ); + virtual ~CTEBeamPoints( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + +public: + CNetworkVector( m_vecStartPoint ); + CNetworkVector( m_vecEndPoint ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamPoints::CTEBeamPoints( const char *name ) : + CTEBaseBeam( name ) +{ + m_vecStartPoint.Init(); + m_vecEndPoint.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamPoints::~CTEBeamPoints( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamPoints::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_nModelIndex = g_sModelIndexSmoke; + m_nStartFrame = 0; + m_nFrameRate = 10; + m_fLife = 2.0; + m_fWidth = 1.0; + m_fAmplitude = 1; + r = 0; + g = 63; + b = 127; + a = 150; + m_nSpeed = 1; + + m_vecStartPoint = current_origin; + + Vector forward, right; + + m_vecStartPoint += Vector( 0, 0, 30 ); + + AngleVectors( current_angles, &forward, &right, 0 ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecStartPoint, 75.0, forward, m_vecStartPoint.GetForModify() ); + VectorMA( m_vecStartPoint, 25.0, right, m_vecEndPoint.GetForModify() ); + VectorMA( m_vecStartPoint, -25.0, right, m_vecStartPoint.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST( CTEBeamPoints, DT_TEBeamPoints) + SendPropVector( SENDINFO(m_vecStartPoint), -1, SPROP_COORD ), + SendPropVector( SENDINFO(m_vecEndPoint), -1, SPROP_COORD ), +END_SEND_TABLE() + + +// Singleton to fire TEBeamPoints objects +static CTEBeamPoints g_TEBeamPoints( "BeamPoints" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *start - +// *end - +// modelindex - +// startframe - +// framerate - +// msg_dest - +// delay - +// origin - +// recipient - +//----------------------------------------------------------------------------- +void TE_BeamPoints( IRecipientFilter& filter, float delay, + const Vector* start, const Vector* end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, float endWidth, int fadeLength, float amplitude, int r, int g, int b, int a, int speed ) +{ + g_TEBeamPoints.m_vecStartPoint = *start; + g_TEBeamPoints.m_vecEndPoint = *end; + g_TEBeamPoints.m_nModelIndex = modelindex; + g_TEBeamPoints.m_nHaloIndex = haloindex; + g_TEBeamPoints.m_nStartFrame = startframe; + g_TEBeamPoints.m_nFrameRate = framerate; + g_TEBeamPoints.m_fLife = life; + g_TEBeamPoints.m_fWidth = width; + g_TEBeamPoints.m_fEndWidth = endWidth; + g_TEBeamPoints.m_nFadeLength = fadeLength; + g_TEBeamPoints.m_fAmplitude = amplitude; + g_TEBeamPoints.m_nSpeed = speed; + g_TEBeamPoints.r = r; + g_TEBeamPoints.g = g; + g_TEBeamPoints.b = b; + g_TEBeamPoints.a = a; + + // Send it over the wire + g_TEBeamPoints.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_beamring.cpp b/sp/src/game/server/te_beamring.cpp new file mode 100644 index 00000000..e6e14bb5 --- /dev/null +++ b/sp/src/game/server/te_beamring.cpp @@ -0,0 +1,135 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches a beam ring between two entities +//----------------------------------------------------------------------------- +class CTEBeamRing : public CTEBaseBeam +{ +public: + DECLARE_CLASS( CTEBeamRing, CTEBaseBeam ); + DECLARE_SERVERCLASS(); + + CTEBeamRing( const char *name ); + virtual ~CTEBeamRing( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + +public: + CNetworkVar( int, m_nStartEntity ); + CNetworkVar( int, m_nEndEntity ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamRing::CTEBeamRing( const char *name ) : + CTEBaseBeam( name ) +{ + m_nStartEntity = 0; + m_nEndEntity = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamRing::~CTEBeamRing( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamRing::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_nStartEntity = 1; + m_nEndEntity = 0; + + m_nModelIndex = g_sModelIndexSmoke; + m_nStartFrame = 0; + m_nFrameRate = 2; + m_fLife = 10.0; + m_fWidth = 2.0; + m_fAmplitude = 1; + r = 255; + g = 255; + b = 0; + a = 127; + m_nSpeed = 5; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + + +IMPLEMENT_SERVERCLASS_ST( CTEBeamRing, DT_TEBeamRing) + SendPropInt( SENDINFO(m_nStartEntity), MAX_EDICT_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nEndEntity), MAX_EDICT_BITS, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEBeamRing objects +static CTEBeamRing g_TEBeamRing( "BeamRing" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// int start - +// end - +// modelindex - +// startframe - +// framerate - +// msg_dest - +// delay - +// origin - +// recipient - +//----------------------------------------------------------------------------- +void TE_BeamRing( IRecipientFilter& filter, float delay, + int start, int end, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, int spread, float amplitude, int r, int g, int b, int a, int speed, int flags ) +{ + g_TEBeamRing.m_nStartEntity = (start & 0x0FFF) | ((1 & 0xF)<<12); + g_TEBeamRing.m_nEndEntity = (end & 0x0FFF) | ((1 & 0xF)<<12); + g_TEBeamRing.m_nModelIndex = modelindex; + g_TEBeamRing.m_nHaloIndex = haloindex; + g_TEBeamRing.m_nStartFrame = startframe; + g_TEBeamRing.m_nFrameRate = framerate; + g_TEBeamRing.m_fLife = life; + g_TEBeamRing.m_fWidth = width; + g_TEBeamRing.m_fEndWidth = width; + g_TEBeamRing.m_nFadeLength = 0; + g_TEBeamRing.m_fAmplitude = amplitude; + g_TEBeamRing.m_nSpeed = speed; + g_TEBeamRing.r = r; + g_TEBeamRing.g = g; + g_TEBeamRing.b = b; + g_TEBeamRing.a = a; + g_TEBeamRing.m_nFlags = flags; + + // Send it over the wire + g_TEBeamRing.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_beamringpoint.cpp b/sp/src/game/server/te_beamringpoint.cpp new file mode 100644 index 00000000..c277142a --- /dev/null +++ b/sp/src/game/server/te_beamringpoint.cpp @@ -0,0 +1,124 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_basebeam.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches a beam ring between two entities +//----------------------------------------------------------------------------- +class CTEBeamRingPoint : public CTEBaseBeam +{ +public: + DECLARE_CLASS( CTEBeamRingPoint, CTEBaseBeam ); + DECLARE_SERVERCLASS(); + + CTEBeamRingPoint( const char *name ); + virtual ~CTEBeamRingPoint( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + +public: + CNetworkVector( m_vecCenter ); + CNetworkVar( float, m_flStartRadius ); + CNetworkVar( float, m_flEndRadius ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamRingPoint::CTEBeamRingPoint( const char *name ) : + CTEBaseBeam( name ) +{ + m_vecCenter.Init(); + m_flStartRadius = 0.0f; + m_flEndRadius = 0.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamRingPoint::~CTEBeamRingPoint( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamRingPoint::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_vecCenter = current_origin; + m_flEndRadius = 256.0f; + m_flStartRadius = 16.0f; + + m_nModelIndex = g_sModelIndexSmoke; + m_nStartFrame = 0; + m_nFrameRate = 2; + m_fLife = 10.0; + m_fWidth = 2.0; + m_fAmplitude = 1; + r = 255; + g = 255; + b = 0; + a = 127; + m_nSpeed = 5; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + + +IMPLEMENT_SERVERCLASS_ST( CTEBeamRingPoint, DT_TEBeamRingPoint) + SendPropVector( SENDINFO(m_vecCenter), -1, SPROP_COORD ), + SendPropFloat( SENDINFO(m_flStartRadius), 16, SPROP_ROUNDUP, 0.0f, 4096.0f ), + SendPropFloat( SENDINFO(m_flEndRadius), 16, SPROP_ROUNDUP, 0.0f, 4096.0f ), +END_SEND_TABLE() + + +// Singleton to fire TEBeamRingPoint objects +static CTEBeamRingPoint g_TEBeamRingPoint( "BeamRingPoint" ); + +void TE_BeamRingPoint( IRecipientFilter& filter, float delay, + const Vector& center, float start_radius, float end_radius, int modelindex, int haloindex, int startframe, int framerate, + float life, float width, int spread, float amplitude, int r, int g, int b, int a, int speed, int flags ) +{ + g_TEBeamRingPoint.m_vecCenter = center; + g_TEBeamRingPoint.m_flStartRadius = start_radius; + g_TEBeamRingPoint.m_flEndRadius = end_radius; + g_TEBeamRingPoint.m_nModelIndex = modelindex; + g_TEBeamRingPoint.m_nHaloIndex = haloindex; + g_TEBeamRingPoint.m_nStartFrame = startframe; + g_TEBeamRingPoint.m_nFrameRate = framerate; + g_TEBeamRingPoint.m_fLife = life; + g_TEBeamRingPoint.m_fWidth = width; + g_TEBeamRingPoint.m_fEndWidth = width; + g_TEBeamRingPoint.m_nFadeLength = 0; + g_TEBeamRingPoint.m_fAmplitude = amplitude; + g_TEBeamRingPoint.m_nSpeed = speed; + g_TEBeamRingPoint.r = r; + g_TEBeamRingPoint.g = g; + g_TEBeamRingPoint.b = b; + g_TEBeamRingPoint.a = a; + g_TEBeamRingPoint.m_nFlags = flags; + + // Send it over the wire + g_TEBeamRingPoint.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_beamspline.cpp b/sp/src/game/server/te_beamspline.cpp new file mode 100644 index 00000000..4253b479 --- /dev/null +++ b/sp/src/game/server/te_beamspline.cpp @@ -0,0 +1,134 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define MAX_SPLINE_POINTS 16 +//----------------------------------------------------------------------------- +// Purpose: Dispatches beam spline tempentity +//----------------------------------------------------------------------------- +class CTEBeamSpline : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEBeamSpline, CBaseTempEntity ); + + CTEBeamSpline( const char *name ); + virtual ~CTEBeamSpline( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkArray( Vector, m_vecPoints, MAX_SPLINE_POINTS ); + CNetworkVar( int, m_nPoints ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBeamSpline::CTEBeamSpline( const char *name ) : + CBaseTempEntity( name ) +{ + int i; + for ( i = 0; i < MAX_SPLINE_POINTS; i++ ) + { + m_vecPoints.GetForModify( i ).Init(); + } + m_nPoints = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBeamSpline::~CTEBeamSpline( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBeamSpline::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nPoints = 6; + Vector m_vecStart = current_origin; + + Vector forward, right; + + m_vecStart[2] += 24; + + AngleVectors( current_angles, &forward, &right, 0 ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecStart, 100.0, forward, m_vecStart ); + + VectorMA( m_vecStart, -128.0, right, m_vecStart ); + + for ( int i = 0; i < m_nPoints; i++ ) + { + m_vecPoints.Set( i, m_vecStart ); + VectorMA( m_vecStart, 128/m_nPoints, right, m_vecStart ); + VectorMA( m_vecStart, 30.0/m_nPoints, forward, m_vecStart ); + } + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CTEBeamSpline, DT_TEBeamSpline) + SendPropInt( SENDINFO( m_nPoints ), 5, SPROP_UNSIGNED ), + + SendPropArray( + SendPropVector( SENDINFO_ARRAY(m_vecPoints), -1, SPROP_COORD), + m_vecPoints) +END_SEND_TABLE() + + +// Singleton to fire TEBeamSpline objects +static CTEBeamSpline g_TEBeamSpline( "BeamSpline" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// points - +// *points - +//----------------------------------------------------------------------------- +void TE_BeamSpline( IRecipientFilter& filter, float delay, + int points, Vector* rgPoints ) +{ + int i; + g_TEBeamSpline.m_nPoints = points; + for ( i = 0; i < points; i++ ) + { + g_TEBeamSpline.m_vecPoints.Set( i, rgPoints[ i ] ); + } + + for ( ; i < MAX_SPLINE_POINTS; i++ ) + { + g_TEBeamSpline.m_vecPoints.GetForModify( i ).Init(); + } + + // Send it over the wire + g_TEBeamSpline.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_bloodsprite.cpp b/sp/src/game/server/te_bloodsprite.cpp new file mode 100644 index 00000000..02be6c93 --- /dev/null +++ b/sp/src/game/server/te_bloodsprite.cpp @@ -0,0 +1,151 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexBloodDrop; // (in combatweapon.cpp) holds the sprite index for the initial blood +extern short g_sModelIndexBloodSpray; // (in combatweapon.cpp) holds the sprite index for splattered blood + +//----------------------------------------------------------------------------- +// Purpose: Display's a blood sprite +//----------------------------------------------------------------------------- +class CTEBloodSprite : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEBloodSprite, CBaseTempEntity ); + + CTEBloodSprite( const char *name ); + virtual ~CTEBloodSprite( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVector( m_vecDirection ); + CNetworkVar( int, m_nSprayModel ); + CNetworkVar( int, m_nDropModel ); + CNetworkVar( int, r ); + CNetworkVar( int, g ); + CNetworkVar( int, b ); + CNetworkVar( int, a ); + CNetworkVar( int, m_nSize ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBloodSprite::CTEBloodSprite( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nSprayModel = 0; + m_nDropModel = 0; + r = 0; + g = 0; + b = 0; + a = 0; + m_nSize = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBloodSprite::~CTEBloodSprite( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBloodSprite::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + r = 255; + g = 255; + b = 63; + a = 255; + m_nSize = 16; + m_vecOrigin = current_origin; + + m_nSprayModel = g_sModelIndexBloodSpray; + m_nDropModel = g_sModelIndexBloodDrop; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CTEBloodSprite, DT_TEBloodSprite) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecDirection), -1, SPROP_COORD), + SendPropInt( SENDINFO(r), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(g), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(b), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(a), 8, SPROP_UNSIGNED ), + SendPropModelIndex( SENDINFO(m_nSprayModel) ), + SendPropModelIndex( SENDINFO(m_nDropModel) ), + SendPropInt( SENDINFO(m_nSize), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + +// Singleton +static CTEBloodSprite g_TEBloodSprite( "Blood Sprite" ); + +//----------------------------------------------------------------------------- +// Purpose: Public interface +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *org - +// r - +// g - +// b - +// a - +// size - +//----------------------------------------------------------------------------- +void TE_BloodSprite( IRecipientFilter& filter, float delay, + const Vector *org, const Vector *dir, int r, int g, int b, int a, int size ) +{ + // Set up parameters + g_TEBloodSprite.m_vecOrigin = *org; + g_TEBloodSprite.m_vecDirection = *dir; + g_TEBloodSprite.r = r; + g_TEBloodSprite.g = g; + g_TEBloodSprite.b = b; + g_TEBloodSprite.a = a; + g_TEBloodSprite.m_nSize = size; + + // Implicit + g_TEBloodSprite.m_nSprayModel = g_sModelIndexBloodSpray; + g_TEBloodSprite.m_nDropModel = g_sModelIndexBloodDrop; + + // Create it + g_TEBloodSprite.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_bloodstream.cpp b/sp/src/game/server/te_bloodstream.cpp new file mode 100644 index 00000000..77ccccc7 --- /dev/null +++ b/sp/src/game/server/te_bloodstream.cpp @@ -0,0 +1,135 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "te_particlesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches blood stream tempentity +//----------------------------------------------------------------------------- +class CTEBloodStream : public CTEParticleSystem +{ +public: + DECLARE_CLASS( CTEBloodStream, CTEParticleSystem ); + + CTEBloodStream( const char *name ); + virtual ~CTEBloodStream( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecDirection ); + CNetworkVar( int, r ); + CNetworkVar( int, g ); + CNetworkVar( int, b ); + CNetworkVar( int, a ); + CNetworkVar( int, m_nAmount ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBloodStream::CTEBloodStream( const char *name ) : + BaseClass( name ) +{ + m_vecDirection.Init(); + r = 0; + g = 0; + b = 0; + a = 0; + m_nAmount = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBloodStream::~CTEBloodStream( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBloodStream::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + r = 247; + g = 0; + b = 0; + a = 255; + m_nAmount = random->RandomInt(50, 150); + m_vecOrigin = current_origin; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + m_vecOrigin += forward * 50; + + m_vecDirection = UTIL_RandomBloodVector(); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEBloodStream, DT_TEBloodStream) + SendPropVector( SENDINFO(m_vecDirection), 11, 0, -10.0, 10.0 ), + SendPropInt( SENDINFO(r), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(g), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(b), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(a), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nAmount), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + +// Singleton to fire TEBloodStream objects +static CTEBloodStream g_TEBloodStream( "Blood Stream" ); + +//----------------------------------------------------------------------------- +// Purpose: Creates a blood stream +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *org - +// *dir - +// r - +// g - +// b - +// a - +// amount - +//----------------------------------------------------------------------------- +void TE_BloodStream( IRecipientFilter& filter, float delay, + const Vector* org, const Vector* dir, int r, int g, int b, int a, int amount ) +{ + g_TEBloodStream.m_vecOrigin = *org; + g_TEBloodStream.m_vecDirection = *dir; + g_TEBloodStream.r = r; + g_TEBloodStream.g = g; + g_TEBloodStream.b = b; + g_TEBloodStream.a = a; + g_TEBloodStream.m_nAmount = amount; + + // Send it over the wire + g_TEBloodStream.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_breakmodel.cpp b/sp/src/game/server/te_breakmodel.cpp new file mode 100644 index 00000000..8db7afca --- /dev/null +++ b/sp/src/game/server/te_breakmodel.cpp @@ -0,0 +1,146 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "vstdlib/random.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches model smash pieces +//----------------------------------------------------------------------------- +class CTEBreakModel : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEBreakModel, CBaseTempEntity ); + + CTEBreakModel( const char *name ); + virtual ~CTEBreakModel( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + virtual void Precache( void ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVector( m_vecSize ); + CNetworkVector( m_vecVelocity ); + CNetworkQAngle( m_angRotation ); + CNetworkVar( int, m_nRandomization ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nCount ); + CNetworkVar( float, m_fTime ); + CNetworkVar( int, m_nFlags ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBreakModel::CTEBreakModel( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_vecSize.Init(); + m_vecVelocity.Init(); + m_angRotation.Init(); + m_nModelIndex = 0; + m_nRandomization = 0; + m_nCount = 0; + m_fTime = 0.0; + m_nFlags = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBreakModel::~CTEBreakModel( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTEBreakModel::Precache( void ) +{ + CBaseEntity::PrecacheModel( "models/gibs/hgibs.mdl" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBreakModel::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = CBaseEntity::PrecacheModel( "models/gibs/hgibs.mdl" ); + m_vecOrigin = current_origin; + m_angRotation = current_angles; + m_vecSize.Init( 16, 16, 16 ); + + m_vecVelocity.Init( random->RandomFloat( -10, 10 ), random->RandomFloat( -10, 10 ), random->RandomFloat( 0, 20 ) ); + + m_nRandomization = 100; + m_nCount = 10; + m_fTime = 5.0; + m_nFlags = 0; + + Vector forward, right; + + m_vecOrigin += Vector( 0, 0, 24 ); + + AngleVectors( current_angles, &forward, &right, 0 ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 25.0, right, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEBreakModel, DT_TEBreakModel) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 0), 13 ), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 1), 13 ), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 2), 13 ), + SendPropVector( SENDINFO(m_vecSize), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecVelocity), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropInt( SENDINFO(m_nRandomization), 9, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nCount), 8, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_fTime), 10, 0, 0, 102.4 ), + SendPropInt( SENDINFO(m_nFlags), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + +// Singleton to fire TEBreakModel objects +static CTEBreakModel g_TEBreakModel( "breakmodel" ); + +void TE_BreakModel( IRecipientFilter& filter, float delay, + const Vector& pos, const QAngle& angles, const Vector& size, const Vector& vel, int modelindex, int randomization, + int count, float time, int flags ) +{ + g_TEBreakModel.m_vecOrigin = pos; + g_TEBreakModel.m_angRotation = angles; + g_TEBreakModel.m_vecSize = size; + g_TEBreakModel.m_vecVelocity = vel; + g_TEBreakModel.m_nModelIndex = modelindex; + g_TEBreakModel.m_nRandomization = randomization; + g_TEBreakModel.m_nCount = count; + g_TEBreakModel.m_fTime = time; + g_TEBreakModel.m_nFlags = flags; + + // Send it over the wire + g_TEBreakModel.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_bspdecal.cpp b/sp/src/game/server/te_bspdecal.cpp new file mode 100644 index 00000000..835ae588 --- /dev/null +++ b/sp/src/game/server/te_bspdecal.cpp @@ -0,0 +1,125 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches BSP decal tempentity +//----------------------------------------------------------------------------- +class CTEBSPDecal : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEBSPDecal, CBaseTempEntity ); + + CTEBSPDecal( const char *name ); + virtual ~CTEBSPDecal( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVar( int, m_nEntity ); + CNetworkVar( int, m_nIndex ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBSPDecal::CTEBSPDecal( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nEntity = 0; + m_nIndex = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBSPDecal::~CTEBSPDecal( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBSPDecal::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nEntity = 0; + m_nIndex = 0; + m_vecOrigin = current_origin; + + Vector vecEnd; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 1024.0, forward, vecEnd ); + + trace_t tr; + + UTIL_TraceLine( m_vecOrigin, vecEnd, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + + m_vecOrigin = tr.endpos; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + + +IMPLEMENT_SERVERCLASS_ST(CTEBSPDecal, DT_TEBSPDecal) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropInt( SENDINFO(m_nEntity), MAX_EDICT_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nIndex), 9, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEBSPDecal objects +static CTEBSPDecal g_TEBSPDecal( "BSP Decal" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// entity - +// index - +// modelindex - +//----------------------------------------------------------------------------- +void TE_BSPDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int entity, int index ) +{ + g_TEBSPDecal.m_vecOrigin = *pos; + g_TEBSPDecal.m_nEntity = entity; + g_TEBSPDecal.m_nIndex = index; + + // Send it over the wire + g_TEBSPDecal.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_bubbles.cpp b/sp/src/game/server/te_bubbles.cpp new file mode 100644 index 00000000..44b0dc66 --- /dev/null +++ b/sp/src/game/server/te_bubbles.cpp @@ -0,0 +1,137 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexBubbles;// holds the index for the bubbles model + +//----------------------------------------------------------------------------- +// Purpose: Dispatches bubbles +//----------------------------------------------------------------------------- +class CTEBubbles : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEBubbles, CBaseTempEntity ); + + CTEBubbles( const char *name ); + virtual ~CTEBubbles( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecMins ); + CNetworkVector( m_vecMaxs ); + CNetworkVar( float, m_fHeight ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nCount ); + CNetworkVar( float, m_fSpeed ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBubbles::CTEBubbles( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecMins.Init(); + m_vecMaxs.Init(); + m_fHeight = 0.0; + m_nModelIndex = 0; + m_nCount = 0; + m_fSpeed = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBubbles::~CTEBubbles( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBubbles::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecMins = current_origin; + + Vector forward; + + m_vecMins.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecMins, 100.0, forward, m_vecMins.GetForModify() ); + + m_vecMaxs = m_vecMins + Vector( 256, 256, 256 ); + + m_fSpeed = 2; + m_nCount = 50; + m_fHeight = 256; + + m_nModelIndex = g_sModelIndexBubbles; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEBubbles, DT_TEBubbles) + SendPropVector( SENDINFO(m_vecMins), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecMaxs), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropFloat( SENDINFO(m_fHeight ), 17, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER ), + SendPropInt( SENDINFO(m_nCount), 8, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_fSpeed ), 17, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER ), +END_SEND_TABLE() + + +// Singleton to fire TEBubbles objects +static CTEBubbles g_TEBubbles( "Bubbles" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *mins - +// *maxs - +// height - +// modelindex - +// count - +// speed - +//----------------------------------------------------------------------------- +void TE_Bubbles( IRecipientFilter& filter, float delay, + const Vector* mins, const Vector* maxs, float height, int modelindex, int count, float speed ) +{ + g_TEBubbles.m_vecMins = *mins; + g_TEBubbles.m_vecMaxs = *maxs; + g_TEBubbles.m_fHeight = height; + g_TEBubbles.m_nModelIndex = modelindex; + g_TEBubbles.m_nCount = count; + g_TEBubbles.m_fSpeed = speed; + + // Send it over the wire + g_TEBubbles.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_bubbletrail.cpp b/sp/src/game/server/te_bubbletrail.cpp new file mode 100644 index 00000000..e0c730f1 --- /dev/null +++ b/sp/src/game/server/te_bubbletrail.cpp @@ -0,0 +1,140 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexBubbles;// holds the index for the bubbles model + +enum +{ + BUBBLE_TRAIL_COUNT_BITS = 8, + BUBBLE_TRAIL_MAX_COUNT = ( (1 << BUBBLE_TRAIL_COUNT_BITS) - 1 ), +}; + + +//----------------------------------------------------------------------------- +// Purpose: Dispatches bubble trail +//----------------------------------------------------------------------------- +class CTEBubbleTrail : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEBubbleTrail, CBaseTempEntity ); + + CTEBubbleTrail( const char *name ); + virtual ~CTEBubbleTrail( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecMins ); + CNetworkVector( m_vecMaxs ); + CNetworkVar( float, m_flWaterZ ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nCount ); + CNetworkVar( float, m_fSpeed ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEBubbleTrail::CTEBubbleTrail( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecMins.Init(); + m_vecMaxs.Init(); + m_flWaterZ = 0.0; + m_nModelIndex = 0; + m_nCount = 0; + m_fSpeed = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEBubbleTrail::~CTEBubbleTrail( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEBubbleTrail::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecMins = current_origin; + + Vector forward; + + m_vecMins.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecMins, 100.0, forward, m_vecMins.GetForModify() ); + + m_vecMaxs = m_vecMins + Vector( 256, 256, 256 ); + + m_fSpeed = 8; + m_nCount = 20; + m_flWaterZ = 0; + + m_nModelIndex = g_sModelIndexBubbles; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEBubbleTrail, DT_TEBubbleTrail) + SendPropVector( SENDINFO(m_vecMins), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecMaxs), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropFloat( SENDINFO(m_flWaterZ ), 17, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER ), + SendPropInt( SENDINFO(m_nCount), BUBBLE_TRAIL_COUNT_BITS, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO(m_fSpeed ), 17, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER ), +END_SEND_TABLE() + + +// Singleton to fire TEBubbleTrail objects +static CTEBubbleTrail g_TEBubbleTrail( "Bubble Trail" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *mins - +// *maxs - +// height - +// modelindex - +// count - +// speed - +//----------------------------------------------------------------------------- +void TE_BubbleTrail( IRecipientFilter& filter, float delay, + const Vector* mins, const Vector* maxs, float flWaterZ, int modelindex, int count, float speed ) +{ + g_TEBubbleTrail.m_vecMins = *mins; + g_TEBubbleTrail.m_vecMaxs = *maxs; + g_TEBubbleTrail.m_flWaterZ = flWaterZ; + g_TEBubbleTrail.m_nModelIndex = modelindex; + g_TEBubbleTrail.m_nCount = MIN( count, BUBBLE_TRAIL_MAX_COUNT ); + g_TEBubbleTrail.m_fSpeed = speed; + + // Send it over the wire + g_TEBubbleTrail.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_clientprojectile.cpp b/sp/src/game/server/te_clientprojectile.cpp new file mode 100644 index 00000000..16e1b941 --- /dev/null +++ b/sp/src/game/server/te_clientprojectile.cpp @@ -0,0 +1,117 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CTEClientProjectile : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEClientProjectile, CBaseTempEntity ); + + CTEClientProjectile( const char *name ); + virtual ~CTEClientProjectile( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVector( m_vecVelocity ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nLifeTime ); + CNetworkHandle( CBaseEntity, m_hOwner ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEClientProjectile::CTEClientProjectile( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_vecVelocity.Init(); + m_nModelIndex = 0; + m_nLifeTime = 0; + m_hOwner = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEClientProjectile::~CTEClientProjectile( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEClientProjectile::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecOrigin = current_origin; + + Vector forward; + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + m_vecVelocity = forward * 2048; + + m_nLifeTime = 5; + m_hOwner = NULL; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEClientProjectile, DT_TEClientProjectile) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecVelocity), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropInt( SENDINFO(m_nLifeTime), 6, SPROP_UNSIGNED ), + SendPropEHandle(SENDINFO(m_hOwner)), +END_SEND_TABLE() + + +// Singleton to fire TEClientProjectile objects +static CTEClientProjectile g_TEClientProjectile( "Client Projectile" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *mins - +// *maxs - +// height - +// modelindex - +// count - +// speed - +//----------------------------------------------------------------------------- +void TE_ClientProjectile( IRecipientFilter& filter, float delay, + const Vector* vecOrigin, const Vector* vecVelocity, int modelindex, int lifetime, CBaseEntity *pOwner ) +{ + g_TEClientProjectile.m_vecOrigin = *vecOrigin; + g_TEClientProjectile.m_vecVelocity = *vecVelocity; + g_TEClientProjectile.m_nModelIndex = modelindex; + g_TEClientProjectile.m_nLifeTime = lifetime; + g_TEClientProjectile.m_hOwner = pOwner; + + // Send it over the wire + g_TEClientProjectile.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_decal.cpp b/sp/src/game/server/te_decal.cpp new file mode 100644 index 00000000..3dab5382 --- /dev/null +++ b/sp/src/game/server/te_decal.cpp @@ -0,0 +1,131 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches decal tempentity +//----------------------------------------------------------------------------- +class CTEDecal : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEDecal, CBaseTempEntity ); + + CTEDecal( const char *name ); + virtual ~CTEDecal( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVector( m_vecStart ); + CNetworkVar( int, m_nEntity ); + CNetworkVar( int, m_nHitbox ); + CNetworkVar( int, m_nIndex ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEDecal::CTEDecal( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nEntity = 0; + m_nIndex = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEDecal::~CTEDecal( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEDecal::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nEntity = 0; + m_nIndex = 0; + m_vecOrigin = current_origin; + + Vector vecEnd; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 1024.0, forward, vecEnd ); + + trace_t tr; + + UTIL_TraceLine( m_vecOrigin, vecEnd, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + + m_vecOrigin = tr.endpos; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + + +IMPLEMENT_SERVERCLASS_ST(CTEDecal, DT_TEDecal) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecStart), -1, SPROP_COORD), + SendPropInt( SENDINFO(m_nEntity), MAX_EDICT_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nHitbox), 12, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nIndex), 9, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEDecal objects +static CTEDecal g_TEDecal( "Entity Decal" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// entity - +// index - +//----------------------------------------------------------------------------- +void TE_Decal( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* start, int entity, int hitbox, int index ) +{ + Assert( pos && start ); + g_TEDecal.m_vecOrigin = *pos; + g_TEDecal.m_vecStart = *start; + g_TEDecal.m_nEntity = entity; + g_TEDecal.m_nHitbox = hitbox; + g_TEDecal.m_nIndex = index; + + // Send it over the wire + g_TEDecal.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_dynamiclight.cpp b/sp/src/game/server/te_dynamiclight.cpp new file mode 100644 index 00000000..616d40ef --- /dev/null +++ b/sp/src/game/server/te_dynamiclight.cpp @@ -0,0 +1,144 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Displays a dynamic light +//----------------------------------------------------------------------------- +class CTEDynamicLight : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEDynamicLight, CBaseTempEntity ); + + CTEDynamicLight( const char *name ); + virtual ~CTEDynamicLight( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVar( float, m_fRadius ); + CNetworkVar( int, r ); + CNetworkVar( int, g ); + CNetworkVar( int, b ); + CNetworkVar( int, exponent ); + CNetworkVar( float, m_fTime ); + CNetworkVar( float, m_fDecay ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEDynamicLight::CTEDynamicLight( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + r = 0; + g = 0; + b = 0; + exponent = 0; + m_fRadius = 0.0; + m_fTime = 0.0; + m_fDecay = 0.0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEDynamicLight::~CTEDynamicLight( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEDynamicLight::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + r = 255; + g = 255; + b = 63; + m_vecOrigin = current_origin; + + m_fRadius = 200; + m_fTime = 2.0; + m_fDecay = 0.0; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEDynamicLight, DT_TEDynamicLight) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropInt( SENDINFO(r), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(g), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(b), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(exponent), 8, 0 ), + SendPropFloat( SENDINFO(m_fRadius), 8, SPROP_ROUNDUP, 0, 2560.0 ), + SendPropFloat( SENDINFO(m_fTime), 8, SPROP_ROUNDDOWN, 0, 25.6 ), + SendPropFloat( SENDINFO(m_fDecay), 8, SPROP_ROUNDDOWN, 0, 2560.0 ), +END_SEND_TABLE() + + +// Singleton +static CTEDynamicLight g_TEDynamicLight( "Dynamic Light" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *org - +// r - +// g - +// b - +// radius - +// time - +// decay - +//----------------------------------------------------------------------------- +void TE_DynamicLight( IRecipientFilter& filter, float delay, + const Vector* org, int r, int g, int b, int exponent, float radius, float time, float decay ) +{ + // Set up parameters + g_TEDynamicLight.m_vecOrigin = *org; + g_TEDynamicLight.r = r; + g_TEDynamicLight.g = g; + g_TEDynamicLight.b = b; + g_TEDynamicLight.exponent = exponent; + g_TEDynamicLight.m_fRadius = radius; + g_TEDynamicLight.m_fTime = time; + g_TEDynamicLight.m_fDecay = decay; + + // Create it + g_TEDynamicLight.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_effect_dispatch.cpp b/sp/src/game/server/te_effect_dispatch.cpp new file mode 100644 index 00000000..140a76f5 --- /dev/null +++ b/sp/src/game/server/te_effect_dispatch.cpp @@ -0,0 +1,91 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "te_effect_dispatch.h" +#include "networkstringtable_gamedll.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: This TE provides a simple interface to dispatch effects by name using DispatchEffect(). +//----------------------------------------------------------------------------- +class CTEEffectDispatch : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEEffectDispatch, CBaseTempEntity ); + + CTEEffectDispatch( const char *name ); + virtual ~CTEEffectDispatch( void ); + + DECLARE_SERVERCLASS(); + +public: + CEffectData m_EffectData; +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEEffectDispatch::CTEEffectDispatch( const char *name ) : + CBaseTempEntity( name ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEEffectDispatch::~CTEEffectDispatch( void ) +{ +} + +IMPLEMENT_SERVERCLASS_ST( CTEEffectDispatch, DT_TEEffectDispatch ) + + SendPropDataTable( SENDINFO_DT( m_EffectData ), &REFERENCE_SEND_TABLE( DT_EffectData ) ) + +END_SEND_TABLE() + + +// Singleton to fire TEEffectDispatch objects +static CTEEffectDispatch g_TEEffectDispatch( "EffectDispatch" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void TE_DispatchEffect( IRecipientFilter& filter, float delay, const Vector &pos, const char *pName, const CEffectData &data ) +{ + // Copy the supplied effect data. + g_TEEffectDispatch.m_EffectData = data; + + // Get the entry index in the string table. + g_TEEffectDispatch.m_EffectData.m_iEffectName = g_pStringTableEffectDispatch->AddString( CBaseEntity::IsServer(), pName ); + + // Send it to anyone who can see the effect's origin. + g_TEEffectDispatch.Create( filter, 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void DispatchEffect( const char *pName, const CEffectData &data ) +{ + CPASFilter filter( data.m_vOrigin ); + DispatchEffect( pName, data, filter ); +} + +void DispatchEffect( const char *pName, const CEffectData &data, CRecipientFilter &filter ) +{ + te->DispatchEffect( filter, 0.0, data.m_vOrigin, pName, data ); +} diff --git a/sp/src/game/server/te_effect_dispatch.h b/sp/src/game/server/te_effect_dispatch.h new file mode 100644 index 00000000..b759ed59 --- /dev/null +++ b/sp/src/game/server/te_effect_dispatch.h @@ -0,0 +1,23 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef TE_EFFECT_DISPATCH_H +#define TE_EFFECT_DISPATCH_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "effect_dispatch_data.h" +#include "recipientfilter.h" + + +void DispatchEffect( const char *pName, const CEffectData &data ); +void DispatchEffect( const char *pName, const CEffectData &data, CRecipientFilter &filter ); + + +#endif // TE_EFFECT_DISPATCH_H diff --git a/sp/src/game/server/te_energysplash.cpp b/sp/src/game/server/te_energysplash.cpp new file mode 100644 index 00000000..b4af75ab --- /dev/null +++ b/sp/src/game/server/te_energysplash.cpp @@ -0,0 +1,112 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "te_particlesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches energy splashes +//----------------------------------------------------------------------------- +class CTEEnergySplash : public CBaseTempEntity +{ +DECLARE_CLASS( CTEEnergySplash, CBaseTempEntity ); + +public: + CTEEnergySplash( const char *name ); + virtual ~CTEEnergySplash( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecPos ); + CNetworkVector( m_vecDir ); + CNetworkVar( bool, m_bExplosive ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEEnergySplash::CTEEnergySplash( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecPos.Init(); + m_vecDir.Init(); + m_bExplosive = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEEnergySplash::~CTEEnergySplash( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEEnergySplash::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecPos = current_origin; + + AngleVectors( current_angles, &m_vecDir.GetForModify() ); + + Vector forward; + + m_vecPos.GetForModify()[2] += 24; + + forward = m_vecDir; + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecPos, 100.0, forward, m_vecPos.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CTEEnergySplash, DT_TEEnergySplash) + SendPropVector( SENDINFO(m_vecPos), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecDir), -1, SPROP_COORD), + SendPropInt( SENDINFO(m_bExplosive), 1, SPROP_UNSIGNED), +END_SEND_TABLE() + +// Singleton to fire TEEnergySplash objects +static CTEEnergySplash g_TEEnergySplash( "Energy Splash" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// scale - +//----------------------------------------------------------------------------- +void TE_EnergySplash( IRecipientFilter& filter, float delay, + const Vector* pos, const Vector* dir, bool bExplosive ) +{ + g_TEEnergySplash.m_vecPos = *pos; + g_TEEnergySplash.m_vecDir = *dir; + g_TEEnergySplash.m_bExplosive = bExplosive; + + // Send it over the wire + g_TEEnergySplash.Create( filter, delay ); +} diff --git a/sp/src/game/server/te_explosion.cpp b/sp/src/game/server/te_explosion.cpp new file mode 100644 index 00000000..2c53b1b1 --- /dev/null +++ b/sp/src/game/server/te_explosion.cpp @@ -0,0 +1,132 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "te_particlesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexFireball; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches explosion tempentity +//----------------------------------------------------------------------------- +class CTEExplosion : public CTEParticleSystem +{ +public: + DECLARE_CLASS( CTEExplosion, CTEParticleSystem ); + DECLARE_SERVERCLASS(); + + CTEExplosion( const char *name ); + virtual ~CTEExplosion( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + +public: + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( float, m_fScale ); + CNetworkVar( int, m_nFrameRate ); + CNetworkVar( int, m_nFlags ); + CNetworkVector( m_vecNormal ); + CNetworkVar( unsigned char, m_chMaterialType ); + CNetworkVar( int, m_nRadius ); + CNetworkVar( int, m_nMagnitude ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEExplosion::CTEExplosion( const char *name ) : + BaseClass( name ) +{ + m_nModelIndex = 0; + m_fScale = 0; + m_nFrameRate = 0; + m_nFlags = 0; + m_vecNormal.Init(); + m_chMaterialType = 'C'; + m_nRadius = 0; + m_nMagnitude = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEExplosion::~CTEExplosion( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEExplosion::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = g_sModelIndexFireball; + m_fScale = 0.5; + m_nFrameRate = 15; + m_nFlags = TE_EXPLFLAG_NONE; + m_vecOrigin = current_origin; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + m_vecOrigin += forward * 50; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEExplosion, DT_TEExplosion) + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropFloat( SENDINFO(m_fScale ), 9, 0, 0.0, 51.2 ), + SendPropInt( SENDINFO(m_nFrameRate), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nFlags), 8, SPROP_UNSIGNED ), + SendPropVector( SENDINFO(m_vecNormal), -1, SPROP_COORD), + SendPropInt( SENDINFO(m_chMaterialType), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nRadius), 32, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nMagnitude), 32, SPROP_UNSIGNED ), +END_SEND_TABLE() + +// Singleton to fire TEExplosion objects +static CTEExplosion g_TEExplosion( "Explosion" ); + +void TE_Explosion( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float scale, int framerate, int flags, int radius, int magnitude, const Vector* normal, unsigned char materialType ) +{ + g_TEExplosion.m_vecOrigin = *pos; + g_TEExplosion.m_nModelIndex = modelindex; + g_TEExplosion.m_fScale = scale; + g_TEExplosion.m_nFrameRate = framerate; + g_TEExplosion.m_nFlags = flags; + g_TEExplosion.m_nRadius = radius; + g_TEExplosion.m_nMagnitude = magnitude; + + if ( normal ) + g_TEExplosion.m_vecNormal = *normal; + else + g_TEExplosion.m_vecNormal = Vector(0,0,1); + g_TEExplosion.m_chMaterialType = materialType; + + // Send it over the wire + g_TEExplosion.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_fizz.cpp b/sp/src/game/server/te_fizz.cpp new file mode 100644 index 00000000..ce570112 --- /dev/null +++ b/sp/src/game/server/te_fizz.cpp @@ -0,0 +1,112 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches Fizz tempentity +//----------------------------------------------------------------------------- +class CTEFizz : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEFizz, CBaseTempEntity ); + + CTEFizz( const char *name ); + virtual ~CTEFizz( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + virtual void Precache( void ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVar( int, m_nEntity ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nDensity ); + CNetworkVar( int, m_nCurrent ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEFizz::CTEFizz( const char *name ) : + CBaseTempEntity( name ) +{ + m_nEntity = 0; + m_nModelIndex = 0; + m_nDensity = 0; + m_nCurrent = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEFizz::~CTEFizz( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEFizz::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = CBaseEntity::PrecacheModel( "sprites/bubble.vmt" );; + m_nDensity = 200; + m_nEntity = 1; + m_nCurrent = 100; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTEFizz::Precache( void ) +{ + CBaseEntity::PrecacheModel( "sprites/bubble.vmt" ); +} + + +IMPLEMENT_SERVERCLASS_ST(CTEFizz, DT_TEFizz) + SendPropInt( SENDINFO(m_nEntity), MAX_EDICT_BITS, SPROP_UNSIGNED ), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropInt( SENDINFO(m_nDensity), 8, SPROP_UNSIGNED ), + SendPropInt(SENDINFO(m_nCurrent), 16 ), +END_SEND_TABLE() + + +// Singleton to fire TEFizz objects +static CTEFizz g_TEFizz( "Fizz" ); + +void TE_Fizz( IRecipientFilter& filter, float delay, + const CBaseEntity *entity, int modelindex, int density, int current ) +{ + Assert( entity ); + + g_TEFizz.m_nEntity = ENTINDEX( (edict_t *)entity->edict() ); + g_TEFizz.m_nModelIndex = modelindex; + g_TEFizz.m_nDensity = density; + g_TEFizz.m_nCurrent = current; + + // Send it over the wire + g_TEFizz.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_footprintdecal.cpp b/sp/src/game/server/te_footprintdecal.cpp new file mode 100644 index 00000000..a7295257 --- /dev/null +++ b/sp/src/game/server/te_footprintdecal.cpp @@ -0,0 +1,91 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches footprint decal tempentity +//----------------------------------------------------------------------------- + +#define FOOTPRINT_DECAY_TIME 3.0f + +class CTEFootprintDecal : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEFootprintDecal, CBaseTempEntity ); + + CTEFootprintDecal( const char *name ); + virtual ~CTEFootprintDecal( void ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVector( m_vecDirection ); + CNetworkVar( int, m_nEntity ); + CNetworkVar( int, m_nIndex ); + CNetworkVar( unsigned char, m_chMaterialType ); +}; + +IMPLEMENT_SERVERCLASS_ST(CTEFootprintDecal, DT_TEFootprintDecal) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecDirection), -1, SPROP_COORD), + SendPropInt( SENDINFO(m_nEntity), 11, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nIndex), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_chMaterialType), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEFootprintDecal objects +static CTEFootprintDecal g_TEFootprintDecal( "Footprint Decal" ); + +//----------------------------------------------------------------------------- +// constructor, destructor +//----------------------------------------------------------------------------- + +CTEFootprintDecal::CTEFootprintDecal( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nEntity = 0; + m_nIndex = 0; + m_chMaterialType = 'C'; +} + +CTEFootprintDecal::~CTEFootprintDecal( void ) +{ +} + +//----------------------------------------------------------------------------- +// places a footprint decal +//----------------------------------------------------------------------------- + +void TE_FootprintDecal( IRecipientFilter& filter, float delay, + const Vector *origin, const Vector *right, int entity, int index, + unsigned char materialType ) +{ + Assert( origin ); + g_TEFootprintDecal.m_vecOrigin = *origin; + g_TEFootprintDecal.m_vecDirection = *right; + g_TEFootprintDecal.m_nEntity = entity; + g_TEFootprintDecal.m_nIndex = index; + g_TEFootprintDecal.m_chMaterialType = materialType; + + VectorNormalize(g_TEFootprintDecal.m_vecDirection.GetForModify()); + + // Send it over the wire + g_TEFootprintDecal.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_glassshatter.cpp b/sp/src/game/server/te_glassshatter.cpp new file mode 100644 index 00000000..b9bc37d9 --- /dev/null +++ b/sp/src/game/server/te_glassshatter.cpp @@ -0,0 +1,155 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "shattersurfacetypes.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches Glass Shatter tempentity +//----------------------------------------------------------------------------- +class CTEShatterSurface : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEShatterSurface, CBaseTempEntity ); + + CTEShatterSurface( const char *name ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkQAngle( m_vecAngles ); + CNetworkVector( m_vecForce ); + CNetworkVector( m_vecForcePos ); + CNetworkVar( float, m_flWidth ); + CNetworkVar( float, m_flHeight ); + CNetworkVar( float, m_flShardSize ); + CNetworkVar( int, m_nSurfaceType ); + CNetworkArray( byte, m_uchFrontColor, 3 ); + CNetworkArray( byte, m_uchBackColor, 3 ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEShatterSurface::CTEShatterSurface( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_vecAngles.Init(); + m_vecForce.Init(); + m_vecForcePos.Init(); + m_flWidth = 16.0f; + m_flHeight = 16.0f; + m_flShardSize = 3.0f; + m_uchFrontColor.Set( 0, 255 ); + m_uchFrontColor.Set( 1, 255 ); + m_uchFrontColor.Set( 2, 255 ); + m_uchBackColor.Set( 0, 255 ); + m_uchBackColor.Set( 1, 255 ); + m_uchBackColor.Set( 2, 255 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEShatterSurface::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecOrigin = current_origin; + + Vector vecEnd; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 1024.0, forward, vecEnd ); + + trace_t tr; + + UTIL_TraceLine( m_vecOrigin, vecEnd, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + + m_vecOrigin = tr.endpos; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEShatterSurface, DT_TEShatterSurface) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecAngles), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecForce), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecForcePos), -1, SPROP_COORD), + SendPropFloat( SENDINFO(m_flWidth), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flHeight), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flShardSize), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO(m_nSurfaceType), 2, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_ARRAYELEM( m_uchFrontColor, 0 ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_ARRAYELEM( m_uchFrontColor, 1 ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_ARRAYELEM( m_uchFrontColor, 2 ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_ARRAYELEM( m_uchBackColor, 0 ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_ARRAYELEM( m_uchBackColor, 1 ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO_ARRAYELEM( m_uchBackColor, 2 ), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEShatterSurface objects +static CTEShatterSurface g_TEShatterSurface( "Surface Shatter" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// entity - +// index - +//----------------------------------------------------------------------------- +void TE_ShatterSurface( IRecipientFilter& filter, float delay, + const Vector* pos, const QAngle* angle, const Vector* force, const Vector* forcepos, + float width, float height, float shardsize, ShatterSurface_t surfacetype, + int front_r, int front_g, int front_b, int back_r, int back_g, int back_b) +{ + g_TEShatterSurface.m_vecOrigin = *pos; + g_TEShatterSurface.m_vecAngles = *angle; + g_TEShatterSurface.m_vecForce = *force; + g_TEShatterSurface.m_vecForcePos = *forcepos; + g_TEShatterSurface.m_flWidth = width; + g_TEShatterSurface.m_flHeight = height; + g_TEShatterSurface.m_flShardSize = shardsize; + g_TEShatterSurface.m_nSurfaceType = surfacetype; + g_TEShatterSurface.m_uchFrontColor.Set( 0, front_r ); + g_TEShatterSurface.m_uchFrontColor.Set( 1, front_g ); + g_TEShatterSurface.m_uchFrontColor.Set( 2, front_b ); + g_TEShatterSurface.m_uchBackColor.Set( 0, back_r ); + g_TEShatterSurface.m_uchBackColor.Set( 1, back_g ); + g_TEShatterSurface.m_uchBackColor.Set( 2, back_b ); + + // Send it over the wire + g_TEShatterSurface.Create( filter, delay ); +} diff --git a/sp/src/game/server/te_glowsprite.cpp b/sp/src/game/server/te_glowsprite.cpp new file mode 100644 index 00000000..d050f8a5 --- /dev/null +++ b/sp/src/game/server/te_glowsprite.cpp @@ -0,0 +1,130 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches Sprite tempentity +//----------------------------------------------------------------------------- +class CTEGlowSprite : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEGlowSprite, CBaseTempEntity ); + + CTEGlowSprite( const char *name ); + virtual ~CTEGlowSprite( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( float, m_fScale ); + CNetworkVar( float, m_fLife ); + CNetworkVar( int, m_nBrightness ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEGlowSprite::CTEGlowSprite( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nModelIndex = 0; + m_fScale = 0; + m_fLife = 0; + m_nBrightness = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEGlowSprite::~CTEGlowSprite( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEGlowSprite::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = g_sModelIndexSmoke; + m_fScale = 0.8; + m_nBrightness = 200; + m_fLife = 2.0; + m_vecOrigin = current_origin; + + Vector forward, right; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward, &right, NULL ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, -25.0, right, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + + +IMPLEMENT_SERVERCLASS_ST(CTEGlowSprite, DT_TEGlowSprite) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropFloat( SENDINFO(m_fScale ), 8, SPROP_ROUNDDOWN, 0.0, 25.6 ), + SendPropFloat( SENDINFO(m_fLife ), 8, SPROP_ROUNDDOWN, 0.0, 25.6 ), + SendPropInt( SENDINFO(m_nBrightness), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEGlowSprite objects +static CTEGlowSprite g_TEGlowSprite( "GlowSprite" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// modelindex - +// life - +// size - +// brightness - +//----------------------------------------------------------------------------- +void TE_GlowSprite( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float life, float size, int brightness ) +{ + g_TEGlowSprite.m_vecOrigin = *pos; + g_TEGlowSprite.m_nModelIndex = modelindex; + g_TEGlowSprite.m_fLife = life; + g_TEGlowSprite.m_fScale = size; + g_TEGlowSprite.m_nBrightness = brightness; + + // Send it over the wire + g_TEGlowSprite.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_impact.cpp b/sp/src/game/server/te_impact.cpp new file mode 100644 index 00000000..4f52ccc9 --- /dev/null +++ b/sp/src/game/server/te_impact.cpp @@ -0,0 +1,97 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Send generic impact messages to the client for visualization +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches Gunshot decal tempentity +//----------------------------------------------------------------------------- +class CTEImpact : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEImpact, CBaseTempEntity ); + + DECLARE_SERVERCLASS(); + + CTEImpact( const char *name ); + virtual ~CTEImpact(); + + void Precache( void ); + void Test( const Vector& current_origin, const Vector& current_normal ); + +public: + + CNetworkVector( m_vecOrigin ); + CNetworkVector( m_vecNormal ); //NOTENOTE: In a multi-play setup we'll probably want non-oriented effects for bandwidth + CNetworkVar( int, m_iType ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +// Output : +//----------------------------------------------------------------------------- +CTEImpact::CTEImpact( const char *name ) : CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_vecNormal.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEImpact::~CTEImpact( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTEImpact::Precache( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEImpact::Test( const Vector& current_origin, const Vector& current_normal ) +{ +} + + +//Server class implementation +IMPLEMENT_SERVERCLASS_ST( CTEImpact, DT_TEImpact) + SendPropVector( SENDINFO( m_vecOrigin ), -1, SPROP_COORD ), + SendPropVector( SENDINFO( m_vecNormal ), -1, SPROP_COORD ), + SendPropInt( SENDINFO( m_iType ), 32, SPROP_UNSIGNED ), +END_SEND_TABLE() + +// Singleton to fire TEImpact objects +static CTEImpact g_TEImpact( "Impact" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +//----------------------------------------------------------------------------- +void TE_Impact( IRecipientFilter& filter, float delay ) +{ + g_TEImpact.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_killplayerattachments.cpp b/sp/src/game/server/te_killplayerattachments.cpp new file mode 100644 index 00000000..1d6ae167 --- /dev/null +++ b/sp/src/game/server/te_killplayerattachments.cpp @@ -0,0 +1,92 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches blood stream tempentity +//----------------------------------------------------------------------------- +class CTEKillPlayerAttachments : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEKillPlayerAttachments, CBaseTempEntity ); + + CTEKillPlayerAttachments( const char *name ); + virtual ~CTEKillPlayerAttachments( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVar( int, m_nPlayer ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEKillPlayerAttachments::CTEKillPlayerAttachments( const char *name ) : + CBaseTempEntity( name ) +{ + m_nPlayer = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEKillPlayerAttachments::~CTEKillPlayerAttachments( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEKillPlayerAttachments::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + m_nPlayer = 1; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + + +IMPLEMENT_SERVERCLASS_ST(CTEKillPlayerAttachments, DT_TEKillPlayerAttachments) + SendPropInt( SENDINFO(m_nPlayer), 5, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEKillPlayerAttachments objects +static CTEKillPlayerAttachments g_TEKillPlayerAttachments( "KillPlayerAttachments" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// player - +//----------------------------------------------------------------------------- +void TE_KillPlayerAttachments( IRecipientFilter& filter, float delay, + int player ) +{ + g_TEKillPlayerAttachments.m_nPlayer = player; + + // Send it over the wire + g_TEKillPlayerAttachments.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_largefunnel.cpp b/sp/src/game/server/te_largefunnel.cpp new file mode 100644 index 00000000..11c1ac6a --- /dev/null +++ b/sp/src/game/server/te_largefunnel.cpp @@ -0,0 +1,103 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "te_particlesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches smoke tempentity +//----------------------------------------------------------------------------- +class CTELargeFunnel : public CTEParticleSystem +{ +public: + DECLARE_CLASS( CTELargeFunnel, CTEParticleSystem ); + DECLARE_SERVERCLASS(); + + CTELargeFunnel( const char *name ); + virtual ~CTELargeFunnel( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + +public: + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nReversed ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTELargeFunnel::CTELargeFunnel( const char *name ) : + BaseClass( name ) +{ + m_nModelIndex = 0; + m_nReversed = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTELargeFunnel::~CTELargeFunnel( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTELargeFunnel::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = g_sModelIndexSmoke; + m_nReversed = 0; + m_vecOrigin = current_origin; + + Vector forward, right; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward, &right, NULL ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin.Get(), 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin.Get(), 25.0, right, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTELargeFunnel, DT_TELargeFunnel) + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropInt( SENDINFO(m_nReversed), 2, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TELargeFunnel objects +static CTELargeFunnel g_TELargeFunnel( "Large Funnel" ); + +void TE_LargeFunnel( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, int reversed ) +{ + g_TELargeFunnel.m_vecOrigin = *pos; + g_TELargeFunnel.m_nModelIndex = modelindex; + g_TELargeFunnel.m_nReversed = reversed; + + // Send it over the wire + g_TELargeFunnel.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_muzzleflash.cpp b/sp/src/game/server/te_muzzleflash.cpp new file mode 100644 index 00000000..5d8afbb7 --- /dev/null +++ b/sp/src/game/server/te_muzzleflash.cpp @@ -0,0 +1,99 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Create a muzzle flash temp ent +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches user tracer stream tempentity +//----------------------------------------------------------------------------- +class CTEMuzzleFlash : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEMuzzleFlash, CBaseTempEntity ); + + DECLARE_SERVERCLASS(); + + CTEMuzzleFlash( const char *name ); + virtual ~CTEMuzzleFlash( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + +public: + + CNetworkVector( m_vecOrigin ); + CNetworkQAngle( m_vecAngles ); + CNetworkVar( float, m_flScale ); + CNetworkVar( int, m_nType ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEMuzzleFlash::CTEMuzzleFlash( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_vecAngles.Init(); + + m_flScale = 1.0f; + m_nType = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEMuzzleFlash::~CTEMuzzleFlash( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEMuzzleFlash::Test( const Vector& current_origin, const QAngle& current_angles ) +{ +} + + +IMPLEMENT_SERVERCLASS_ST( CTEMuzzleFlash, DT_TEMuzzleFlash ) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD ), + SendPropVector( SENDINFO(m_vecAngles), -1, SPROP_COORD ), + SendPropFloat( SENDINFO(m_flScale), -1, SPROP_NOSCALE ), + SendPropInt( SENDINFO(m_nType), 32, SPROP_UNSIGNED ), +END_SEND_TABLE() + +// Singleton to fire TEMuzzleFlash objects +static CTEMuzzleFlash g_TEMuzzleFlash( "MuzzleFlash" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// origin - +// *recipient - +// *origin - +// *dir - +// scale - +// type - +//----------------------------------------------------------------------------- +void TE_MuzzleFlash( IRecipientFilter& filter, float delay, + const Vector &start, const QAngle &angles, float scale, int type ) +{ + g_TEMuzzleFlash.m_vecOrigin = start; + g_TEMuzzleFlash.m_vecAngles = angles; + g_TEMuzzleFlash.m_flScale = scale; + g_TEMuzzleFlash.m_nType = type; + + // Send it over the wire + g_TEMuzzleFlash.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_particlesystem.cpp b/sp/src/game/server/te_particlesystem.cpp new file mode 100644 index 00000000..adacb066 --- /dev/null +++ b/sp/src/game/server/te_particlesystem.cpp @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "dt_send.h" +#include "server_class.h" +#include "te_particlesystem.h" +#include "coordsize.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +IMPLEMENT_SERVERCLASS_ST(CTEParticleSystem, DT_TEParticleSystem) +#if defined( TF_DLL ) + SendPropFloat( SENDINFO_VECTORELEM( m_vecOrigin, 0 ), -1, SPROP_COORD_MP_INTEGRAL ), + SendPropFloat( SENDINFO_VECTORELEM( m_vecOrigin, 1 ), -1, SPROP_COORD_MP_INTEGRAL ), + SendPropFloat( SENDINFO_VECTORELEM( m_vecOrigin, 2 ), -1, SPROP_COORD_MP_INTEGRAL ), +#else + SendPropFloat( SENDINFO_VECTORELEM(m_vecOrigin, 0), -1, SPROP_COORD), + SendPropFloat( SENDINFO_VECTORELEM(m_vecOrigin, 1), -1, SPROP_COORD), + SendPropFloat( SENDINFO_VECTORELEM(m_vecOrigin, 2), -1, SPROP_COORD), +#endif +END_SEND_TABLE() + + + + diff --git a/sp/src/game/server/te_particlesystem.h b/sp/src/game/server/te_particlesystem.h new file mode 100644 index 00000000..c4149209 --- /dev/null +++ b/sp/src/game/server/te_particlesystem.h @@ -0,0 +1,33 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef TE_PARTICLESYSTEM_H +#define TE_PARTICLESYSTEM_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "basetempentity.h" + + +class CTEParticleSystem : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEParticleSystem, CBaseTempEntity ); + DECLARE_SERVERCLASS(); + + CTEParticleSystem(const char *pName) : BaseClass(pName) + { + m_vecOrigin.GetForModify().Init(); + } + + CNetworkVector( m_vecOrigin ); +}; + + +#endif // TE_PARTICLESYSTEM_H diff --git a/sp/src/game/server/te_physicsprop.cpp b/sp/src/game/server/te_physicsprop.cpp new file mode 100644 index 00000000..df4d061a --- /dev/null +++ b/sp/src/game/server/te_physicsprop.cpp @@ -0,0 +1,133 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: create clientside physics prop, as breaks model if needed +//----------------------------------------------------------------------------- +class CTEPhysicsProp : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEPhysicsProp, CBaseTempEntity ); + + CTEPhysicsProp( const char *name ); + virtual ~CTEPhysicsProp( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + virtual void Precache( void ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkQAngle( m_angRotation ); + CNetworkVector( m_vecVelocity ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nSkin ); + CNetworkVar( int, m_nFlags ); + CNetworkVar( int, m_nEffects ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEPhysicsProp::CTEPhysicsProp( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_angRotation.Init(); + m_vecVelocity.Init(); + m_nModelIndex = 0; + m_nSkin = 0; + m_nFlags = 0; + m_nEffects = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEPhysicsProp::~CTEPhysicsProp( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTEPhysicsProp::Precache( void ) +{ + CBaseEntity::PrecacheModel( "models/gibs/hgibs.mdl" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEPhysicsProp::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = CBaseEntity::PrecacheModel( "models/gibs/hgibs.mdl" ); + m_nSkin = 0; + m_vecOrigin = current_origin; + m_angRotation = current_angles; + + m_vecVelocity.Init( random->RandomFloat( -10, 10 ), random->RandomFloat( -10, 10 ), random->RandomFloat( 0, 20 ) ); + m_nFlags = 0; + m_nEffects = 0; + + Vector forward, right; + + m_vecOrigin += Vector( 0, 0, 24 ); + + AngleVectors( current_angles, &forward, &right, 0 ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 25.0, right, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEPhysicsProp, DT_TEPhysicsProp) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 0), 13 ), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 1), 13 ), + SendPropAngle( SENDINFO_VECTORELEM(m_angRotation, 2), 13 ), + SendPropVector( SENDINFO(m_vecVelocity), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropInt( SENDINFO(m_nSkin), ANIMATION_SKIN_BITS), + SendPropInt( SENDINFO(m_nFlags), 2, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nEffects), EF_MAX_BITS, SPROP_UNSIGNED), +END_SEND_TABLE() + +// Singleton to fire TEBreakModel objects +static CTEPhysicsProp s_TEPhysicsProp( "physicsprop" ); + +void TE_PhysicsProp( IRecipientFilter& filter, float delay, + int modelindex, int skin, const Vector& pos, const QAngle &angles, const Vector& vel, int flags, int effects ) +{ + s_TEPhysicsProp.m_vecOrigin = pos; + s_TEPhysicsProp.m_angRotation = angles; + s_TEPhysicsProp.m_vecVelocity = vel; + s_TEPhysicsProp.m_nModelIndex = modelindex; + s_TEPhysicsProp.m_nSkin = skin; + s_TEPhysicsProp.m_nFlags = flags; + s_TEPhysicsProp.m_nEffects = effects; + + // Send it over the wire + s_TEPhysicsProp.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_playerdecal.cpp b/sp/src/game/server/te_playerdecal.cpp new file mode 100644 index 00000000..8885e122 --- /dev/null +++ b/sp/src/game/server/te_playerdecal.cpp @@ -0,0 +1,124 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches decal tempentity +//----------------------------------------------------------------------------- +class CTEPlayerDecal : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEPlayerDecal, CBaseTempEntity ); + + CTEPlayerDecal( const char *name ); + virtual ~CTEPlayerDecal( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVar( int, m_nPlayer ); + CNetworkVector( m_vecOrigin ); + CNetworkVar( int, m_nEntity ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEPlayerDecal::CTEPlayerDecal( const char *name ) : + CBaseTempEntity( name ) +{ + m_nPlayer = 0; + m_vecOrigin.Init(); + m_nEntity = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEPlayerDecal::~CTEPlayerDecal( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEPlayerDecal::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nPlayer = 1; + m_nEntity = 0; + m_vecOrigin = current_origin; + + Vector vecEnd; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 1024.0, forward, vecEnd ); + + trace_t tr; + + UTIL_TraceLine( m_vecOrigin, vecEnd, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + + m_vecOrigin = tr.endpos; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEPlayerDecal, DT_TEPlayerDecal) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropInt( SENDINFO(m_nEntity), MAX_EDICT_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nPlayer), Q_log2( MAX_PLAYERS ), SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEPlayerDecal objects +static CTEPlayerDecal g_TEPlayerDecal( "Player Decal" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// player - +// entity - +// index - +//----------------------------------------------------------------------------- +void TE_PlayerDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int player, int entity ) +{ + g_TEPlayerDecal.m_vecOrigin = *pos; + g_TEPlayerDecal.m_nPlayer = player; + g_TEPlayerDecal.m_nEntity = entity; + + // Send it over the wire + g_TEPlayerDecal.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_projecteddecal.cpp b/sp/src/game/server/te_projecteddecal.cpp new file mode 100644 index 00000000..83e40e1a --- /dev/null +++ b/sp/src/game/server/te_projecteddecal.cpp @@ -0,0 +1,122 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches BSP decal tempentity +//----------------------------------------------------------------------------- +class CTEProjectedDecal : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEProjectedDecal, CBaseTempEntity ); + + CTEProjectedDecal( const char *name ); + virtual ~CTEProjectedDecal( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVar( int, m_nIndex ); + CNetworkVar( float, m_flDistance ); + CNetworkQAngle( m_angRotation ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEProjectedDecal::CTEProjectedDecal( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_angRotation.Init(); + m_flDistance = 64.0f; + m_nIndex = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEProjectedDecal::~CTEProjectedDecal( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEProjectedDecal::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_flDistance = 1024.0f; + m_nIndex = 0; + m_vecOrigin = current_origin; + m_angRotation = current_angles; + + Vector vecEnd; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 24.0, forward, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEProjectedDecal, DT_TEProjectedDecal) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropQAngles( SENDINFO(m_angRotation), 10 ), + SendPropFloat( SENDINFO(m_flDistance), 10, SPROP_ROUNDUP, 0, 1024 ), + SendPropInt( SENDINFO(m_nIndex), 9, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEBSPDecal objects +static CTEProjectedDecal g_TEProjectedDecal( "Projected Decal" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// entity - +// index - +// modelindex - +//----------------------------------------------------------------------------- +void TE_ProjectDecal( IRecipientFilter& filter, float delay, + const Vector* pos, const QAngle *angles, float distance, int index ) +{ + g_TEProjectedDecal.m_vecOrigin = *pos; + g_TEProjectedDecal.m_angRotation = *angles; + g_TEProjectedDecal.m_flDistance = distance; + g_TEProjectedDecal.m_nIndex = index; + + // Send it over the wire + g_TEProjectedDecal.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_showline.cpp b/sp/src/game/server/te_showline.cpp new file mode 100644 index 00000000..99d39184 --- /dev/null +++ b/sp/src/game/server/te_showline.cpp @@ -0,0 +1,107 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "te_particlesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches line +//----------------------------------------------------------------------------- +class CTEShowLine : public CTEParticleSystem +{ +public: + DECLARE_CLASS( CTEShowLine, CTEParticleSystem ); + DECLARE_SERVERCLASS(); + + CTEShowLine( const char *name ); + virtual ~CTEShowLine( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + +public: + CNetworkVector( m_vecEnd ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEShowLine::CTEShowLine( const char *name ) : + BaseClass( name ) +{ + m_vecEnd.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEShowLine::~CTEShowLine( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEShowLine::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecOrigin = current_origin; + + Vector forward, right; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward, &right, NULL ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 100.0, forward, m_vecEnd.GetForModify() ); + + m_vecOrigin = m_vecEnd + right * -128; + m_vecEnd += right * 128; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST( CTEShowLine, DT_TEShowLine) + SendPropVector( SENDINFO(m_vecEnd), -1, SPROP_COORD), +END_SEND_TABLE() + + +// Singleton to fire TEShowLine objects +static CTEShowLine g_TEShowLine( "Show Line" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *start - +// *end - +//----------------------------------------------------------------------------- +void TE_ShowLine( IRecipientFilter& filter, float delay, + const Vector* start, const Vector* end ) +{ + g_TEShowLine.m_vecOrigin = *start; + g_TEShowLine.m_vecEnd = *end; + + // Send it over the wire + g_TEShowLine.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_smoke.cpp b/sp/src/game/server/te_smoke.cpp new file mode 100644 index 00000000..b9757702 --- /dev/null +++ b/sp/src/game/server/te_smoke.cpp @@ -0,0 +1,112 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches smoke tempentity +//----------------------------------------------------------------------------- +class CTESmoke : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTESmoke, CBaseTempEntity ); + + CTESmoke( const char *name ); + virtual ~CTESmoke( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( float, m_fScale ); + CNetworkVar( int, m_nFrameRate ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTESmoke::CTESmoke( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nModelIndex = 0; + m_fScale = 0; + m_nFrameRate = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTESmoke::~CTESmoke( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTESmoke::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = g_sModelIndexSmoke; + m_fScale = 5.0; + m_nFrameRate = 12; + m_vecOrigin = current_origin; + + Vector forward, right; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward, &right, NULL ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 25.0, right, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTESmoke, DT_TESmoke) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropFloat( SENDINFO(m_fScale ), 8, SPROP_ROUNDDOWN, 0.0, 25.6 ), + SendPropInt( SENDINFO(m_nFrameRate), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TESmoke objects +static CTESmoke g_TESmoke( "Smoke" ); + +void TE_Smoke( IRecipientFilter& filter, float delay, + const Vector* pos, int modelindex, float scale, int framerate ) +{ + g_TESmoke.m_vecOrigin = *pos; + g_TESmoke.m_nModelIndex = modelindex; + g_TESmoke.m_fScale = scale; + g_TESmoke.m_nFrameRate = framerate; + + // Send it over the wire + g_TESmoke.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_sparks.cpp b/sp/src/game/server/te_sparks.cpp new file mode 100644 index 00000000..af8f9947 --- /dev/null +++ b/sp/src/game/server/te_sparks.cpp @@ -0,0 +1,101 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "te_particlesystem.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches sparks +//----------------------------------------------------------------------------- +class CTESparks : public CTEParticleSystem +{ +public: + DECLARE_CLASS( CTESparks, CTEParticleSystem ); + DECLARE_SERVERCLASS(); + + CTESparks( const char *name ); + virtual ~CTESparks( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + CNetworkVar( int, m_nMagnitude ); + CNetworkVar( int, m_nTrailLength ); + CNetworkVector( m_vecDir ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTESparks::CTESparks( const char *name ) : + BaseClass( name ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTESparks::~CTESparks( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTESparks::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_vecOrigin = current_origin; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + m_vecOrigin += forward * 100; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTESparks, DT_TESparks) + SendPropInt( SENDINFO( m_nMagnitude ), 4, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_nTrailLength ), 4, SPROP_UNSIGNED ), + SendPropVector( SENDINFO( m_vecDir ), -1, SPROP_COORD ), +END_SEND_TABLE() + + +// Singleton to fire TESparks objects +static CTESparks g_TESparks( "Sparks" ); + +void TE_Sparks( IRecipientFilter& filter, float delay, + const Vector *pos, int nMagnitude, int nTrailLength, const Vector *pDir ) +{ + g_TESparks.m_vecOrigin = *pos; + g_TESparks.m_nMagnitude = nMagnitude; + g_TESparks.m_nTrailLength = nTrailLength; + + if ( pDir ) + { + g_TESparks.m_vecDir = *pDir; + } + else + { + g_TESparks.m_vecDir = vec3_origin; + } + + // Send it over the wire + g_TESparks.Create( filter, delay ); +} diff --git a/sp/src/game/server/te_sprite.cpp b/sp/src/game/server/te_sprite.cpp new file mode 100644 index 00000000..63289a24 --- /dev/null +++ b/sp/src/game/server/te_sprite.cpp @@ -0,0 +1,121 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches Sprite tempentity +//----------------------------------------------------------------------------- +class CTESprite : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTESprite, CBaseTempEntity ); + + CTESprite( const char *name ); + virtual ~CTESprite( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + virtual void Precache( void ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( float, m_fScale ); + CNetworkVar( int, m_nBrightness ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTESprite::CTESprite( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nModelIndex = 0; + m_fScale = 0; + m_nBrightness = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTESprite::~CTESprite( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTESprite::Precache( void ) +{ + CBaseEntity::PrecacheModel("sprites/gunsmoke.vmt"); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTESprite::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = CBaseEntity::PrecacheModel("sprites/gunsmoke.vmt"); + m_fScale = 0.8; + m_nBrightness = 200; + m_vecOrigin = current_origin; + + Vector forward, right; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward, &right, NULL ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, -25.0, right, m_vecOrigin.GetForModify() ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + + +IMPLEMENT_SERVERCLASS_ST(CTESprite, DT_TESprite) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropModelIndex( SENDINFO(m_nModelIndex) ), + SendPropFloat( SENDINFO(m_fScale ), 8, SPROP_ROUNDDOWN, 0.0, 25.6 ), + SendPropInt( SENDINFO(m_nBrightness), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TESprite objects +static CTESprite g_TESprite( "Sprite" ); + +void TE_Sprite( IRecipientFilter& filter, float delay, + const Vector *pos, int modelindex, float size, int brightness ) +{ + g_TESprite.m_vecOrigin = *pos; + g_TESprite.m_nModelIndex = modelindex; + g_TESprite.m_fScale = size; + g_TESprite.m_nBrightness = brightness; + + // Send it over the wire + g_TESprite.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_spritespray.cpp b/sp/src/game/server/te_spritespray.cpp new file mode 100644 index 00000000..6ff03af2 --- /dev/null +++ b/sp/src/game/server/te_spritespray.cpp @@ -0,0 +1,136 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud + +//----------------------------------------------------------------------------- +// Purpose: Dispatches Sprite Spray tempentity +//----------------------------------------------------------------------------- +class CTESpriteSpray : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTESpriteSpray, CBaseTempEntity ); + + CTESpriteSpray( const char *name ); + virtual ~CTESpriteSpray( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVector( m_vecDirection ); + CNetworkVar( int, m_nModelIndex ); + CNetworkVar( int, m_nSpeed ); + CNetworkVar( float, m_fNoise ); + CNetworkVar( int, m_nCount ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTESpriteSpray::CTESpriteSpray( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_vecDirection.Init(); + m_nModelIndex = 0; + m_fNoise = 0; + m_nSpeed = 0; + m_nCount = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTESpriteSpray::~CTESpriteSpray( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTESpriteSpray::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nModelIndex = g_sModelIndexSmoke; + m_fNoise = 0.8; + m_nCount = 5; + m_nSpeed = 30; + m_vecOrigin = current_origin; + + Vector forward, right; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward, &right, NULL ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, -25.0, right, m_vecOrigin.GetForModify() ); + + m_vecDirection.Init( random->RandomInt( -100, 100 ), random->RandomInt( -100, 100 ), random->RandomInt( 0, 100 ) ); + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTESpriteSpray, DT_TESpriteSpray) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_vecDirection), -1, SPROP_COORD), + SendPropModelIndex(SENDINFO(m_nModelIndex)), + SendPropFloat( SENDINFO(m_fNoise ), 8, SPROP_ROUNDDOWN, 0.0, 2.56 ), + SendPropInt( SENDINFO(m_nSpeed ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO(m_nCount), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TESpriteSpray objects +static CTESpriteSpray g_TESpriteSpray( "Sprite Spray" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// *dir - +// modelindex - +// speed - +// noise - +// count - +//----------------------------------------------------------------------------- +void TE_SpriteSpray( IRecipientFilter& filter, float delay, + const Vector *pos, const Vector *dir, int modelindex, int speed, float noise, int count ) +{ + g_TESpriteSpray.m_vecOrigin = *pos; + g_TESpriteSpray.m_vecDirection = *dir; + g_TESpriteSpray.m_nModelIndex = modelindex; + g_TESpriteSpray.m_nSpeed = speed; + g_TESpriteSpray.m_fNoise = noise; + g_TESpriteSpray.m_nCount = count; + + // Send it over the wire + g_TESpriteSpray.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/te_worlddecal.cpp b/sp/src/game/server/te_worlddecal.cpp new file mode 100644 index 00000000..028e0bb7 --- /dev/null +++ b/sp/src/game/server/te_worlddecal.cpp @@ -0,0 +1,121 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches world decal tempentity +//----------------------------------------------------------------------------- +class CTEWorldDecal : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEWorldDecal, CBaseTempEntity ); + + CTEWorldDecal( const char *name ); + virtual ~CTEWorldDecal( void ); + + virtual void Test( const Vector& current_origin, const QAngle& current_angles ); + + DECLARE_SERVERCLASS(); + +public: + CNetworkVector( m_vecOrigin ); + CNetworkVar( int, m_nIndex ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEWorldDecal::CTEWorldDecal( const char *name ) : + CBaseTempEntity( name ) +{ + m_vecOrigin.Init(); + m_nIndex = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEWorldDecal::~CTEWorldDecal( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *current_origin - +// *current_angles - +//----------------------------------------------------------------------------- +void CTEWorldDecal::Test( const Vector& current_origin, const QAngle& current_angles ) +{ + // Fill in data + m_nIndex = 0; + m_vecOrigin = current_origin; + + Vector vecEnd; + + Vector forward; + + m_vecOrigin.GetForModify()[2] += 24; + + AngleVectors( current_angles, &forward ); + forward[2] = 0.0; + VectorNormalize( forward ); + + VectorMA( m_vecOrigin, 50.0, forward, m_vecOrigin.GetForModify() ); + VectorMA( m_vecOrigin, 1024.0, forward, vecEnd ); + + trace_t tr; + + UTIL_TraceLine( m_vecOrigin, vecEnd, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); + + m_vecOrigin = tr.endpos; + + CBroadcastRecipientFilter filter; + Create( filter, 0.0 ); +} + +IMPLEMENT_SERVERCLASS_ST(CTEWorldDecal, DT_TEWorldDecal) +#if defined( TF_DLL ) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD_MP_INTEGRAL ), +#else + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD), +#endif + SendPropInt( SENDINFO(m_nIndex), 9, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire TEWorldDecal objects +static CTEWorldDecal g_TEWorldDecal( "World Decal" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : msg_dest - +// delay - +// *origin - +// *recipient - +// *pos - +// index - +//----------------------------------------------------------------------------- +void TE_WorldDecal( IRecipientFilter& filter, float delay, + const Vector* pos, int index ) +{ + g_TEWorldDecal.m_vecOrigin = *pos; + g_TEWorldDecal.m_nIndex = index; + + // Send it over the wire + g_TEWorldDecal.Create( filter, delay ); +} \ No newline at end of file diff --git a/sp/src/game/server/team.cpp b/sp/src/game/server/team.cpp new file mode 100644 index 00000000..354e08e9 --- /dev/null +++ b/sp/src/game/server/team.cpp @@ -0,0 +1,346 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Team management class. Contains all the details for a specific team +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "team.h" +#include "player.h" +#include "team_spawnpoint.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CUtlVector< CTeam * > g_Teams; + +//----------------------------------------------------------------------------- +// Purpose: SendProxy that converts the Team's player UtlVector to entindexes +//----------------------------------------------------------------------------- +void SendProxy_PlayerList( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID ) +{ + CTeam *pTeam = (CTeam*)pData; + + // If this assertion fails, then SendProxyArrayLength_PlayerArray must have failed. + Assert( iElement < pTeam->m_aPlayers.Size() ); + + CBasePlayer *pPlayer = pTeam->m_aPlayers[iElement]; + pOut->m_Int = pPlayer->entindex(); +} + + +int SendProxyArrayLength_PlayerArray( const void *pStruct, int objectID ) +{ + CTeam *pTeam = (CTeam*)pStruct; + return pTeam->m_aPlayers.Count(); +} + + +// Datatable +IMPLEMENT_SERVERCLASS_ST_NOBASE(CTeam, DT_Team) + SendPropInt( SENDINFO(m_iTeamNum), 5 ), + SendPropInt( SENDINFO(m_iScore), 0 ), + SendPropInt( SENDINFO(m_iRoundsWon), 8 ), + SendPropString( SENDINFO( m_szTeamname ) ), + + SendPropArray2( + SendProxyArrayLength_PlayerArray, + SendPropInt("player_array_element", 0, 4, 10, SPROP_UNSIGNED, SendProxy_PlayerList), + MAX_PLAYERS, + 0, + "player_array" + ) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( team_manager, CTeam ); + +//----------------------------------------------------------------------------- +// Purpose: Get a pointer to the specified team manager +//----------------------------------------------------------------------------- +CTeam *GetGlobalTeam( int iIndex ) +{ + if ( iIndex < 0 || iIndex >= GetNumberOfTeams() ) + return NULL; + + return g_Teams[ iIndex ]; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the number of team managers +//----------------------------------------------------------------------------- +int GetNumberOfTeams( void ) +{ + return g_Teams.Size(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Needed because this is an entity, but should never be used +//----------------------------------------------------------------------------- +CTeam::CTeam( void ) +{ + memset( m_szTeamname.GetForModify(), 0, sizeof(m_szTeamname) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTeam::~CTeam( void ) +{ + m_aSpawnPoints.Purge(); + m_aPlayers.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: Called every frame +//----------------------------------------------------------------------------- +void CTeam::Think( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Teams are always transmitted to clients +//----------------------------------------------------------------------------- +int CTeam::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Visibility/scanners +//----------------------------------------------------------------------------- +bool CTeam::ShouldTransmitToPlayer( CBasePlayer* pRecipient, CBaseEntity* pEntity ) +{ + // Always transmit the observer target to players + if ( pRecipient && pRecipient->IsObserver() && pRecipient->GetObserverTarget() == pEntity ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Initialization +//----------------------------------------------------------------------------- +void CTeam::Init( const char *pName, int iNumber ) +{ + InitializeSpawnpoints(); + InitializePlayers(); + + m_iScore = 0; + + Q_strncpy( m_szTeamname.GetForModify(), pName, MAX_TEAM_NAME_LENGTH ); + m_iTeamNum = iNumber; +} + +//----------------------------------------------------------------------------- +// DATA HANDLING +//----------------------------------------------------------------------------- +int CTeam::GetTeamNumber( void ) const +{ + return m_iTeamNum; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the team's name +//----------------------------------------------------------------------------- +const char *CTeam::GetName( void ) +{ + return m_szTeamname; +} + + +//----------------------------------------------------------------------------- +// Purpose: Update the player's client data +//----------------------------------------------------------------------------- +void CTeam::UpdateClientData( CBasePlayer *pPlayer ) +{ +} + +//------------------------------------------------------------------------------------------------------------------ +// SPAWNPOINTS +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeam::InitializeSpawnpoints( void ) +{ + m_iLastSpawn = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeam::AddSpawnpoint( CTeamSpawnPoint *pSpawnpoint ) +{ + m_aSpawnPoints.AddToTail( pSpawnpoint ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeam::RemoveSpawnpoint( CTeamSpawnPoint *pSpawnpoint ) +{ + for (int i = 0; i < m_aSpawnPoints.Size(); i++ ) + { + if ( m_aSpawnPoints[i] == pSpawnpoint ) + { + m_aSpawnPoints.Remove( i ); + return; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn the player at one of this team's spawnpoints. Return true if successful. +//----------------------------------------------------------------------------- +CBaseEntity *CTeam::SpawnPlayer( CBasePlayer *pPlayer ) +{ + if ( m_aSpawnPoints.Size() == 0 ) + return NULL; + + // Randomize the start spot + int iSpawn = m_iLastSpawn + random->RandomInt( 1,3 ); + if ( iSpawn >= m_aSpawnPoints.Size() ) + iSpawn -= m_aSpawnPoints.Size(); + int iStartingSpawn = iSpawn; + + // Now loop through the spawnpoints and pick one + int loopCount = 0; + do + { + if ( iSpawn >= m_aSpawnPoints.Size() ) + { + ++loopCount; + iSpawn = 0; + } + + // check if pSpot is valid, and that the player is on the right team + if ( (loopCount > 3) || m_aSpawnPoints[iSpawn]->IsValid( pPlayer ) ) + { + // DevMsg( 1, "player: spawning at (%s)\n", STRING(m_aSpawnPoints[iSpawn]->m_iName) ); + m_aSpawnPoints[iSpawn]->m_OnPlayerSpawn.FireOutput( pPlayer, m_aSpawnPoints[iSpawn] ); + + m_iLastSpawn = iSpawn; + return m_aSpawnPoints[iSpawn]; + } + + iSpawn++; + } while ( iSpawn != iStartingSpawn ); // loop if we're not back to the start + + return NULL; +} + +//------------------------------------------------------------------------------------------------------------------ +// PLAYERS +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeam::InitializePlayers( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Add the specified player to this team. Remove them from their current team, if any. +//----------------------------------------------------------------------------- +void CTeam::AddPlayer( CBasePlayer *pPlayer ) +{ + m_aPlayers.AddToTail( pPlayer ); + NetworkStateChanged(); +} + +//----------------------------------------------------------------------------- +// Purpose: Remove this player from the team +//----------------------------------------------------------------------------- +void CTeam::RemovePlayer( CBasePlayer *pPlayer ) +{ + m_aPlayers.FindAndRemove( pPlayer ); + NetworkStateChanged(); +} + +//----------------------------------------------------------------------------- +// Purpose: Return the number of players in this team. +//----------------------------------------------------------------------------- +int CTeam::GetNumPlayers( void ) +{ + return m_aPlayers.Size(); +} + +//----------------------------------------------------------------------------- +// Purpose: Get a specific player +//----------------------------------------------------------------------------- +CBasePlayer *CTeam::GetPlayer( int iIndex ) +{ + Assert( iIndex >= 0 && iIndex < m_aPlayers.Size() ); + return m_aPlayers[ iIndex ]; +} + +//------------------------------------------------------------------------------------------------------------------ +// SCORING +//----------------------------------------------------------------------------- +// Purpose: Add / Remove score for this team +//----------------------------------------------------------------------------- +void CTeam::AddScore( int iScore ) +{ + m_iScore += iScore; +} + +void CTeam::SetScore( int iScore ) +{ + m_iScore = iScore; +} + +//----------------------------------------------------------------------------- +// Purpose: Get this team's score +//----------------------------------------------------------------------------- +int CTeam::GetScore( void ) +{ + return m_iScore; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeam::ResetScores( void ) +{ + SetScore(0); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeam::AwardAchievement( int iAchievement ) +{ + Assert( iAchievement >= 0 && iAchievement < 255 ); // must fit in short + + CRecipientFilter filter; + + int iNumPlayers = GetNumPlayers(); + + for ( int i=0;iIsAlive() ) + { + iAlive++; + } + } + + return iAlive; +} \ No newline at end of file diff --git a/sp/src/game/server/team.h b/sp/src/game/server/team.h new file mode 100644 index 00000000..cdbe16da --- /dev/null +++ b/sp/src/game/server/team.h @@ -0,0 +1,101 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Team management class. Contains all the details for a specific team +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef TEAM_H +#define TEAM_H +#ifdef _WIN32 +#pragma once +#endif + +#include "shareddefs.h" +#include "utlvector.h" + +class CBasePlayer; +class CTeamSpawnPoint; + +class CTeam : public CBaseEntity +{ + DECLARE_CLASS( CTeam, CBaseEntity ); +public: + CTeam( void ); + virtual ~CTeam( void ); + + DECLARE_SERVERCLASS(); + + virtual void Precache( void ) { return; }; + + virtual void Think( void ); + virtual int UpdateTransmitState( void ); + + //----------------------------------------------------------------------------- + // Initialization + //----------------------------------------------------------------------------- + virtual void Init( const char *pName, int iNumber ); + + //----------------------------------------------------------------------------- + // Data Handling + //----------------------------------------------------------------------------- + virtual int GetTeamNumber( void ) const; + virtual const char *GetName( void ); + virtual void UpdateClientData( CBasePlayer *pPlayer ); + virtual bool ShouldTransmitToPlayer( CBasePlayer* pRecipient, CBaseEntity* pEntity ); + + //----------------------------------------------------------------------------- + // Spawnpoints + //----------------------------------------------------------------------------- + virtual void InitializeSpawnpoints( void ); + virtual void AddSpawnpoint( CTeamSpawnPoint *pSpawnpoint ); + virtual void RemoveSpawnpoint( CTeamSpawnPoint *pSpawnpoint ); + virtual CBaseEntity *SpawnPlayer( CBasePlayer *pPlayer ); + + //----------------------------------------------------------------------------- + // Players + //----------------------------------------------------------------------------- + virtual void InitializePlayers( void ); + virtual void AddPlayer( CBasePlayer *pPlayer ); + virtual void RemovePlayer( CBasePlayer *pPlayer ); + virtual int GetNumPlayers( void ); + virtual CBasePlayer *GetPlayer( int iIndex ); + + //----------------------------------------------------------------------------- + // Scoring + //----------------------------------------------------------------------------- + virtual void AddScore( int iScore ); + virtual void SetScore( int iScore ); + virtual int GetScore( void ); + virtual void ResetScores( void ); + + // Round scoring + virtual int GetRoundsWon( void ) { return m_iRoundsWon; } + virtual void SetRoundsWon( int iRounds ) { m_iRoundsWon = iRounds; } + virtual void IncrementRoundsWon( void ) { m_iRoundsWon++; } + + void AwardAchievement( int iAchievement ); + + virtual int GetAliveMembers( void ); + +public: + CUtlVector< CTeamSpawnPoint * > m_aSpawnPoints; + CUtlVector< CBasePlayer * > m_aPlayers; + + // Data + CNetworkString( m_szTeamname, MAX_TEAM_NAME_LENGTH ); + CNetworkVar( int, m_iScore ); + CNetworkVar( int, m_iRoundsWon ); + int m_iDeaths; + + // Spawnpoints + int m_iLastSpawn; // Index of the last spawnpoint used + + CNetworkVar( int, m_iTeamNum ); // Which team is this? +}; + +extern CUtlVector< CTeam * > g_Teams; +extern CTeam *GetGlobalTeam( int iIndex ); +extern int GetNumberOfTeams( void ); + +#endif // TEAM_H diff --git a/sp/src/game/server/team_control_point.cpp b/sp/src/game/server/team_control_point.cpp new file mode 100644 index 00000000..1bbec70a --- /dev/null +++ b/sp/src/game/server/team_control_point.cpp @@ -0,0 +1,1077 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "cbase.h" +#include "team_control_point.h" +#include "player.h" +#include "teamplay_gamerules.h" +#include "teamplayroundbased_gamerules.h" +#include "team.h" +#include "team_control_point_master.h" +#include "mp_shareddefs.h" +#include "engine/IEngineSound.h" +#include "soundenvelope.h" + +#ifdef TF_DLL +#include "tf_shareddefs.h" +#endif + +#define CONTROL_POINT_UNLOCK_THINK "UnlockThink" + +BEGIN_DATADESC(CTeamControlPoint) + DEFINE_KEYFIELD( m_iszPrintName, FIELD_STRING, "point_printname" ), + DEFINE_KEYFIELD( m_iCPGroup, FIELD_INTEGER, "point_group" ), + DEFINE_KEYFIELD( m_iDefaultOwner, FIELD_INTEGER, "point_default_owner" ), + DEFINE_KEYFIELD( m_iPointIndex, FIELD_INTEGER, "point_index" ), + DEFINE_KEYFIELD( m_iWarnOnCap, FIELD_INTEGER, "point_warn_on_cap" ), + DEFINE_KEYFIELD( m_iszWarnSound, FIELD_STRING, "point_warn_sound" ), + + DEFINE_KEYFIELD( m_iszCaptureStartSound, FIELD_STRING, "point_capture_start_sound" ), + DEFINE_KEYFIELD( m_iszCaptureEndSound, FIELD_STRING, "point_capture_end_sound" ), + DEFINE_KEYFIELD( m_iszCaptureInProgress, FIELD_STRING, "point_capture_progress_sound" ), + DEFINE_KEYFIELD( m_iszCaptureInterrupted, FIELD_STRING, "point_capture_interrupted_sound" ), + DEFINE_KEYFIELD( m_bRandomOwnerOnRestart, FIELD_BOOLEAN, "random_owner_on_restart" ), + DEFINE_KEYFIELD( m_bLocked, FIELD_BOOLEAN, "point_start_locked" ), + + DEFINE_FUNCTION( UnlockThink ), + +// DEFINE_FIELD( m_iTeam, FIELD_INTEGER ), +// DEFINE_FIELD( m_iIndex, FIELD_INTEGER ), +// DEFINE_FIELD( m_TeamData, CUtlVector < perteamdata_t > ), +// DEFINE_FIELD( m_bPointVisible, FIELD_INTEGER ), +// DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_iszName, FIELD_STRING ), +// DEFINE_FIELD( m_bStartDisabled, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_flLastContestedAt, FIELD_FLOAT ), +// DEFINE_FIELD( m_pCaptureInProgressSound, CSoundPatch ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetOwner", InputSetOwner ), + DEFINE_INPUTFUNC( FIELD_VOID, "ShowModel", InputShowModel ), + DEFINE_INPUTFUNC( FIELD_VOID, "HideModel", InputHideModel ), + DEFINE_INPUTFUNC( FIELD_VOID, "RoundActivate", InputRoundActivate ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetLocked", InputSetLocked ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetUnlockTime", InputSetUnlockTime ), + + DEFINE_OUTPUT( m_OnCapTeam1, "OnCapTeam1" ), // these are fired whenever the point changes modes + DEFINE_OUTPUT( m_OnCapTeam2, "OnCapTeam2" ), + DEFINE_OUTPUT( m_OnCapReset, "OnCapReset" ), + + DEFINE_OUTPUT( m_OnOwnerChangedToTeam1, "OnOwnerChangedToTeam1" ), // these are fired when a team does the work to change the owner + DEFINE_OUTPUT( m_OnOwnerChangedToTeam2, "OnOwnerChangedToTeam2" ), + + DEFINE_OUTPUT( m_OnRoundStartOwnedByTeam1, "OnRoundStartOwnedByTeam1" ), // these are fired when a round is starting + DEFINE_OUTPUT( m_OnRoundStartOwnedByTeam2, "OnRoundStartOwnedByTeam2" ), + + DEFINE_OUTPUT( m_OnUnlocked, "OnUnlocked" ), + + DEFINE_THINKFUNC( AnimThink ), +END_DATADESC(); + +LINK_ENTITY_TO_CLASS( team_control_point, CTeamControlPoint ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTeamControlPoint::CTeamControlPoint() +{ + m_TeamData.SetSize( GetNumberOfTeams() ); + m_pCaptureInProgressSound = NULL; + + m_bLocked = false; + m_flUnlockTime = -1; + +#ifdef TF_DLL + UseClientSideAnimation(); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::Spawn( void ) +{ + // Validate our default team + if ( m_iDefaultOwner < 0 || m_iDefaultOwner >= GetNumberOfTeams() ) + { + Warning( "team_control_point '%s' has bad point_default_owner.\n", GetDebugName() ); + m_iDefaultOwner = TEAM_UNASSIGNED; + } + +#ifdef TF_DLL + if ( m_iszCaptureStartSound == NULL_STRING ) + { + m_iszCaptureStartSound = AllocPooledString( "Hologram.Start" ); + } + if ( m_iszCaptureEndSound == NULL_STRING ) + { + m_iszCaptureEndSound = AllocPooledString( "Hologram.Stop" ); + } + if ( m_iszCaptureInProgress == NULL_STRING ) + { + m_iszCaptureInProgress = AllocPooledString( "Hologram.Move" ); + } + if ( m_iszCaptureInterrupted == NULL_STRING ) + { + m_iszCaptureInterrupted = AllocPooledString( "Hologram.Interrupted" ); + } +#endif + + Precache(); + + InternalSetOwner( m_iDefaultOwner, false ); //init the owner of this point + TeamplayRoundBasedRules()->RecalculateControlPointState(); + + SetActive( !m_bStartDisabled ); + + BaseClass::Spawn(); + + SetPlaybackRate( 1.0 ); + SetThink( &CTeamControlPoint::AnimThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + if ( FBitSet( m_spawnflags, SF_CAP_POINT_HIDE_MODEL ) ) + { + AddEffects( EF_NODRAW ); + } + + if ( FBitSet( m_spawnflags, SF_CAP_POINT_HIDE_SHADOW ) ) + { + AddEffects( EF_NOSHADOW ); + } + + m_flLastContestedAt = -1; + + m_pCaptureInProgressSound = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPoint::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( !Q_strncmp( szKeyName, "team_capsound_", 14 ) ) + { + int iTeam = atoi(szKeyName+14); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iszCapSound = AllocPooledString(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_model_", 11 ) ) + { + int iTeam = atoi(szKeyName+11); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iszModel = AllocPooledString(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_timedpoints_", 17 ) ) + { + int iTeam = atoi(szKeyName+17); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iTimedPoints = atoi(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_bodygroup_", 15 ) ) + { + int iTeam = atoi(szKeyName+15); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iModelBodygroup = atoi(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_icon_", 10 ) ) + { + int iTeam = atoi(szKeyName+10); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iszIcon = AllocPooledString(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_overlay_", 13 ) ) + { + int iTeam = atoi(szKeyName+13); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iszOverlay = AllocPooledString(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_previouspoint_", 19 ) ) + { + int iTeam; + int iPoint = 0; + sscanf( szKeyName+19, "%d_%d", &iTeam, &iPoint ); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + Assert( iPoint >= 0 && iPoint < MAX_PREVIOUS_POINTS ); + m_TeamData[iTeam].iszPreviousPoint[iPoint] = AllocPooledString(szValue); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::Precache( void ) +{ + for ( int i = 0; i < m_TeamData.Count(); i++ ) + { + // Skip over spectator + if ( i == TEAM_SPECTATOR ) + continue; + + if ( m_TeamData[i].iszCapSound != NULL_STRING ) + { + PrecacheScriptSound( STRING(m_TeamData[i].iszCapSound) ); + } + + if ( m_TeamData[i].iszModel != NULL_STRING ) + { + PrecacheModel( STRING(m_TeamData[i].iszModel) ); + } + + if ( m_TeamData[i].iszIcon != NULL_STRING ) + { + PrecacheMaterial( STRING( m_TeamData[i].iszIcon ) ); + m_TeamData[i].iIcon = GetMaterialIndex( STRING( m_TeamData[i].iszIcon ) ); + Assert( m_TeamData[i].iIcon != 0 ); + } + + if ( !m_TeamData[i].iIcon ) + { + Warning( "Invalid hud icon material for team %d in control point '%s' ( point index %d )\n", i, GetDebugName(), GetPointIndex() ); + } + + if ( m_TeamData[i].iszOverlay != NULL_STRING ) + { + PrecacheMaterial( STRING( m_TeamData[i].iszOverlay ) ); + m_TeamData[i].iOverlay = GetMaterialIndex( STRING( m_TeamData[i].iszOverlay ) ); + Assert( m_TeamData[i].iOverlay != 0 ); + + if ( !m_TeamData[i].iOverlay ) + { + Warning( "Invalid hud overlay material for team %d in control point '%s' ( point index %d )\n", i, GetDebugName(), GetPointIndex() ); + } + } + } + + PrecacheScriptSound( STRING( m_iszCaptureStartSound ) ); + PrecacheScriptSound( STRING( m_iszCaptureEndSound ) ); + PrecacheScriptSound( STRING( m_iszCaptureInProgress ) ); + PrecacheScriptSound( STRING( m_iszCaptureInterrupted ) ); + + if ( m_iszWarnSound != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_iszWarnSound ) ); + } + +#ifdef TF_DLL + PrecacheScriptSound( "Announcer.ControlPointContested" ); +#endif +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTeamControlPoint::AnimThink( void ) +{ + StudioFrameAdvance(); + DispatchAnimEvents(this); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Used by ControlMaster to this point to its default owner +//----------------------------------------------------------------------------- +void CTeamControlPoint::InputReset( inputdata_t &input ) +{ + m_flLastContestedAt = -1; + InternalSetOwner( m_iDefaultOwner, false ); + ObjectiveResource()->SetOwningTeam( GetPointIndex(), m_iTeam ); + TeamplayRoundBasedRules()->RecalculateControlPointState(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::HandleScoring( int iTeam ) +{ + if ( TeamplayRoundBasedRules() && !TeamplayRoundBasedRules()->ShouldScorePerRound() ) + { + GetGlobalTeam( iTeam )->AddScore( 1 ); + TeamplayRoundBasedRules()->HandleTeamScoreModify( iTeam, 1 ); + + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster && !pMaster->WouldNewCPOwnerWinGame( this, iTeam ) ) + { +#ifdef TF_DLL + if ( TeamplayRoundBasedRules()->GetGameType() == TF_GAMETYPE_ESCORT ) + { + CBroadcastRecipientFilter filter; + EmitSound( filter, entindex(), "Hud.EndRoundScored" ); + } + else +#endif + { + CTeamRecipientFilter filter( iTeam ); + EmitSound( filter, entindex(), "Hud.EndRoundScored" ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Used by Area caps to set the owner +//----------------------------------------------------------------------------- +void CTeamControlPoint::InputSetOwner( inputdata_t &input ) +{ + int iCapTeam = input.value.Int(); + + Assert( iCapTeam >= 0 && iCapTeam < GetNumberOfTeams() ); + + Assert( input.pCaller ); + + if ( !input.pCaller ) + return; + + if ( GetOwner() == iCapTeam ) + return; + + if ( TeamplayGameRules()->PointsMayBeCaptured() ) + { + // must be done before setting the owner + HandleScoring( iCapTeam ); + + if ( input.pCaller->IsPlayer() ) + { + int iCappingPlayer = input.pCaller->entindex(); + InternalSetOwner( iCapTeam, true, 1, &iCappingPlayer ); + } + else + { + InternalSetOwner( iCapTeam, false ); + } + + ObjectiveResource()->SetOwningTeam( GetPointIndex(), m_iTeam ); + TeamplayRoundBasedRules()->RecalculateControlPointState(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::InputShowModel( inputdata_t &input ) +{ + RemoveEffects( EF_NODRAW ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::InputHideModel( inputdata_t &input ) +{ + AddEffects( EF_NODRAW ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::GetCurrentHudIconIndex( void ) +{ + return m_TeamData[GetOwner()].iIcon; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::GetHudIconIndexForTeam( int iGameTeam ) +{ + return m_TeamData[iGameTeam].iIcon; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::GetHudOverlayIndexForTeam( int iGameTeam ) +{ + return m_TeamData[iGameTeam].iOverlay; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::GetPreviousPointForTeam( int iGameTeam, int iPrevPoint ) +{ + Assert( iPrevPoint >= 0 && iPrevPoint < MAX_PREVIOUS_POINTS ); + + int iRetVal = -1; + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, STRING(m_TeamData[iGameTeam].iszPreviousPoint[iPrevPoint]) ); + + if ( pEntity ) + { + CTeamControlPoint *pPoint = dynamic_cast( pEntity ); + + if ( pPoint ) + { + iRetVal = pPoint->GetPointIndex(); + } + } + + return iRetVal; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::ForceOwner( int iTeam ) +{ + InternalSetOwner( iTeam, false, 0, 0 ); + ObjectiveResource()->SetOwningTeam( GetPointIndex(), m_iTeam ); + TeamplayRoundBasedRules()->RecalculateControlPointState(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::SetOwner( int iCapTeam, bool bMakeSound, int iNumCappers, int *pCappingPlayers ) +{ + if ( TeamplayGameRules()->PointsMayBeCaptured() ) + { + // must be done before setting the owner + HandleScoring( iCapTeam ); + + InternalSetOwner( iCapTeam, bMakeSound, iNumCappers, pCappingPlayers ); + ObjectiveResource()->SetOwningTeam( GetPointIndex(), m_iTeam ); + TeamplayRoundBasedRules()->RecalculateControlPointState(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::CaptureStart( int iCapTeam, int iNumCappingPlayers, int *pCappingPlayers ) +{ + int iNumCappers = iNumCappingPlayers; + + float flLastOwnershipChangeTime = -1.f; + CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, GetControlPointMasterName() ); + while( pEnt ) + { + CTeamControlPointMaster *pMaster = dynamic_cast( pEnt ); + if ( pMaster && pMaster->IsActive() ) + { + flLastOwnershipChangeTime = pMaster->GetLastOwnershipChangeTime(); + } + pEnt = gEntList.FindEntityByClassname( pEnt, GetControlPointMasterName() ); + } + + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_point_startcapture" ); + if ( event ) + { + event->SetInt( "cp", m_iPointIndex ); + event->SetString( "cpname", STRING( m_iszPrintName ) ); + event->SetInt( "team", m_iTeam ); + event->SetInt( "capteam", iCapTeam ); + event->SetFloat( "captime", gpGlobals->curtime - flLastOwnershipChangeTime ); + + // safety check + if ( iNumCappers > 8 ) + { + iNumCappers = 8; + } + + char cappers[9]; // pCappingPlayers should be max length 8 + int i; + for( i = 0 ; i < iNumCappers ; i++ ) + { + cappers[i] = (char)pCappingPlayers[i]; + } + + cappers[i] = '\0'; + + // pCappingPlayers is a null terminated list of player indices + event->SetString( "cappers", cappers ); + event->SetInt( "priority", 7 ); + + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::CaptureEnd( void ) +{ + StopLoopingSounds(); + + if ( !FBitSet( m_spawnflags, SF_CAP_POINT_NO_CAP_SOUNDS ) ) + { + EmitSound( STRING( m_iszCaptureEndSound ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::CaptureInterrupted( bool bBlocked ) +{ + StopLoopingSounds(); + + if ( FBitSet( m_spawnflags, SF_CAP_POINT_NO_CAP_SOUNDS ) ) + { + return; + } + + const char *pSoundName = NULL; + + if ( bBlocked == true ) + { + pSoundName = STRING( m_iszCaptureInterrupted ); + } + else + { + pSoundName = STRING( m_iszCaptureInProgress ); + EmitSound( STRING( m_iszCaptureStartSound ) ); + } + + if ( m_pCaptureInProgressSound == NULL && pSoundName != NULL ) + { + CPASFilter filter( GetAbsOrigin() ); + + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + m_pCaptureInProgressSound = controller.SoundCreate( filter, entindex(), pSoundName ); + + controller.Play( m_pCaptureInProgressSound, 1.0, 100 ); + } + +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::StopLoopingSounds( void ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + if ( m_pCaptureInProgressSound ) + { + controller.SoundDestroy( m_pCaptureInProgressSound ); + m_pCaptureInProgressSound = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the new owner of the point, plays the appropriate sound and shows the right model +//----------------------------------------------------------------------------- +void CTeamControlPoint::InternalSetOwner( int iCapTeam, bool bMakeSound, int iNumCappers, int *pCappingPlayers ) +{ + Assert( iCapTeam >= 0 && iCapTeam < GetNumberOfTeams() ); + + int iOldTeam = m_iTeam; + + m_iTeam = iCapTeam; + ChangeTeam( iCapTeam ); + + if ( bMakeSound ) + { + CBroadcastRecipientFilter filter; + EmitSound( filter, entindex(), STRING( m_TeamData[m_iTeam].iszCapSound ) ); + } + + // Update visuals + SetModel( STRING(m_TeamData[m_iTeam].iszModel) ); + SetBodygroup( 0, m_iTeam ); + m_nSkin = ( m_iTeam == TEAM_UNASSIGNED ) ? 2 : (m_iTeam - 2); + ResetSequence( LookupSequence("idle") ); + + // We add 1 to the index because we consider the default "no points capped" as 0. + TeamplayGameRules()->SetLastCapPointChanged( m_iPointIndex+1 ); + + // Determine the pose parameters for each team + for ( int i = 0; i < m_TeamData.Count(); i++ ) + { + // Skip spectator + if ( i == TEAM_SPECTATOR ) + continue; + + if ( GetModelPtr() && GetModelPtr()->SequencesAvailable() ) + { + m_TeamData[i].iTeamPoseParam = LookupPoseParameter( UTIL_VarArgs( "cappoint_%d_percentage", i ) ); + } + else + { + m_TeamData[i].iTeamPoseParam = -1; + } + } + UpdateCapPercentage(); + + if ( m_iTeam == TEAM_UNASSIGNED ) + { + m_OnCapReset.FireOutput( this, this ); + } + else + { + // Remap team to get first game team = 1 + switch ( m_iTeam - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnCapTeam1.FireOutput( this, this ); + break; + case 2: + m_OnCapTeam2.FireOutput( this, this ); + break; + default: + Assert(0); + break; + } + } + + // If we're playing a sound, this is a true cap by players. + if ( bMakeSound ) + { + if ( iOldTeam > LAST_SHARED_TEAM && iOldTeam != m_iTeam ) + { + // Make the members of our old team say something + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pPlayer ) + continue; + if ( pPlayer->GetTeamNumber() == iOldTeam ) + { + pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_LOST_CONTROL_POINT ); + } + } + } + + for( int i = 0; i < iNumCappers; i++ ) + { + int playerIndex = pCappingPlayers[i]; + + Assert( playerIndex > 0 && playerIndex <= gpGlobals->maxClients ); + + PlayerCapped( ToBaseMultiplayerPlayer(UTIL_PlayerByIndex( playerIndex )) ); + } + + // Remap team to get first game team = 1 + switch ( m_iTeam - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnOwnerChangedToTeam1.FireOutput( this, this ); + break; + case 2: + m_OnOwnerChangedToTeam2.FireOutput( this, this ); + break; + } + + if ( m_iTeam != TEAM_UNASSIGNED && iNumCappers ) + { + SendCapString( m_iTeam, iNumCappers, pCappingPlayers ); + } + } + + // Have control point master check the win conditions now! + CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, GetControlPointMasterName() ); + + while( pEnt ) + { + CTeamControlPointMaster *pMaster = dynamic_cast( pEnt ); + + if ( pMaster->IsActive() ) + { + pMaster->CheckWinConditions(); + pMaster->SetLastOwnershipChangeTime( gpGlobals->curtime ); + } + + pEnt = gEntList.FindEntityByClassname( pEnt, GetControlPointMasterName() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::SendCapString( int iCapTeam, int iNumCappingPlayers, int *pCappingPlayers ) +{ + if ( strlen( STRING(m_iszPrintName) ) <= 0 ) + return; + + int iNumCappers = iNumCappingPlayers; + + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_point_captured" ); + if ( event ) + { + event->SetInt( "cp", m_iPointIndex ); + event->SetString( "cpname", STRING( m_iszPrintName ) ); + event->SetInt( "team", iCapTeam ); + + // safety check + if ( iNumCappers > 8 ) + { + iNumCappers = 8; + } + + char cappers[9]; // pCappingPlayers should be max length 8 + int i; + for( i = 0 ; i < iNumCappers ; i++ ) + { + cappers[i] = (char)pCappingPlayers[i]; + } + + cappers[i] = '\0'; + + // pCappingPlayers is a null terminated list of player indices + event->SetString( "cappers", cappers ); + event->SetInt( "priority", 9 ); + + gameeventmanager->FireEvent( event ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::CaptureBlocked( CBaseMultiplayerPlayer *pPlayer ) +{ + if( strlen( STRING(m_iszPrintName) ) <= 0 ) + return; + + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_capture_blocked" ); + + if ( event ) + { + event->SetInt( "cp", m_iPointIndex ); + event->SetString( "cpname", STRING(m_iszPrintName) ); + event->SetInt( "blocker", pPlayer->entindex() ); + event->SetInt( "priority", 9 ); + + gameeventmanager->FireEvent( event ); + } + + PlayerBlocked( pPlayer ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::GetOwner( void ) const +{ + return m_iTeam; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::GetDefaultOwner( void ) const +{ + return m_iDefaultOwner; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::GetCPGroup( void ) +{ + return m_iCPGroup; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the time-based point value of this control point +//----------------------------------------------------------------------------- +int CTeamControlPoint::PointValue( void ) +{ + if ( GetOwner() != m_iDefaultOwner ) + return m_TeamData[ GetOwner() ].iTimedPoints; + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::SetActive( bool active ) +{ + m_bActive = active; + + if( active ) + { + RemoveEffects( EF_NODRAW ); + } + else + { + AddEffects( EF_NODRAW ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::SetCappersRequiredForTeam( int iGameTeam, int iCappers ) +{ + m_TeamData[iGameTeam].iPlayersRequired = iCappers; +} + + +//----------------------------------------------------------------------------- +// Purpose: Return true if this point has ever been contested, false if the enemy has never contested this point yet +//----------------------------------------------------------------------------- +bool CTeamControlPoint::HasBeenContested( void ) const +{ + return m_flLastContestedAt > 0.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTeamControlPoint::LastContestedAt( void ) +{ + return m_flLastContestedAt; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::SetLastContestedAt( float flTime ) +{ + m_flLastContestedAt = flTime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::UpdateCapPercentage( void ) +{ + for ( int i = LAST_SHARED_TEAM+1; i < m_TeamData.Count(); i++ ) + { + // Skip spectator + if ( i == TEAM_SPECTATOR ) + continue; + + float flPerc = GetTeamCapPercentage(i); + + if ( m_TeamData[i].iTeamPoseParam != -1 ) + { + SetPoseParameter( m_TeamData[i].iTeamPoseParam, flPerc ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTeamControlPoint::GetTeamCapPercentage( int iTeam ) +{ + int iCappingTeam = ObjectiveResource()->GetCappingTeam( GetPointIndex() ); + if ( iCappingTeam == TEAM_UNASSIGNED ) + { + // No-one's capping this point. + if ( iTeam == m_iTeam ) + return 1.0; + + return 0.0; + } + + float flCapPerc = ObjectiveResource()->GetCPCapPercentage( GetPointIndex() ); + if ( iTeam == iCappingTeam ) + return (1.0 - flCapPerc); + if ( iTeam == m_iTeam ) + return flCapPerc; + + return 0.0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPoint::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[1024]; + Q_snprintf(tempstr, sizeof(tempstr), "INDEX: (%d)", GetPointIndex() ); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf( tempstr, sizeof(tempstr), "Red Previous Points: "); + for ( int i = 0; i < MAX_PREVIOUS_POINTS; i++ ) + { + if ( m_TeamData[2].iszPreviousPoint[i] != NULL_STRING ) + { + Q_strncat( tempstr, STRING(m_TeamData[2].iszPreviousPoint[i]), 1024, COPY_ALL_CHARACTERS ); + Q_strncat( tempstr, ", ", 1024, COPY_ALL_CHARACTERS ); + } + } + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf( tempstr, sizeof(tempstr), "Blue Previous Points: " ); + for ( int i = 0; i < MAX_PREVIOUS_POINTS; i++ ) + { + if ( m_TeamData[3].iszPreviousPoint[i] != NULL_STRING ) + { + Q_strncat( tempstr, STRING(m_TeamData[3].iszPreviousPoint[i]), 1024, COPY_ALL_CHARACTERS ); + Q_strncat( tempstr, ", ", 1024, COPY_ALL_CHARACTERS ); + } + } + EntityText(text_offset,tempstr,0); + text_offset++; + + for ( int i = 0; i < MAX_CONTROL_POINT_TEAMS; i++ ) + { + if ( ObjectiveResource()->GetBaseControlPointForTeam(i) == GetPointIndex() ) + { + Q_snprintf(tempstr, sizeof(tempstr), "Base Control Point for Team %d", i ); + EntityText(text_offset,tempstr,0); + text_offset++; + } + } + } + + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: The specified player took part in capping this point. +//----------------------------------------------------------------------------- +void CTeamControlPoint::PlayerCapped( CBaseMultiplayerPlayer *pPlayer ) +{ + if ( pPlayer ) + { + pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_CAPTURED_POINT ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: The specified player blocked the enemy team from capping this point. +//----------------------------------------------------------------------------- +void CTeamControlPoint::PlayerBlocked( CBaseMultiplayerPlayer *pPlayer ) +{ + if ( pPlayer ) + { + pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_CAPTURE_BLOCKED ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::InputRoundActivate( inputdata_t &inputdata ) +{ + switch ( m_iTeam - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnRoundStartOwnedByTeam1.FireOutput( this, this ); + break; + case 2: + m_OnRoundStartOwnedByTeam2.FireOutput( this, this ); + break; + } + + InternalSetLocked( m_bLocked ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::InputSetLocked( inputdata_t &inputdata ) +{ + // never lock/unlock the point if we're in waiting for players + if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->IsInWaitingForPlayers() ) + return; + + bool bLocked = inputdata.value.Int() > 0; + InternalSetLocked( bLocked ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::InternalSetLocked( bool bLocked ) +{ + if ( !bLocked && m_bLocked ) + { + // unlocked this point + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_point_unlocked" ); + if ( event ) + { + event->SetInt( "cp", m_iPointIndex ); + event->SetString( "cpname", STRING( m_iszPrintName ) ); + event->SetInt( "team", m_iTeam ); + gameeventmanager->FireEvent( event ); + } + } + else if ( bLocked && !m_bLocked ) + { + // locked this point + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_point_locked" ); + if ( event ) + { + event->SetInt( "cp", m_iPointIndex ); + event->SetString( "cpname", STRING( m_iszPrintName ) ); + event->SetInt( "team", m_iTeam ); + gameeventmanager->FireEvent( event ); + } + } + + m_bLocked = bLocked; + + if ( ObjectiveResource() && GetPointIndex() < ObjectiveResource()->GetNumControlPoints() ) + { + ObjectiveResource()->SetCPLocked( GetPointIndex(), m_bLocked ); + ObjectiveResource()->SetCPUnlockTime( GetPointIndex(), 0.0f ); + } + + if ( !m_bLocked ) + { + m_flUnlockTime = -1; + m_OnUnlocked.FireOutput( this, this ); + SetContextThink( NULL, 0, CONTROL_POINT_UNLOCK_THINK ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::InputSetUnlockTime( inputdata_t &inputdata ) +{ + // never lock/unlock the point if we're in waiting for players + if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->IsInWaitingForPlayers() ) + return; + + int nTime = inputdata.value.Int(); + + if ( nTime <= 0 ) + { + InternalSetLocked( false ); + return; + } + + m_flUnlockTime = gpGlobals->curtime + nTime; + + if ( ObjectiveResource() ) + { + ObjectiveResource()->SetCPUnlockTime( GetPointIndex(), m_flUnlockTime ); + } + + SetContextThink( &CTeamControlPoint::UnlockThink, gpGlobals->curtime + 0.1, CONTROL_POINT_UNLOCK_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPoint::UnlockThink( void ) +{ + if ( m_flUnlockTime > 0 && + m_flUnlockTime < gpGlobals->curtime && + ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->State_Get() == GR_STATE_RND_RUNNING ) ) + { + InternalSetLocked( false ); + return; + } + + SetContextThink( &CTeamControlPoint::UnlockThink, gpGlobals->curtime + 0.1, CONTROL_POINT_UNLOCK_THINK ); +} diff --git a/sp/src/game/server/team_control_point.h b/sp/src/game/server/team_control_point.h new file mode 100644 index 00000000..816a3ac1 --- /dev/null +++ b/sp/src/game/server/team_control_point.h @@ -0,0 +1,195 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef TEAM_CONTROL_POINT_H +#define TEAM_CONTROL_POINT_H +#ifdef _WIN32 +#pragma once +#endif + +#include "basemultiplayerplayer.h" + +// Spawnflags +#define SF_CAP_POINT_HIDEFLAG (1<<0) +#define SF_CAP_POINT_HIDE_MODEL (1<<1) +#define SF_CAP_POINT_HIDE_SHADOW (1<<2) +#define SF_CAP_POINT_NO_CAP_SOUNDS (1<<3) +#define SF_CAP_POINT_NO_ANNOUNCER (1<<4) + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CTeamControlPoint : public CBaseAnimating +{ + DECLARE_CLASS( CTeamControlPoint, CBaseAnimating ); +public: + DECLARE_DATADESC(); + + CTeamControlPoint(); + + // Derived, game-specific control points must override these functions +public: + // Used to find game specific entities + virtual const char *GetControlPointMasterName( void ) { return "team_control_point_master"; } + +public: + virtual void Spawn( void ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual void Precache( void ); + virtual int DrawDebugTextOverlays( void ); + + //Inputs + inline void Enable( inputdata_t &input ) { SetActive( false ); } + inline void Disable( inputdata_t &input ) { SetActive( true ); } + void InputReset( inputdata_t &input ); + void InputSetOwner( inputdata_t &input ); + void InputShowModel( inputdata_t &input ); + void InputHideModel( inputdata_t &input ); + void InputRoundActivate( inputdata_t &inputdata ); + void InputSetLocked( inputdata_t &inputdata ); + void InputSetUnlockTime( inputdata_t &inputdata ); + + // Owner handling + void ForceOwner( int iTeam ); // used when selecting a specific round to play + void SetOwner( int iCapTeam, bool bMakeSound = true, int iNumCappers = 0, int *iCappingPlayers = NULL ); + int GetOwner( void ) const; + int GetDefaultOwner( void ) const; + bool RandomOwnerOnRestart( void ){ return m_bRandomOwnerOnRestart; } + + void SetActive( bool active ); + inline bool IsActive( void ) { return m_bActive; } + void AnimThink( void ); + + bool PointIsVisible( void ) { return !( FBitSet( m_spawnflags, SF_CAP_POINT_HIDEFLAG ) ); } + + inline const char *GetName( void ) { return STRING(m_iszPrintName); } + int GetCPGroup( void ); + int GetPointIndex( void ) { return m_iPointIndex; } + void SetPointIndex( int index ) { m_iPointIndex = index; } + + int GetWarnOnCap( void ) { return m_iWarnOnCap; } + string_t GetWarnSound( void ) { return m_iszWarnSound; } + + int GetTeamIcon( int iTeam ); + + int GetCurrentHudIconIndex( void ); + int GetHudIconIndexForTeam( int iGameTeam ); + int GetHudOverlayIndexForTeam( int iGameTeam ); + int GetPreviousPointForTeam( int iGameTeam, int iPrevPoint ); + + void SetCappersRequiredForTeam( int iGameTeam, int iCappers ); + + void CaptureBlocked( CBaseMultiplayerPlayer *pPlayer ); + + int PointValue( void ); + + bool HasBeenContested( void ) const; // return true if this point has ever been contested, false if the enemy has never contested this point yet + float LastContestedAt( void ); + void SetLastContestedAt( float flTime ); + + void UpdateCapPercentage( void ); + float GetTeamCapPercentage( int iTeam ); + + // The specified player took part in capping this point. + virtual void PlayerCapped( CBaseMultiplayerPlayer *pPlayer ); + + // The specified player blocked the enemy team from capping this point. + virtual void PlayerBlocked( CBaseMultiplayerPlayer *pPlayer ); + + void CaptureEnd( void ); + void CaptureStart( int iCapTeam, int iNumCappingPlayers, int *pCappingPlayers ); + void CaptureInterrupted( bool bBlocked ); + + virtual void StopLoopingSounds( void ); + + bool IsLocked( void ){ return m_bLocked; } + + void EXPORT UnlockThink( void ); + +private: + void SendCapString( int iCapTeam, int iNumCappingPlayers, int *pCappingPlayers ); + void InternalSetOwner( int iCapTeam, bool bMakeSound = true, int iNumCappers = 0, int *iCappingPlayers = NULL ); + void HandleScoring( int iTeam ); + void InternalSetLocked( bool bLocked ); + + int m_iTeam; + int m_iDefaultOwner; // Team that initially owns the cap point + int m_iIndex; // The index of this point in the controlpointArray + int m_iWarnOnCap; // Warn the team that owns the control point when the opposing team starts to capture it. + string_t m_iszPrintName; + string_t m_iszWarnSound; // Sound played if the team needs to be warned about this point being captured + bool m_bRandomOwnerOnRestart; // Do we want to randomize the owner after a restart? + bool m_bLocked; + float m_flUnlockTime; // Time to unlock + + // We store a copy of this data for each team, +1 for the un-owned state. + struct perteamdata_t + { + perteamdata_t() + { + iszCapSound = NULL_STRING; + iszModel = NULL_STRING; + iModelBodygroup = -1; + iIcon = 0; + iszIcon = NULL_STRING; + iOverlay = 0; + iszOverlay = NULL_STRING; + iPlayersRequired = 0; + iTimedPoints = 0; + for ( int i = 0; i < MAX_PREVIOUS_POINTS; i++ ) + { + iszPreviousPoint[i] = NULL_STRING; + } + iTeamPoseParam = 0; + } + + string_t iszCapSound; + string_t iszModel; + int iModelBodygroup; + int iTeamPoseParam; + int iIcon; + string_t iszIcon; + int iOverlay; + string_t iszOverlay; + int iPlayersRequired; + int iTimedPoints; + string_t iszPreviousPoint[MAX_PREVIOUS_POINTS]; + }; + CUtlVector m_TeamData; + + COutputEvent m_OnCapReset; + + COutputEvent m_OnCapTeam1; + COutputEvent m_OnCapTeam2; + + COutputEvent m_OnOwnerChangedToTeam1; + COutputEvent m_OnOwnerChangedToTeam2; + + COutputEvent m_OnRoundStartOwnedByTeam1; + COutputEvent m_OnRoundStartOwnedByTeam2; + + COutputEvent m_OnUnlocked; + + int m_bPointVisible; //should this capture point be visible on the hud? + int m_iPointIndex; //the mapper set index value of this control point + + int m_iCPGroup; //the group that this control point belongs to + bool m_bActive; // + + string_t m_iszName; //Name used in cap messages + + bool m_bStartDisabled; + + float m_flLastContestedAt; + + CSoundPatch *m_pCaptureInProgressSound; + string_t m_iszCaptureStartSound; + string_t m_iszCaptureEndSound; + string_t m_iszCaptureInProgress; + string_t m_iszCaptureInterrupted; +}; + +#endif // TEAM_CONTROL_POINT_H diff --git a/sp/src/game/server/team_control_point_master.cpp b/sp/src/game/server/team_control_point_master.cpp new file mode 100644 index 00000000..8ad9796d --- /dev/null +++ b/sp/src/game/server/team_control_point_master.cpp @@ -0,0 +1,1347 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "team_objectiveresource.h" +#include "team_control_point_master.h" +#include "teamplayroundbased_gamerules.h" + +#if defined ( TF_DLL ) +#include "tf_gamerules.h" +#endif + +BEGIN_DATADESC( CTeamControlPointMaster ) + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_KEYFIELD( m_iszCapLayoutInHUD, FIELD_STRING, "caplayout" ), + DEFINE_KEYFIELD( m_iInvalidCapWinner, FIELD_INTEGER, "cpm_restrict_team_cap_win" ), + DEFINE_KEYFIELD( m_bSwitchTeamsOnWin, FIELD_BOOLEAN, "switch_teams" ), + DEFINE_KEYFIELD( m_bScorePerCapture, FIELD_BOOLEAN, "score_style" ), + DEFINE_KEYFIELD( m_bPlayAllRounds, FIELD_BOOLEAN, "play_all_rounds" ), + + DEFINE_KEYFIELD( m_flPartialCapturePointsRate, FIELD_FLOAT, "partial_cap_points_rate" ), + + DEFINE_KEYFIELD( m_flCustomPositionX, FIELD_FLOAT, "custom_position_x" ), + DEFINE_KEYFIELD( m_flCustomPositionY, FIELD_FLOAT, "custom_position_y" ), + +// DEFINE_FIELD( m_ControlPoints, CUtlMap < int , CTeamControlPoint * > ), +// DEFINE_FIELD( m_bFoundPoints, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_ControlPointRounds, CUtlVector < CTeamControlPointRound * > ), +// DEFINE_FIELD( m_iCurrentRoundIndex, FIELD_INTEGER ), +// DEFINE_ARRAY( m_iszTeamBaseIcons, FIELD_STRING, MAX_TEAMS ), +// DEFINE_ARRAY( m_iTeamBaseIcons, FIELD_INTEGER, MAX_TEAMS ), +// DEFINE_FIELD( m_bFirstRoundAfterRestart, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetWinner", InputSetWinner ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetWinnerAndForceCaps", InputSetWinnerAndForceCaps ), + DEFINE_INPUTFUNC( FIELD_VOID, "RoundSpawn", InputRoundSpawn ), + DEFINE_INPUTFUNC( FIELD_VOID, "RoundActivate", InputRoundActivate ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetCapLayout", InputSetCapLayout ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetCapLayoutCustomPositionX", InputSetCapLayoutCustomPositionX ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetCapLayoutCustomPositionY", InputSetCapLayoutCustomPositionY ), + + DEFINE_FUNCTION( CPMThink ), + + DEFINE_OUTPUT( m_OnWonByTeam1, "OnWonByTeam1" ), + DEFINE_OUTPUT( m_OnWonByTeam2, "OnWonByTeam2" ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( team_control_point_master, CTeamControlPointMaster ); + +ConVar mp_time_between_capscoring( "mp_time_between_capscoring", "30", FCVAR_GAMEDLL, "Delay between scoring of owned capture points.", true, 1, false, 0 ); + +// sort function for the list of control_point_rounds (we're sorting them by priority...highest first) +int ControlPointRoundSort( CTeamControlPointRound* const *p1, CTeamControlPointRound* const *p2 ) +{ + // check the priority + if ( (*p2)->GetPriorityValue() > (*p1)->GetPriorityValue() ) + { + return 1; + } + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: init +//----------------------------------------------------------------------------- +CTeamControlPointMaster::CTeamControlPointMaster() +{ + m_flPartialCapturePointsRate = 0.0f; + m_flCustomPositionX = -1.f; + m_flCustomPositionY = -1.f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::Spawn( void ) +{ + Precache(); + + SetTouch( NULL ); + m_bFoundPoints = false; + SetDefLessFunc( m_ControlPoints ); + + m_iCurrentRoundIndex = -1; + m_bFirstRoundAfterRestart = true; + m_flLastOwnershipChangeTime = -1; + + BaseClass::Spawn(); + + if ( g_hControlPointMasters.Find(this) == g_hControlPointMasters.InvalidIndex() ) + { + g_hControlPointMasters.AddToTail( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::UpdateOnRemove( void ) +{ + BaseClass::UpdateOnRemove(); + + g_hControlPointMasters.FindAndRemove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( !Q_strncmp( szKeyName, "team_base_icon_", 15 ) ) + { + int iTeam = atoi(szKeyName+15); + Assert( iTeam >= 0 && iTeam < MAX_TEAMS ); + + m_iszTeamBaseIcons[iTeam] = AllocPooledString(szValue); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::Precache( void ) +{ + for ( int i = 0; i < MAX_TEAMS; i++ ) + { + if ( m_iszTeamBaseIcons[i] != NULL_STRING ) + { + PrecacheMaterial( STRING( m_iszTeamBaseIcons[i] ) ); + m_iTeamBaseIcons[i] = GetMaterialIndex( STRING( m_iszTeamBaseIcons[i] ) ); + Assert( m_iTeamBaseIcons[i] != 0 ); + } + } + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::Activate( void ) +{ + BaseClass::Activate(); + + // Find control points right away. This allows client hud elements to know the + // number & starting state of control points before the game actually starts. + FindControlPoints(); + FindControlPointRounds(); + + SetBaseControlPoints(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::RoundRespawn( void ) +{ + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::Reset( void ) +{ + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::FindControlPoints( void ) +{ + //go through all the points + CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, GetControlPointName() ); + + int numFound = 0; + + while( pEnt ) + { + CTeamControlPoint *pPoint = assert_cast(pEnt); + + if( pPoint->IsActive() && !pPoint->IsMarkedForDeletion() ) + { + int index = pPoint->GetPointIndex(); + + Assert( index >= 0 ); + + if( m_ControlPoints.Find( index ) == m_ControlPoints.InvalidIndex()) + { + DevMsg( 2, "**** Adding control point %s with index %d to control point master\n", pPoint->GetName(), index ); + m_ControlPoints.Insert( index, pPoint ); + numFound++; + } + else + { + Warning( "!!!!\nMultiple control points with the same index, duplicates ignored\n!!!!\n" ); + UTIL_Remove( pPoint ); + } + } + + pEnt = gEntList.FindEntityByClassname( pEnt, GetControlPointName() ); + } + + if( numFound > MAX_CONTROL_POINTS ) + { + Warning( "Too many control points! Max is %d\n", MAX_CONTROL_POINTS ); + } + + //Remap the indeces of the control points so they are 0-based + //====================== + unsigned int j; + + bool bHandled[MAX_CONTROL_POINTS]; + memset( bHandled, 0, sizeof(bHandled) ); + + unsigned int numPoints = m_ControlPoints.Count(); + unsigned int newIndex = 0; + + while( newIndex < numPoints ) + { + //Find the lowest numbered, unhandled point + int lowestIndex = -1; + int lowestValue = 999; + + //find the lowest unhandled index + for( j=0; jGetPointIndex() < lowestValue ) + { + lowestIndex = j; + lowestValue = m_ControlPoints[j]->GetPointIndex(); + } + } + + //Don't examine this point again + bHandled[lowestIndex] = true; + + //Give it its new index + m_ControlPoints[lowestIndex]->SetPointIndex( newIndex ); + newIndex++; + } + + if( m_ControlPoints.Count() == 0 ) + { + Warning( "Error! No control points found in map!\n"); + return false; + } + + // Now setup the objective resource + ObjectiveResource()->SetNumControlPoints( m_ControlPoints.Count() ); + for ( unsigned int i = 0; i < m_ControlPoints.Count(); i++ ) + { + CTeamControlPoint *pPoint = m_ControlPoints[i]; + + int iPointIndex = m_ControlPoints[i]->GetPointIndex(); + + ObjectiveResource()->SetOwningTeam( iPointIndex, pPoint->GetOwner() ); + ObjectiveResource()->SetCPVisible( iPointIndex, pPoint->PointIsVisible() ); + ObjectiveResource()->SetCPPosition( iPointIndex, pPoint->GetAbsOrigin() ); + ObjectiveResource()->SetWarnOnCap( iPointIndex, pPoint->GetWarnOnCap() ); + ObjectiveResource()->SetWarnSound( iPointIndex, pPoint->GetWarnSound() ); + ObjectiveResource()->SetCPGroup( iPointIndex, pPoint->GetCPGroup() ); + for ( int team = 0; team < GetNumberOfTeams(); team++ ) + { + ObjectiveResource()->SetCPIcons( iPointIndex, team, pPoint->GetHudIconIndexForTeam(team) ); + ObjectiveResource()->SetCPOverlays( iPointIndex, team, pPoint->GetHudOverlayIndexForTeam(team) ); + for ( int prevpoint = 0; prevpoint < MAX_PREVIOUS_POINTS; prevpoint++ ) + { + ObjectiveResource()->SetPreviousPoint( iPointIndex, team, prevpoint, pPoint->GetPreviousPointForTeam(team, prevpoint) ); + } + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::SetBaseControlPoints( void ) +{ + for ( int team = 0; team < GetNumberOfTeams(); team++ ) + { + ObjectiveResource()->SetTeamBaseIcons( team, m_iTeamBaseIcons[team] ); + ObjectiveResource()->SetBaseCP( GetBaseControlPoint(team), team ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::FindControlPointRounds( void ) +{ + bool bFoundRounds = false; + + m_ControlPointRounds.RemoveAll(); + CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, GetControlPointRoundName() ); + + while( pEnt ) + { + CTeamControlPointRound *pRound = assert_cast( pEnt ); + + if( pRound && ( m_ControlPointRounds.Find( pRound ) == m_ControlPointRounds.InvalidIndex() ) ) + { + DevMsg( 2, "**** Adding control point round %s to control point master\n", pRound->GetEntityName().ToCStr() ); + m_ControlPointRounds.AddToHead( pRound ); + } + + pEnt = gEntList.FindEntityByClassname( pEnt, GetControlPointRoundName() ); + } + + if ( m_ControlPointRounds.Count() > 0 ) + { + // sort them in our list by priority (highest priority first) + m_ControlPointRounds.Sort( ControlPointRoundSort ); + bFoundRounds = true; + } + + if ( g_pObjectiveResource ) + { + g_pObjectiveResource->SetPlayingMiniRounds( bFoundRounds ); + g_pObjectiveResource->SetCapLayoutInHUD( STRING(m_iszCapLayoutInHUD) ); + g_pObjectiveResource->SetCapLayoutCustomPosition( m_flCustomPositionX, m_flCustomPositionY ); + } + + return bFoundRounds; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::IsInRound( CTeamControlPoint *pPoint ) +{ + // are we playing a round and is this point in the round? + if ( m_ControlPointRounds.Count() > 0 && m_iCurrentRoundIndex != -1 ) + { + return m_ControlPointRounds[m_iCurrentRoundIndex]->IsControlPointInRound( pPoint ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPointMaster::NumPlayableControlPointRounds( void ) +{ + int nRetVal = 0; + + for ( int i = 0 ; i < m_ControlPointRounds.Count() ; ++i ) + { + CTeamControlPointRound *pRound = m_ControlPointRounds[i]; + + if ( pRound ) + { + if ( pRound->IsPlayable() ) + { + // we found one that's playable + nRetVal++; + } + } + } + + return nRetVal; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::SelectSpecificRound( void ) +{ + CTeamControlPointRound *pRound = NULL; + CTeamplayRoundBasedRules *pRules = dynamic_cast( GameRules() ); + + if ( pRules ) + { + if ( pRules->GetRoundToPlayNext() != NULL_STRING ) + { + // do we have the name of a round? + pRound = dynamic_cast( gEntList.FindEntityByName( NULL, STRING( pRules->GetRoundToPlayNext() ) ) ); + + if ( pRound ) + { + if ( ( m_ControlPointRounds.Find( pRound )== m_ControlPointRounds.InvalidIndex() ) || + ( !pRound->IsPlayable() && !pRound->MakePlayable() ) ) + { + pRound = NULL; + } + } + + pRules->SetRoundToPlayNext( NULL_STRING ); + } + } + + // do we have a round to play? + if ( pRound ) + { + m_iCurrentRoundIndex = m_ControlPointRounds.Find( pRound ); + m_ControlPointRounds[m_iCurrentRoundIndex]->SelectedToPlay(); + + if ( pRules ) + { + pRules->SetRoundOverlayDetails(); + } + + FireRoundStartOutput(); + DevMsg( 2, "**** Selected round %s to play\n", m_ControlPointRounds[m_iCurrentRoundIndex]->GetEntityName().ToCStr() ); + + if ( !pRules->IsInWaitingForPlayers() ) + { + UTIL_LogPrintf( "World triggered \"Mini_Round_Selected\" (round \"%s\")\n", m_ControlPointRounds[m_iCurrentRoundIndex]->GetEntityName().ToCStr() ); + UTIL_LogPrintf( "World triggered \"Mini_Round_Start\"\n" ); + } + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::RegisterRoundBeingPlayed( void ) +{ + // let the game rules know what round we're playing + CTeamplayRoundBasedRules *pRules = dynamic_cast( GameRules() ); + if ( pRules ) + { + string_t iszEntityName = m_ControlPointRounds[m_iCurrentRoundIndex]->GetEntityName(); + + pRules->AddPlayedRound( iszEntityName ); + + if ( m_bFirstRoundAfterRestart ) + { + pRules->SetFirstRoundPlayed( iszEntityName ); + m_bFirstRoundAfterRestart = false; + } + } + + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_round_selected" ); + if ( event ) + { + event->SetString( "round", m_ControlPointRounds[m_iCurrentRoundIndex]->GetEntityName().ToCStr() ); + gameeventmanager->FireEvent( event ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::GetControlPointRoundToPlay( void ) +{ + int i = 0; + + // are we trying to pick a specific round? + if ( SelectSpecificRound() ) + { + SetBaseControlPoints(); + RegisterRoundBeingPlayed(); + return true; + } + + // rounds are sorted with the higher priority rounds first + for ( i = 0 ; i < m_ControlPointRounds.Count() ; ++i ) + { + CTeamControlPointRound *pRound = m_ControlPointRounds[i]; + + if ( pRound ) + { + if ( pRound->IsPlayable() ) + { + // we found one that's playable + break; + } + } + } + + if ( i >= m_ControlPointRounds.Count() || m_ControlPointRounds[i] == NULL ) + { + // we didn't find one to play + m_iCurrentRoundIndex = -1; + return false; + } + + // we have a priority value, now we need to randomly pick a round with this priority that's playable + int nPriority = m_ControlPointRounds[i]->GetPriorityValue(); + CUtlVector nRounds; + + CTeamplayRoundBasedRules *pRules = dynamic_cast( GameRules() ); + string_t iszLastRoundPlayed = pRules ? pRules->GetLastPlayedRound() : NULL_STRING; + int iLastRoundPlayed = -1; + + string_t iszFirstRoundPlayed = pRules ? pRules->GetFirstRoundPlayed() : NULL_STRING; + int iFirstRoundPlayed = -1; // after a full restart + + // loop through and find the rounds with this priority value + for ( i = 0 ; i < m_ControlPointRounds.Count() ; ++i ) + { + CTeamControlPointRound *pRound = m_ControlPointRounds[i]; + + if ( pRound ) + { + string_t iszRoundName = pRound->GetEntityName(); + + if ( pRound->IsPlayable() && pRound->GetPriorityValue() == nPriority ) + { + if ( iszLastRoundPlayed == iszRoundName ) // is this the last round we played? + { + iLastRoundPlayed = i; + } + + if ( m_bFirstRoundAfterRestart ) + { + // is this the first round we played after the last full restart? + if ( ( iszFirstRoundPlayed != NULL_STRING ) && ( iszFirstRoundPlayed == iszRoundName ) ) + { + iFirstRoundPlayed = i; + } + } + + nRounds.AddToHead(i); + } + } + } + + if ( nRounds.Count() <= 0 ) + { + // we didn't find one to play + m_iCurrentRoundIndex = -1; + return false; + } + + // if we have more than one and the last played round is in our list, remove it + if ( nRounds.Count() > 1 ) + { + if ( iLastRoundPlayed != -1 ) + { + int elementIndex = nRounds.Find( iLastRoundPlayed ); + nRounds.Remove( elementIndex ); + } + } + + // if this is the first round after a full restart, we still have more than one round in our list, + // and the first played round (after the last full restart) is in our list, remove it + if ( m_bFirstRoundAfterRestart ) + { + if ( nRounds.Count() > 1 ) + { + if ( iFirstRoundPlayed != -1 ) + { + int elementIndex = nRounds.Find( iFirstRoundPlayed ); + nRounds.Remove( elementIndex ); + } + } + } + + // pick one to play but try to avoid picking one that we have recently played if there are other rounds to play + int index = random->RandomInt( 0, nRounds.Count() - 1 ); + + // only need to check this if we have more than one round with this priority value + if ( pRules && nRounds.Count() > 1 ) + { + // keep picking a round until we find one that's not a previously played round + // or until we don't have any more rounds to choose from + while ( pRules->IsPreviouslyPlayedRound( m_ControlPointRounds[ nRounds[ index ] ]->GetEntityName() ) && + nRounds.Count() > 1 ) + { + nRounds.Remove( index ); // we have played this round recently so get it out of the list + index = random->RandomInt( 0, nRounds.Count() - 1 ); + } + } + + // pick one to play and fire its OnSelected output + m_iCurrentRoundIndex = nRounds[ index ]; + m_ControlPointRounds[m_iCurrentRoundIndex]->SelectedToPlay(); + + if ( pRules ) + { + pRules->SetRoundOverlayDetails(); + } + + FireRoundStartOutput(); + DevMsg( 2, "**** Selected round %s to play\n", m_ControlPointRounds[m_iCurrentRoundIndex]->GetEntityName().ToCStr() ); + + if ( !pRules->IsInWaitingForPlayers() ) + { + UTIL_LogPrintf( "World triggered \"Mini_Round_Selected\" (round \"%s\")\n", m_ControlPointRounds[m_iCurrentRoundIndex]->GetEntityName().ToCStr() ); + UTIL_LogPrintf( "World triggered \"Mini_Round_Start\"\n" ); + } + + SetBaseControlPoints(); + RegisterRoundBeingPlayed(); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Called every 0.1 seconds and checks the status of all the control points +// if one team owns them all, it gives points and resets +// Think also gives the time based points at the specified time intervals +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::CPMThink( void ) +{ + if ( m_bDisabled || !TeamplayGameRules()->PointsMayBeCaptured() ) + { + SetContextThink( &CTeamControlPointMaster::CPMThink, gpGlobals->curtime + 0.2, CPM_THINK ); + return; + } + + // If we call this from team_control_point, this function should never + // trigger a win. but we'll leave it here just in case. + CheckWinConditions(); + + // the next time we 'think' + SetContextThink( &CTeamControlPointMaster::CPMThink, gpGlobals->curtime + 0.2, CPM_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::CheckWinConditions( void ) +{ + if ( m_bDisabled ) + return; + + if ( m_ControlPointRounds.Count() > 0 ) + { + if ( m_iCurrentRoundIndex != -1 ) + { + // Check the current round to see if one team is a winner yet + int iWinners = m_ControlPointRounds[m_iCurrentRoundIndex]->CheckWinConditions(); + if ( iWinners != -1 && iWinners >= FIRST_GAME_TEAM ) + { + bool bForceMapReset = ( NumPlayableControlPointRounds() == 0 ); // are there any more rounds to play? + + if ( !bForceMapReset ) + { + // we have more rounds to play + TeamplayGameRules()->SetWinningTeam( iWinners, WINREASON_ALL_POINTS_CAPTURED, bForceMapReset ); + } + else + { + // we have played all of the available rounds + TeamplayGameRules()->SetWinningTeam( iWinners, WINREASON_ALL_POINTS_CAPTURED, bForceMapReset, m_bSwitchTeamsOnWin ); + } + + FireTeamWinOutput( iWinners ); + } + } + } + else + { + // Check that the points aren't all held by one team...if they are + // this will reset the round and will reset all the points + int iWinners = TeamOwnsAllPoints(); + if ( ( m_iInvalidCapWinner != 1 ) && + ( iWinners >= FIRST_GAME_TEAM ) && + ( iWinners != m_iInvalidCapWinner ) ) + { + bool bWinner = true; + +#if defined( TF_DLL) + if ( TFGameRules() && TFGameRules()->IsInKothMode() ) + { + CTeamRoundTimer *pTimer = NULL; + if ( iWinners == TF_TEAM_RED ) + { + pTimer = TFGameRules()->GetRedKothRoundTimer(); + } + else if ( iWinners == TF_TEAM_BLUE ) + { + pTimer = TFGameRules()->GetBlueKothRoundTimer(); + } + + if ( pTimer ) + { + if ( pTimer->GetTimeRemaining() > 0 || TFGameRules()->TimerMayExpire() == false ) + { + bWinner = false; + } + } + } +#endif + if ( bWinner ) + { + TeamplayGameRules()->SetWinningTeam( iWinners, WINREASON_ALL_POINTS_CAPTURED, true, m_bSwitchTeamsOnWin ); + FireTeamWinOutput( iWinners ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputSetWinner( inputdata_t &input ) +{ + int iTeam = input.value.Int(); + InternalSetWinner( iTeam ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputSetWinnerAndForceCaps( inputdata_t &input ) +{ + int iTeam = input.value.Int(); + + // Set all cap points in the current round to be owned by the winning team + for ( unsigned int i = 0; i < m_ControlPoints.Count(); i++ ) + { + CTeamControlPoint *pPoint = m_ControlPoints[i]; + if ( pPoint && (!PlayingMiniRounds() || ObjectiveResource()->IsInMiniRound(pPoint->GetPointIndex()) ) ) + { + pPoint->ForceOwner( iTeam ); + } + } + + InternalSetWinner( iTeam ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InternalSetWinner( int iTeam ) +{ + bool bForceMapReset = true; + + if ( m_ControlPointRounds.Count() > 0 ) + { + // if we're playing rounds and there are more to play, don't do a full reset + bForceMapReset = ( NumPlayableControlPointRounds() == 0 ); + } + + if ( iTeam == TEAM_UNASSIGNED ) + { + TeamplayGameRules()->SetStalemate( STALEMATE_TIMER, bForceMapReset ); + } + else + { + if ( !bForceMapReset ) + { + TeamplayGameRules()->SetWinningTeam( iTeam, WINREASON_ALL_POINTS_CAPTURED, bForceMapReset ); + } + else + { + TeamplayGameRules()->SetWinningTeam( iTeam, WINREASON_ALL_POINTS_CAPTURED, bForceMapReset, m_bSwitchTeamsOnWin ); + } + + FireTeamWinOutput( iTeam ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::HandleRandomOwnerControlPoints( void ) +{ + CUtlVector vecPoints; + CUtlVector vecTeams; + + int i = 0; + + // loop through and find all of the points that want random owners after a full restart + for ( i = 0 ; i < (int)m_ControlPoints.Count() ; i++ ) + { + CTeamControlPoint *pPoint = m_ControlPoints[i]; + + if ( pPoint && pPoint->RandomOwnerOnRestart() ) + { + vecPoints.AddToHead( pPoint ); + vecTeams.AddToHead( pPoint->GetTeamNumber() ); + } + } + + // now loop through and mix up the owners (if we found any points with this flag set) + for ( i = 0 ; i < vecPoints.Count() ; i++ ) + { + CTeamControlPoint *pPoint = vecPoints[i]; + + if ( pPoint ) + { + int index = random->RandomInt( 0, vecTeams.Count() - 1 ); + pPoint->ForceOwner( vecTeams[index] ); + + vecTeams.Remove( index ); + } + } + + vecPoints.RemoveAll(); + vecTeams.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputRoundSpawn( inputdata_t &input ) +{ + //clear out old control points + m_ControlPoints.RemoveAll(); + + //find the control points, and if successful, do CPMThink + if ( FindControlPoints() ) + { +/* if ( m_bFirstRoundAfterRestart ) + { + CTeamplayRoundBasedRules *pRules = dynamic_cast( GameRules() ); + if ( pRules && ( pRules->GetRoundToPlayNext() == NULL_STRING ) ) + { + // we only want to handle the random points if we don't have a specific round to play next + // (prevents points being randomized again after "waiting for players" has finished and we're going to play the same round) + HandleRandomOwnerControlPoints(); + } + } +*/ + SetContextThink( &CTeamControlPointMaster::CPMThink, gpGlobals->curtime + 0.1, CPM_THINK ); + } + + // clear out the old rounds + m_ControlPointRounds.RemoveAll(); + + // find the rounds (if the map has any) + FindControlPointRounds(); + + SetBaseControlPoints(); + + ObjectiveResource()->ResetControlPoints(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputRoundActivate( inputdata_t &input ) +{ + // if we're using mini-rounds and haven't picked one yet, find one to play + if ( PlayingMiniRounds() && GetCurrentRound() == NULL ) + { + GetControlPointRoundToPlay(); + } + + if ( PlayingMiniRounds() ) + { + // Tell the objective resource what control points are in use in the selected mini-round + CTeamControlPointRound *pRound = GetCurrentRound(); + if ( pRound ) + { + for ( unsigned int i = 0; i < m_ControlPoints.Count(); i++ ) + { + CTeamControlPoint *pPoint = m_ControlPoints[i]; + if ( pPoint ) + { + ObjectiveResource()->SetInMiniRound( pPoint->GetPointIndex(), pRound->IsControlPointInRound( pPoint ) ); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputSetCapLayout( inputdata_t &inputdata ) +{ + m_iszCapLayoutInHUD = inputdata.value.StringID(); + g_pObjectiveResource->SetCapLayoutInHUD( STRING(m_iszCapLayoutInHUD) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputSetCapLayoutCustomPositionX( inputdata_t &inputdata ) +{ + m_flCustomPositionX = inputdata.value.Float(); + g_pObjectiveResource->SetCapLayoutCustomPosition( m_flCustomPositionX, m_flCustomPositionY ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputSetCapLayoutCustomPositionY( inputdata_t &inputdata ) +{ + m_flCustomPositionY = inputdata.value.Float(); + g_pObjectiveResource->SetCapLayoutCustomPosition( m_flCustomPositionX, m_flCustomPositionY ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::FireTeamWinOutput( int iWinningTeam ) +{ + // Remap team so that first game team = 1 + switch( iWinningTeam - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnWonByTeam1.FireOutput(this,this); + break; + case 2: + m_OnWonByTeam2.FireOutput(this,this); + break; + default: + Assert(0); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::FireRoundStartOutput( void ) +{ + CTeamControlPointRound *pRound = GetCurrentRound(); + + if ( pRound ) + { + pRound->FireOnStartOutput(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::FireRoundEndOutput( void ) +{ + CTeamControlPointRound *pRound = GetCurrentRound(); + + if ( pRound ) + { + pRound->FireOnEndOutput(); + m_iCurrentRoundIndex = -1; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTeamControlPointMaster::PointLastContestedAt( int point ) +{ + CTeamControlPoint *pPoint = GetControlPoint(point); + if ( pPoint ) + return pPoint->LastContestedAt(); + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: This function returns the team that owns all the cap points. +// If its not the case that one team owns them all, it returns 0. +// CPs are broken into groups. A team can win by owning all flags within a single group. +// +// Can be passed an overriding team. If this is not null, the passed team +// number will be used for that cp. Used to predict if that CP changing would +// win the game. +//----------------------------------------------------------------------------- +int CTeamControlPointMaster::TeamOwnsAllPoints( CTeamControlPoint *pOverridePoint /* = NULL */, int iOverrideNewTeam /* = TEAM_UNASSIGNED */ ) +{ + unsigned int i; + + int iWinningTeam[MAX_CONTROL_POINT_GROUPS]; + + for( i=0;iGetCPGroup(); + int owner = m_ControlPoints[i]->GetOwner(); + + if ( pOverridePoint == m_ControlPoints[i] ) + { + owner = iOverrideNewTeam; + } + + // the first one we find in this group, set the win to true + if ( iWinningTeam[group] == TEAM_INVALID ) + { + iWinningTeam[group] = owner; + } + // unassigned means this group is already contested, move on + else if ( iWinningTeam[group] == TEAM_UNASSIGNED ) + { + continue; + } + // if we find another one in the group that isn't the same owner, set the win to false + else if ( owner != iWinningTeam[group] ) + { + iWinningTeam[group] = TEAM_UNASSIGNED; + } + } + + // report the first win we find as the winner + for ( i=0;i= FIRST_GAME_TEAM ) + return iWinningTeam[i]; + } + + // no wins yet + return TEAM_UNASSIGNED; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::WouldNewCPOwnerWinGame( CTeamControlPoint *pPoint, int iNewOwner ) +{ + return ( TeamOwnsAllPoints( pPoint, iNewOwner ) == iNewOwner ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointMaster::IsBaseControlPoint( int iPointIndex ) +{ + bool retVal = false; + + for ( int iTeam = LAST_SHARED_TEAM+1; iTeam < GetNumberOfTeams(); iTeam++ ) + { + if ( GetBaseControlPoint( iTeam ) == iPointIndex ) + { + retVal = true; + break; + } + } + + return retVal; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the control point for the specified team that's at their end of +// the control point chain. +//----------------------------------------------------------------------------- +int CTeamControlPointMaster::GetBaseControlPoint( int iTeam ) +{ + int iRetVal = -1; + int nLowestValue = 999, nHighestValue = -1; + int iLowestIndex = 0, iHighestIndex = 0; + + for( int i = 0 ; i < (int)m_ControlPoints.Count() ; i++ ) + { + CTeamControlPoint *pPoint = m_ControlPoints[i]; + + int iPointIndex = m_ControlPoints[i]->GetPointIndex(); + + if ( PlayingMiniRounds() && iTeam > LAST_SHARED_TEAM ) + { + if ( IsInRound( pPoint ) ) // is this point in the current round? + { + if ( iPointIndex > nHighestValue ) + { + nHighestValue = iPointIndex; + iHighestIndex = i; + } + + if ( iPointIndex < nLowestValue ) + { + nLowestValue = iPointIndex; + iLowestIndex = i; + } + } + } + else + { + if ( pPoint->GetDefaultOwner() != iTeam ) + { + continue; + } + + // If it's the first or the last point, it's their base + if ( iPointIndex == 0 || iPointIndex == (((int)m_ControlPoints.Count())-1) ) + { + iRetVal = iPointIndex; + break; + } + } + } + + if ( PlayingMiniRounds() && iTeam > LAST_SHARED_TEAM ) + { + if ( nLowestValue != 999 && nHighestValue != -1 ) + { + CTeamControlPoint *pLowestPoint = m_ControlPoints[iLowestIndex]; + CTeamControlPoint *pHighestPoint = m_ControlPoints[iHighestIndex]; + + // which point is owned by this team? + if ( ( pLowestPoint->GetDefaultOwner() == iTeam && pHighestPoint->GetDefaultOwner() == iTeam ) || // if the same team owns both, take the highest value to be the last point + ( pHighestPoint->GetDefaultOwner() == iTeam ) ) + { + iRetVal = nHighestValue; + } + else if ( pLowestPoint->GetDefaultOwner() == iTeam ) + { + iRetVal = nLowestValue; + } + } + } + + return iRetVal; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputEnable( inputdata_t &input ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::InputDisable( inputdata_t &input ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPointMaster::GetNumPointsOwnedByTeam( int iTeam ) +{ + int nCount = 0; + + for( int i = 0 ; i < (int)m_ControlPoints.Count() ; i++ ) + { + CTeamControlPoint *pPoint = m_ControlPoints[i]; + + if ( pPoint && ( pPoint->GetTeamNumber() == iTeam ) ) + { + nCount++; + } + } + + return nCount; +} + +//----------------------------------------------------------------------------- +// Purpose: returns how many more mini-rounds it will take for specified team +// to win, if they keep winning every mini-round +//----------------------------------------------------------------------------- +int CTeamControlPointMaster::CalcNumRoundsRemaining( int iTeam ) +{ + // To determine how many rounds remain for a given team if it consistently wins mini-rounds, we have to + // simulate forward each mini-round and track the control point ownership that would result + + // vector of control points the team owns in our forward-simulation + CUtlVector vecControlPointsOwned; + + // start with all the control points the team currently owns + FOR_EACH_MAP_FAST( m_ControlPoints, iControlPoint ) + { + if ( m_ControlPoints[iControlPoint]->GetOwner() == iTeam ) + { + vecControlPointsOwned.AddToTail( m_ControlPoints[iControlPoint] ); + } + } + + int iRoundsRemaining = 0; + + // keep simulating what will happen next if this team keeps winning, until + // it owns all the control points in the map + while ( vecControlPointsOwned.Count() < (int) m_ControlPoints.Count() ) + { + iRoundsRemaining++; + + // choose the next highest-priority round that is playable + for ( int i = 0 ; i < m_ControlPointRounds.Count() ; ++i ) + { + CTeamControlPointRound *pRound = m_ControlPointRounds[i]; + if ( !pRound ) + continue; + + // see if one team owns all control points in this round + int iRoundOwningTeam = TEAM_INVALID; + int iControlPoint; + for ( iControlPoint = 0; iControlPoint < pRound->m_ControlPoints.Count(); iControlPoint++ ) + { + CTeamControlPoint *pControlPoint = pRound->m_ControlPoints[iControlPoint]; + int iControlPointOwningTeam = TEAM_INVALID; + + // determine who owns this control point. + // First, check our simulated ownership + if ( vecControlPointsOwned.InvalidIndex() != vecControlPointsOwned.Find( pControlPoint ) ) + { + // This team has won this control point in forward simulation + iControlPointOwningTeam = iTeam; + } + else + { + // use actual control point ownership + iControlPointOwningTeam = pControlPoint->GetOwner(); + } + + if ( 0 == iControlPoint ) + { + // if this is the first control point, assign ownership to the team that owns this control point + iRoundOwningTeam = iControlPointOwningTeam; + } + else + { + // for all other control points, if the control point ownership does not match other control points, reset + // round ownership to no team + if ( iRoundOwningTeam != iControlPointOwningTeam ) + { + iRoundOwningTeam = TEAM_INVALID; + } + } + } + // this round is playable if all control points are not owned by one team (or owned by a team that can't win by capping them) + bool bPlayable = ( ( iRoundOwningTeam < FIRST_GAME_TEAM ) || ( pRound->GetInvalidCapWinner() == 1 ) || ( iRoundOwningTeam == pRound->GetInvalidCapWinner() ) ); + if ( !bPlayable ) + continue; + + // Pretend this team played and won this round. It now owns all control points from this round. Add all the + // control points from this round that are not already own the owned list to the owned list + int iNewControlPointsOwned = 0; + FOR_EACH_VEC( pRound->m_ControlPoints, iControlPoint ) + { + CTeamControlPoint *pControlPoint = pRound->m_ControlPoints[iControlPoint]; + if ( vecControlPointsOwned.InvalidIndex() == vecControlPointsOwned.Find( pControlPoint ) ) + { + vecControlPointsOwned.AddToTail( pControlPoint ); + iNewControlPointsOwned++; + } + } + // sanity check: team being simulated should be owning at least one more new control point per round, or they're not making progress + Assert( iNewControlPointsOwned > 0 ); + + // now go back and pick the next playable round (if any) given the control points this team now owns, + // repeat until all control points are owned. The number of iterations it takes is the # of rounds remaining + // for this team to win. + break; + } + } + + return iRoundsRemaining; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CTeamControlPointMaster::GetPartialCapturePointRate( void ) +{ + return m_flPartialCapturePointsRate; +} + +/* +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointMaster::ListRounds( void ) +{ + if ( PlayingMiniRounds() ) + { + ConMsg( "Rounds in this map:\n\n" ); + + for ( int i = 0; i < m_ControlPointRounds.Count() ; ++i ) + { + CTeamControlPointRound* pRound = m_ControlPointRounds[i]; + + if ( pRound ) + { + const char *pszName = STRING( pRound->GetEntityName() ); + ConMsg( "%s\n", pszName ); + } + } + } + else + { + ConMsg( "* No rounds in this map *\n" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void cc_ListRounds( void ) +{ + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + pMaster->ListRounds(); + } +} + +static ConCommand listrounds( "listrounds", cc_ListRounds, "List the rounds for the current map", FCVAR_CHEAT ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void cc_PlayRound( const CCommand& args ) +{ + if ( args.ArgC() > 1 ) + { + CTeamplayRoundBasedRules *pRules = dynamic_cast( GameRules() ); + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + + if ( pRules && pMaster ) + { + if ( pMaster->PlayingMiniRounds() ) + { + // did we get the name of a round? + CTeamControlPointRound *pRound = dynamic_cast( gEntList.FindEntityByName( NULL, args[1] ) ); + + if ( pRound ) + { + pRules->SetRoundToPlayNext( pRound->GetEntityName() ); + mp_restartgame.SetValue( 5 ); + } + else + { + ConMsg( "* Round \"%s\" not found in this map *\n", args[1] ); + } + } + } + } + else + { + ConMsg( "Usage: playround < round name >\n" ); + } +} + +static ConCommand playround( "playround", cc_PlayRound, "Play the selected round\n\tArgument: {round name given by \"listrounds\" command}", FCVAR_CHEAT ); +*/ \ No newline at end of file diff --git a/sp/src/game/server/team_control_point_master.h b/sp/src/game/server/team_control_point_master.h new file mode 100644 index 00000000..b6a7d35d --- /dev/null +++ b/sp/src/game/server/team_control_point_master.h @@ -0,0 +1,214 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef TEAM_CONTROL_POINT_MASTER_H +#define TEAM_CONTROL_POINT_MASTER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "utlmap.h" +#include "team.h" +#include "teamplay_gamerules.h" +#include "team_control_point.h" +#include "trigger_area_capture.h" +#include "team_objectiveresource.h" +#include "team_control_point_round.h" + +#define CPM_THINK "CTeamControlPointMasterCPMThink" +#define CPM_POSTINITTHINK "CTeamControlPointMasterCPMPostInitThink" + +//----------------------------------------------------------------------------- +// Purpose: One ControlPointMaster is spawned per level. Shortly after spawning it detects all the Control +// points in the map and puts them into the m_ControlPoints. From there it detects the state +// where all points are captured and resets them if necessary It gives points every time interval to +// the owners of the points +//----------------------------------------------------------------------------- +class CTeamControlPointMaster : public CBaseEntity +{ + DECLARE_CLASS( CTeamControlPointMaster, CBaseEntity ); + + // Derived, game-specific control point masters must override these functions +public: + CTeamControlPointMaster(); + + // Used to find game specific entities + virtual const char *GetControlPointName( void ) { return "team_control_point"; } + virtual const char *GetControlPointRoundName( void ) { return "team_control_point_round"; } + +public: + virtual void Spawn( void ); + virtual void UpdateOnRemove( void ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual void Precache( void ); + virtual void Activate( void ); + + void RoundRespawn( void ); + void Reset( void ); + + int GetNumPoints( void ){ return m_ControlPoints.Count(); } + int GetNumPointsOwnedByTeam( int iTeam ); + int CalcNumRoundsRemaining( int iTeam ); + + bool IsActive( void ) { return ( m_bDisabled == false ); } + + void FireTeamWinOutput( int iWinningTeam ); + + bool IsInRound( CTeamControlPoint *pPoint ); + void CheckWinConditions( void ); + + bool WouldNewCPOwnerWinGame( CTeamControlPoint *pPoint, int iNewOwner ); + + int GetBaseControlPoint( int iTeam ); + bool IsBaseControlPoint( int iPointIndex ); + + bool PlayingMiniRounds( void ){ return ( m_ControlPointRounds.Count() > 0 ); } + + float PointLastContestedAt( int point ); + CTeamControlPoint *GetControlPoint( int point ) + { + Assert( point >= 0 ); + Assert( point < MAX_CONTROL_POINTS ); + + for ( unsigned int i = 0; i < m_ControlPoints.Count(); i++ ) + { + CTeamControlPoint *pPoint = m_ControlPoints[i]; + if ( pPoint && pPoint->GetPointIndex() == point ) + return pPoint; + } + + return NULL; + } + + CTeamControlPointRound *GetCurrentRound( void ) + { + if ( !PlayingMiniRounds() || m_iCurrentRoundIndex == -1 ) + { + return NULL; + } + + return m_ControlPointRounds[m_iCurrentRoundIndex]; + } + + string_t GetRoundToUseAfterRestart( void ) + { + int nCurrentPriority = -1; + int nHighestPriority = -1; + + string_t nRetVal = NULL_STRING; + + if ( PlayingMiniRounds() && GetCurrentRound() ) + { + nCurrentPriority = GetCurrentRound()->GetPriorityValue(); + nHighestPriority = GetHighestRoundPriorityValue(); + + // if the current round has the highest priority, then use it again + if ( nCurrentPriority == nHighestPriority ) + { + nRetVal = GetCurrentRound()->GetEntityName(); + } + } + + return nRetVal; + } + + void FireRoundStartOutput( void ); + void FireRoundEndOutput( void ); + + bool ShouldScorePerCapture( void ){ return m_bScorePerCapture; } + bool ShouldPlayAllControlPointRounds( void ){ return m_bPlayAllRounds; } + int NumPlayableControlPointRounds( void ); // checks to see if there are any more rounds to play (but doesn't actually "get" one to play) + +// void ListRounds( void ); + + float GetPartialCapturePointRate( void ); + + void SetLastOwnershipChangeTime( float m_flTime ) { m_flLastOwnershipChangeTime = m_flTime; } + float GetLastOwnershipChangeTime( void ) { return m_flLastOwnershipChangeTime; } + +private: + void EXPORT CPMThink( void ); + + void SetBaseControlPoints( void ); + int TeamOwnsAllPoints( CTeamControlPoint *pOverridePoint = NULL, int iOverrideNewTeam = TEAM_UNASSIGNED ); + + bool FindControlPoints( void ); // look in the map to find active control points + bool FindControlPointRounds( void ); // look in the map to find active control point rounds + bool GetControlPointRoundToPlay( void ); // gets the next round we should play + bool SelectSpecificRound( void ); // selects a specific round to play + + int GetHighestRoundPriorityValue( void ) + { + int nRetVal = -1; + + // rounds are sorted with the higher priority rounds first + for ( int i = 0 ; i < m_ControlPointRounds.Count() ; ++i ) + { + CTeamControlPointRound *pRound = m_ControlPointRounds[i]; + + if ( pRound ) + { + if ( pRound->GetPriorityValue() > nRetVal ) + { + nRetVal = pRound->GetPriorityValue(); + } + } + } + + return nRetVal; + } + + void RegisterRoundBeingPlayed( void ); + + CUtlMap m_ControlPoints; + + bool m_bFoundPoints; // true when the control points have been found and the array is initialized + + CUtlVector m_ControlPointRounds; + int m_iCurrentRoundIndex; + + DECLARE_DATADESC(); + + bool m_bDisabled; + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + void InputRoundSpawn( inputdata_t &inputdata ); + void InputRoundActivate( inputdata_t &inputdata ); + void InputSetWinner( inputdata_t &inputdata ); + void InputSetWinnerAndForceCaps( inputdata_t &inputdata ); + void InputSetCapLayout( inputdata_t &inputdata ); + void InputSetCapLayoutCustomPositionX( inputdata_t &inputdata ); + void InputSetCapLayoutCustomPositionY( inputdata_t &inputdata ); + + void InternalSetWinner( int iTeam ); + + void HandleRandomOwnerControlPoints( void ); + + string_t m_iszTeamBaseIcons[MAX_TEAMS]; + int m_iTeamBaseIcons[MAX_TEAMS]; + string_t m_iszCapLayoutInHUD; + + float m_flCustomPositionX; + float m_flCustomPositionY; + + int m_iInvalidCapWinner; + bool m_bSwitchTeamsOnWin; + bool m_bScorePerCapture; + bool m_bPlayAllRounds; + + bool m_bFirstRoundAfterRestart; + + COutputEvent m_OnWonByTeam1; + COutputEvent m_OnWonByTeam2; + + float m_flPartialCapturePointsRate; + float m_flLastOwnershipChangeTime; +}; + +extern CUtlVector< CHandle > g_hControlPointMasters; + +#endif // TEAM_CONTROL_POINT_MASTER_H diff --git a/sp/src/game/server/team_control_point_round.cpp b/sp/src/game/server/team_control_point_round.cpp new file mode 100644 index 00000000..8a9a7e7c --- /dev/null +++ b/sp/src/game/server/team_control_point_round.cpp @@ -0,0 +1,414 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "team_control_point_master.h" +#include "teamplayroundbased_gamerules.h" +#include "team_control_point_round.h" + +#if defined ( TF_DLL ) +#include "tf_gamerules.h" +#endif + +BEGIN_DATADESC( CTeamControlPointRound ) + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_KEYFIELD( m_iszCPNames, FIELD_STRING, "cpr_cp_names" ), + DEFINE_KEYFIELD( m_nPriority, FIELD_INTEGER, "cpr_priority" ), + DEFINE_KEYFIELD( m_iInvalidCapWinner, FIELD_INTEGER, "cpr_restrict_team_cap_win" ), + DEFINE_KEYFIELD( m_iszPrintName, FIELD_STRING, "cpr_printname" ), +// DEFINE_FIELD( m_ControlPoints, CUtlVector < CHandle < CTeamControlPoint > > ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + DEFINE_INPUTFUNC( FIELD_VOID, "RoundSpawn", InputRoundSpawn ), + + DEFINE_OUTPUT( m_OnStart, "OnStart" ), + DEFINE_OUTPUT( m_OnEnd, "OnEnd" ), + DEFINE_OUTPUT( m_OnWonByTeam1, "OnWonByTeam1" ), + DEFINE_OUTPUT( m_OnWonByTeam2, "OnWonByTeam2" ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( team_control_point_round, CTeamControlPointRound ); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::Spawn( void ) +{ + SetTouch( NULL ); + + BaseClass::Spawn(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::Activate( void ) +{ + BaseClass::Activate(); + + FindControlPoints(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::FindControlPoints( void ) +{ + // Let out control point masters know that the round has started + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + + if ( pMaster ) + { + // go through all the points + CBaseEntity *pEnt = gEntList.FindEntityByClassname( NULL, pMaster->GetControlPointName() ); + + while( pEnt ) + { + CTeamControlPoint *pPoint = assert_cast(pEnt); + + if ( pPoint ) + { + const char *pString = STRING( m_iszCPNames ); + const char *pName = STRING( pPoint->GetEntityName() ); + + // HACK to work around a problem with cp_a being returned for an entity name with cp_A + const char *pos = Q_stristr( pString, pName ); + if ( pos ) + { + int len = Q_strlen( STRING( pPoint->GetEntityName() ) ); + if ( *(pos + len) == ' ' || *(pos + len) == '\0' ) + { + if( m_ControlPoints.Find( pPoint ) == m_ControlPoints.InvalidIndex() ) + { + DevMsg( 2, "Adding control point %s to control point round %s\n", pPoint->GetEntityName().ToCStr(), GetEntityName().ToCStr() ); + m_ControlPoints.AddToHead( pPoint ); + } + } + } + } + + pEnt = gEntList.FindEntityByClassname( pEnt, pMaster->GetControlPointName() ); + } + } + + if( m_ControlPoints.Count() == 0 ) + { + Warning( "Error! No control points found in map for team_game_round %s!\n", GetEntityName().ToCStr() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Check that the points aren't all held by one team if they are +// this will reset the round and will reset all the points +//----------------------------------------------------------------------------- +int CTeamControlPointRound::CheckWinConditions( void ) +{ + int iWinners = TeamOwnsAllPoints(); + if ( ( m_iInvalidCapWinner != 1 ) && + ( iWinners >= FIRST_GAME_TEAM ) && + ( iWinners != m_iInvalidCapWinner ) ) + { + bool bWinner = true; + +#if defined( TF_DLL) + if ( TFGameRules() && TFGameRules()->IsInKothMode() ) + { + CTeamRoundTimer *pTimer = NULL; + if ( iWinners == TF_TEAM_RED ) + { + pTimer = TFGameRules()->GetRedKothRoundTimer(); + } + else if ( iWinners == TF_TEAM_BLUE ) + { + pTimer = TFGameRules()->GetBlueKothRoundTimer(); + } + + if ( pTimer ) + { + if ( pTimer->GetTimeRemaining() > 0 || TFGameRules()->TimerMayExpire() == false ) + { + bWinner = false; + } + } + } +#endif + if ( bWinner ) + { + FireTeamWinOutput( iWinners ); + return iWinners; + } + } + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::FireTeamWinOutput( int iWinningTeam ) +{ + // Remap team so that first game team = 1 + switch( iWinningTeam - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnWonByTeam1.FireOutput( this, this ); + break; + case 2: + m_OnWonByTeam2.FireOutput( this, this ); + break; + default: + Assert(0); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CTeamControlPointRound::GetPointOwner( int point ) +{ + Assert( point >= 0 ); + Assert( point < MAX_CONTROL_POINTS ); + + CTeamControlPoint *pPoint = m_ControlPoints[point]; + + if ( pPoint ) + return pPoint->GetOwner(); + + return TEAM_UNASSIGNED; +} + +//----------------------------------------------------------------------------- +// Purpose: This function returns the team that owns all the cap points. +// If its not the case that one team owns them all, it returns 0. +// +// Can be passed an overriding team. If this is not null, the passed team +// number will be used for that cp. Used to predict if that CP changing would +// win the game. +//----------------------------------------------------------------------------- +int CTeamControlPointRound::TeamOwnsAllPoints( CTeamControlPoint *pOverridePoint /* = NULL */, int iOverrideNewTeam /* = TEAM_UNASSIGNED */ ) +{ + int i; + + int iWinningTeam[MAX_CONTROL_POINT_GROUPS]; + + for( i = 0 ; i < MAX_CONTROL_POINT_GROUPS ; i++ ) + { + iWinningTeam[i] = TEAM_INVALID; + } + + // if TEAM_INVALID, haven't found a flag for this group yet + // if TEAM_UNASSIGNED, the group is still being contested + + // for each control point + for( i = 0 ; i < m_ControlPoints.Count() ; i++ ) + { + int group = m_ControlPoints[i]->GetCPGroup(); + int owner = m_ControlPoints[i]->GetOwner(); + + if ( pOverridePoint == m_ControlPoints[i] ) + { + owner = iOverrideNewTeam; + } + + // the first one we find in this group, set the win to true + if ( iWinningTeam[group] == TEAM_INVALID ) + { + iWinningTeam[group] = owner; + } + // unassigned means this group is already contested, move on + else if ( iWinningTeam[group] == TEAM_UNASSIGNED ) + { + continue; + } + // if we find another one in the group that isn't the same owner, set the win to false + else if ( owner != iWinningTeam[group] ) + { + iWinningTeam[group] = TEAM_UNASSIGNED; + } + } + + // report the first win we find as the winner + for ( i = 0 ; i < MAX_CONTROL_POINT_GROUPS ; i++ ) + { + if ( iWinningTeam[i] >= FIRST_GAME_TEAM ) + return iWinningTeam[i]; + } + + // no wins yet + return TEAM_UNASSIGNED; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointRound::WouldNewCPOwnerWinGame( CTeamControlPoint *pPoint, int iNewOwner ) +{ + return ( TeamOwnsAllPoints( pPoint, iNewOwner ) == iNewOwner ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::InputEnable( inputdata_t &input ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::InputDisable( inputdata_t &input ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::InputRoundSpawn( inputdata_t &input ) +{ + // clear out old control points + m_ControlPoints.RemoveAll(); + + // find the control points + FindControlPoints(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::SetupSpawnPoints( void ) +{ + CTeamplayRoundBasedRules *pRules = TeamplayRoundBasedRules(); + + if ( pRules ) + { + pRules->SetupSpawnPointsForRound(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::SelectedToPlay( void ) +{ + SetupSpawnPoints(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::FireOnStartOutput( void ) +{ + m_OnStart.FireOutput( this, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamControlPointRound::FireOnEndOutput( void ) +{ + m_OnEnd.FireOutput( this, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointRound::IsControlPointInRound( CTeamControlPoint *pPoint ) +{ + if ( !pPoint ) + { + return false; + } + + return ( m_ControlPoints.Find( pPoint ) != m_ControlPoints.InvalidIndex() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointRound::IsPlayable( void ) +{ + int iWinners = TeamOwnsAllPoints(); + + if ( m_iInvalidCapWinner == 1 ) // neither team can win this round by capping + { + return true; + } + + if ( ( iWinners >= FIRST_GAME_TEAM ) && + ( iWinners != m_iInvalidCapWinner ) ) + { + return false; // someone has already won this round + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTeamControlPointRound::MakePlayable( void ) +{ + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + if ( !IsPlayable() ) + { + // we need to try switching the owners of the teams to make this round playable + for ( int iTeam = FIRST_GAME_TEAM ; iTeam < GetNumberOfTeams() ; iTeam++ ) + { + for ( int iControlPoint = 0 ; iControlPoint < m_ControlPoints.Count() ; iControlPoint++ ) + { + if ( ( !pMaster->IsBaseControlPoint( m_ControlPoints[iControlPoint]->GetPointIndex() ) ) && // this is NOT the base point for one of the teams (we don't want to assign the base to the wrong team) + ( !WouldNewCPOwnerWinGame( m_ControlPoints[iControlPoint], iTeam ) ) ) // making this change would make this round playable + { + // need to find the trigger area associated with this point + for ( int iObj=0; iObj( ITriggerAreaCaptureAutoList::AutoList()[iObj] ); + if ( pArea->TeamCanCap( iTeam ) ) + { + CHandle hPoint = pArea->GetControlPoint(); + if ( hPoint == m_ControlPoints[iControlPoint] ) + { + // found! + pArea->ForceOwner( iTeam ); // this updates the trigger_area *and* the control_point + return true; + } + } + } + } + } + } + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: returns the first point found that the given team owns +//----------------------------------------------------------------------------- +CHandle CTeamControlPointRound::GetPointOwnedBy( int iTeam ) +{ + for( int i = 0 ; i < m_ControlPoints.Count() ; i++ ) + { + if ( m_ControlPoints[i]->GetOwner() == iTeam ) + { + return m_ControlPoints[i]; + } + } + + return NULL; +} diff --git a/sp/src/game/server/team_control_point_round.h b/sp/src/game/server/team_control_point_round.h new file mode 100644 index 00000000..06cb33ad --- /dev/null +++ b/sp/src/game/server/team_control_point_round.h @@ -0,0 +1,83 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef TEAM_CONTROL_POINT_ROUND_H +#define TEAM_CONTROL_POINT_ROUND_H +#ifdef _WIN32 +#pragma once +#endif + +#include "utlmap.h" +#include "team.h" +#include "teamplay_gamerules.h" +#include "team_control_point.h" +#include "trigger_area_capture.h" +#include "team_objectiveresource.h" + + +class CTeamControlPointRound : public CBaseEntity +{ + DECLARE_CLASS( CTeamControlPointRound, CBaseEntity ); + +public: + virtual void Spawn( void ); + virtual void Activate( void ); + + bool IsDisabled( void ){ return m_bDisabled; } + + int GetPointOwner( int point ); +// int CountAdvantageFlags( int team ); + bool WouldNewCPOwnerWinGame( CTeamControlPoint *pPoint, int iNewOwner ); + + void FireTeamWinOutput( int iWinningTeam ); + + void SelectedToPlay( void ); + + int CheckWinConditions( void ); // returns the team number of the team that's won, or returns -1 if no winner + + int GetPriorityValue( void ) const { return m_nPriority; } + + bool IsPlayable( void ); + bool MakePlayable( void ); + bool IsControlPointInRound( CTeamControlPoint *pPoint ); + + void FireOnStartOutput( void ); + void FireOnEndOutput( void ); + + inline const char *GetName( void ) { return STRING(m_iszPrintName); } + + CHandle GetPointOwnedBy( int iTeam ); + + bool RoundOwnedByTeam( int iTeam ){ return ( TeamOwnsAllPoints() == iTeam ); } + int GetInvalidCapWinner() { return m_iInvalidCapWinner; } + + CUtlVector< CHandle > m_ControlPoints; + +private: + void FindControlPoints( void ); //look in the map to find the control points for this round + void SetupSpawnPoints( void ); + int TeamOwnsAllPoints( CTeamControlPoint *pOverridePoint = NULL, int iOverrideNewTeam = TEAM_UNASSIGNED ); + + DECLARE_DATADESC(); + + bool m_bDisabled; + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + void InputRoundSpawn( inputdata_t &inputdata ); + + string_t m_iszCPNames; + int m_nPriority; + int m_iInvalidCapWinner; + string_t m_iszPrintName; + + COutputEvent m_OnStart; + COutputEvent m_OnEnd; + COutputEvent m_OnWonByTeam1; + COutputEvent m_OnWonByTeam2; +}; + +#endif // TEAM_CONTROL_POINT_ROUND_H diff --git a/sp/src/game/server/team_objectiveresource.cpp b/sp/src/game/server/team_objectiveresource.cpp new file mode 100644 index 00000000..aaa9c5c9 --- /dev/null +++ b/sp/src/game/server/team_objectiveresource.cpp @@ -0,0 +1,573 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: An entity that networks the state of the game's objectives. +// May contain data for objectives that aren't used by your mod, but +// the extra data will never be networked as long as it's zeroed out. +// +//============================================================================= +#include "cbase.h" +#include "team_objectiveresource.h" +#include "shareddefs.h" +#include +#include "team.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define CAPHUD_PARITY_BITS 6 +#define CAPHUD_PARITY_MASK ((1<curtime + LAZY_UPDATE_TIME ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::ObjectiveThink( void ) +{ + SetNextThink( gpGlobals->curtime + LAZY_UPDATE_TIME ); + + for ( int i = 0; i < m_iNumControlPoints; i++ ) + { + if ( m_iCappingTeam[i] ) + { + m_flLazyCapPerc.Set( i, m_flCapPercentages[i] ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: The objective resource is always transmitted to clients +//----------------------------------------------------------------------------- +int CBaseTeamObjectiveResource::UpdateTransmitState() +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: Round is starting, reset state +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::ResetControlPoints( void ) +{ + for ( int i=0; i < MAX_CONTROL_POINTS; i++ ) + { + m_iCappingTeam.Set( i, TEAM_UNASSIGNED ); + m_iTeamInZone.Set( i, TEAM_UNASSIGNED ); + m_bInMiniRound.Set( i, true ); + + for ( int team = 0; team < MAX_CONTROL_POINT_TEAMS; team++ ) + { + m_iNumTeamMembers.Set( TEAM_ARRAY( i, team ), 0.0f ); + } + } + + UpdateCapHudElement(); + m_bControlPointsReset = !m_bControlPointsReset; +} + +//----------------------------------------------------------------------------- +// Purpose: Data setting functions +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetNumControlPoints( int num ) +{ + Assert( num <= MAX_CONTROL_POINTS ); + m_iNumControlPoints = num; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPIcons( int index, int iTeam, int iIcon ) +{ + AssertValidIndex(index); + m_iTeamIcons.Set( TEAM_ARRAY( index, iTeam ), iIcon ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPOverlays( int index, int iTeam, int iIcon ) +{ + AssertValidIndex(index); + m_iTeamOverlays.Set( TEAM_ARRAY( index, iTeam ), iIcon ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetTeamBaseIcons( int iTeam, int iBaseIcon ) +{ + Assert( iTeam >= 0 && iTeam < MAX_TEAMS ); + m_iTeamBaseIcons.Set( iTeam, iBaseIcon ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPPosition( int index, const Vector& vPosition ) +{ + AssertValidIndex(index); + m_vCPPositions.Set( index, vPosition ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPVisible( int index, bool bVisible ) +{ + AssertValidIndex(index); + m_bCPIsVisible.Set( index, bVisible ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetWarnOnCap( int index, int iWarnLevel ) +{ + AssertValidIndex(index); + m_iWarnOnCap.Set( index, iWarnLevel ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetWarnSound( int index, string_t iszSound ) +{ + AssertValidIndex(index); + m_iszWarnSound.Set( index, iszSound ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPGroup( int index, int iCPGroup ) +{ + AssertValidIndex(index); + m_iCPGroup.Set( index, iCPGroup ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPRequiredCappers( int index, int iTeam, int iReqPlayers ) +{ + AssertValidIndex(index); + m_iTeamReqCappers.Set( TEAM_ARRAY( index, iTeam ), iReqPlayers ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPCapTime( int index, int iTeam, float flTime ) +{ + AssertValidIndex(index); + m_flTeamCapTime.Set( TEAM_ARRAY( index, iTeam ), flTime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPCapPercentage( int index, float flTime ) +{ + AssertValidIndex(index); + m_flCapPercentages[index] = flTime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CBaseTeamObjectiveResource::GetCPCapPercentage( int index ) +{ + AssertValidIndex(index); + return m_flCapPercentages[index]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPUnlockTime( int index, float flTime ) +{ + AssertValidIndex(index); + m_flUnlockTimes.Set( index, flTime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPTimerTime( int index, float flTime ) +{ + AssertValidIndex(index); + m_flCPTimerTimes.Set( index, flTime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPCapTimeScalesWithPlayers( int index, bool bScales ) +{ + AssertValidIndex(index); + m_bCPCapRateScalesWithPlayers.Set( index, bScales ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetTeamCanCap( int index, int iTeam, bool bCanCap ) +{ + AssertValidIndex(index); + m_bTeamCanCap.Set( TEAM_ARRAY( index, iTeam ), bCanCap ); + UpdateCapHudElement(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetBaseCP( int index, int iTeam ) +{ + Assert( iTeam < MAX_TEAMS ); + m_iBaseControlPoints.Set( iTeam, index ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetPreviousPoint( int index, int iTeam, int iPrevIndex, int iPrevPoint ) +{ + AssertValidIndex(index); + Assert( iPrevIndex >= 0 && iPrevIndex < MAX_PREVIOUS_POINTS ); + int iIntIndex = iPrevIndex + (index * MAX_PREVIOUS_POINTS) + (iTeam * MAX_CONTROL_POINTS * MAX_PREVIOUS_POINTS); + m_iPreviousPoints.Set( iIntIndex, iPrevPoint ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseTeamObjectiveResource::GetPreviousPointForPoint( int index, int team, int iPrevIndex ) +{ + AssertValidIndex(index); + Assert( iPrevIndex >= 0 && iPrevIndex < MAX_PREVIOUS_POINTS ); + int iIntIndex = iPrevIndex + (index * MAX_PREVIOUS_POINTS) + (team * MAX_CONTROL_POINTS * MAX_PREVIOUS_POINTS); + return m_iPreviousPoints[ iIntIndex ]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseTeamObjectiveResource::TeamCanCapPoint( int index, int team ) +{ + AssertValidIndex(index); + return m_bTeamCanCap[ TEAM_ARRAY( index, team ) ]; +} + +//----------------------------------------------------------------------------- +// Purpose: Data setting functions +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetNumPlayers( int index, int team, int iNumPlayers ) +{ + AssertValidIndex(index); + m_iNumTeamMembers.Set( TEAM_ARRAY( index, team ), iNumPlayers ); + UpdateCapHudElement(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::StartCap( int index, int team ) +{ + AssertValidIndex(index); + if ( m_iCappingTeam.Get( index ) != team ) + { + m_iCappingTeam.Set( index, team ); + UpdateCapHudElement(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetOwningTeam( int index, int team ) +{ + AssertValidIndex(index); + m_iOwner.Set( index, team ); + + // clear the capper + m_iCappingTeam.Set( index, TEAM_UNASSIGNED ); + UpdateCapHudElement(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCappingTeam( int index, int team ) +{ + AssertValidIndex(index); + if ( m_iCappingTeam.Get( index ) != team ) + { + m_iCappingTeam.Set( index, team ); + UpdateCapHudElement(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetTeamInZone( int index, int team ) +{ + AssertValidIndex(index); + if ( m_iTeamInZone.Get( index ) != team ) + { + m_iTeamInZone.Set( index, team ); + UpdateCapHudElement(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCapBlocked( int index, bool bBlocked ) +{ + AssertValidIndex(index); + if ( m_bBlocked.Get( index ) != bBlocked ) + { + m_bBlocked.Set( index, bBlocked ); + UpdateCapHudElement(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseTeamObjectiveResource::GetOwningTeam( int index ) +{ + if ( index >= m_iNumControlPoints ) + return TEAM_UNASSIGNED; + + return m_iOwner[index]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::UpdateCapHudElement( void ) +{ + m_iUpdateCapHudParity = (m_iUpdateCapHudParity + 1) & CAPHUD_PARITY_MASK; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetTrainPathDistance( int index, float flDistance ) +{ + AssertValidIndex(index); + + m_flPathDistance.Set( index, flDistance ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetCPLocked( int index, bool bLocked ) +{ + // This assert always fires on map load and interferes with daily development + //AssertValidIndex(index); + m_bCPLocked.Set( index, bLocked ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTeamObjectiveResource::SetTrackAlarm( int index, bool bAlarm ) +{ + Assert( index < TEAM_TRAIN_MAX_TEAMS ); + m_bTrackAlarm.Set( index, bAlarm ); +} diff --git a/sp/src/game/server/team_objectiveresource.h b/sp/src/game/server/team_objectiveresource.h new file mode 100644 index 00000000..8d07294f --- /dev/null +++ b/sp/src/game/server/team_objectiveresource.h @@ -0,0 +1,240 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef TEAM_OBJECTIVERESOURCE_H +#define TEAM_OBJECTIVERESOURCE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "shareddefs.h" + +#define TEAM_ARRAY( index, team ) (index + (team * MAX_CONTROL_POINTS)) + +//----------------------------------------------------------------------------- +// Purpose: An entity that networks the state of the game's objectives. +// May contain data for objectives that aren't used by your mod, but +// the extra data will never be networked as long as it's zeroed out. +//----------------------------------------------------------------------------- +class CBaseTeamObjectiveResource : public CBaseEntity +{ + DECLARE_CLASS( CBaseTeamObjectiveResource, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CBaseTeamObjectiveResource(); + ~CBaseTeamObjectiveResource(); + + virtual void Spawn( void ); + virtual int UpdateTransmitState(void); + + virtual void ObjectiveThink( void ); + + //-------------------------------------------------------------------- + // CONTROL POINT DATA + //-------------------------------------------------------------------- +public: + void ResetControlPoints( void ); + + // Data functions, called to set up the state at the beginning of a round + void SetNumControlPoints( int num ); + int GetNumControlPoints( void ) { return m_iNumControlPoints; } + void SetCPIcons( int index, int iTeam, int iIcon ); + void SetCPOverlays( int index, int iTeam, int iIcon ); + void SetTeamBaseIcons( int iTeam, int iBaseIcon ); + void SetCPPosition( int index, const Vector& vPosition ); + void SetCPVisible( int index, bool bVisible ); + void SetCPRequiredCappers( int index, int iTeam, int iReqPlayers ); + void SetCPCapTime( int index, int iTeam, float flTime ); + void SetCPCapPercentage( int index, float flTime ); + float GetCPCapPercentage( int index ); + void SetTeamCanCap( int index, int iTeam, bool bCanCap ); + void SetBaseCP( int index, int iTeam ); + void SetPreviousPoint( int index, int iTeam, int iPrevIndex, int iPrevPoint ); + int GetPreviousPointForPoint( int index, int team, int iPrevIndex ); + bool TeamCanCapPoint( int index, int team ); + void SetCapLayoutInHUD( const char *pszLayout ) { Q_strncpy(m_pszCapLayoutInHUD.GetForModify(), pszLayout, MAX_CAPLAYOUT_LENGTH ); } + void SetCapLayoutCustomPosition( float flPositionX, float flPositionY ) { m_flCustomPositionX = flPositionX; m_flCustomPositionY = flPositionY; } + void SetWarnOnCap( int index, int iWarnLevel ); + void SetWarnSound( int index, string_t iszSound ); + void SetCPGroup( int index, int iCPGroup ); + void SetCPLocked( int index, bool bLocked ); + void SetTrackAlarm( int index, bool bAlarm ); + void SetCPUnlockTime( int index, float flTime ); + void SetCPTimerTime( int index, float flTime ); + void SetCPCapTimeScalesWithPlayers( int index, bool bScales ); + + // State functions, called many times + void SetNumPlayers( int index, int team, int iNumPlayers ); + void StartCap( int index, int team ); + void SetOwningTeam( int index, int team ); + void SetCappingTeam( int index, int team ); + void SetTeamInZone( int index, int team ); + void SetCapBlocked( int index, bool bBlocked ); + int GetOwningTeam( int index ); + + void AssertValidIndex( int index ) + { + Assert( 0 <= index && index <= MAX_CONTROL_POINTS && index < m_iNumControlPoints ); + } + + int GetBaseControlPointForTeam( int iTeam ) + { + Assert( iTeam < MAX_TEAMS ); + return m_iBaseControlPoints[iTeam]; + } + + int GetCappingTeam( int index ) + { + if ( index >= m_iNumControlPoints ) + return TEAM_UNASSIGNED; + + return m_iCappingTeam[index]; + } + + void SetTimerInHUD( CBaseEntity *pTimer ) + { + m_iTimerToShowInHUD = pTimer ? pTimer->entindex() : 0; + } + + + void SetStopWatchTimer( CBaseEntity *pTimer ) + { + m_iStopWatchTimer = pTimer ? pTimer->entindex() : 0; + } + + int GetTimerInHUD( void ) { return m_iTimerToShowInHUD; } + + // Mini-rounds data + void SetPlayingMiniRounds( bool bPlayingMiniRounds ){ m_bPlayingMiniRounds = bPlayingMiniRounds; } + bool PlayingMiniRounds( void ){ return m_bPlayingMiniRounds; } + void SetInMiniRound( int index, bool bInRound ) { m_bInMiniRound.Set( index, bInRound ); } + bool IsInMiniRound( int index ) { return m_bInMiniRound[index]; } + + void UpdateCapHudElement( void ); + + // Train Path data + void SetTrainPathDistance( int index, float flDistance ); + + bool GetCPLocked( int index ) + { + Assert( index < m_iNumControlPoints ); + return m_bCPLocked[index]; + } + + void ResetHillData( int team ) + { + if ( team < TEAM_TRAIN_MAX_TEAMS ) + { + m_nNumNodeHillData.Set( team, 0 ); + + int nNumEntriesPerTeam = TEAM_TRAIN_MAX_HILLS * TEAM_TRAIN_FLOATS_PER_HILL; + int iStartingIndex = team * nNumEntriesPerTeam; + for ( int i = 0 ; i < nNumEntriesPerTeam ; i++ ) + { + m_flNodeHillData.Set( iStartingIndex + i, 0 ); + } + + iStartingIndex = team * TEAM_TRAIN_MAX_HILLS; + for ( int i = 0; i < TEAM_TRAIN_MAX_HILLS; i++ ) + { + m_bHillIsDownhill.Set( iStartingIndex + i, 0 ); + } + } + } + + void SetHillData( int team, float flStart, float flEnd, bool bDownhill ) + { + if ( team < TEAM_TRAIN_MAX_TEAMS ) + { + int index = ( m_nNumNodeHillData[team] * TEAM_TRAIN_FLOATS_PER_HILL ) + ( team * TEAM_TRAIN_MAX_HILLS * TEAM_TRAIN_FLOATS_PER_HILL ); + if ( index < TEAM_TRAIN_HILLS_ARRAY_SIZE - 1 ) // - 1 because we want to add 2 entries + { + m_flNodeHillData.Set( index, flStart ); + m_flNodeHillData.Set( index + 1, flEnd ); + + if ( m_nNumNodeHillData[team] < TEAM_TRAIN_MAX_HILLS ) + { + m_bHillIsDownhill.Set( m_nNumNodeHillData[team] + ( team * TEAM_TRAIN_MAX_HILLS ), bDownhill ); + } + + m_nNumNodeHillData.Set( team, m_nNumNodeHillData[team] + 1); + } + } + } + +private: + CNetworkVar( int, m_iTimerToShowInHUD ); + CNetworkVar( int, m_iStopWatchTimer ); + + CNetworkVar( int, m_iNumControlPoints ); + CNetworkVar( bool, m_bPlayingMiniRounds ); + CNetworkVar( bool, m_bControlPointsReset ); + CNetworkVar( int, m_iUpdateCapHudParity ); + + // data variables + CNetworkArray( Vector, m_vCPPositions, MAX_CONTROL_POINTS ); + CNetworkArray( int, m_bCPIsVisible, MAX_CONTROL_POINTS ); + CNetworkArray( float, m_flLazyCapPerc, MAX_CONTROL_POINTS ); + CNetworkArray( int, m_iTeamIcons, MAX_CONTROL_POINTS * MAX_CONTROL_POINT_TEAMS ); + CNetworkArray( int, m_iTeamOverlays, MAX_CONTROL_POINTS * MAX_CONTROL_POINT_TEAMS ); + CNetworkArray( int, m_iTeamReqCappers, MAX_CONTROL_POINTS * MAX_CONTROL_POINT_TEAMS ); + CNetworkArray( float, m_flTeamCapTime, MAX_CONTROL_POINTS * MAX_CONTROL_POINT_TEAMS ); + CNetworkArray( int, m_iPreviousPoints, MAX_CONTROL_POINTS * MAX_CONTROL_POINT_TEAMS * MAX_PREVIOUS_POINTS ); + CNetworkArray( bool, m_bTeamCanCap, MAX_CONTROL_POINTS * MAX_CONTROL_POINT_TEAMS ); + CNetworkArray( int, m_iTeamBaseIcons, MAX_TEAMS ); + CNetworkArray( int, m_iBaseControlPoints, MAX_TEAMS ); + CNetworkArray( bool, m_bInMiniRound, MAX_CONTROL_POINTS ); + CNetworkArray( int, m_iWarnOnCap, MAX_CONTROL_POINTS ); + CNetworkArray( string_t, m_iszWarnSound, MAX_CONTROL_POINTS ); + CNetworkArray( float, m_flPathDistance, MAX_CONTROL_POINTS ); + CNetworkArray( bool, m_bCPLocked, MAX_CONTROL_POINTS ); + CNetworkArray( float, m_flUnlockTimes, MAX_CONTROL_POINTS ); + CNetworkArray( float, m_flCPTimerTimes, MAX_CONTROL_POINTS ); + + // change when players enter/exit an area + CNetworkArray( int, m_iNumTeamMembers, MAX_CONTROL_POINTS * MAX_CONTROL_POINT_TEAMS ); + + // changes when a cap starts. start and end times are calculated on client + CNetworkArray( int, m_iCappingTeam, MAX_CONTROL_POINTS ); + + CNetworkArray( int, m_iTeamInZone, MAX_CONTROL_POINTS ); + CNetworkArray( bool, m_bBlocked, MAX_CONTROL_POINTS ); + + // changes when a point is successfully captured + CNetworkArray( int, m_iOwner, MAX_CONTROL_POINTS ); + CNetworkArray( bool, m_bCPCapRateScalesWithPlayers, MAX_CONTROL_POINTS ); + + // describes how to lay out the cap points in the hud + CNetworkString( m_pszCapLayoutInHUD, MAX_CAPLAYOUT_LENGTH ); + + // custom screen position for the cap points in the hud + CNetworkVar( float, m_flCustomPositionX ); + CNetworkVar( float, m_flCustomPositionY ); + + // the groups the points belong to + CNetworkArray( int, m_iCPGroup, MAX_CONTROL_POINTS ); + + // Not networked, because the client recalculates it + float m_flCapPercentages[ MAX_CONTROL_POINTS ]; + + // hill data for multi-escort payload maps + CNetworkArray( int, m_nNumNodeHillData, TEAM_TRAIN_MAX_TEAMS ); + CNetworkArray( float, m_flNodeHillData, TEAM_TRAIN_HILLS_ARRAY_SIZE ); + + CNetworkArray( bool, m_bTrackAlarm, TEAM_TRAIN_MAX_TEAMS ); + CNetworkArray( bool, m_bHillIsDownhill, TEAM_TRAIN_MAX_HILLS*TEAM_TRAIN_MAX_TEAMS ); +}; + +extern CBaseTeamObjectiveResource *g_pObjectiveResource; + +inline CBaseTeamObjectiveResource *ObjectiveResource() +{ + return g_pObjectiveResource; +} + +#endif // TEAM_OBJECTIVERESOURCE_H diff --git a/sp/src/game/server/team_spawnpoint.cpp b/sp/src/game/server/team_spawnpoint.cpp new file mode 100644 index 00000000..ab3699e4 --- /dev/null +++ b/sp/src/game/server/team_spawnpoint.cpp @@ -0,0 +1,133 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Team spawnpoint handling +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "entitylist.h" +#include "entityoutput.h" +#include "player.h" +#include "eventqueue.h" +#include "gamerules.h" +#include "team_spawnpoint.h" +#include "team.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( info_player_teamspawn, CTeamSpawnPoint ); + +BEGIN_DATADESC( CTeamSpawnPoint ) + + // keys + DEFINE_KEYFIELD( m_iDisabled, FIELD_INTEGER, "StartDisabled" ), + + // input functions + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + + // outputs + DEFINE_OUTPUT( m_OnPlayerSpawn, "OnPlayerSpawn" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Attach this spawnpoint to it's team +//----------------------------------------------------------------------------- +void CTeamSpawnPoint::Activate( void ) +{ + BaseClass::Activate(); + if ( GetTeamNumber() > 0 && GetTeamNumber() <= MAX_TEAMS ) + { + GetGlobalTeam( GetTeamNumber() )->AddSpawnpoint( this ); + } + else + { + Warning( "info_player_teamspawn with invalid team number: %d\n", GetTeamNumber() ); + UTIL_Remove( this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Is this spawnpoint ready for a player to spawn in? +//----------------------------------------------------------------------------- +bool CTeamSpawnPoint::IsValid( CBasePlayer *pPlayer ) +{ + CBaseEntity *ent = NULL; + for ( CEntitySphereQuery sphere( GetAbsOrigin(), 128 ); ( ent = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) + { + // if ent is a client, don't spawn on 'em + CBaseEntity *plent = ent; + if ( plent && plent->IsPlayer() && plent != pPlayer ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamSpawnPoint::InputEnable( inputdata_t &inputdata ) +{ + m_iDisabled = FALSE; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTeamSpawnPoint::InputDisable( inputdata_t &inputdata ) +{ + m_iDisabled = TRUE; +} + + +//=========================================================================================================== +// VEHICLE SPAWNPOINTS +//=========================================================================================================== +LINK_ENTITY_TO_CLASS( info_vehicle_groundspawn, CTeamVehicleSpawnPoint ); + +BEGIN_DATADESC( CTeamVehicleSpawnPoint ) + + // outputs + DEFINE_OUTPUT( m_OnVehicleSpawn, "OnVehicleSpawn" ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Is this spawnpoint ready for a vehicle to spawn in? +//----------------------------------------------------------------------------- +bool CTeamVehicleSpawnPoint::IsValid( void ) +{ + CBaseEntity *ent = NULL; + for ( CEntitySphereQuery sphere( GetAbsOrigin(), 128 ); ( ent = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) + { + // if ent is a client, don't spawn on 'em + CBaseEntity *plent = ent; + if ( plent && plent->IsPlayer() ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Attach this spawnpoint to it's team +//----------------------------------------------------------------------------- +void CTeamVehicleSpawnPoint::Activate( void ) +{ + BaseClass::Activate(); + if ( GetTeamNumber() > 0 && GetTeamNumber() <= MAX_TEAMS ) + { + // Don't add vehicle spawnpoints to the team for now + //GetGlobalTeam( GetTeamNumber() )->AddSpawnpoint( this ); + } + else + { + Warning( "info_vehicle_groundspawn with invalid team number: %d\n", GetTeamNumber() ); + UTIL_Remove( this ); + } +} diff --git a/sp/src/game/server/team_spawnpoint.h b/sp/src/game/server/team_spawnpoint.h new file mode 100644 index 00000000..76187ff8 --- /dev/null +++ b/sp/src/game/server/team_spawnpoint.h @@ -0,0 +1,56 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Team spawnpoint entity +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef TF_TEAMSPAWNPOINT_H +#define TF_TEAMSPAWNPOINT_H +#pragma once + +#include "baseentity.h" +#include "entityoutput.h" + +class CTeam; + +//----------------------------------------------------------------------------- +// Purpose: points at which the player can spawn, restricted by team +//----------------------------------------------------------------------------- +class CTeamSpawnPoint : public CPointEntity +{ +public: + DECLARE_CLASS( CTeamSpawnPoint, CPointEntity ); + + void Activate( void ); + virtual bool IsValid( CBasePlayer *pPlayer ); + + COutputEvent m_OnPlayerSpawn; + +protected: + int m_iDisabled; + + // Input handlers + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + DECLARE_DATADESC(); +}; + +//----------------------------------------------------------------------------- +// Purpose: points at which vehicles can spawn, restricted by team +//----------------------------------------------------------------------------- +class CTeamVehicleSpawnPoint : public CTeamSpawnPoint +{ + DECLARE_CLASS( CTeamVehicleSpawnPoint, CTeamSpawnPoint ); +public: + void Activate( void ); + bool IsValid( void ); + + COutputEvent m_OnVehicleSpawn; + + DECLARE_DATADESC(); +}; + + +#endif // TF_TEAMSPAWNPOINT_H diff --git a/sp/src/game/server/team_train_watcher.cpp b/sp/src/game/server/team_train_watcher.cpp new file mode 100644 index 00000000..b8ef36c3 --- /dev/null +++ b/sp/src/game/server/team_train_watcher.cpp @@ -0,0 +1,1555 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "cbase.h" +#include "team_train_watcher.h" +#include "team_control_point.h" +#include "trains.h" +#include "team_objectiveresource.h" +#include "teamplayroundbased_gamerules.h" +#include "team_control_point.h" +#include "team_control_point_master.h" +#include "engine/IEngineSound.h" +#include "soundenvelope.h" +#include "mp_shareddefs.h" +#include "props.h" +#include "physconstraint.h" + +#ifdef TF_DLL +#include "tf_shareddefs.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" +/* +#define TWM_FIRSTSTAGEOUTCOME01 "Announcer.PLR_FirstStageOutcome01" +#define TWM_FIRSTSTAGEOUTCOME02 "Announcer.PLR_FirstStageOutcome02" +#define TWM_RACEGENERAL01 "Announcer.PLR_RaceGeneral01" +#define TWM_RACEGENERAL02 "Announcer.PLR_RaceGeneral02" +#define TWM_RACEGENERAL03 "Announcer.PLR_RaceGeneral03" +#define TWM_RACEGENERAL04 "Announcer.PLR_RaceGeneral04" +#define TWM_RACEGENERAL05 "Announcer.PLR_RaceGeneral05" +#define TWM_RACEGENERAL08 "Announcer.PLR_RaceGeneral08" +#define TWM_RACEGENERAL06 "Announcer.PLR_RaceGeneral06" +#define TWM_RACEGENERAL07 "Announcer.PLR_RaceGeneral07" +#define TWM_RACEGENERAL09 "Announcer.PLR_RaceGeneral09" +#define TWM_RACEGENERAL12 "Announcer.PLR_RaceGeneral12" +#define TWM_RACEGENERAL13 "Announcer.PLR_RaceGeneral13" +#define TWM_RACEGENERAL14 "Announcer.PLR_RaceGeneral14" +#define TWM_RACEGENERAL15 "Announcer.PLR_RaceGeneral15" +#define TWM_RACEGENERAL10 "Announcer.PLR_RaceGeneral10" +#define TWM_RACEGENERAL11 "Announcer.PLR_RaceGeneral11" +#define TWM_SECONDSTAGEOUTCOME01 "Announcer.PLR_SecondStageOutcome01" +#define TWM_SECONDSTAGEOUTCOME04 "Announcer.PLR_SecondStageOutcome04" +#define TWM_SECONDSTAGEOUTCOME02 "Announcer.PLR_SecondStageOutcome02" +#define TWM_SECONDSTAGEOUTCOME03 "Announcer.PLR_SecondStageOutcome03" +#define TWM_FINALSTAGEOUTCOME01 "Announcer.PLR_FinalStageOutcome01" +#define TWM_FINALSTAGEOUTCOME02 "Announcer.PLR_FinalStageOutcome02" +#define TWM_FINALSTAGESTART01 "Announcer.PLR_FinalStageStart01" +#define TWM_FINALSTAGESTART04 "Announcer.PLR_FinalStageStart04" +#define TWM_FINALSTAGESTART08 "Announcer.PLR_FinalStageStart08" +#define TWM_FINALSTAGESTART09 "Announcer.PLR_FinalStageStart09" +#define TWM_FINALSTAGESTART07 "Announcer.PLR_FinalStageStart07" +#define TWM_FINALSTAGESTART02 "Announcer.PLR_FinalStageStart02" +#define TWM_FINALSTAGESTART03 "Announcer.PLR_FinalStageStart03" +#define TWM_FINALSTAGESTART05 "Announcer.PLR_FinalStageStart05" +#define TWM_FINALSTAGESTART06 "Announcer.PLR_FinalStageStart06" + +EHANDLE g_hTeamTrainWatcherMaster = NULL; +*/ +#define MAX_ALARM_TIME_NO_RECEDE 18 // max amount of time to play the alarm if the train isn't going to recede + +BEGIN_DATADESC( CTeamTrainWatcher ) + + // Inputs. + DEFINE_INPUTFUNC( FIELD_VOID, "RoundActivate", InputRoundActivate ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetNumTrainCappers", InputSetNumTrainCappers ), + DEFINE_INPUTFUNC( FIELD_VOID, "OnStartOvertime", InputOnStartOvertime ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedForwardModifier", InputSetSpeedForwardModifier ), + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetTrainRecedeTime", InputSetTrainRecedeTime ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetTrainCanRecede", InputSetTrainCanRecede ), + + // Outputs + DEFINE_OUTPUT( m_OnTrainStartRecede, "OnTrainStartRecede" ), + + // key + DEFINE_KEYFIELD( m_iszTrain, FIELD_STRING, "train" ), + DEFINE_KEYFIELD( m_iszStartNode, FIELD_STRING, "start_node" ), + DEFINE_KEYFIELD( m_iszGoalNode, FIELD_STRING, "goal_node" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[0], FIELD_STRING, "linked_pathtrack_1" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[0], FIELD_STRING, "linked_cp_1" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[1], FIELD_STRING, "linked_pathtrack_2" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[1], FIELD_STRING, "linked_cp_2" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[2], FIELD_STRING, "linked_pathtrack_3" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[2], FIELD_STRING, "linked_cp_3" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[3], FIELD_STRING, "linked_pathtrack_4" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[3], FIELD_STRING, "linked_cp_4" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[4], FIELD_STRING, "linked_pathtrack_5" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[4], FIELD_STRING, "linked_cp_5" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[5], FIELD_STRING, "linked_pathtrack_6" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[5], FIELD_STRING, "linked_cp_6" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[6], FIELD_STRING, "linked_pathtrack_7" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[6], FIELD_STRING, "linked_cp_7" ), + + DEFINE_KEYFIELD( m_iszLinkedPathTracks[7], FIELD_STRING, "linked_pathtrack_8" ), + DEFINE_KEYFIELD( m_iszLinkedCPs[7], FIELD_STRING, "linked_cp_8" ), + + DEFINE_KEYFIELD( m_bTrainCanRecede, FIELD_BOOLEAN, "train_can_recede" ), + + DEFINE_KEYFIELD( m_bHandleTrainMovement, FIELD_BOOLEAN, "handle_train_movement" ), + + // can be up to 8 links + + // min speed for train hud speed levels + DEFINE_KEYFIELD( m_flSpeedLevels[0], FIELD_FLOAT, "hud_min_speed_level_1" ), + DEFINE_KEYFIELD( m_flSpeedLevels[1], FIELD_FLOAT, "hud_min_speed_level_2" ), + DEFINE_KEYFIELD( m_flSpeedLevels[2], FIELD_FLOAT, "hud_min_speed_level_3" ), + + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_KEYFIELD( m_iszSparkName, FIELD_STRING, "env_spark_name" ), + + DEFINE_KEYFIELD( m_flSpeedForwardModifier, FIELD_FLOAT, "speed_forward_modifier" ), + + DEFINE_KEYFIELD( m_nTrainRecedeTime, FIELD_INTEGER, "train_recede_time" ), + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST(CTeamTrainWatcher, DT_TeamTrainWatcher) + + SendPropFloat( SENDINFO( m_flTotalProgress ), 11, 0, 0.0f, 1.0f ), + SendPropInt( SENDINFO( m_iTrainSpeedLevel ), 4 ), + SendPropTime( SENDINFO( m_flRecedeTime ) ), + SendPropInt( SENDINFO( m_nNumCappers ) ), +#ifdef GLOWS_ENABLE + SendPropEHandle( SENDINFO( m_hGlowEnt ) ), +#endif // GLOWS_ENABLE + +END_SEND_TABLE() + + +LINK_ENTITY_TO_CLASS( team_train_watcher, CTeamTrainWatcher ); + +IMPLEMENT_AUTO_LIST( ITFTeamTrainWatcher ); + +/* +LINK_ENTITY_TO_CLASS( team_train_watcher_master, CTeamTrainWatcherMaster ); +PRECACHE_REGISTER( team_train_watcher_master ); + +CTeamTrainWatcherMaster::CTeamTrainWatcherMaster() +{ + m_pBlueWatcher = NULL; + m_pRedWatcher = NULL; + + m_flBlueProgress = 0.0f; + m_flRedProgress = 0.0f; + + ListenForGameEvent( "teamplay_round_start" ); + ListenForGameEvent( "teamplay_round_win" ); +} + +CTeamTrainWatcherMaster::~CTeamTrainWatcherMaster() +{ + if ( g_hTeamTrainWatcherMaster.Get() == this ) + { + g_hTeamTrainWatcherMaster = NULL; + } +} + +void CTeamTrainWatcherMaster::Precache( void ) +{ + PrecacheScriptSound( TWM_FIRSTSTAGEOUTCOME01 ); + PrecacheScriptSound( TWM_FIRSTSTAGEOUTCOME02 ); + PrecacheScriptSound( TWM_RACEGENERAL01 ); + PrecacheScriptSound( TWM_RACEGENERAL02 ); + PrecacheScriptSound( TWM_RACEGENERAL03 ); + PrecacheScriptSound( TWM_RACEGENERAL04 ); + PrecacheScriptSound( TWM_RACEGENERAL05 ); + PrecacheScriptSound( TWM_RACEGENERAL08 ); + PrecacheScriptSound( TWM_RACEGENERAL06 ); + PrecacheScriptSound( TWM_RACEGENERAL07 ); + PrecacheScriptSound( TWM_RACEGENERAL09 ); + PrecacheScriptSound( TWM_RACEGENERAL12 ); + PrecacheScriptSound( TWM_RACEGENERAL13 ); + PrecacheScriptSound( TWM_RACEGENERAL14 ); + PrecacheScriptSound( TWM_RACEGENERAL15 ); + PrecacheScriptSound( TWM_RACEGENERAL10 ); + PrecacheScriptSound( TWM_RACEGENERAL11 ); + PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME01 ); + PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME04 ); + PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME02 ); + PrecacheScriptSound( TWM_SECONDSTAGEOUTCOME03 ); + PrecacheScriptSound( TWM_FINALSTAGEOUTCOME01 ); + PrecacheScriptSound( TWM_FINALSTAGEOUTCOME02 ); + PrecacheScriptSound( TWM_FINALSTAGESTART01 ); + PrecacheScriptSound( TWM_FINALSTAGESTART04 ); + PrecacheScriptSound( TWM_FINALSTAGESTART08 ); + PrecacheScriptSound( TWM_FINALSTAGESTART09 ); + PrecacheScriptSound( TWM_FINALSTAGESTART07 ); + PrecacheScriptSound( TWM_FINALSTAGESTART02 ); + PrecacheScriptSound( TWM_FINALSTAGESTART03 ); + PrecacheScriptSound( TWM_FINALSTAGESTART05 ); + PrecacheScriptSound( TWM_FINALSTAGESTART06 ); + + BaseClass::Precache(); +} + +bool CTeamTrainWatcherMaster::FindTrainWatchers( void ) +{ + m_pBlueWatcher = NULL; + m_pRedWatcher = NULL; + + // find the train_watchers for this round + CTeamTrainWatcher *pTrainWatcher = (CTeamTrainWatcher *)gEntList.FindEntityByClassname( NULL, "team_train_watcher" ); + while ( pTrainWatcher ) + { + if ( pTrainWatcher->IsDisabled() == false ) + { + if ( pTrainWatcher->GetTeamNumber() == TF_TEAM_BLUE ) + { + m_pBlueWatcher = pTrainWatcher; + } + else if ( pTrainWatcher->GetTeamNumber() == TF_TEAM_RED ) + { + m_pRedWatcher = pTrainWatcher; + } + } + + pTrainWatcher = (CTeamTrainWatcher *)gEntList.FindEntityByClassname( pTrainWatcher, "team_train_watcher" ); + } + + return ( m_pBlueWatcher && m_pRedWatcher ); +} + +void CTeamTrainWatcherMaster::TWMThink( void ) +{ + if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->State_Get() != GR_STATE_RND_RUNNING ) + { + // the next time we 'think' + SetContextThink( &CTeamTrainWatcherMaster::TWMThink, gpGlobals->curtime + 0.2, TWMASTER_THINK ); + return; + } + + + + // the next time we 'think' + SetContextThink( &CTeamTrainWatcherMaster::TWMThink, gpGlobals->curtime + 0.2, TWMASTER_THINK ); +} + +void CTeamTrainWatcherMaster::FireGameEvent( IGameEvent *event ) +{ + const char *eventname = event->GetName();` + + if ( FStrEq( "teamplay_round_start", eventname ) ) + { + if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->HasMultipleTrains() ) + { + if ( FindTrainWatchers() ) + { + // we found train watchers so start thinking + SetContextThink( &CTeamTrainWatcherMaster::TWMThink, gpGlobals->curtime + 0.2, TWMASTER_THINK ); + } + } + } + else if ( FStrEq( "teamplay_round_win", eventname ) ) + { + if ( TeamplayRoundBasedRules() ) + { + int iWinningTeam = event->GetInt( "team" ); + int iLosingTeam = ( iWinningTeam == TF_TEAM_RED ) ? TF_TEAM_BLUE : TF_TEAM_RED; + bool bFullRound = event->GetBool( "full_round" ); + + CTeamRecipientFilter filterWinner( iWinningTeam, true ); + CTeamRecipientFilter filterLoser( iLosingTeam, true ); + + if ( bFullRound ) + { + EmitSound( filterWinner, entindex(), TWM_FINALSTAGEOUTCOME01 ); + EmitSound( filterLoser, entindex(), TWM_FINALSTAGEOUTCOME02 ); + } + else + { + EmitSound( filterWinner, entindex(), TWM_FIRSTSTAGEOUTCOME01 ); + EmitSound( filterLoser, entindex(), TWM_FIRSTSTAGEOUTCOME02 ); + } + } + } +} +*/ +CTeamTrainWatcher::CTeamTrainWatcher() +{ + m_bDisabled = false; + m_flRecedeTime = 0; + m_bWaitingToRecede = false; + m_bCapBlocked = false; + + m_flNextSpeakForwardConceptTime = 0; + m_hAreaCap = NULL; + + m_bTrainCanRecede = true; + m_bAlarmPlayed = false; + m_pAlarm = NULL; + m_flAlarmEndTime = -1; + + m_bHandleTrainMovement = false; + m_flSpeedForwardModifier = 1.0f; + m_iCurrentHillType = HILL_TYPE_NONE; + m_flCurrentSpeed = 0.0f; + m_bReceding = false; + + m_flTrainDistanceFromStart = 0.0f; + + m_nTrainRecedeTime = 0; + +#ifdef GLOWS_ENABLE + m_hGlowEnt.Set( NULL ); +#endif // GLOWS_ENABLE + +#ifdef TF_DLL + ChangeTeam( TF_TEAM_BLUE ); +#else + ChangeTeam( TEAM_UNASSIGNED ); +#endif +/* + // create a CTeamTrainWatcherMaster entity + if ( g_hTeamTrainWatcherMaster.Get() == NULL ) + { + g_hTeamTrainWatcherMaster = CreateEntityByName( "team_train_watcher_master" ); + } +*/ + ListenForGameEvent( "path_track_passed" ); +} + +CTeamTrainWatcher::~CTeamTrainWatcher() +{ + m_Sparks.Purge(); +} + +void CTeamTrainWatcher::UpdateOnRemove( void ) +{ + StopCaptureAlarm(); + + BaseClass::UpdateOnRemove(); +} + +int CTeamTrainWatcher::UpdateTransmitState() +{ + if ( m_bDisabled ) + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } + + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +void CTeamTrainWatcher::InputRoundActivate( inputdata_t &inputdata ) +{ + StopCaptureAlarm(); + + if ( !m_bDisabled ) + { + WatcherActivate(); + } +} + +void CTeamTrainWatcher::InputEnable( inputdata_t &inputdata ) +{ + StopCaptureAlarm(); + + m_bDisabled = false; + + WatcherActivate(); + + UpdateTransmitState(); +} + +void CTeamTrainWatcher::InputDisable( inputdata_t &inputdata ) +{ + StopCaptureAlarm(); + + m_bDisabled = true; + SetContextThink( NULL, 0, TW_THINK ); + + m_bWaitingToRecede = false; + + m_Sparks.Purge(); + +#ifdef GLOWS_ENABLE + m_hGlowEnt.Set( NULL ); +#endif // GLOWS_ENABLE + + // if we're moving the train, let's shut it down + if ( m_bHandleTrainMovement ) + { + m_flCurrentSpeed = 0.0f; + + if ( m_hTrain ) + { + m_hTrain->SetSpeedDirAccel( m_flCurrentSpeed ); + } + + // handle the sparks under the train + HandleSparks( false ); + } + + UpdateTransmitState(); +} + +ConVar tf_escort_recede_time( "tf_escort_recede_time", "30", 0, "", true, 0, false, 0 ); +ConVar tf_escort_recede_time_overtime( "tf_escort_recede_time_overtime", "5", 0, "", true, 0, false, 0 ); + +void CTeamTrainWatcher::FireGameEvent( IGameEvent *event ) +{ + if ( IsDisabled() || !m_bHandleTrainMovement ) + return; + + const char *pszEventName = event->GetName(); + if ( FStrEq( pszEventName, "path_track_passed" ) ) + { + int iIndex = event->GetInt( "index" ); + CPathTrack *pNode = dynamic_cast< CPathTrack* >( UTIL_EntityByIndex( iIndex ) ); + + if ( pNode ) + { + bool bHandleEvent = false; + CPathTrack *pTempNode = m_hStartNode.Get(); + + // is this a node in the track we're watching? + while ( pTempNode ) + { + if ( pTempNode == pNode ) + { + bHandleEvent = true; + break; + } + + pTempNode = pTempNode->GetNext(); + } + + if ( bHandleEvent ) + { + // If we're receding and we've hit a node but the next node (going backwards) is disabled + // the train is going to stop (like at the base of a downhill section) when we start forward + // again we won't pass this node again so don't change our hill state based on this node. + if ( m_bReceding ) + { + if ( pNode->GetPrevious() && pNode->GetPrevious()->IsDisabled() ) + { + return; + } + } + + int iHillType = pNode->GetHillType(); + bool bUpdate = ( m_iCurrentHillType != iHillType ); + + if ( !bUpdate ) + { + // the hill settings are the same, but are we leaving an uphill or downhill segment? + if ( m_iCurrentHillType != HILL_TYPE_NONE ) + { + // let's peek at the next node + CPathTrack *pNextNode = pNode->GetNext(); + if ( m_flCurrentSpeed < 0 ) + { + // we're going backwards + pNextNode = pNode->GetPrevious(); + } + + if ( pNextNode ) + { + int iNextHillType = pNextNode->GetHillType(); + if ( m_iCurrentHillType != iNextHillType ) + { + // we're leaving an uphill or downhill segment...so reset our state until we pass the next node + bUpdate = true; + iHillType = HILL_TYPE_NONE; + } + } + } + } + + if ( bUpdate ) + { + m_iCurrentHillType = iHillType; + HandleTrainMovement(); + } + } + } + } +} + +void CTeamTrainWatcher::HandleSparks( bool bSparks ) +{ + if ( IsDisabled() || !m_bHandleTrainMovement ) + return; + + for ( int i = 0 ; i < m_Sparks.Count() ; i++ ) + { + CEnvSpark* pSpark = m_Sparks[i].Get(); + if ( pSpark && ( pSpark->IsSparking() != bSparks ) ) + { + if ( bSparks ) + { + pSpark->StartSpark(); + } + else + { + pSpark->StopSpark(); + } + } + } +} + +void CTeamTrainWatcher::HandleTrainMovement( bool bStartReceding /* = false */ ) +{ + if ( IsDisabled() || !m_bHandleTrainMovement ) + return; + + if ( m_hTrain ) + { + float flSpeed = 0.0f; + + if ( bStartReceding ) + { + flSpeed = -0.1f; + m_bReceding = true; + } + else + { + // do we have cappers on the train? + if ( m_nNumCappers > 0 ) + { + m_bReceding = false; + + if ( m_iCurrentHillType == HILL_TYPE_DOWNHILL ) + { + flSpeed = 1.0f; + } + else + { + switch( m_nNumCappers ) + { + case 1: + flSpeed = 0.55f; + break; + case 2: + flSpeed = 0.77f; + break; + case 3: + default: + flSpeed = 1.0f; + break; + } + } + } + else if ( m_nNumCappers == -1 ) + { + // we'll get a -1 for a blocked cart (speed should be 0 for that unless we're on a hill) + if ( m_iCurrentHillType == HILL_TYPE_DOWNHILL ) + { + flSpeed = 1.0f; + } + } + else + { + // there's nobody on the train, what should it be doing? + if ( m_flCurrentSpeed > 0 ) + { + if ( m_iCurrentHillType == HILL_TYPE_DOWNHILL ) + { + flSpeed = 1.0f; + } + } + else + { + // we're rolling backwards + if ( m_iCurrentHillType == HILL_TYPE_UPHILL ) + { + flSpeed = -1.0f; + } + else + { + if ( m_bReceding ) + { + // resume our previous backup speed + flSpeed = -0.1f; + } + } + } + } + } + + // only need to update the train if our speed has changed + if ( m_flCurrentSpeed != flSpeed ) + { + if ( flSpeed >= 0.0f ) + { + m_bReceding = false; + } + + m_flCurrentSpeed = flSpeed; + m_hTrain->SetSpeedDirAccel( m_flCurrentSpeed ); + + // handle the sparks under the train + bool bSparks = false; + if ( m_flCurrentSpeed < 0 ) + { + bSparks = true; + } + + HandleSparks( bSparks ); + } + } +} + +void CTeamTrainWatcher::InputSetSpeedForwardModifier( inputdata_t &inputdata ) +{ + InternalSetSpeedForwardModifier( inputdata.value.Float() ); +} + +void CTeamTrainWatcher::InternalSetSpeedForwardModifier( float flModifier ) +{ + if ( IsDisabled() || !m_bHandleTrainMovement ) + return; + + // store the passed value + float flSpeedForwardModifier = flModifier; + flSpeedForwardModifier = fabs( flSpeedForwardModifier ); + + m_flSpeedForwardModifier = clamp( flSpeedForwardModifier, 0.f, 1.f ); + + if ( m_hTrain ) + { + m_hTrain->SetSpeedForwardModifier( m_flSpeedForwardModifier ); + } +} + +void CTeamTrainWatcher::InternalSetNumTrainCappers( int iNumCappers, CBaseEntity *pTrigger ) +{ + if ( IsDisabled() ) + return; + + m_nNumCappers = iNumCappers; + + // inputdata.pCaller is hopefully an area capture + // lets see if its blocked, and not start receding if it is + CTriggerAreaCapture *pAreaCap = dynamic_cast( pTrigger ); + if ( pAreaCap ) + { + m_bCapBlocked = pAreaCap->IsBlocked(); + m_hAreaCap = pAreaCap; + } + + if ( iNumCappers <= 0 && !m_bCapBlocked && m_bTrainCanRecede ) + { + if ( !m_bWaitingToRecede ) + { + // start receding in [tf_escort_cart_recede_time] seconds + m_bWaitingToRecede = true; + + if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->InOvertime() ) + { + m_flRecedeTotalTime = tf_escort_recede_time_overtime.GetFloat(); + } + else + { + m_flRecedeTotalTime = tf_escort_recede_time.GetFloat(); + if ( m_nTrainRecedeTime > 0 ) + { + m_flRecedeTotalTime = m_nTrainRecedeTime; + } + } + + m_flRecedeStartTime = gpGlobals->curtime; + m_flRecedeTime = m_flRecedeStartTime + m_flRecedeTotalTime; + } + } + else + { + // cancel receding + m_bWaitingToRecede = false; + m_flRecedeTime = 0; + } + + HandleTrainMovement(); +} + +// only used for train watchers that control the train movement +void CTeamTrainWatcher::SetNumTrainCappers( int iNumCappers, CBaseEntity *pTrigger ) +{ + if ( IsDisabled() || !m_bHandleTrainMovement ) + return; + + InternalSetNumTrainCappers( iNumCappers, pTrigger ); +} + +void CTeamTrainWatcher::InputSetNumTrainCappers( inputdata_t &inputdata ) +{ + InternalSetNumTrainCappers( inputdata.value.Int(), inputdata.pCaller ); +} + +void CTeamTrainWatcher::InputSetTrainRecedeTime( inputdata_t &inputdata ) +{ + int nSeconds = inputdata.value.Int(); + if ( nSeconds >= 0 ) + { + m_nTrainRecedeTime = nSeconds; + } + else + { + m_nTrainRecedeTime = 0; + } +} + +void CTeamTrainWatcher::InputSetTrainCanRecede( inputdata_t &inputdata ) +{ + m_bTrainCanRecede = inputdata.value.Bool(); +} + +void CTeamTrainWatcher::InputOnStartOvertime( inputdata_t &inputdata ) +{ + // recalculate the recede time + if ( m_bWaitingToRecede ) + { + float flRecedeTimeRemaining = m_flRecedeTime - gpGlobals->curtime; + float flOvertimeRecedeLen = tf_escort_recede_time_overtime.GetFloat(); + + // drop to overtime recede time if it's more than that + if ( flRecedeTimeRemaining > flOvertimeRecedeLen ) + { + m_flRecedeTotalTime = flOvertimeRecedeLen; + m_flRecedeStartTime = gpGlobals->curtime; + m_flRecedeTime = m_flRecedeStartTime + m_flRecedeTotalTime; + } + } +} + +#ifdef GLOWS_ENABLE +void CTeamTrainWatcher::FindGlowEntity( void ) +{ + if ( m_hTrain && ( m_hTrain->GetEntityName() != NULL_STRING ) ) + { + string_t iszTrainName = m_hTrain->GetEntityName(); + CBaseEntity *pGlowEnt = NULL; + + // first try to find a phys_constraint relationship with the train + CPhysFixed *pPhysConstraint = dynamic_cast( gEntList.FindEntityByClassname( NULL, "phys_constraint" ) ); + while ( pPhysConstraint ) + { + string_t iszName1 = pPhysConstraint->GetNameAttach1(); + string_t iszName2 = pPhysConstraint->GetNameAttach2(); + + if ( iszTrainName == iszName1 ) + { + pGlowEnt = gEntList.FindEntityByName( NULL, STRING( iszName2 ) ); + break; + } + else if ( iszTrainName == iszName2 ) + { + pGlowEnt = gEntList.FindEntityByName( NULL, STRING( iszName1 ) ); + break; + } + + pPhysConstraint = dynamic_cast( gEntList.FindEntityByClassname( pPhysConstraint, "phys_constraint" ) ); + } + + if ( !pGlowEnt ) + { + // if we're here, we haven't found the glow entity yet...try all of the prop_dynamic entities + CDynamicProp *pPropDynamic = dynamic_cast( gEntList.FindEntityByClassname( NULL, "prop_dynamic" ) ); + while ( pPropDynamic ) + { + if ( pPropDynamic->GetParent() == m_hTrain ) + { + pGlowEnt = pPropDynamic; + break; + } + + pPropDynamic = dynamic_cast( gEntList.FindEntityByClassname( pPropDynamic, "prop_dynamic" ) ); + } + } + + // if we still haven't found a glow entity, just have the CFuncTrackTrain glow + if ( !pGlowEnt ) + { + pGlowEnt = m_hTrain.Get(); + } + + if ( pGlowEnt ) + { + pGlowEnt->SetTransmitState( FL_EDICT_ALWAYS ); + m_hGlowEnt.Set( pGlowEnt ); + } + } +} +#endif // GLOWS_ENABLE + +// ========================================================== +// given a start node and a list of goal nodes +// calculate the distance between each +// ========================================================== +void CTeamTrainWatcher::WatcherActivate( void ) +{ + m_flRecedeTime = 0; + m_bWaitingToRecede = false; + m_bCapBlocked = false; + m_flNextSpeakForwardConceptTime = 0; + m_hAreaCap = NULL; + m_flTrainDistanceFromStart = 0.0f; + + m_bAlarmPlayed = false; + + m_Sparks.Purge(); + + StopCaptureAlarm(); + + // init our train + m_hTrain = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszTrain ) ); + if ( !m_hTrain ) + { + Warning("%s failed to find train named '%s'\n", GetClassname(), STRING( m_iszTrain ) ); + } + + // find the trigger area that will give us movement updates and find the sparks (if we're going to handle the train movement) + if ( m_bHandleTrainMovement ) + { + if ( m_hTrain ) + { + for ( int i=0; i( ITriggerAreaCaptureAutoList::AutoList()[i] ); + if ( pArea->GetParent() == m_hTrain.Get() ) + { + // this is the capture area we care about, so let it know that we want updates on the capture numbers + pArea->SetTrainWatcher( this ); + break; + } + } + } + + // init the sprites (if any) + CEnvSpark *pSpark = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszSparkName ) ); + while ( pSpark ) + { + m_Sparks.AddToTail( pSpark ); + pSpark = dynamic_cast( gEntList.FindEntityByName( pSpark, m_iszSparkName ) ); + } + } + + // init our array of path_tracks linked to control points + m_iNumCPLinks = 0; + + int i; + for ( i = 0 ; i < MAX_CONTROL_POINTS ; i++ ) + { + CPathTrack *pPathTrack = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszLinkedPathTracks[i] ) ); + CTeamControlPoint *pCP = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszLinkedCPs[i] ) ); + if ( pPathTrack && pCP ) + { + m_CPLinks[m_iNumCPLinks].hPathTrack = pPathTrack; + m_CPLinks[m_iNumCPLinks].hCP = pCP; + m_CPLinks[m_iNumCPLinks].flDistanceFromStart = 0; // filled in when we parse the nodes + m_CPLinks[m_iNumCPLinks].bAlertPlayed = false; + m_iNumCPLinks++; + } + } + + // init our start and goal nodes + m_hStartNode = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszStartNode ) ); + if ( !m_hStartNode ) + { + Warning("%s failed to find path_track named '%s'\n", GetClassname(), STRING(m_iszStartNode) ); + } + + m_hGoalNode = dynamic_cast( gEntList.FindEntityByName( NULL, m_iszGoalNode ) ); + if ( !m_hGoalNode ) + { + Warning("%s failed to find path_track named '%s'\n", GetClassname(), STRING(m_iszGoalNode) ); + } + + m_flTotalPathDistance = 0.0f; + + CUtlVector< float > hillData; + bool bOnHill = false; + + bool bDownHillData[TEAM_TRAIN_MAX_HILLS]; + Q_memset( bDownHillData, 0, sizeof( bDownHillData ) ); + int iHillCount = 0; + + if( m_hStartNode.Get() && m_hGoalNode.Get() ) + { + CPathTrack *pNode = m_hStartNode; + CPathTrack *pPrev = pNode; + CPathTrack *pHillStart = NULL; + pNode = pNode->GetNext(); + int iHillType = HILL_TYPE_NONE; + + // don't check the start node for links. If it's linked, it will have 0 distance anyway + while ( pNode ) + { + Vector dir = pNode->GetLocalOrigin() - pPrev->GetLocalOrigin(); + float length = dir.Length(); + + m_flTotalPathDistance += length; + + // gather our hill data for the HUD + if ( pNode->GetHillType() != iHillType ) + { + if ( !bOnHill ) // we're at the start of a hill + { + hillData.AddToTail( m_flTotalPathDistance ); + bOnHill = true; + pHillStart = pNode; + + if ( iHillCount < TEAM_TRAIN_MAX_HILLS ) + { + bDownHillData[iHillCount] = pNode->IsDownHill() ? true : false; + iHillCount++; + } + } + else // we're at the end of a hill + { + float flDistance = m_flTotalPathDistance - length; // subtract length because the prev node was the end of the hill (not this one) + + if ( pHillStart && ( pHillStart == pPrev ) ) + { + flDistance = m_flTotalPathDistance; // we had a single node marked as a hill, so we'll use the current distance as the next marker + } + + hillData.AddToTail( flDistance ); + + // is our current node the start of another hill? + if ( pNode->GetHillType() != HILL_TYPE_NONE ) + { + hillData.AddToTail( m_flTotalPathDistance ); + bOnHill = true; + pHillStart = pNode; + + if ( iHillCount < TEAM_TRAIN_MAX_HILLS ) + { + bDownHillData[iHillCount] = pNode->IsDownHill() ? true : false; + iHillCount++; + } + } + else + { + bOnHill = false; + pHillStart = NULL; + } + } + + iHillType = pNode->GetHillType(); + } + + // if pNode is one of our cp nodes, store its distance from m_hStartNode + for ( i = 0 ; i < m_iNumCPLinks ; i++ ) + { + if ( m_CPLinks[i].hPathTrack == pNode ) + { + m_CPLinks[i].flDistanceFromStart = m_flTotalPathDistance; + break; + } + } + + if ( pNode == m_hGoalNode ) + break; + + pPrev = pNode; + pNode = pNode->GetNext(); + } + } + + // if we don't have an even number of entries in our hill data (beginning/end) add the final distance + if ( ( hillData.Count() % 2 ) != 0 ) + { + hillData.AddToTail( m_flTotalPathDistance ); + } + + if ( ObjectiveResource() ) + { + ObjectiveResource()->ResetHillData( GetTeamNumber() ); + + // convert our hill data into 0-1 percentages for networking + if ( m_flTotalPathDistance > 0 && hillData.Count() > 0 ) + { + i = 0; + while ( i < hillData.Count() ) + { + if ( i < TEAM_TRAIN_HILLS_ARRAY_SIZE - 1 ) // - 1 because we want to use 2 entries + { + // add/subtract to the hill start/end to fix rounding errors in the HUD when the train + // stops at the bottom/top of a hill but the HUD thinks the train is still on the hill + ObjectiveResource()->SetHillData( GetTeamNumber(), (hillData[i] / m_flTotalPathDistance) + 0.005f, (hillData[i+1] / m_flTotalPathDistance) - 0.005f, bDownHillData[i/2] ); + } + i = i + 2; + } + } + } + + // We have total distance and increments in our links array + for ( i=0;iGetPointIndex(); +// This can be pulled once DoD includes team_objectiveresource.* and c_team_objectiveresource.* +#ifndef DOD_DLL + ObjectiveResource()->SetTrainPathDistance( iCPIndex, m_CPLinks[i].flDistanceFromStart / m_flTotalPathDistance ); +#endif + } + +#ifdef GLOWS_ENABLE + FindGlowEntity(); +#endif // GLOWS_ENABLE + + InternalSetSpeedForwardModifier( m_flSpeedForwardModifier ); + + SetContextThink( &CTeamTrainWatcher::WatcherThink, gpGlobals->curtime + 0.1, TW_THINK ); +} + +void CTeamTrainWatcher::StopCaptureAlarm( void ) +{ + if ( m_pAlarm ) + { + CSoundEnvelopeController::GetController().SoundDestroy( m_pAlarm ); + m_pAlarm = NULL; + m_flAlarmEndTime = -1.0f; + } + + SetContextThink( NULL, 0, TW_ALARM_THINK ); +} + +void CTeamTrainWatcher::StartCaptureAlarm( CTeamControlPoint *pPoint ) +{ + StopCaptureAlarm(); + + if ( pPoint ) + { + CReliableBroadcastRecipientFilter filter; + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + m_pAlarm = controller.SoundCreate( filter, pPoint->entindex(), CHAN_STATIC, TEAM_TRAIN_ALARM, ATTN_NORM ); + controller.Play( m_pAlarm, 1.0, PITCH_NORM ); + + m_flAlarmEndTime = gpGlobals->curtime + MAX_ALARM_TIME_NO_RECEDE; + } +} + +void CTeamTrainWatcher::PlayCaptureAlert( CTeamControlPoint *pPoint, bool bFinalPointInMap ) +{ + if ( !pPoint ) + return; + + if ( TeamplayRoundBasedRules() ) + { + TeamplayRoundBasedRules()->PlayTrainCaptureAlert( pPoint, bFinalPointInMap ); + } +} + + +ConVar tf_show_train_path( "tf_show_train_path", "0", FCVAR_CHEAT ); + +void CTeamTrainWatcher::WatcherThink( void ) +{ + if ( m_bWaitingToRecede ) + { + if ( m_flRecedeTime < gpGlobals->curtime ) + { + m_bWaitingToRecede = false; + + // don't actually recede in overtime + if ( TeamplayRoundBasedRules() && !TeamplayRoundBasedRules()->InOvertime() ) + { + // fire recede output + m_OnTrainStartRecede.FireOutput( this, this ); + HandleTrainMovement( true ); + } + } + } + + bool bDisableAlarm = (TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->State_Get() != GR_STATE_RND_RUNNING); + if ( bDisableAlarm ) + { + StopCaptureAlarm(); + } + + // given its next node, we can walk the nodes and find the linear + // distance to the next cp node, or to the goal node + + CFuncTrackTrain *pTrain = m_hTrain; + if ( pTrain ) + { + int iOldTrainSpeedLevel = m_iTrainSpeedLevel; + + // how fast is the train moving? + float flSpeed = pTrain->GetDesiredSpeed(); + + // divide speed into regions + // anything negative is -1 + + if ( flSpeed < 0 ) + { + m_iTrainSpeedLevel = -1; + + // even though our desired speed might be negative, + // our actual speed might be zero if we're at a dead end... + // this will turn off the < image when the train is done moving backwards + if ( pTrain->GetCurrentSpeed() == 0 ) + { + m_iTrainSpeedLevel = 0; + } + } + else if ( flSpeed > m_flSpeedLevels[2] ) + { + m_iTrainSpeedLevel = 3; + } + else if ( flSpeed > m_flSpeedLevels[1] ) + { + m_iTrainSpeedLevel = 2; + } + else if ( flSpeed > m_flSpeedLevels[0] ) + { + m_iTrainSpeedLevel = 1; + } + else + { + m_iTrainSpeedLevel = 0; + } + + if ( m_iTrainSpeedLevel != iOldTrainSpeedLevel ) + { + // make sure the sparks are off if we're not moving backwards anymore + if ( m_bHandleTrainMovement ) + { + if ( m_iTrainSpeedLevel == 0 && iOldTrainSpeedLevel != 0 ) + { + HandleSparks( false ); + } + } + + // play any concepts that we might need to play + if ( TeamplayRoundBasedRules() ) + { + if ( m_iTrainSpeedLevel == 0 && iOldTrainSpeedLevel != 0 ) + { + TeamplayRoundBasedRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_CART_STOP ); + m_flNextSpeakForwardConceptTime = 0; + } + else if ( m_iTrainSpeedLevel < 0 && iOldTrainSpeedLevel == 0 ) + { + TeamplayRoundBasedRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_CART_MOVING_BACKWARD ); + m_flNextSpeakForwardConceptTime = 0; + } + } + } + + if ( m_iTrainSpeedLevel > 0 && m_flNextSpeakForwardConceptTime < gpGlobals->curtime ) + { + if ( m_hAreaCap.Get() ) + { + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer ) + { + if ( m_hAreaCap->IsTouching( pPlayer ) ) + { + pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_CART_MOVING_FORWARD ); + } + } + } + } + + m_flNextSpeakForwardConceptTime = gpGlobals->curtime + 3.0; + } + + // what percent progress are we at? + CPathTrack *pNode = ( pTrain->m_ppath ) ? pTrain->m_ppath->GetNext() : NULL; + + // if we're moving backwards, GetNext is going to be wrong + if ( flSpeed < 0 ) + { + pNode = pTrain->m_ppath; + } + + if ( pNode ) + { + float flDistanceToGoal = 0; + + // distance to next node + Vector vecDir = pNode->GetLocalOrigin() - pTrain->GetLocalOrigin(); + flDistanceToGoal = vecDir.Length(); + + // distance of next node to goal node + if ( pNode && pNode != m_hGoalNode ) + { + // walk this until we get to goal node, or a dead end + CPathTrack *pPrev = pNode; + pNode = pNode->GetNext(); + while ( pNode ) + { + vecDir = pNode->GetLocalOrigin() - pPrev->GetLocalOrigin(); + flDistanceToGoal += vecDir.Length(); + + if ( pNode == m_hGoalNode ) + break; + + pPrev = pNode; + pNode = pNode->GetNext(); + } + } + + if ( m_flTotalPathDistance <= 0 ) + { + Assert( !"No path distance in team_train_watcher\n" ); + m_flTotalPathDistance = 1; + } + + m_flTotalProgress = clamp( 1.0 - ( flDistanceToGoal / m_flTotalPathDistance ), 0.0, 1.0 ); + + m_flTrainDistanceFromStart = m_flTotalPathDistance - flDistanceToGoal; + + // play alert sounds if necessary + for ( int iCount = 0 ; iCount < m_iNumCPLinks ; iCount++ ) + { + if ( m_flTrainDistanceFromStart < m_CPLinks[iCount].flDistanceFromStart - TEAM_TRAIN_ALERT_DISTANCE ) + { + // back up twice the alert distance before resetting our flag to play the warning again + if ( ( m_flTrainDistanceFromStart < m_CPLinks[iCount].flDistanceFromStart - ( TEAM_TRAIN_ALERT_DISTANCE * 2 ) ) || // has receded back twice the alert distance or... + ( !m_bTrainCanRecede ) ) // used to catch the case where the train doesn't normally recede but has rolled back down a hill away from the CP + { + // reset our alert flag + m_CPLinks[iCount].bAlertPlayed = false; + } + } + else + { + if ( m_flTrainDistanceFromStart < m_CPLinks[iCount].flDistanceFromStart && !m_CPLinks[iCount].bAlertPlayed ) + { + m_CPLinks[iCount].bAlertPlayed = true; + bool bFinalPointInMap = false; + + CTeamControlPoint *pCurrentPoint = m_CPLinks[iCount].hCP.Get(); + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + // if we're not playing mini-rounds + if ( !pMaster->PlayingMiniRounds() ) + { + for ( int i = FIRST_GAME_TEAM ; i < MAX_CONTROL_POINT_TEAMS ; i++ ) + { + if ( ObjectiveResource() && ObjectiveResource()->TeamCanCapPoint( pCurrentPoint->GetPointIndex(), i ) ) + { + if ( pMaster->WouldNewCPOwnerWinGame( pCurrentPoint, i ) ) + { + bFinalPointInMap = true; + } + } + } + } + else + { + // or this is the last round + if ( pMaster->NumPlayableControlPointRounds() == 1 ) + { + CTeamControlPointRound *pRound = pMaster->GetCurrentRound(); + if ( pRound ) + { + for ( int i = FIRST_GAME_TEAM ; i < MAX_CONTROL_POINT_TEAMS ; i++ ) + { + if ( ObjectiveResource() && ObjectiveResource()->TeamCanCapPoint( pCurrentPoint->GetPointIndex(), i ) ) + { + if ( pRound->WouldNewCPOwnerWinGame( pCurrentPoint, i ) ) + { + bFinalPointInMap = true; + } + } + } + } + } + } + } + + PlayCaptureAlert( pCurrentPoint, bFinalPointInMap ); + } + } + } + + // check to see if we need to start or stop the alarm + if ( flDistanceToGoal <= TEAM_TRAIN_ALARM_DISTANCE ) + { + if ( ObjectiveResource() ) + { + ObjectiveResource()->SetTrackAlarm( GetTeamNumber(), true ); + } + + if ( !bDisableAlarm ) + { + if ( !m_pAlarm ) + { + if ( m_iNumCPLinks > 0 && !m_bAlarmPlayed ) + { + // start the alarm at the final point + StartCaptureAlarm( m_CPLinks[m_iNumCPLinks-1].hCP.Get() ); + m_bAlarmPlayed = true; // used to prevent the alarm from starting again on maps where the train doesn't recede (alarm loops for short time then only plays singles) + } + } + else + { + if ( !m_bTrainCanRecede ) // if the train won't recede, we only want to play the alarm for a short time + { + if ( m_flAlarmEndTime > 0 && m_flAlarmEndTime < gpGlobals->curtime ) + { + StopCaptureAlarm(); + SetContextThink( &CTeamTrainWatcher::WatcherAlarmThink, gpGlobals->curtime + TW_ALARM_THINK_INTERVAL, TW_ALARM_THINK ); + } + } + } + } + } + else + { + if ( ObjectiveResource() ) + { + ObjectiveResource()->SetTrackAlarm( GetTeamNumber(), false ); + } + + StopCaptureAlarm(); + m_bAlarmPlayed = false; + } + } + + if ( tf_show_train_path.GetBool() ) + { + CPathTrack *nextNode = NULL; + CPathTrack *node = m_hStartNode; + + CPathTrack::BeginIteration(); + while( node ) + { + node->Visit(); + nextNode = node->GetNext(); + + if ( !nextNode || nextNode->HasBeenVisited() ) + break; + + NDebugOverlay::Line( node->GetAbsOrigin(), nextNode->GetAbsOrigin(), 255, 255, 0, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + + node = nextNode; + } + CPathTrack::EndIteration(); + + // show segment of path train is actually on + node = pTrain->m_ppath; + if ( node && node->GetNext() ) + { + NDebugOverlay::HorzArrow( node->GetAbsOrigin(), node->GetNext()->GetAbsOrigin(), 5.0f, 255, 0, 0, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + + SetContextThink( &CTeamTrainWatcher::WatcherThink, gpGlobals->curtime + 0.1, TW_THINK ); +} + +void CTeamTrainWatcher::WatcherAlarmThink( void ) +{ + CTeamControlPoint *pPoint = m_CPLinks[m_iNumCPLinks-1].hCP.Get(); + if ( pPoint ) + { + pPoint->EmitSound( TEAM_TRAIN_ALARM_SINGLE ); + } + + SetContextThink( &CTeamTrainWatcher::WatcherAlarmThink, gpGlobals->curtime + TW_ALARM_THINK_INTERVAL, TW_ALARM_THINK ); +} + +CBaseEntity *CTeamTrainWatcher::GetTrainEntity( void ) +{ + return m_hTrain.Get(); +} + +bool CTeamTrainWatcher::TimerMayExpire( void ) +{ + if ( IsDisabled() ) + { + return true; + } + + // Still in overtime if we're waiting to recede + if ( m_bWaitingToRecede ) + return false; + + // capture blocked so we're not receding, but game shouldn't end + if ( m_bCapBlocked ) + return false; + + // not waiting, so we're capping, in which case the area capture + // will not let us expire + return true; +} + + +// Project the given position onto the track and return the point and how far along that projected position is +void CTeamTrainWatcher::ProjectPointOntoPath( const Vector &pos, Vector *posOnPathResult, float *distanceAlongPathResult ) const +{ + CPathTrack *nextNode = NULL; + CPathTrack *node = m_hStartNode; + + Vector toPos; + Vector alongPath; + float distanceAlong = 0.0f; + + Vector closestPointOnPath = vec3_origin; + float closestPerpendicularDistanceSq = FLT_MAX; + float closestDistanceAlongPath = FLT_MAX; + + CPathTrack::BeginIteration(); + while( node ) + { + node->Visit(); + nextNode = node->GetNext(); + + if ( !nextNode || nextNode->HasBeenVisited() ) + break; + + alongPath = nextNode->GetAbsOrigin() - node->GetAbsOrigin(); + float segmentLength = alongPath.NormalizeInPlace(); + + toPos = pos - node->GetAbsOrigin(); + float segmentOverlap = DotProduct( toPos, alongPath ); + + if ( segmentOverlap >= 0.0f && segmentOverlap < segmentLength ) + { + // projection is within segment bounds + Vector onPath = node->GetAbsOrigin() + alongPath * segmentOverlap; + + float perpendicularDistanceSq = ( onPath - pos ).LengthSqr(); + if ( perpendicularDistanceSq < closestPerpendicularDistanceSq ) + { + closestPointOnPath = onPath; + closestPerpendicularDistanceSq = perpendicularDistanceSq; + closestDistanceAlongPath = distanceAlong + segmentOverlap; + } + } + + distanceAlong += segmentLength; + node = nextNode; + } + CPathTrack::EndIteration(); + + if ( posOnPathResult ) + { + *posOnPathResult = closestPointOnPath; + } + + if ( distanceAlongPathResult ) + { + *distanceAlongPathResult = closestDistanceAlongPath; + } +} + + +// Return true if the given position is farther down the track than the train is +bool CTeamTrainWatcher::IsAheadOfTrain( const Vector &pos ) const +{ + float distanceAlongPath; + ProjectPointOntoPath( pos, NULL, &distanceAlongPath ); + + return ( distanceAlongPath > m_flTrainDistanceFromStart ); +} + + +// return true if the train is almost at the next checkpoint +bool CTeamTrainWatcher::IsTrainNearCheckpoint( void ) const +{ + for( int i = 0; i < m_iNumCPLinks ; ++i ) + { + if ( m_flTrainDistanceFromStart > m_CPLinks[i].flDistanceFromStart - TEAM_TRAIN_ALERT_DISTANCE && + m_flTrainDistanceFromStart < m_CPLinks[i].flDistanceFromStart ) + { + return true; + } + } + + return false; +} + + +// return true if the train hasn't left its starting position yet +bool CTeamTrainWatcher::IsTrainAtStart( void ) const +{ + return ( m_flTrainDistanceFromStart < TEAM_TRAIN_ALARM_DISTANCE ); +} + + +// return world space location of next checkpoint along the path +Vector CTeamTrainWatcher::GetNextCheckpointPosition( void ) const +{ + for( int i = 0; i < m_iNumCPLinks ; ++i ) + { + if ( m_flTrainDistanceFromStart < m_CPLinks[i].flDistanceFromStart ) + { + return m_CPLinks[i].hPathTrack->GetAbsOrigin(); + } + } + + Assert( !"No checkpoint found in team train watcher\n" ); + return vec3_origin; +} + +#if defined( STAGING_ONLY ) && defined( TF_DLL ) +CON_COMMAND_F( tf_dumptrainstats, "Dump the stats for the current train watcher to the console", FCVAR_GAMEDLL ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CTeamTrainWatcher *pWatcher = NULL; + while( ( pWatcher = dynamic_cast< CTeamTrainWatcher * >( gEntList.FindEntityByClassname( pWatcher, "team_train_watcher" ) ) ) != NULL ) + { + pWatcher->DumpStats(); + } +} + +void CTeamTrainWatcher::DumpStats( void ) +{ + float flLastPosition = 0.0f; + float flTotalDistance = 0.0f; + char szOutput[2048]; + char szTemp[256]; + + V_strcpy_safe( szOutput, "\n\nTrain Watcher stats for team " ); + V_strcat_safe( szOutput, ( GetTeamNumber() == TF_TEAM_RED ) ? "Red\n" : "Blue\n" ); + + for( int i = 0; i < m_iNumCPLinks ; ++i ) + { + float flDistance = m_CPLinks[i].flDistanceFromStart - flLastPosition; + if ( i == 0 ) + { + V_sprintf_safe( szTemp, "\tControl Point: %d\tDistance from start: %0.2f\n", i + 1, flDistance ); + } + else + { + V_sprintf_safe( szTemp, "\tControl Point: %d\tDistance from previous point: %0.2f\n", i + 1, flDistance ); + } + V_strcat_safe( szOutput, szTemp ); + flTotalDistance += flDistance; + flLastPosition = m_CPLinks[i].flDistanceFromStart; + } + + V_sprintf_safe( szTemp, "\tTotal Distance: %0.2f\n\n", flTotalDistance ); + V_strcat_safe( szOutput, szTemp ); + Msg( "%s", szOutput ); +} +#endif // STAGING_ONLY && TF_DLL + + diff --git a/sp/src/game/server/team_train_watcher.h b/sp/src/game/server/team_train_watcher.h new file mode 100644 index 00000000..22b82c6f --- /dev/null +++ b/sp/src/game/server/team_train_watcher.h @@ -0,0 +1,230 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef TEAM_TRAIN_WATCHER_H +#define TEAM_TRAIN_WATCHER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "cbase.h" +#include "trigger_area_capture.h" +#include "shareddefs.h" +#include "envspark.h" +#include "GameEventListener.h" + +class CFuncTrackTrain; +class CPathTrack; +class CTeamControlPoint; + +#define TEAM_TRAIN_ALERT_DISTANCE 750 // alert is the VO warning +#define TEAM_TRAIN_ALARM_DISTANCE 200 // alarm is the looping sound played at the control point + +#define TEAM_TRAIN_ALERT "Announcer.Cart.Warning" +#define TEAM_TRAIN_FINAL_ALERT "Announcer.Cart.FinalWarning" +#define TEAM_TRAIN_ALARM "Cart.Warning" +#define TEAM_TRAIN_ALARM_SINGLE "Cart.WarningSingle" + +#define TW_THINK "CTeamTrainWatcherThink" +#define TW_ALARM_THINK "CTeamTrainWatcherAlarmThink" +#define TW_ALARM_THINK_INTERVAL 8.0 + +// #define TWMASTER_THINK "CTeamTrainWatcherMasterThink" + +DECLARE_AUTO_LIST( ITFTeamTrainWatcher ); + +class CTeamTrainWatcher : public CBaseEntity, public CGameEventListener, public ITFTeamTrainWatcher +{ + DECLARE_CLASS( CTeamTrainWatcher, CBaseEntity ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CTeamTrainWatcher(); + ~CTeamTrainWatcher(); + + virtual void UpdateOnRemove( void ); + virtual int UpdateTransmitState(); + + void InputRoundActivate( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + void InputSetNumTrainCappers( inputdata_t &inputdata ); + void InputOnStartOvertime( inputdata_t &inputdata ); + void InputSetSpeedForwardModifier( inputdata_t &inputdata ); + void InputSetTrainRecedeTime( inputdata_t &inputdata ); + void InputSetTrainCanRecede( inputdata_t &inputdata ); + + // ========================================================== + // given a start node and a list of goal nodes + // calculate the distance between each + // ========================================================== + void WatcherActivate( void ); + + void WatcherThink( void ); + void WatcherAlarmThink( void ); + + CBaseEntity *GetTrainEntity( void ); + bool IsDisabled( void ) { return m_bDisabled; } + + bool TimerMayExpire( void ); + + void StopCaptureAlarm( void ); + + void SetNumTrainCappers( int iNumCappers, CBaseEntity *pTrigger ); // only used for train watchers that control the train movement + + virtual void FireGameEvent( IGameEvent * event ); + + int GetCapturerCount( void ) const; // return the number of players who are "capturing" the payload, or -1 if the payload is blocked + + void ProjectPointOntoPath( const Vector &pos, Vector *posOnPath, float *distanceAlongPath ) const; // project the given position onto the track and return the point and how far along that projected position is + bool IsAheadOfTrain( const Vector &pos ) const; // return true if the given position is farther down the track than the train is + + bool IsTrainAtStart( void ) const; // return true if the train hasn't left its starting position yet + bool IsTrainNearCheckpoint( void ) const; // return true if the train is almost at the next checkpoint + + float GetTrainDistanceAlongTrack( void ) const; + Vector GetNextCheckpointPosition( void ) const; // return world space location of next checkpoint along the path + +#if defined( STAGING_ONLY ) && defined( TF_DLL ) + void DumpStats( void ); +#endif // STAGING_ONLY && TF_DLL + +private: + + void StartCaptureAlarm( CTeamControlPoint *pPoint ); + void PlayCaptureAlert( CTeamControlPoint *pPoint, bool bFinalPointInMap ); + void InternalSetNumTrainCappers( int iNumCappers, CBaseEntity *pTrigger ); + void InternalSetSpeedForwardModifier( float flModifier ); +#ifdef GLOWS_ENABLE + void FindGlowEntity( void ); +#endif // GLOWS_ENABLE + void HandleTrainMovement( bool bStartReceding = false ); + void HandleSparks( bool bSparks ); + +private: + + bool m_bDisabled; + bool m_bTrainCanRecede; + // === Data === + + // pointer to the train that we're checking + CHandle m_hTrain; + + // start node + CHandle m_hStartNode; + + // goal node + CHandle m_hGoalNode; + + string_t m_iszTrain; + string_t m_iszStartNode; + string_t m_iszGoalNode; + + // list of node associations with control points + typedef struct + { + CHandle hPathTrack; + CHandle hCP; + float flDistanceFromStart; + bool bAlertPlayed; + } node_cp_pair_t; + + node_cp_pair_t m_CPLinks[MAX_CONTROL_POINTS]; + int m_iNumCPLinks; + + string_t m_iszLinkedPathTracks[MAX_CONTROL_POINTS]; + string_t m_iszLinkedCPs[MAX_CONTROL_POINTS]; + + float m_flTotalPathDistance; // calculated only at round start, node graph + // may get chopped as the round progresses + + float m_flTrainDistanceFromStart; // actual distance along path of train, for comparing against m_CPLinks[].flDistanceFromStart + + float m_flSpeedLevels[3]; + + // === Networked Data === + + // current total progress, percentage + CNetworkVar( float, m_flTotalProgress ); + + CNetworkVar( int, m_iTrainSpeedLevel ); + + CNetworkVar( int, m_nNumCappers ); + + bool m_bWaitingToRecede; + CNetworkVar( float, m_flRecedeTime ); + float m_flRecedeTotalTime; + float m_flRecedeStartTime; + COutputEvent m_OnTrainStartRecede; + + bool m_bCapBlocked; + + float m_flNextSpeakForwardConceptTime; // used to have players speak the forward concept every X seconds + CHandle m_hAreaCap; + + CSoundPatch *m_pAlarm; + float m_flAlarmEndTime; + bool m_bAlarmPlayed; + + // added for new mode where the train_watcher handles the train movement + bool m_bHandleTrainMovement; + string_t m_iszSparkName; + CUtlVector< CHandle > m_Sparks; + float m_flSpeedForwardModifier; + int m_iCurrentHillType; + float m_flCurrentSpeed; + bool m_bReceding; + + int m_nTrainRecedeTime; + +#ifdef GLOWS_ENABLE + CNetworkVar( EHANDLE, m_hGlowEnt ); +#endif // GLOWS_ENABLE +}; + + +inline float CTeamTrainWatcher::GetTrainDistanceAlongTrack( void ) const +{ + return m_flTrainDistanceFromStart; +} + +inline int CTeamTrainWatcher::GetCapturerCount( void ) const +{ + return m_nNumCappers; +} + + +/* +class CTeamTrainWatcherMaster : public CBaseEntity, public CGameEventListener +{ + DECLARE_CLASS( CTeamTrainWatcherMaster, CBaseEntity ); + +public: + CTeamTrainWatcherMaster(); + ~CTeamTrainWatcherMaster(); + + void Precache( void ); + +private: + void TWMThink( void ); + void FireGameEvent( IGameEvent *event ); + + bool FindTrainWatchers( void ); + +private: + CTeamTrainWatcher *m_pBlueWatcher; + CTeamTrainWatcher *m_pRedWatcher; + + float m_flBlueProgress; + float m_flRedProgress; +}; + +extern EHANDLE g_hTeamTrainWatcherMaster; +*/ + +#endif //TEAM_TRAIN_WATCHER_H diff --git a/sp/src/game/server/tempmonster.cpp b/sp/src/game/server/tempmonster.cpp new file mode 100644 index 00000000..3100c55b --- /dev/null +++ b/sp/src/game/server/tempmonster.cpp @@ -0,0 +1,105 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +//========================================================= +// NPC template +//========================================================= +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#if 0 + +//========================================================= +// NPC's Anim Events Go Here +//========================================================= + +class CMyNPC : public CAI_BaseNPC +{ +public: + DECLARE_CLASS( CMyNPC, CAI_BaseNPC ); + + void Spawn( void ); + void Precache( void ); + void MaxYawSpeed( void ); + int Classify ( void ); + void HandleAnimEvent( animevent_t *pEvent ); +}; +LINK_ENTITY_TO_CLASS( my_NPC, CMyNPC ); + +//========================================================= +// Classify - indicates this NPC's place in the +// relationship table. +//========================================================= +int CMyNPC::Classify ( void ) +{ + return CLASS_MY_NPC; +} + +//========================================================= +// SetYawSpeed - allows each sequence to have a different +// turn rate associated with it. +//========================================================= +float CMyNPC::MaxYawSpeed ( void ) +{ + switch ( m_Activity ) + { + case ACT_IDLE: + default: + return 90; + } +} + +//========================================================= +// HandleAnimEvent - catches the NPC-specific messages +// that occur when tagged animation frames are played. +//========================================================= +void CMyNPC::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case 0: + default: + CAI_BaseNPC::HandleAnimEvent( pEvent ); + break; + } +} + +//========================================================= +// Spawn +//========================================================= +void CMyNPC::Spawn() +{ + Precache( ); + + engine.SetModel(edict(), "models/mymodel.mdl"); + UTIL_SetSize( this, Vector( -12, -12, 0 ), Vector( 12, 12, 24 ) ); + + SetSolid( SOLID_SLIDEBOX ); + SetMoveType( MOVETYPE_STEP ); + m_bloodColor = BLOOD_COLOR_GREEN; + m_iHealth = 8; + m_vecViewOffset = Vector ( 0, 0, 0 );// position of the eyes relative to NPC's origin. + m_flFieldOfView = 0.5;// indicates the width of this NPC's forward view cone ( as a dotproduct result ) + m_NPCState = NPCSTATE_NONE; + + NPCInit(); +} + +//========================================================= +// Precache - precaches all resources this NPC needs +//========================================================= +void CMyNPC::Precache() +{ + engine.PrecacheModel("models/mymodel.mdl"); +} + +//========================================================= +// AI Schedules Specific to this NPC +//========================================================= +#endif diff --git a/sp/src/game/server/tesla.cpp b/sp/src/game/server/tesla.cpp new file mode 100644 index 00000000..288fc8e2 --- /dev/null +++ b/sp/src/game/server/tesla.cpp @@ -0,0 +1,180 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "tesla.h" +#include "te_effect_dispatch.h" +#include "sendproxy.h" +#include "engine/IEngineSound.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( point_tesla, CTesla ); + +BEGIN_DATADESC( CTesla ) + + DEFINE_KEYFIELD( m_SourceEntityName,FIELD_STRING, "m_SourceEntityName" ), + DEFINE_KEYFIELD( m_SoundName, FIELD_STRING, "m_SoundName" ), + DEFINE_KEYFIELD( m_iszSpriteName, FIELD_STRING, "texture" ), + + DEFINE_KEYFIELD( m_Color, FIELD_COLOR32, "m_Color" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "m_flRadius" ), + + //DEFINE_ARRAY( m_flThickness, FIELD_FLOAT, 2 ), + DEFINE_KEYFIELD( m_flThickness[0], FIELD_FLOAT, "thick_min" ), + DEFINE_KEYFIELD( m_flThickness[1], FIELD_FLOAT, "thick_max" ), + + //DEFINE_ARRAY( m_flTimeVisible, FIELD_FLOAT, 2 ), + DEFINE_KEYFIELD( m_flTimeVisible[0],FIELD_FLOAT, "lifetime_min" ), + DEFINE_KEYFIELD( m_flTimeVisible[1],FIELD_FLOAT, "lifetime_max" ), + + //DEFINE_ARRAY( m_flArcInterval, FIELD_FLOAT, 2 ), + DEFINE_KEYFIELD( m_flArcInterval[0],FIELD_FLOAT, "interval_min" ), + DEFINE_KEYFIELD( m_flArcInterval[1],FIELD_FLOAT, "interval_max" ), + + //DEFINE_ARRAY( m_NumBeams, FIELD_INTEGER, 2 ), + DEFINE_KEYFIELD( m_NumBeams[0], FIELD_INTEGER, "beamcount_min" ), + DEFINE_KEYFIELD( m_NumBeams[1], FIELD_INTEGER, "beamcount_max" ), + + DEFINE_KEYFIELD( m_bOn, FIELD_BOOLEAN, "m_bOn" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), + DEFINE_INPUTFUNC( FIELD_VOID, "DoSpark", InputDoSpark ), + + DEFINE_FUNCTION( ShootArcThink ) + +END_DATADESC() + + +IMPLEMENT_SERVERCLASS_ST( CTesla, DT_Tesla ) + SendPropStringT( SENDINFO( m_SoundName ) ), + SendPropStringT( SENDINFO( m_iszSpriteName ) ) +END_SEND_TABLE() + + +CTesla::CTesla() +{ + m_SourceEntityName = NULL_STRING; + m_SoundName = NULL_STRING; + m_iszSpriteName = NULL_STRING; + m_NumBeams[0] = m_NumBeams[1] = 6; + m_flRadius = 200; + m_flThickness[0] = m_flThickness[1] = 5; + m_flTimeVisible[0] = 0.3; + m_flTimeVisible[1] = 0.55; + m_flArcInterval[0] = m_flArcInterval[1] = 0.5; + + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); +} + + +void CTesla::Spawn() +{ + if ( m_iszSpriteName.Get() == NULL_STRING ) + { + m_iszSpriteName = AllocPooledString("sprites/physbeam.vmt"); + } + + Precache(); + BaseClass::Spawn(); +} + + +void CTesla::Activate() +{ + BaseClass::Activate(); + + SetThink( &CTesla::ShootArcThink ); + SetupForNextArc(); +} + + +void CTesla::Precache() +{ + PrecacheModel( STRING(m_iszSpriteName.Get()) ); + BaseClass::Precache(); + + PrecacheScriptSound( STRING( m_SoundName.Get() ) ); +} + + +void CTesla::SetupForNextArc() +{ + if (m_bOn) + { + float flTimeToNext = RandomFloat( m_flArcInterval[0], m_flArcInterval[1] ); + SetNextThink( gpGlobals->curtime + flTimeToNext ); + } + else + { + SetNextThink( TICK_NEVER_THINK ); + } +} + + +CBaseEntity* CTesla::GetSourceEntity() +{ + if ( m_SourceEntityName != NULL_STRING ) + { + CBaseEntity *pRet = gEntList.FindEntityByName( NULL, m_SourceEntityName ); + if ( pRet ) + return pRet; + } + + return this; +} + + +void CTesla::ShootArcThink() +{ + DoSpark(); + SetupForNextArc(); +} + + +void CTesla::DoSpark() +{ + // Shoot out an arc. + EntityMessageBegin( this ); + + CBaseEntity *pEnt = GetSourceEntity(); + + WRITE_VEC3COORD( pEnt->GetAbsOrigin() ); + WRITE_SHORT( pEnt->entindex() ); + WRITE_FLOAT( m_flRadius ); + WRITE_BYTE( m_Color.r ); + WRITE_BYTE( m_Color.g ); + WRITE_BYTE( m_Color.b ); + WRITE_BYTE( m_Color.a ); + WRITE_CHAR( RandomInt( m_NumBeams[0], m_NumBeams[1] ) ); + WRITE_FLOAT( RandomFloat( m_flThickness[0], m_flThickness[1] ) ); + WRITE_FLOAT( RandomFloat( m_flTimeVisible[0], m_flTimeVisible[1] ) ); + + MessageEnd(); +} + + +void CTesla::InputDoSpark( inputdata_t &inputdata ) +{ + DoSpark(); +} + + +void CTesla::InputTurnOn( inputdata_t &inputdata ) +{ + m_bOn = true; + SetupForNextArc(); +} + + +void CTesla::InputTurnOff( inputdata_t &inputdata ) +{ + m_bOn = false; + SetupForNextArc(); +} + diff --git a/sp/src/game/server/tesla.h b/sp/src/game/server/tesla.h new file mode 100644 index 00000000..5728e3b6 --- /dev/null +++ b/sp/src/game/server/tesla.h @@ -0,0 +1,62 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef TESLA_H +#define TESLA_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" + + +class CTesla : public CBaseEntity +{ +public: + DECLARE_CLASS( CTesla, CBaseEntity ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CTesla(); + + virtual void Spawn(); + virtual void Activate(); + virtual void Precache(); + + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); + void InputDoSpark( inputdata_t &inputdata ); + + void DoSpark(); + void ShootArcThink(); + + void SetupForNextArc(); + CBaseEntity* GetSourceEntity(); + + +public: + + // Tesla parameters. + string_t m_SourceEntityName; // Which entity the arcs come from. + CNetworkVar( string_t, m_SoundName ); // What sound to play when arcing. + + color32 m_Color; + int m_NumBeams[2]; // Number of beams per spark. + + float m_flRadius; // Radius it looks for surfaces to arc to. + + float m_flThickness[2]; // Beam thickness. + float m_flTimeVisible[2]; // How long each beam stays around (min/max). + float m_flArcInterval[2]; // Time between args (min/max). + + bool m_bOn; + + CNetworkVar( string_t, m_iszSpriteName ); +}; + + +#endif // TESLA_H diff --git a/sp/src/game/server/test_proxytoggle.cpp b/sp/src/game/server/test_proxytoggle.cpp new file mode 100644 index 00000000..750d37b2 --- /dev/null +++ b/sp/src/game/server/test_proxytoggle.cpp @@ -0,0 +1,110 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CTest_ProxyToggle_Networkable; +static CTest_ProxyToggle_Networkable *g_pTestObj = 0; +static bool g_bEnableProxy = true; + + +// ---------------------------------------------------------------------------------------- // +// CTest_ProxyToggle_Networkable +// ---------------------------------------------------------------------------------------- // + +class CTest_ProxyToggle_Networkable : public CBaseEntity +{ +public: + DECLARE_CLASS( CTest_ProxyToggle_Networkable, CBaseEntity ); + DECLARE_SERVERCLASS(); + + CTest_ProxyToggle_Networkable() + { + m_WithProxy = 1241; + g_pTestObj = this; + } + + ~CTest_ProxyToggle_Networkable() + { + g_pTestObj = NULL; + } + + int UpdateTransmitState() + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + CNetworkVar( int, m_WithProxy ); +}; + +void* SendProxy_TestProxyToggle( const SendProp *pProp, const void *pStructBase, const void *pData, CSendProxyRecipients *pRecipients, int objectID ) +{ + if ( g_bEnableProxy ) + { + return (void*)pData; + } + else + { + pRecipients->ClearAllRecipients(); + return NULL; + } +} +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_TestProxyToggle ); + + +// ---------------------------------------------------------------------------------------- // +// Datatables. +// ---------------------------------------------------------------------------------------- // + +LINK_ENTITY_TO_CLASS( test_proxytoggle, CTest_ProxyToggle_Networkable ); + +BEGIN_SEND_TABLE_NOBASE( CTest_ProxyToggle_Networkable, DT_ProxyToggle_ProxiedData ) + SendPropInt( SENDINFO( m_WithProxy ) ) +END_SEND_TABLE() + +IMPLEMENT_SERVERCLASS_ST( CTest_ProxyToggle_Networkable, DT_ProxyToggle ) + SendPropDataTable( "blah", 0, &REFERENCE_SEND_TABLE( DT_ProxyToggle_ProxiedData ), SendProxy_TestProxyToggle ) +END_SEND_TABLE() + + + +// ---------------------------------------------------------------------------------------- // +// Console commands for this test. +// ---------------------------------------------------------------------------------------- // + +void Test_ProxyToggle_EnableProxy( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Error( "Test_ProxyToggle_EnableProxy: requires parameter (0 or 1)." ); + } + + g_bEnableProxy = !!atoi( args[ 1 ] ); +} + +void Test_ProxyToggle_SetValue( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Error( "Test_ProxyToggle_SetValue: requires value parameter." ); + } + else if ( !g_pTestObj ) + { + Error( "Test_ProxyToggle_SetValue: no entity present." ); + } + + g_pTestObj->m_WithProxy = atoi( args[ 1 ] ); +} + +ConCommand cc_Test_ProxyToggle_EnableProxy( "Test_ProxyToggle_EnableProxy", Test_ProxyToggle_EnableProxy, 0, FCVAR_CHEAT ); +ConCommand cc_Test_ProxyToggle_SetValue( "Test_ProxyToggle_SetValue", Test_ProxyToggle_SetValue, 0, FCVAR_CHEAT ); + + diff --git a/sp/src/game/server/test_stressentities.cpp b/sp/src/game/server/test_stressentities.cpp new file mode 100644 index 00000000..b98e18df --- /dev/null +++ b/sp/src/game/server/test_stressentities.cpp @@ -0,0 +1,152 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "test_stressentities.h" +#include "vstdlib/random.h" +#include "world.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CStressEntityReg *CStressEntityReg::s_pHead = NULL; + + +// CStressEntityReg::s_pHead in array form for convenient access. +CUtlVector g_StressEntityRegs; + +CUtlVector g_StressEntities; + + +CBaseEntity* MoveToRandomSpot( CBaseEntity *pEnt ) +{ + if ( pEnt ) + { + CBasePlayer *pLocalPlayer = UTIL_GetLocalPlayer(); + if ( pLocalPlayer ) + { + Vector vForward; + pLocalPlayer->EyeVectors(&vForward ); + + UTIL_SetOrigin( pEnt, GetRandomSpot() ); + } + } + + return pEnt; +} + + +Vector GetRandomSpot() +{ + CWorld *pEnt = GetWorldEntity(); + if ( pEnt ) + { + Vector vMin, vMax; + pEnt->GetWorldBounds( vMin, vMax ); + return Vector( + RandomFloat( vMin.x, vMax.x ), + RandomFloat( vMin.y, vMax.y ), + RandomFloat( vMin.z, vMax.z ) ); + } + else + { + return Vector( 0, 0, 0 ); + } +} + + +void Test_InitRandomEntitySpawner( const CCommand &args ) +{ + // Put the list of registered functions into array form for convenience. + g_StressEntityRegs.Purge(); + for ( CStressEntityReg *pCur=CStressEntityReg::GetListHead(); pCur; pCur=pCur->GetNext() ) + g_StressEntityRegs.AddToTail( pCur ); + + // Create slots for all the entities.. + int nSlots = 100; + if ( args.ArgC() >= 2 ) + nSlots = atoi( args[ 1 ] ); + + g_StressEntities.Purge(); + g_StressEntities.SetSize( nSlots ); + + Msg( "Test_InitRandomEntitySpawner: created %d slots.\n", nSlots ); +} + + +void Test_SpawnRandomEntities( const CCommand &args ) +{ + if ( args.ArgC() < 3 ) + { + Error( "Test_SpawnRandomEntities missing arguments." ); + } + + if ( g_StressEntities.Count() == 0 ) + { + Error( "Test_SpawnRandomEntities: not initialized (call Test_InitRandomEntitySpawner frst)." ); + } + + int nMin = atoi( args[ 1 ] ); + int nMax = atoi( args[ 2 ] ); + int count = RandomInt( nMin, nMax ); + + for ( int i=0; i < count; i++ ) + { + int iSlot = RandomInt( 0, g_StressEntities.Count() - 1 ); + + // Remove any old entity in this slot. + if ( g_StressEntities[iSlot].Get() ) + UTIL_RemoveImmediate( g_StressEntities[iSlot] ); + + // Create a new one in this slot. + int iType = RandomInt( 0, g_StressEntityRegs.Count() - 1 ); + g_StressEntities[iSlot] = g_StressEntityRegs[iType]->GetFn()(); + } +} + + +void Test_RandomizeInPVS( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Error( "Test_RandomizeInPVS " ); + } + + int percent = atoi( args[ 1 ] ); + for ( int i=0; i < g_StressEntities.Count(); i++ ) + { + CBaseEntity *pEnt = g_StressEntities[i]; + + if ( pEnt ) + { + if ( RandomInt( 0, 100 ) < percent ) + { + if ( pEnt->IsEffectActive( EF_NODRAW ) ) + pEnt->RemoveEffects( EF_NODRAW ); + else + pEnt->AddEffects( EF_NODRAW ); + } + } + } +} + + +void Test_RemoveAllRandomEntities() +{ + for ( int i=0; i < g_StressEntities.Count(); i++ ) + { + if ( g_StressEntities[i].Get() ) + UTIL_Remove( g_StressEntities[i] ); + } +} + + +ConCommand cc_Test_InitRandomEntitySpawner( "Test_InitRandomEntitySpawner", Test_InitRandomEntitySpawner, 0, FCVAR_CHEAT ); +ConCommand cc_Test_SpawnRandomEntities( "Test_SpawnRandomEntities", Test_SpawnRandomEntities, 0, FCVAR_CHEAT ); +ConCommand cc_Test_RandomizeInPVS( "Test_RandomizeInPVS", Test_RandomizeInPVS, 0, FCVAR_CHEAT ); +ConCommand cc_Test_RemoveAllRandomEntities( "Test_RemoveAllRandomEntities", Test_RemoveAllRandomEntities, 0, FCVAR_CHEAT ); + diff --git a/sp/src/game/server/test_stressentities.h b/sp/src/game/server/test_stressentities.h new file mode 100644 index 00000000..7583e4ec --- /dev/null +++ b/sp/src/game/server/test_stressentities.h @@ -0,0 +1,56 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef TEST_STRESSENTITIES_H +#define TEST_STRESSENTITIES_H +#ifdef _WIN32 +#pragma once +#endif + + +class CBaseEntity; + + +typedef CBaseEntity* (*StressEntityFn)(); // Function to create an entity for the stress test. + + +// Each game DLL can instantiate these to register types of entities it can create +// for the entity stress test. +class CStressEntityReg +{ +public: + + CStressEntityReg( StressEntityFn fn ) + { + m_pFn = fn; + m_pNext = s_pHead; + s_pHead = this; + } + + static CStressEntityReg*GetListHead() { return s_pHead; } + CStressEntityReg* GetNext() { return m_pNext; } + StressEntityFn GetFn() { return m_pFn; } + + +private: + static CStressEntityReg *s_pHead; // List of all CStressEntityReg's. + CStressEntityReg *m_pNext; + StressEntityFn m_pFn; +}; + + +// Use this macro to register a function to create stresstest entities. +#define REGISTER_STRESS_ENTITY( fnName ) static CStressEntityReg s_##fnName##__( fnName ); + + +// Helper function for the functions that create the stress entities. +// Moves the entity to a random place in the level and returns the entity. +CBaseEntity* MoveToRandomSpot( CBaseEntity *pEnt ); +Vector GetRandomSpot(); + + +#endif // TEST_STRESSENTITIES_H diff --git a/sp/src/game/server/testfunctions.cpp b/sp/src/game/server/testfunctions.cpp new file mode 100644 index 00000000..00de09cd --- /dev/null +++ b/sp/src/game/server/testfunctions.cpp @@ -0,0 +1,88 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "convar.h" +#include "tier0/dbg.h" +#include "player.h" +#include "world.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void Test_CreateEntity( const CCommand &args ) +{ + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + + // Require a player entity or that the command was entered from the dedicated server console + if ( !pPlayer && UTIL_GetCommandClientIndex() > 0 ) + { + return; + } + + if ( args.ArgC() < 2 ) + { + Error( "Test_CreateEntity: requires entity classname argument." ); + } + + const char *pClassName = args[ 1 ]; + + // Don't allow regular users to create point_servercommand entities for the same reason as blocking ent_fire + if ( pPlayer && !Q_stricmp( pClassName, "point_servercommand" ) ) + { + if ( engine->IsDedicatedServer() ) + { + // We allow people with disabled autokick to do it, because they already have rcon. + if ( pPlayer->IsAutoKickDisabled() == false ) + return; + } + else if ( gpGlobals->maxClients > 1 ) + { + // On listen servers with more than 1 player, only allow the host to create point_servercommand. + CBasePlayer *pHostPlayer = UTIL_GetListenServerHost(); + if ( pPlayer != pHostPlayer ) + return; + } + } + + if ( !CreateEntityByName( pClassName ) ) + { + Error( "Test_CreateEntity( %s ) failed.", pClassName ); + } +} + + +void Test_RandomPlayerPosition() +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + CWorld *pWorld = GetWorldEntity(); + if ( !pPlayer ) + { + Error( "Test_RandomPlayerPosition: no local player entity." ); + } + else if ( !pWorld ) + { + Error( "Test_RandomPlayerPosition: no world entity." ); + } + + + + Vector vMin, vMax; + pWorld->GetWorldBounds( vMin, vMax ); + + Vector vecOrigin; + vecOrigin.x = RandomFloat( vMin.x, vMax.x ); + vecOrigin.y = RandomFloat( vMin.y, vMax.y ); + vecOrigin.z = RandomFloat( vMin.z, vMax.z ); + pPlayer->ForceOrigin( vecOrigin ); +} + + +ConCommand cc_Test_CreateEntity( "Test_CreateEntity", Test_CreateEntity, 0, FCVAR_CHEAT ); +ConCommand cc_Test_RandomPlayerPosition( "Test_RandomPlayerPosition", Test_RandomPlayerPosition, 0, FCVAR_CHEAT ); + + diff --git a/sp/src/game/server/testtraceline.cpp b/sp/src/game/server/testtraceline.cpp new file mode 100644 index 00000000..d017a1b2 --- /dev/null +++ b/sp/src/game/server/testtraceline.cpp @@ -0,0 +1,82 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// -------------------------------------------------------------------------------- // +// An entity used to test traceline +// -------------------------------------------------------------------------------- // + +class CTestTraceline : public CPointEntity +{ +public: + DECLARE_CLASS( CTestTraceline, CPointEntity ); + + void Spawn( void ); + int UpdateTransmitState(); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + +private: + void Spin( void ); +}; + + +// This table encodes the CBaseEntity data. +IMPLEMENT_SERVERCLASS_ST_NOBASE(CTestTraceline, DT_TestTraceline) + SendPropInt (SENDINFO(m_clrRender), 32, SPROP_UNSIGNED ), + SendPropVector (SENDINFO(m_vecOrigin), 19, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER), + SendPropFloat (SENDINFO_VECTORELEM(m_angRotation, 0), 19, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER), + SendPropFloat (SENDINFO_VECTORELEM(m_angRotation, 1), 19, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER), + SendPropFloat (SENDINFO_VECTORELEM(m_angRotation, 2), 19, 0, MIN_COORD_INTEGER, MAX_COORD_INTEGER), + SendPropEHandle (SENDINFO_NAME(m_hMoveParent, moveparent)), +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( test_traceline, CTestTraceline ); + +BEGIN_DATADESC( CTestTraceline ) + + // Function Pointers + DEFINE_FUNCTION( Spin ), + +END_DATADESC() + + +void CTestTraceline::Spawn( void ) +{ + SetRenderColor( 255, 255, 255, 255 ); + SetNextThink( gpGlobals->curtime ); + + SetThink( &CTestTraceline::Spin ); +} + +void CTestTraceline::Spin( void ) +{ + static ConVar traceline_spin( "traceline_spin","1" ); + + if (traceline_spin.GetInt()) + { + float s = sin( gpGlobals->curtime ); + QAngle angles = GetLocalAngles(); + + angles[0] = 180.0 * 0.5 * (s * s * s + 1.0f) + 90; + angles[1] = gpGlobals->curtime * 10; + + SetLocalAngles( angles ); + + } + SetNextThink( gpGlobals->curtime ); +} + +int CTestTraceline::UpdateTransmitState() +{ + return SetTransmitState( FL_EDICT_ALWAYS ); +} diff --git a/sp/src/game/server/textstatsmgr.cpp b/sp/src/game/server/textstatsmgr.cpp new file mode 100644 index 00000000..602b61cd --- /dev/null +++ b/sp/src/game/server/textstatsmgr.cpp @@ -0,0 +1,174 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "textstatsmgr.h" +#include "tier0/dbg.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CTextStatsMgr g_TextStatsMgr; // The default text stats manager. + + +// ------------------------------------------------------------------------------------------ // +// CTextStatsMgr implementation. +// ------------------------------------------------------------------------------------------ // +CTextStatsMgr::CTextStatsMgr( void ) +{ + m_szStatFilename[0] = 0; +} + +bool CTextStatsMgr::WriteFile( IFileSystem *pFileSys, const char *pFilename ) +{ + // If no filename was specified, use out preset one + if ( !pFilename ) + { + pFilename = m_szStatFilename; + } + + FileHandle_t hFile = pFileSys->Open( pFilename, "wt", "LOGDIR" ); + if ( hFile == FILESYSTEM_INVALID_HANDLE ) + return false; + + CTextStat *pHead = CTextStat::GetTextStatsList(); + for ( CTextStat *pCur=pHead->m_pNext; pCur != pHead; pCur=pCur->m_pNext ) + { + if ( pCur->m_pMgr == this ) + pCur->m_PrintFn( pFileSys, hFile, pCur->m_pUserData ); + } + + pFileSys->Close( hFile ); + + // Call each CTextStatFile.. + for( CTextStatFile *pCurFile=CTextStatFile::s_pHead; pCurFile; pCurFile=pCurFile->m_pNext ) + { + pCurFile->m_pFn(); + } + + return true; +} + +char *CTextStatsMgr::GetStatsFilename( void ) +{ + return m_szStatFilename; +} + +void CTextStatsMgr::SetStatsFilename( char *sFilename ) +{ + Assert( sFilename && sFilename[0] ); + + Q_strncpy( m_szStatFilename, sFilename, sizeof(m_szStatFilename) ); +} + +// ------------------------------------------------------------------------------------------ // +// CTextStat implementation. +// ------------------------------------------------------------------------------------------ // + +CTextStat::CTextStat() +{ + m_pPrev = m_pNext = this; + m_pMgr = NULL; +} + + +CTextStat::CTextStat( TextStatPrintFn printFn, void *pUserData, CTextStatsMgr *pMgr ) +{ + m_pPrev = m_pNext = this; + Init( printFn, pUserData, pMgr ); +} + + +CTextStat::~CTextStat() +{ + Term(); +} + + +void CTextStat::Init( TextStatPrintFn printFn, void *pUserData, CTextStatsMgr *pMgr ) +{ + Term(); + + m_pPrev = GetTextStatsList(); + m_pNext = GetTextStatsList()->m_pNext; + m_pPrev->m_pNext = m_pNext->m_pPrev = this; + + m_PrintFn = printFn; + m_pUserData = pUserData; + m_pMgr = pMgr; +} + + +void CTextStat::Term() +{ + // Remove from the global list. + m_pPrev->m_pNext = m_pNext; + m_pNext->m_pPrev = m_pPrev; + m_pPrev = m_pNext = this; + m_pMgr = NULL; +} + + +CTextStat::CTextStat( bool bGlobalListHead ) +{ + Assert( bGlobalListHead ); + m_pPrev = m_pNext = this; +} + + +CTextStat* CTextStat::GetTextStatsList() +{ + static CTextStat theList( true ); + return &theList; +} + + +void CTextStat::RemoveFn( void *pUserData ) +{ + CTextStat *pReg = (CTextStat*)pUserData; + pReg->Term(); +} + + +// ------------------------------------------------------------------------------------------ // +// CTextStatInt implementation. +// ------------------------------------------------------------------------------------------ // + +CTextStatInt::CTextStatInt( const char *pName, int initialValue, CTextStatsMgr *pMgr ) +{ + m_pName = pName; + m_Value = initialValue; + m_Reg.Init( &CTextStatInt::PrintFn, this, pMgr ); +} + + +void CTextStatInt::PrintFn( IFileSystem *pFileSys, FileHandle_t hFile, void *pUserData ) +{ + CTextStatInt *pStat = (CTextStatInt*)pUserData; + pFileSys->FPrintf( hFile, "%s %d\n", pStat->m_pName, pStat->m_Value ); +} + + + +// ------------------------------------------------------------------------------------------ // +// CTextStatFile functions. +// ------------------------------------------------------------------------------------------ // + +CTextStatFile *CTextStatFile::s_pHead = NULL; + + +CTextStatFile::CTextStatFile( TextStatFileFn fn ) +{ + m_pFn = fn; + m_pNext = CTextStatFile::s_pHead; + CTextStatFile::s_pHead = this; +} + + + + + diff --git a/sp/src/game/server/textstatsmgr.h b/sp/src/game/server/textstatsmgr.h new file mode 100644 index 00000000..473e1a0e --- /dev/null +++ b/sp/src/game/server/textstatsmgr.h @@ -0,0 +1,133 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef TEXTSTATSMGR_H +#define TEXTSTATSMGR_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "filesystem.h" +#include "utllinkedlist.h" + + +// Text stats get to print their stuff by implementing this type of function. +typedef void (*TextStatPrintFn)( IFileSystem *pFileSys, FileHandle_t hFile, void *pUserData ); +typedef void (*TextStatFileFn)(); + + +// The CTextStatsMgr is just a collection of CTextStat's that go into the same file. +class CTextStatsMgr +{ +public: + CTextStatsMgr( void ); + + // Write a file with all the registered stats. + bool WriteFile( IFileSystem *pFileSystem, const char *pFilename = NULL ); + + // Get the preset filename to write stats to, if none is specified when writing + char *GetStatsFilename( void ); + + // Set the filename to write stats to, if none is specified when writing + void SetStatsFilename( char *sFilename ); + +private: + char m_szStatFilename[ MAX_PATH ]; +}; + + +// This is the default CTextStatsMgr, but there can be any number of them. +extern CTextStatsMgr g_TextStatsMgr; + + + +// Make these to register text stat functions. +class CTextStat +{ +friend class CTextStatsMgr; + +public: + CTextStat(); + CTextStat( TextStatPrintFn fn, void *pUserData, CTextStatsMgr *pMgr=&g_TextStatsMgr ); + ~CTextStat(); + + // This can be called if you don't want to pass parameters into the constructor. + void Init( TextStatPrintFn printFn, void *pUserData, CTextStatsMgr *pMgr=&g_TextStatsMgr ); + void Term(); + + +private: + + // Special constructor to just tie off the linked list. + CTextStat( bool bGlobalListHead ); + + // The global list of CTextStats. + static CTextStat* GetTextStatsList(); + + static void RemoveFn( void *pUserData ); + + // Link it into the global list. + CTextStat *m_pPrev; + CTextStat *m_pNext; + + CTextStatsMgr *m_pMgr; + + TextStatPrintFn m_PrintFn; + void *m_pUserData; +}; + + +// This class registers like a ConVar and acts like an int. When the game is shutdown, +// its value will be saved in the stats file along with its name.s +class CTextStatInt +{ +public: + CTextStatInt( const char *pName, int initialValue=0, CTextStatsMgr *pMgr=&g_TextStatsMgr ); + + operator int() const { return m_Value; } + int operator=( int val ) { m_Value = val; return m_Value; } + + int operator++() { m_Value++; return m_Value; } + int operator--() { m_Value--; return m_Value; } + int operator+=( int val ) { m_Value += val; return m_Value; } + int operator-=( int val ) { m_Value -= val; return m_Value; } + int operator*=( int val ) { m_Value *= val; return m_Value; } + int operator/=( int val ) { m_Value /= val; return m_Value; } + + +private: + + static void PrintFn( IFileSystem *pFileSys, FileHandle_t hFile, void *pUserData ); + + +private: + + const char *m_pName; + int m_Value; + + CTextStat m_Reg; // Use to register ourselves. +}; + + +// This can be registered to get a callback when the text stats mgr is saving its files. +// You can write data out to your own file in here. +class CTextStatFile +{ +public: + CTextStatFile( TextStatFileFn fn ); + +private: + friend class CTextStatsMgr; + + static CTextStatFile *s_pHead; + CTextStatFile *m_pNext; + TextStatFileFn m_pFn; +}; + + +#endif // TEXTSTATSMGR_H diff --git a/sp/src/game/server/timedeventmgr.cpp b/sp/src/game/server/timedeventmgr.cpp new file mode 100644 index 00000000..3f221584 --- /dev/null +++ b/sp/src/game/server/timedeventmgr.cpp @@ -0,0 +1,151 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "timedeventmgr.h" +#include "tier0/vprof.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// ------------------------------------------------------------------------------------------ // +// CEventRegister. +// ------------------------------------------------------------------------------------------ // + +CEventRegister::CEventRegister() +{ + m_bRegistered = false; + m_pEventMgr = NULL; +} + + +CEventRegister::~CEventRegister() +{ + Term(); +} + + +void CEventRegister::Init( CTimedEventMgr *pMgr, IEventRegisterCallback *pCallback ) +{ + Term(); + m_pEventMgr = pMgr; + m_pCallback = pCallback; +} + + +void CEventRegister::Term() +{ + // Unregister. + if ( m_pEventMgr && m_bRegistered ) + { + m_pEventMgr->RemoveEvent( this ); + } +} + + +void CEventRegister::SetUpdateInterval( float interval ) +{ + Assert( m_pEventMgr ); + + if ( m_pEventMgr ) + { + // Register for this event. + m_flUpdateInterval = interval; + m_flNextEventTime = gpGlobals->curtime + m_flUpdateInterval; + + m_pEventMgr->RegisterForNextEvent( this ); + } +} + + +void CEventRegister::StopUpdates() +{ + if ( m_pEventMgr ) + { + // Unregister our next event. + m_pEventMgr->RemoveEvent( this ); + } +} + + +void CEventRegister::Reregister() +{ + if ( m_flUpdateInterval > 1e-6 && m_pEventMgr ) + { + while ( m_flNextEventTime <= gpGlobals->curtime ) + { + m_flNextEventTime += m_flUpdateInterval; + } + + m_pEventMgr->RegisterForNextEvent( this ); + } +} + + +// ------------------------------------------------------------------------------------------ // +// CTimedEventMgr. +// ------------------------------------------------------------------------------------------ // + +bool TimedEventMgr_LessFunc( CEventRegister* const &a, CEventRegister* const &b ) +{ + return a->m_flNextEventTime > b->m_flNextEventTime; +} + + +CTimedEventMgr::CTimedEventMgr() +{ + m_Events.SetLessFunc( TimedEventMgr_LessFunc ); +} + + +void CTimedEventMgr::FireEvents() +{ + VPROF( "CTimedEventMgr::FireEvents" ); + while ( m_Events.Count() ) + { + // Fire the top element, then break out. + CEventRegister *pEvent = m_Events.ElementAtHead(); + if ( gpGlobals->curtime >= pEvent->m_flNextEventTime ) + { + // Reregister for the timed event, then fire the callback for the event. + m_Events.RemoveAtHead(); + pEvent->m_bRegistered = false; + pEvent->Reregister(); + + pEvent->m_pCallback->FireEvent(); + } + else + { + break; + } + } +} + + +void CTimedEventMgr::RegisterForNextEvent( CEventRegister *pEvent ) +{ + RemoveEvent( pEvent ); + m_Events.Insert( pEvent ); + pEvent->m_bRegistered = true; +} + + +void CTimedEventMgr::RemoveEvent( CEventRegister *pEvent ) +{ + if ( pEvent->m_bRegistered ) + { + // Find the event in the list and remove it. + int cnt = m_Events.Count(); + for ( int i=0; i < cnt; i++ ) + { + if ( m_Events.Element( i ) == pEvent ) + { + m_Events.RemoveAt( i ); + break; + } + } + } +} diff --git a/sp/src/game/server/timedeventmgr.h b/sp/src/game/server/timedeventmgr.h new file mode 100644 index 00000000..66b15861 --- /dev/null +++ b/sp/src/game/server/timedeventmgr.h @@ -0,0 +1,93 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef TIMEDEVENTMGR_H +#define TIMEDEVENTMGR_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "utlpriorityqueue.h" + + +// +// +// These classes provide fast timed event callbacks. To use them, make a CTimedEventMgr +// and put CEventRegister objects in your objects that want the timed events. +// +// + + +class CTimedEventMgr; + + +abstract_class IEventRegisterCallback +{ +public: + virtual void FireEvent() = 0; +}; + + +class CEventRegister +{ +friend bool TimedEventMgr_LessFunc( CEventRegister* const &a, CEventRegister* const &b ); +friend class CTimedEventMgr; + +public: + CEventRegister(); + ~CEventRegister(); + + // Call this before ever calling SetUpdateInterval(). + void Init( CTimedEventMgr *pMgr, IEventRegisterCallback *pCallback ); + + // Use these to start and stop getting updates. + void SetUpdateInterval( float interval ); + void StopUpdates(); + + inline bool IsRegistered() const { return m_bRegistered; } + +private: + + void Reregister(); // After having an event processed, this is called to have it register for the next one. + void Term(); + + +private: + + CTimedEventMgr *m_pEventMgr; + float m_flNextEventTime; + float m_flUpdateInterval; + IEventRegisterCallback *m_pCallback; + bool m_bRegistered; +}; + + +class CTimedEventMgr +{ +friend class CEventRegister; + +public: + CTimedEventMgr(); + + // Call this each frame to fire events. + void FireEvents(); + + +private: + + // Things used by CEventRegister. + void RegisterForNextEvent( CEventRegister *pEvent ); + void RemoveEvent( CEventRegister *pEvent ); + +private: + + // Events, sorted by the time at which they will fire. + CUtlPriorityQueue m_Events; +}; + + +#endif // TIMEDEVENTMGR_H diff --git a/sp/src/game/server/toolframework_server.cpp b/sp/src/game/server/toolframework_server.cpp new file mode 100644 index 00000000..7bbe2b56 --- /dev/null +++ b/sp/src/game/server/toolframework_server.cpp @@ -0,0 +1,138 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//===========================================================================// +#include "cbase.h" +#include "igamesystem.h" +#include "toolframework/iserverenginetools.h" +#include "init_factory.h" + +//----------------------------------------------------------------------------- +// Purpose: This is an autogame system which is used to call back into the engine at appropriate points +// so that IToolSystems can get these hooks at the correct time +//----------------------------------------------------------------------------- +class CToolFrameworkServer : public CAutoGameSystemPerFrame, public IToolFrameworkServer +{ +public: + virtual bool Init(); + // Level init, shutdown + virtual void LevelInitPreEntity(); + // entities are created / spawned / precached here + virtual void LevelInitPostEntity(); + virtual void LevelShutdownPreEntity(); + // Entities are deleted / released here... + virtual void LevelShutdownPostEntity(); + // Called each frame before entities think + virtual void FrameUpdatePreEntityThink(); + // called after entities think + virtual void FrameUpdatePostEntityThink(); + virtual void PreClientUpdate(); + virtual void PreSetupVisibility(); + + + IServerEngineTools *m_pTools; +}; + +// Singleton +static CToolFrameworkServer g_ToolFrameworkServer; +IToolFrameworkServer *g_pToolFrameworkServer = &g_ToolFrameworkServer; + +#ifndef NO_TOOLFRAMEWORK + +bool ToolsEnabled() +{ + return g_ToolFrameworkServer.m_pTools && g_ToolFrameworkServer.m_pTools->InToolMode() && !engine->IsDedicatedServer(); +} + +#endif + +bool CToolFrameworkServer::Init() +{ + factorylist_t list; + FactoryList_Retrieve( list ); + + // Latch onto internal interface + m_pTools = ( IServerEngineTools * )list.engineFactory( VSERVERENGINETOOLS_INTERFACE_VERSION, NULL ); + + if ( !m_pTools && !engine->IsDedicatedServer() ) + { + return false; + } + + return true; +} + +void CToolFrameworkServer::LevelInitPreEntity() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->LevelInitPreEntityAllTools(); +} + +void CToolFrameworkServer::LevelInitPostEntity() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->LevelInitPostEntityAllTools(); +} + +void CToolFrameworkServer::LevelShutdownPreEntity() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->LevelShutdownPreEntityAllTools(); +} + +void CToolFrameworkServer::LevelShutdownPostEntity() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->LevelShutdownPostEntityAllTools(); +} + +void CToolFrameworkServer::FrameUpdatePreEntityThink() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->FrameUpdatePreEntityThinkAllTools(); +} + +void CToolFrameworkServer::FrameUpdatePostEntityThink() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->FrameUpdatePostEntityThinkAllTools(); +} + +void CToolFrameworkServer::PreClientUpdate() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->PreClientUpdateAllTools(); +} + +void CToolFrameworkServer::PreSetupVisibility() +{ + if ( !m_pTools ) + { + return; + } + m_pTools->PreSetupVisibilityAllTools(); +} diff --git a/sp/src/game/server/toolframework_server.h b/sp/src/game/server/toolframework_server.h new file mode 100644 index 00000000..941df135 --- /dev/null +++ b/sp/src/game/server/toolframework_server.h @@ -0,0 +1,27 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//===========================================================================// + +#ifndef TOOLFRAMEWORK_SERVER_H +#define TOOLFRAMEWORK_SERVER_H + +#ifdef _WIN32 +#pragma once +#endif + + +//----------------------------------------------------------------------------- +// Are tools enabled? +//----------------------------------------------------------------------------- +#ifndef NO_TOOLFRAMEWORK +bool ToolsEnabled(); +#else +#define ToolsEnabled() 0 +#endif + + +#endif // TOOLFRAMEWORK_SERVER_H diff --git a/sp/src/game/server/trains.cpp b/sp/src/game/server/trains.cpp new file mode 100644 index 00000000..bf01ae6f --- /dev/null +++ b/sp/src/game/server/trains.cpp @@ -0,0 +1,3378 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Spawn, think, and touch functions for trains, etc. +// +//=============================================================================// + +#include "cbase.h" +#include "ai_basenpc.h" +#include "trains.h" +#include "ndebugoverlay.h" +#include "entitylist.h" +#include "engine/IEngineSound.h" +#include "soundenvelope.h" +#include "physics_npc_solver.h" +#include "vphysics/friction.h" +#include "hierarchy.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static void PlatSpawnInsideTrigger(edict_t *pevPlatform); + +#define SF_PLAT_TOGGLE 0x0001 + +class CBasePlatTrain : public CBaseToggle +{ + DECLARE_CLASS( CBasePlatTrain, CBaseToggle ); + +public: + ~CBasePlatTrain(); + bool KeyValue( const char *szKeyName, const char *szValue ); + void Precache( void ); + + // This is done to fix spawn flag collisions between this class and a derived class + virtual bool IsTogglePlat( void ) { return (m_spawnflags & SF_PLAT_TOGGLE) ? true : false; } + + DECLARE_DATADESC(); + + void PlayMovingSound(); + void StopMovingSound(); + + string_t m_NoiseMoving; // sound a plat makes while moving + string_t m_NoiseArrived; + + CSoundPatch *m_pMovementSound; +#ifdef HL1_DLL + int m_MoveSound; + int m_StopSound; +#endif + + float m_volume; // Sound volume + float m_flTWidth; + float m_flTLength; +}; + +BEGIN_DATADESC( CBasePlatTrain ) + + DEFINE_KEYFIELD( m_NoiseMoving, FIELD_SOUNDNAME, "noise1" ), + DEFINE_KEYFIELD( m_NoiseArrived, FIELD_SOUNDNAME, "noise2" ), + +#ifdef HL1_DLL + DEFINE_KEYFIELD( m_MoveSound, FIELD_INTEGER, "movesnd" ), + DEFINE_KEYFIELD( m_StopSound, FIELD_INTEGER, "stopsnd" ), + +#endif + DEFINE_SOUNDPATCH( m_pMovementSound ), + + DEFINE_KEYFIELD( m_volume, FIELD_FLOAT, "volume" ), + + DEFINE_FIELD( m_flTWidth, FIELD_FLOAT ), + DEFINE_FIELD( m_flTLength, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flLip, FIELD_FLOAT, "lip" ), + DEFINE_KEYFIELD( m_flWait, FIELD_FLOAT, "wait" ), + DEFINE_KEYFIELD( m_flHeight, FIELD_FLOAT, "height" ), + +END_DATADESC() + + +bool CBasePlatTrain::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "rotation")) + { + m_vecFinalAngle.x = atof(szValue); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +CBasePlatTrain::~CBasePlatTrain() +{ + StopMovingSound(); +} + +void CBasePlatTrain::PlayMovingSound() +{ + StopMovingSound(); + if(m_NoiseMoving != NULL_STRING ) + { + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + CPASAttenuationFilter filter( this ); + m_pMovementSound = controller.SoundCreate( filter, entindex(), CHAN_STATIC, STRING(m_NoiseMoving), ATTN_NORM ); + + controller.Play( m_pMovementSound, m_volume, PITCH_NORM ); + } +} + +void CBasePlatTrain::StopMovingSound() +{ + if ( m_pMovementSound ) + { + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + controller.SoundDestroy( m_pMovementSound ); + m_pMovementSound = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBasePlatTrain::Precache( void ) +{ + //Fill in a default value if necessary + UTIL_ValidateSoundName( m_NoiseMoving, "Plat.DefaultMoving" ); + UTIL_ValidateSoundName( m_NoiseArrived, "Plat.DefaultArrive" ); + +#ifdef HL1_DLL +// set the plat's "in-motion" sound + switch (m_MoveSound) + { + default: + case 0: + m_NoiseMoving = MAKE_STRING( "Plat.DefaultMoving" ); + break; + case 1: + m_NoiseMoving = MAKE_STRING("Plat.BigElev1"); + break; + case 2: + m_NoiseMoving = MAKE_STRING("Plat.BigElev2"); + break; + case 3: + m_NoiseMoving = MAKE_STRING("Plat.TechElev1"); + break; + case 4: + m_NoiseMoving = MAKE_STRING("Plat.TechElev2"); + break; + case 5: + m_NoiseMoving = MAKE_STRING("Plat.TechElev3"); + break; + case 6: + m_NoiseMoving = MAKE_STRING("Plat.FreightElev1"); + break; + case 7: + m_NoiseMoving = MAKE_STRING("Plat.FreightElev2"); + break; + case 8: + m_NoiseMoving = MAKE_STRING("Plat.HeavyElev"); + break; + case 9: + m_NoiseMoving = MAKE_STRING("Plat.RackElev"); + break; + case 10: + m_NoiseMoving = MAKE_STRING("Plat.RailElev"); + break; + case 11: + m_NoiseMoving = MAKE_STRING("Plat.SqueakElev"); + break; + case 12: + m_NoiseMoving = MAKE_STRING("Plat.OddElev1"); + break; + case 13: + m_NoiseMoving = MAKE_STRING("Plat.OddElev2"); + break; + } + +// set the plat's 'reached destination' stop sound + switch (m_StopSound) + { + default: + case 0: + m_NoiseArrived = MAKE_STRING( "Plat.DefaultArrive" ); + break; + case 1: + m_NoiseArrived = MAKE_STRING("Plat.BigElevStop1"); + break; + case 2: + m_NoiseArrived = MAKE_STRING("Plat.BigElevStop2"); + break; + case 3: + m_NoiseArrived = MAKE_STRING("Plat.FreightElevStop"); + break; + case 4: + m_NoiseArrived = MAKE_STRING("Plat.HeavyElevStop"); + break; + case 5: + m_NoiseArrived = MAKE_STRING("Plat.RackStop"); + break; + case 6: + m_NoiseArrived = MAKE_STRING("Plat.RailStop"); + break; + case 7: + m_NoiseArrived = MAKE_STRING("Plat.SqueakStop"); + break; + case 8: + m_NoiseArrived = MAKE_STRING("Plat.QuickStop"); + break; + } + +#endif // HL1_DLL + + //Precache them all + PrecacheScriptSound( (char *) STRING(m_NoiseMoving) ); + PrecacheScriptSound( (char *) STRING(m_NoiseArrived) ); + +} + + +class CFuncPlat : public CBasePlatTrain +{ + DECLARE_CLASS( CFuncPlat, CBasePlatTrain ); +public: + void Spawn( void ); + void Precache( void ); + bool CreateVPhysics(); + void Setup( void ); + + virtual void Blocked( CBaseEntity *pOther ); + void PlatUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void CallGoDown( void ) { GoDown(); } + void CallHitTop( void ) { HitTop(); } + void CallHitBottom( void ) { HitBottom(); } + + virtual void GoUp( void ); + virtual void GoDown( void ); + virtual void HitTop( void ); + virtual void HitBottom( void ); + + void InputToggle(inputdata_t &data); + void InputGoUp(inputdata_t &data); + void InputGoDown(inputdata_t &data); + + DECLARE_DATADESC(); + +private: + + string_t m_sNoise; +}; + + +BEGIN_DATADESC( CFuncPlat ) + + DEFINE_FIELD( m_sNoise, FIELD_STRING ), + + // Function Pointers + DEFINE_FUNCTION( PlatUse ), + DEFINE_FUNCTION( CallGoDown ), + DEFINE_FUNCTION( CallHitTop ), + DEFINE_FUNCTION( CallHitBottom ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "GoUp", InputGoUp ), + DEFINE_INPUTFUNC( FIELD_VOID, "GoDown", InputGoDown ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_plat, CFuncPlat ); + +//================================================== +// CPlatTrigger +//================================================== +class CPlatTrigger : public CBaseEntity +{ + DECLARE_CLASS( CPlatTrigger, CBaseEntity ); +public: + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_DONT_SAVE; } + void SpawnInsideTrigger( CFuncPlat *pPlatform ); + void Touch( CBaseEntity *pOther ); + CFuncPlat *m_pPlatform; +}; + +void CFuncPlat::Setup( void ) +{ + if (m_flTLength == 0) + { + m_flTLength = 80; + } + + if (m_flTWidth == 0) + { + m_flTWidth = 10; + } + + SetLocalAngles( vec3_angle ); + SetSolid( SOLID_BSP ); + SetMoveType( MOVETYPE_PUSH ); + + // Set size and link into world + SetModel( STRING( GetModelName() ) ); + + m_vecPosition1 = GetLocalOrigin(); //Top + m_vecPosition2 = GetLocalOrigin(); //Bottom + + if ( m_flHeight != 0 ) + { + m_vecPosition2.z = GetLocalOrigin().z - m_flHeight; + } + else + { + // NOTE: This works because the angles were set to vec3_angle above + m_vecPosition2.z = GetLocalOrigin().z - CollisionProp()->OBBSize().z + 8; + } + + if (m_flSpeed == 0) + { + m_flSpeed = 150; + } + + if ( m_volume == 0.0f ) + { + m_volume = 0.85f; + } +} + + +void CFuncPlat::Precache( ) +{ + BaseClass::Precache(); + + if ( IsTogglePlat() == false ) + { + // Create the "start moving" trigger + PlatSpawnInsideTrigger( edict() ); + } +} + + +void CFuncPlat::Spawn( ) +{ + Setup(); + Precache(); + + // If this platform is the target of some button, it starts at the TOP position, + // and is brought down by that button. Otherwise, it starts at BOTTOM. + if ( GetEntityName() != NULL_STRING ) + { + UTIL_SetOrigin( this, m_vecPosition1); + m_toggle_state = TS_AT_TOP; + SetUse( &CFuncPlat::PlatUse ); + } + else + { + UTIL_SetOrigin( this, m_vecPosition2); + m_toggle_state = TS_AT_BOTTOM; + } + CreateVPhysics(); +} + +bool CFuncPlat::CreateVPhysics() +{ + VPhysicsInitShadow( false, false ); + return true; +} + + +static void PlatSpawnInsideTrigger(edict_t* pevPlatform) +{ + // old code: //GetClassPtr( (CPlatTrigger *)NULL)->SpawnInsideTrigger( GetClassPtr( (CFuncPlat *)pevPlatform ) ); + CPlatTrigger *plattrig = CREATE_UNSAVED_ENTITY( CPlatTrigger, "plat_trigger" ); + plattrig->SpawnInsideTrigger( (CFuncPlat *)GetContainingEntity( pevPlatform ) ); +} + + +// +// Create a trigger entity for a platform. +// +void CPlatTrigger::SpawnInsideTrigger( CFuncPlat *pPlatform ) +{ + m_pPlatform = pPlatform; + // Create trigger entity, "point" it at the owning platform, give it a touch method + SetSolid( SOLID_BSP ); + AddSolidFlags( FSOLID_TRIGGER ); + SetMoveType( MOVETYPE_NONE ); + SetLocalOrigin( pPlatform->GetLocalOrigin() ); + + // Establish the trigger field's size + CCollisionProperty *pCollision = m_pPlatform->CollisionProp(); + Vector vecTMin = pCollision->OBBMins() + Vector ( 25 , 25 , 0 ); + Vector vecTMax = pCollision->OBBMaxs() + Vector ( 25 , 25 , 8 ); + vecTMin.z = vecTMax.z - ( m_pPlatform->m_vecPosition1.z - m_pPlatform->m_vecPosition2.z + 8 ); + if ( pCollision->OBBSize().x <= 50 ) + { + vecTMin.x = (pCollision->OBBMins().x + pCollision->OBBMaxs().x) / 2; + vecTMax.x = vecTMin.x + 1; + } + if ( pCollision->OBBSize().y <= 50 ) + { + vecTMin.y = (pCollision->OBBMins().y + pCollision->OBBMaxs().y) / 2; + vecTMax.y = vecTMin.y + 1; + } + UTIL_SetSize ( this, vecTMin, vecTMax ); +} + + +// +// When the platform's trigger field is touched, the platform ??? +// +void CPlatTrigger::Touch( CBaseEntity *pOther ) +{ + // Ignore touches by non-players + if ( !pOther->IsPlayer() ) + return; + + // Ignore touches by corpses + if (!pOther->IsAlive()) + return; + + // Make linked platform go up/down. + if (m_pPlatform->m_toggle_state == TS_AT_BOTTOM) + m_pPlatform->GoUp(); + else if (m_pPlatform->m_toggle_state == TS_AT_TOP) + m_pPlatform->SetMoveDoneTime( 1 );// delay going down +} + + + +//----------------------------------------------------------------------------- +// Purpose: Used when a platform is the target of a button. +// Start bringing platform down. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CFuncPlat::InputToggle(inputdata_t &data) +{ + if ( IsTogglePlat() ) + { + if (m_toggle_state == TS_AT_TOP) + GoDown(); + else if ( m_toggle_state == TS_AT_BOTTOM ) + GoUp(); + } + else + { + SetUse( NULL ); + + if (m_toggle_state == TS_AT_TOP) + GoDown(); + } +} + +void CFuncPlat::InputGoUp(inputdata_t &data) +{ + if ( m_toggle_state == TS_AT_BOTTOM ) + GoUp(); +} + +void CFuncPlat::InputGoDown(inputdata_t &data) +{ + if ( m_toggle_state == TS_AT_TOP ) + GoDown(); +} + +//----------------------------------------------------------------------------- +// Purpose: Used when a platform is the target of a button. +// Start bringing platform down. +// Input : pActivator - +// pCaller - +// useType - +// value - +//----------------------------------------------------------------------------- +void CFuncPlat::PlatUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( IsTogglePlat() ) + { + // Top is off, bottom is on + bool on = (m_toggle_state == TS_AT_BOTTOM) ? true : false; + + if ( !ShouldToggle( useType, on ) ) + return; + + if (m_toggle_state == TS_AT_TOP) + GoDown(); + else if ( m_toggle_state == TS_AT_BOTTOM ) + GoUp(); + } + else + { + SetUse( NULL ); + + if (m_toggle_state == TS_AT_TOP) + GoDown(); + } +} + + +// +// Platform is at top, now starts moving down. +// +void CFuncPlat::GoDown( void ) +{ + PlayMovingSound(); + + ASSERT(m_toggle_state == TS_AT_TOP || m_toggle_state == TS_GOING_UP); + m_toggle_state = TS_GOING_DOWN; + SetMoveDone(&CFuncPlat::CallHitBottom); + LinearMove(m_vecPosition2, m_flSpeed); +} + + +// +// Platform has hit bottom. Stops and waits forever. +// +void CFuncPlat::HitBottom( void ) +{ + StopMovingSound(); + + if ( m_NoiseArrived != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_WEAPON; + ep.m_pSoundName = STRING(m_NoiseArrived); + ep.m_flVolume = m_volume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + ASSERT(m_toggle_state == TS_GOING_DOWN); + m_toggle_state = TS_AT_BOTTOM; +} + + +// +// Platform is at bottom, now starts moving up +// +void CFuncPlat::GoUp( void ) +{ + PlayMovingSound(); + + ASSERT(m_toggle_state == TS_AT_BOTTOM || m_toggle_state == TS_GOING_DOWN); + m_toggle_state = TS_GOING_UP; + SetMoveDone(&CFuncPlat::CallHitTop); + LinearMove(m_vecPosition1, m_flSpeed); +} + + +// +// Platform has hit top. Pauses, then starts back down again. +// +void CFuncPlat::HitTop( void ) +{ + StopMovingSound(); + + if ( m_NoiseArrived != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_WEAPON; + ep.m_pSoundName = STRING(m_NoiseArrived); + ep.m_flVolume = m_volume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + ASSERT(m_toggle_state == TS_GOING_UP); + m_toggle_state = TS_AT_TOP; + + if ( !IsTogglePlat() ) + { + // After a delay, the platform will automatically start going down again. + SetMoveDone( &CFuncPlat::CallGoDown ); + SetMoveDoneTime( 3 ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when we are blocked. +//----------------------------------------------------------------------------- +void CFuncPlat::Blocked( CBaseEntity *pOther ) +{ + DevMsg( 2, "%s Blocked by %s\n", GetClassname(), pOther->GetClassname() ); + + // Hurt the blocker a little + pOther->TakeDamage( CTakeDamageInfo( this, this, 1, DMG_CRUSH ) ); + + if (m_sNoise != NULL_STRING) + { + StopSound(entindex(), CHAN_STATIC, (char*)STRING(m_sNoise)); + } + + // Send the platform back where it came from + ASSERT(m_toggle_state == TS_GOING_UP || m_toggle_state == TS_GOING_DOWN); + if (m_toggle_state == TS_GOING_UP) + { + GoDown(); + } + else if (m_toggle_state == TS_GOING_DOWN) + { + GoUp (); + } +} + + +class CFuncPlatRot : public CFuncPlat +{ + DECLARE_CLASS( CFuncPlatRot, CFuncPlat ); +public: + void Spawn( void ); + void SetupRotation( void ); + + virtual void GoUp( void ); + virtual void GoDown( void ); + virtual void HitTop( void ); + virtual void HitBottom( void ); + + void RotMove( QAngle &destAngle, float time ); + DECLARE_DATADESC(); + + QAngle m_end, m_start; +}; + +LINK_ENTITY_TO_CLASS( func_platrot, CFuncPlatRot ); + +BEGIN_DATADESC( CFuncPlatRot ) + + DEFINE_FIELD( m_end, FIELD_VECTOR ), + DEFINE_FIELD( m_start, FIELD_VECTOR ), + +END_DATADESC() + + +void CFuncPlatRot::SetupRotation( void ) +{ + if ( m_vecFinalAngle.x != 0 ) // This plat rotates too! + { + CBaseToggle::AxisDir(); + m_start = GetLocalAngles(); + m_end = GetLocalAngles() + m_vecMoveAng * m_vecFinalAngle.x; + } + else + { + m_start = vec3_angle; + m_end = vec3_angle; + } + if ( GetEntityName() != NULL_STRING ) // Start at top + { + SetLocalAngles( m_end ); + } +} + + +void CFuncPlatRot::Spawn( void ) +{ + BaseClass::Spawn(); + SetupRotation(); +} + +void CFuncPlatRot::GoDown( void ) +{ + BaseClass::GoDown(); + RotMove( m_start, GetMoveDoneTime() ); +} + + +// +// Platform has hit bottom. Stops and waits forever. +// +void CFuncPlatRot::HitBottom( void ) +{ + BaseClass::HitBottom(); + SetLocalAngularVelocity( vec3_angle ); + SetLocalAngles( m_start ); +} + + +// +// Platform is at bottom, now starts moving up +// +void CFuncPlatRot::GoUp( void ) +{ + BaseClass::GoUp(); + RotMove( m_end, GetMoveDoneTime() ); +} + + +// +// Platform has hit top. Pauses, then starts back down again. +// +void CFuncPlatRot::HitTop( void ) +{ + BaseClass::HitTop(); + SetLocalAngularVelocity( vec3_angle ); + SetLocalAngles( m_end ); +} + + +void CFuncPlatRot::RotMove( QAngle &destAngle, float time ) +{ + // set destdelta to the vector needed to move + QAngle vecDestDelta = destAngle - GetLocalAngles(); + + // Travel time is so short, we're practically there already; so make it so. + if ( time >= 0.1) + SetLocalAngularVelocity( vecDestDelta * (1.0 / time) ); + else + { + SetLocalAngularVelocity( vecDestDelta ); + SetMoveDoneTime( 1 ); + } +} + + +class CFuncTrain : public CBasePlatTrain +{ + DECLARE_CLASS( CFuncTrain, CBasePlatTrain ); +public: + void Spawn( void ); + void Precache( void ); + void Activate( void ); + void OnRestore( void ); + + void SetupTarget( void ); + void Blocked( CBaseEntity *pOther ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void Wait( void ); + void Next( void ); + + //Inputs + void InputToggle(inputdata_t &data); + void InputStart(inputdata_t &data); + void InputStop(inputdata_t &data); + + void Start( void ); + void Stop( void ); + + DECLARE_DATADESC(); + +public: + EHANDLE m_hCurrentTarget; + + bool m_activated; + EHANDLE m_hEnemy; + float m_flBlockDamage; // Damage to inflict when blocked. + float m_flNextBlockTime; + string_t m_iszLastTarget; + +}; + +LINK_ENTITY_TO_CLASS( func_train, CFuncTrain ); + + +BEGIN_DATADESC( CFuncTrain ) + + DEFINE_FIELD( m_hCurrentTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_activated, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hEnemy, FIELD_EHANDLE ), + DEFINE_FIELD( m_iszLastTarget, FIELD_STRING ), + DEFINE_FIELD( m_flNextBlockTime, FIELD_TIME ), + + DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), + + // Function Pointers + DEFINE_FUNCTION( Wait ), + DEFINE_FUNCTION( Next ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Start", InputStart ), + DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Purpose: Handles a train being blocked by an entity. +// Input : pOther - What was hit. +//----------------------------------------------------------------------------- +void CFuncTrain::Blocked( CBaseEntity *pOther ) +{ + if ( gpGlobals->curtime < m_flNextBlockTime ) + return; + + m_flNextBlockTime = gpGlobals->curtime + 0.5; + + //Inflict damage + pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); +} + + +void CFuncTrain::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + //If we've been waiting to be retriggered, move to the next destination + if ( m_spawnflags & SF_TRAIN_WAIT_RETRIGGER ) + { + // Move toward my target + m_spawnflags &= ~SF_TRAIN_WAIT_RETRIGGER; + Next(); + } + else + { + m_spawnflags |= SF_TRAIN_WAIT_RETRIGGER; + + // Pop back to last target if it's available + if ( m_hEnemy ) + { + m_target = m_hEnemy->GetEntityName(); + } + + SetNextThink( TICK_NEVER_THINK ); + SetLocalVelocity( vec3_origin ); + + if ( m_NoiseArrived != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = STRING(m_NoiseArrived); + ep.m_flVolume = m_volume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + } +} + +void CFuncTrain::Wait( void ) +{ + //If we're moving passed a path track, then trip its output + variant_t emptyVariant; + m_hCurrentTarget->AcceptInput( "InPass", this, this, emptyVariant, 0 ); + + // need pointer to LAST target. + if ( m_hCurrentTarget->HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) || HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) + { + AddSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ); + + // Clear the sound channel. + StopMovingSound(); + + if ( m_NoiseArrived != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = STRING(m_NoiseArrived); + ep.m_flVolume = m_volume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + SetMoveDoneTime( -1 ); + + return; + } + + //NOTENOTE: -1 wait will wait forever + if ( m_flWait != 0 ) + { + SetMoveDoneTime( m_flWait ); + + StopMovingSound(); + + if ( m_NoiseArrived != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = STRING(m_NoiseArrived); + ep.m_flVolume = m_volume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + SetMoveDone( &CFuncTrain::Next ); + } + else + { + // Do it right now + Next(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Advances the train to the next path corner on the path. +//----------------------------------------------------------------------------- +void CFuncTrain::Next( void ) +{ + //Find our next target + CBaseEntity *pTarg = GetNextTarget(); + + //If none, we're done + if ( pTarg == NULL ) + { + //Stop the moving sound + StopMovingSound(); + + // Play stop sound + if ( m_NoiseArrived != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = STRING(m_NoiseArrived); + ep.m_flVolume = m_volume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + return; + } + + // Save last target in case we need to find it again + m_iszLastTarget = m_target; + + m_target = pTarg->m_target; + m_flWait = pTarg->GetDelay(); + + // If our target has a speed, take it + if ( m_hCurrentTarget && m_hCurrentTarget->m_flSpeed != 0 ) + { + m_flSpeed = m_hCurrentTarget->m_flSpeed; + DevMsg( 2, "Train %s speed to %4.2f\n", GetDebugName(), m_flSpeed ); + } + + // Keep track of this since path corners change our target for us + m_hCurrentTarget = pTarg; + m_hEnemy = pTarg; + + //Check for teleport + if ( m_hCurrentTarget->HasSpawnFlags( SF_CORNER_TELEPORT ) ) + { + IncrementInterpolationFrame(); + + // This is supposed to place the center of the func_train at the target's origin. + // FIXME: This is totally busted! It's using the wrong space for the computation... + UTIL_SetOrigin( this, pTarg->GetLocalOrigin() - CollisionProp()->OBBCenter() ); + + // Get on with doing the next path corner. + Wait(); + } + else + { + // Normal linear move + PlayMovingSound(); + + SetMoveDone( &CFuncTrain::Wait ); + + // This is supposed to place the center of the func_train at the target's origin. + // FIXME: This is totally busted! It's using the wrong space for the computation... + LinearMove ( pTarg->GetLocalOrigin() - CollisionProp()->OBBCenter(), m_flSpeed ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called after all the entities spawn. +//----------------------------------------------------------------------------- +void CFuncTrain::Activate( void ) +{ + BaseClass::Activate(); + + // Not yet active, so teleport to first target + if ( m_activated == false ) + { + SetupTarget(); + + m_activated = true; + + if ( m_hCurrentTarget.Get() == NULL ) + return; + + // This is supposed to place the center of the func_train at the target's origin. + // FIXME: This is totally busted! It's using the wrong space for the computation... + UTIL_SetOrigin( this, m_hCurrentTarget->GetLocalOrigin() - CollisionProp()->OBBCenter() ); + if ( GetSolid() == SOLID_BSP ) + { + VPhysicsInitShadow( false, false ); + } + + // Start immediately if not triggered + if ( !GetEntityName() ) + { + SetMoveDoneTime( 0.1 ); + SetMoveDone( &CFuncTrain::Next ); + } + else + { + m_spawnflags |= SF_TRAIN_WAIT_RETRIGGER; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrain::SetupTarget( void ) +{ + // Find our target whenever we don't have one (level transition) + if ( !m_hCurrentTarget ) + { + CBaseEntity *pTarg = gEntList.FindEntityByName( NULL, m_target ); + + if ( pTarg == NULL ) + { + Msg( "Can't find target of train %s\n", STRING(m_target) ); + return; + } + + // Keep track of this since path corners change our target for us + m_target = pTarg->m_target; + m_hCurrentTarget = pTarg; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrain::Spawn( void ) +{ + Precache(); + + if ( m_flSpeed == 0 ) + { + m_flSpeed = 100; + } + + if ( !m_target ) + { + Warning("FuncTrain '%s' has no target.\n", GetDebugName()); + } + + if ( m_flBlockDamage == 0 ) + { + m_flBlockDamage = 2; + } + + SetMoveType( MOVETYPE_PUSH ); + SetSolid( SOLID_BSP ); + SetModel( STRING( GetModelName() ) ); + if ( m_spawnflags & SF_TRACKTRAIN_PASSABLE ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + m_activated = false; + + if ( m_volume == 0.0f ) + { + m_volume = 0.85f; + } +} + + +void CFuncTrain::Precache( void ) +{ + BaseClass::Precache(); +} + + +void CFuncTrain::OnRestore( void ) +{ + BaseClass::OnRestore(); + + // Are we moving? + if ( IsMoving() ) + { + // Continue moving to the same target + m_target = m_iszLastTarget; + } + + SetupTarget(); +} + + +void CFuncTrain::InputToggle( inputdata_t &data ) +{ + //If we've been waiting to be retriggered, move to the next destination + if( HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) + { + Start(); + } + else + { + Stop(); + } +} + + +void CFuncTrain::InputStart( inputdata_t &data ) +{ + Start(); +} + + +void CFuncTrain::InputStop( inputdata_t &data ) +{ + Stop(); +} + + +void CFuncTrain::Start( void ) +{ + //start moving + if( HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) + { + // Move toward my target + RemoveSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ); + Next(); + } +} + + +void CFuncTrain::Stop( void ) +{ + //stop moving + if( !HasSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ) ) + { + AddSpawnFlags( SF_TRAIN_WAIT_RETRIGGER ); + + // Pop back to last target if it's available + if ( m_hEnemy ) + { + m_target = m_hEnemy->GetEntityName(); + } + + SetNextThink( TICK_NEVER_THINK ); + SetAbsVelocity( vec3_origin ); + + if ( m_NoiseArrived != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = STRING(m_NoiseArrived); + ep.m_flVolume = m_volume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + + //Do not teleport to our final move destination + SetMoveDone( NULL ); + SetMoveDoneTime( -1 ); + } +} + +BEGIN_DATADESC( CFuncTrackTrain ) + + DEFINE_KEYFIELD( m_length, FIELD_FLOAT, "wheels" ), + DEFINE_KEYFIELD( m_height, FIELD_FLOAT, "height" ), + DEFINE_KEYFIELD( m_maxSpeed, FIELD_FLOAT, "startspeed" ), + DEFINE_KEYFIELD( m_flBank, FIELD_FLOAT, "bank" ), + DEFINE_KEYFIELD( m_flBlockDamage, FIELD_FLOAT, "dmg" ), + DEFINE_KEYFIELD( m_iszSoundMove, FIELD_SOUNDNAME, "MoveSound" ), + DEFINE_KEYFIELD( m_iszSoundMovePing, FIELD_SOUNDNAME, "MovePingSound" ), + DEFINE_KEYFIELD( m_iszSoundStart, FIELD_SOUNDNAME, "StartSound" ), + DEFINE_KEYFIELD( m_iszSoundStop, FIELD_SOUNDNAME, "StopSound" ), + DEFINE_KEYFIELD( m_nMoveSoundMinPitch, FIELD_INTEGER, "MoveSoundMinPitch" ), + DEFINE_KEYFIELD( m_nMoveSoundMaxPitch, FIELD_INTEGER, "MoveSoundMaxPitch" ), + DEFINE_KEYFIELD( m_flMoveSoundMinTime, FIELD_FLOAT, "MoveSoundMinTime" ), + DEFINE_KEYFIELD( m_flMoveSoundMaxTime, FIELD_FLOAT, "MoveSoundMaxTime" ), + DEFINE_FIELD( m_flNextMoveSoundTime, FIELD_TIME ), + DEFINE_KEYFIELD( m_eVelocityType, FIELD_INTEGER, "velocitytype" ), + DEFINE_KEYFIELD( m_eOrientationType, FIELD_INTEGER, "orientationtype" ), + + DEFINE_FIELD( m_ppath, FIELD_CLASSPTR ), + DEFINE_FIELD( m_dir, FIELD_FLOAT ), + DEFINE_FIELD( m_controlMins, FIELD_VECTOR ), + DEFINE_FIELD( m_controlMaxs, FIELD_VECTOR ), + DEFINE_FIELD( m_flVolume, FIELD_FLOAT ), + DEFINE_FIELD( m_oldSpeed, FIELD_FLOAT ), + //DEFINE_FIELD( m_lastBlockPos, FIELD_POSITION_VECTOR ), // temp values for blocking, don't save + //DEFINE_FIELD( m_lastBlockTick, FIELD_INTEGER ), + + DEFINE_FIELD( m_bSoundPlaying, FIELD_BOOLEAN ), + + DEFINE_KEYFIELD( m_bManualSpeedChanges, FIELD_BOOLEAN, "ManualSpeedChanges" ), + DEFINE_KEYFIELD( m_flAccelSpeed, FIELD_FLOAT, "ManualAccelSpeed" ), + DEFINE_KEYFIELD( m_flDecelSpeed, FIELD_FLOAT, "ManualDecelSpeed" ), + +#ifdef HL1_DLL + DEFINE_FIELD( m_bOnTrackChange, FIELD_BOOLEAN ), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartForward", InputStartForward ), + DEFINE_INPUTFUNC( FIELD_VOID, "StartBackward", InputStartBackward ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Resume", InputResume ), + DEFINE_INPUTFUNC( FIELD_VOID, "Reverse", InputReverse ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeed", InputSetSpeed ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedDir", InputSetSpeedDir ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedReal", InputSetSpeedReal ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedDirAccel", InputSetSpeedDirAccel ), + DEFINE_INPUTFUNC( FIELD_STRING, "TeleportToPathTrack", InputTeleportToPathTrack ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetSpeedForwardModifier", InputSetSpeedForwardModifier ), + + // Outputs + DEFINE_OUTPUT( m_OnStart, "OnStart" ), + DEFINE_OUTPUT( m_OnNext, "OnNextPoint" ), + + // Function Pointers + DEFINE_FUNCTION( Next ), + DEFINE_FUNCTION( Find ), + DEFINE_FUNCTION( NearestPath ), + DEFINE_FUNCTION( DeadEnd ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_tracktrain, CFuncTrackTrain ); + + +//----------------------------------------------------------------------------- +// Datatable +//----------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST( CFuncTrackTrain, DT_FuncTrackTrain ) +END_SEND_TABLE() + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CFuncTrackTrain::CFuncTrackTrain() +{ +#ifdef _DEBUG + m_controlMins.Init(); + m_controlMaxs.Init(); +#endif + + // These defaults match old func_tracktrains. Changing these defaults would + // require a vmf_tweak of older content to keep it from breaking. + m_eOrientationType = TrainOrientation_AtPathTracks; + m_eVelocityType = TrainVelocity_Instantaneous; + m_lastBlockPos.Init(); + m_lastBlockTick = gpGlobals->tickcount; + + m_flSpeedForwardModifier = 1.0f; + m_flUnmodifiedDesiredSpeed = 0.0f; + + m_bDamageChild = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CFuncTrackTrain::DrawDebugTextOverlays( void ) +{ + int nOffset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + Q_snprintf( tempstr,sizeof(tempstr), "angles: %g %g %g", (double)GetLocalAngles()[PITCH], (double)GetLocalAngles()[YAW], (double)GetLocalAngles()[ROLL] ); + EntityText( nOffset, tempstr, 0 ); + nOffset++; + + float flCurSpeed = GetLocalVelocity().Length(); + Q_snprintf( tempstr,sizeof(tempstr), "current speed (goal): %g (%g)", (double)flCurSpeed, (double)m_flSpeed ); + EntityText( nOffset, tempstr, 0 ); + nOffset++; + + Q_snprintf( tempstr,sizeof(tempstr), "max speed: %g", (double)m_maxSpeed ); + EntityText( nOffset, tempstr, 0 ); + nOffset++; + } + + return nOffset; +} + + +void CFuncTrackTrain::DrawDebugGeometryOverlays() +{ + BaseClass::DrawDebugGeometryOverlays(); + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + NDebugOverlay::Box( GetAbsOrigin(), -Vector(4,4,4),Vector(4,4,4), 255, 0, 255, 0, 0); + Vector out; + VectorTransform( Vector(m_length,0,0), EntityToWorldTransform(), out ); + NDebugOverlay::Box( out, -Vector(4,4,4),Vector(4,4,4), 255, 0, 255, 0, 0); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFuncTrackTrain::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "volume")) + { + m_flVolume = (float) (atoi(szValue)); + m_flVolume *= 0.1f; + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that stops the train. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputStop( inputdata_t &inputdata ) +{ + Stop(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler that starts the train moving. +//------------------------------------------------------------------------------ +void CFuncTrackTrain::InputResume( inputdata_t &inputdata ) +{ + m_flSpeed = m_oldSpeed; + Start(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler that reverses the trains current direction of motion. +//------------------------------------------------------------------------------ +void CFuncTrackTrain::InputReverse( inputdata_t &inputdata ) +{ + SetDirForward( !IsDirForward() ); + SetSpeed( m_flSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns whether we are travelling forward along our path. +//----------------------------------------------------------------------------- +bool CFuncTrackTrain::IsDirForward() +{ + return ( m_dir == 1 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets whether we go forward or backward along our path. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::SetDirForward( bool bForward ) +{ + if ( bForward && ( m_dir != 1 ) ) + { + // Reverse direction. + if ( m_ppath && m_ppath->GetPrevious() ) + { + m_ppath = m_ppath->GetPrevious(); + } + + m_dir = 1; + } + else if ( !bForward && ( m_dir != -1 ) ) + { + // Reverse direction. + if ( m_ppath && m_ppath->GetNext() ) + { + m_ppath = m_ppath->GetNext(); + } + + m_dir = -1; + } +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler that starts the train moving. +//------------------------------------------------------------------------------ +void CFuncTrackTrain::InputStartForward( inputdata_t &inputdata ) +{ + SetDirForward( true ); + SetSpeed( m_maxSpeed ); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler that starts the train moving. +//------------------------------------------------------------------------------ +void CFuncTrackTrain::InputStartBackward( inputdata_t &inputdata ) +{ + SetDirForward( false ); + SetSpeed( m_maxSpeed ); +} + + +//------------------------------------------------------------------------------ +// Purpose: Starts the train moving. +//------------------------------------------------------------------------------ +void CFuncTrackTrain::Start( void ) +{ + m_OnStart.FireOutput(this,this); + Next(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Toggles the train between moving and not moving. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputToggle( inputdata_t &inputdata ) +{ + if ( m_flSpeed == 0 ) + { + SetSpeed( m_maxSpeed ); + } + else + { + SetSpeed( 0 ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Handles player use so players can control the speed of the train. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // player +USE + if ( useType == USE_SET ) + { + float delta = value; + + delta = ((int)(m_flSpeed * 4) / (int)m_maxSpeed)*0.25 + 0.25 * delta; + if ( delta > 1 ) + delta = 1; + else if ( delta < -0.25 ) + delta = -0.25; + if ( m_spawnflags & SF_TRACKTRAIN_FORWARDONLY ) + { + if ( delta < 0 ) + delta = 0; + } + SetDirForward( delta >= 0 ); + delta = fabs(delta); + SetSpeed( m_maxSpeed * delta ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that sets the speed of the train. +// Input : Float speed from 0 to max speed, in units per second. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputSetSpeedReal( inputdata_t &inputdata ) +{ + SetSpeed( clamp( inputdata.value.Float(), 0.f, m_maxSpeed ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that sets the speed of the train. +// Input : Float speed scale from 0 to 1. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputSetSpeed( inputdata_t &inputdata ) +{ + float flScale = clamp( inputdata.value.Float(), 0.f, 1.f ); + SetSpeed( m_maxSpeed * flScale ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Input handler that sets the speed of the train and the direction +// based on the sign of the speed. +// Input : Float speed scale from -1 to 1. Negatives values indicate a reversed +// direction. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputSetSpeedDir( inputdata_t &inputdata ) +{ + float newSpeed = inputdata.value.Float(); + SetDirForward( newSpeed >= 0 ); + newSpeed = fabs(newSpeed); + float flScale = clamp( newSpeed, 0.f, 1.f ); + SetSpeed( m_maxSpeed * flScale ); +} + +//----------------------------------------------------------------------------- +// Purpose: Input handler that sets the speed of the train and the direction +// based on the sign of the speed, and accels/decels to that speed +// Input : Float speed scale from -1 to 1. Negatives values indicate a reversed +// direction. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputSetSpeedDirAccel( inputdata_t &inputdata ) +{ + SetSpeedDirAccel( inputdata.value.Float() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrackTrain::SetSpeedDirAccel( float flNewSpeed ) +{ + float newSpeed = flNewSpeed; + SetDirForward( newSpeed >= 0 ); + newSpeed = fabs( newSpeed ); + float flScale = clamp( newSpeed, 0.f, 1.f ); + SetSpeed( m_maxSpeed * flScale, true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputSetSpeedForwardModifier( inputdata_t &inputdata ) +{ + SetSpeedForwardModifier( inputdata.value.Float() ) ; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrackTrain::SetSpeedForwardModifier( float flModifier ) +{ + float flSpeedForwardModifier = flModifier; + flSpeedForwardModifier = fabs( flSpeedForwardModifier ); + + m_flSpeedForwardModifier = clamp( flSpeedForwardModifier, 0.f, 1.f ); + SetSpeed( m_flUnmodifiedDesiredSpeed, true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrackTrain::InputTeleportToPathTrack( inputdata_t &inputdata ) +{ + const char *pszName = inputdata.value.String(); + CPathTrack *pTrack = dynamic_cast( gEntList.FindEntityByName( NULL, pszName ) ); + + if ( pTrack ) + { + TeleportToPathTrack( pTrack ); + m_ppath = pTrack; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the speed of the train to the given value in units per second. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::SetSpeed( float flSpeed, bool bAccel /*= false */ ) +{ + m_bAccelToSpeed = bAccel; + + m_flUnmodifiedDesiredSpeed = flSpeed; + float flOldSpeed = m_flSpeed; + + // are we using a speed forward modifier? + if ( m_flSpeedForwardModifier < 1.0 && m_dir > 0 ) + { + flSpeed = flSpeed * m_flSpeedForwardModifier; + } + + if ( m_bAccelToSpeed ) + { + m_flDesiredSpeed = fabs( flSpeed ) * m_dir; + m_flSpeedChangeTime = gpGlobals->curtime; + + if ( m_flSpeed == 0 && abs(m_flDesiredSpeed) > 0 ) + { + m_flSpeed = 0.1; // little push to get us going + } + + Start(); + + return; + } + + m_flSpeed = fabs( flSpeed ) * m_dir; + + if ( m_flSpeed != flOldSpeed) + { + // Changing speed. + if ( m_flSpeed != 0 ) + { + if ( flOldSpeed == 0 ) + { + // Starting to move. + Start(); + } + else + { + // Continuing to move. + Next(); + } + } + else + { + // Stopping. + Stop(); + } + } + + DevMsg( 2, "TRAIN(%s), speed to %.2f\n", GetDebugName(), m_flSpeed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Stops the train. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::Stop( void ) +{ + SetLocalVelocity( vec3_origin ); + SetLocalAngularVelocity( vec3_angle ); + m_oldSpeed = m_flSpeed; + m_flSpeed = 0; + SoundStop(); + SetThink(NULL); +} + +static CBaseEntity *FindPhysicsBlockerForHierarchy( CBaseEntity *pParentEntity ) +{ + CUtlVector list; + GetAllInHierarchy( pParentEntity, list ); + CBaseEntity *pPhysicsBlocker = NULL; + float maxForce = 0; + for ( int i = 0; i < list.Count(); i++ ) + { + IPhysicsObject *pPhysics = list[i]->VPhysicsGetObject(); + if ( pPhysics ) + { + IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + CBaseEntity *pOtherEntity = static_cast(pOther->GetGameData()); + if ( pOtherEntity && pOtherEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + Vector normal; + pSnapshot->GetSurfaceNormal(normal); + float dot = DotProduct( pParentEntity->GetAbsVelocity(), pSnapshot->GetNormalForce() * normal ); + if ( !pPhysicsBlocker || dot > maxForce ) + { + pPhysicsBlocker = pOtherEntity; + maxForce = dot; + } + } + pSnapshot->NextFrictionData(); + } + pPhysics->DestroyFrictionSnapshot( pSnapshot ); + } + } + return pPhysicsBlocker; +} + +//----------------------------------------------------------------------------- +// Purpose: Called when we are blocked by another entity. +// Input : pOther - +//----------------------------------------------------------------------------- +void CFuncTrackTrain::Blocked( CBaseEntity *pOther ) +{ + // Blocker is on-ground on the train + if ( ( pOther->GetFlags() & FL_ONGROUND ) && pOther->GetGroundEntity() == this ) + { + DevMsg( 1, "TRAIN(%s): Blocked by %s\n", GetDebugName(), pOther->GetClassname() ); + float deltaSpeed = fabs(m_flSpeed); + if ( deltaSpeed > 50 ) + deltaSpeed = 50; + + Vector vecNewVelocity; + pOther->GetVelocity( &vecNewVelocity ); + if ( !vecNewVelocity.z ) + { + pOther->ApplyAbsVelocityImpulse( Vector(0,0,deltaSpeed) ); + } + return; + } + else + { + Vector vecNewVelocity; + vecNewVelocity = pOther->GetAbsOrigin() - GetAbsOrigin(); + VectorNormalize(vecNewVelocity); + vecNewVelocity *= m_flBlockDamage; + pOther->SetAbsVelocity( vecNewVelocity ); + } + if ( HasSpawnFlags(SF_TRACKTRAIN_UNBLOCKABLE_BY_PLAYER) ) + { + CBaseEntity *pPhysicsBlocker = FindPhysicsBlockerForHierarchy(this); + if ( pPhysicsBlocker ) + { + // This code keeps track of how long this train has been blocked + // The heuristic here is to keep instantaneous blocks from invoking the somewhat + // heavy-handed solver (which will disable collisions until we're clear) in cases + // where physics can solve it easily enough. + int ticksBlocked = gpGlobals->tickcount - m_lastBlockTick; + float dist = 0.0f; + // wait at least 10 ticks and make sure the train isn't actually moving before really blocking + const int MIN_BLOCKED_TICKS = 10; + if ( ticksBlocked > MIN_BLOCKED_TICKS ) + { + dist = (GetAbsOrigin() - m_lastBlockPos).Length(); + // must have moved at least 10% of normal velocity over the blocking interval, or we're being blocked + float minLength = GetAbsVelocity().Length() * TICK_INTERVAL * MIN_BLOCKED_TICKS * 0.10f; + if ( dist < minLength ) + { + // been stuck for more than one tick without moving much? + // yes, disable collisions with the physics object most likely to be blocking us + EntityPhysics_CreateSolver( this, pPhysicsBlocker, true, 4.0f ); + } + } + // first time blocking or moved too far since last block, reset + if ( dist > 1.0f || m_lastBlockTick < 0 ) + { + m_lastBlockPos = GetAbsOrigin(); + m_lastBlockTick = gpGlobals->tickcount; + } + } + // unblockable shouldn't damage the player in this case + if ( pOther->IsPlayer() ) + return; + } + + DevWarning( 2, "TRAIN(%s): Blocked by %s (dmg:%.2f)\n", GetDebugName(), pOther->GetClassname(), m_flBlockDamage ); + if ( m_flBlockDamage <= 0 ) + return; + + // we can't hurt this thing, so we're not concerned with it + pOther->TakeDamage( CTakeDamageInfo( this, this, m_flBlockDamage, DMG_CRUSH ) ); +} + + +extern void FixupAngles( QAngle &v ); + +#define TRAIN_MAXSPEED 1000 // approx max speed for sound pitch calculation + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrackTrain::SoundStop( void ) +{ + // if sound playing, stop it + if ( m_bSoundPlaying ) + { + if ( m_iszSoundMove != NULL_STRING ) + { + StopSound( entindex(), CHAN_STATIC, STRING( m_iszSoundMove ) ); + } + + if ( m_iszSoundStop != NULL_STRING ) + { + CPASAttenuationFilter filter( this ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = STRING(m_iszSoundStop); + ep.m_flVolume = m_flVolume; + ep.m_SoundLevel = SNDLVL_NORM; + + EmitSound( filter, entindex(), ep ); + } + } + + m_bSoundPlaying = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Update pitch based on speed, start sound if not playing. +// NOTE: when train goes through transition, m_bSoundPlaying should become +// false, which will cause the looped sound to restart. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::SoundUpdate( void ) +{ + if ( ( !m_iszSoundMove ) && ( !m_iszSoundStart ) && ( !m_iszSoundMovePing )) + { + return; + } + + // In multiplayer, only update the sound once a second + if ( g_pGameRules->IsMultiplayer() && m_bSoundPlaying ) + { + if ( m_flNextMPSoundTime > gpGlobals->curtime ) + return; + + m_flNextMPSoundTime = gpGlobals->curtime + 1.0; + } + + float flSpeedRatio = 0; + if ( HasSpawnFlags( SF_TRACKTRAIN_USE_MAXSPEED_FOR_PITCH ) ) + { + flSpeedRatio = clamp( fabs( m_flSpeed ) / m_maxSpeed, 0.f, 1.f ); + } + else + { + flSpeedRatio = clamp( fabs( m_flSpeed ) / TRAIN_MAXSPEED, 0.f, 1.f ); + } + + float flpitch = RemapVal( flSpeedRatio, 0, 1, m_nMoveSoundMinPitch, m_nMoveSoundMaxPitch ); + + CPASAttenuationFilter filter( this ); + CPASAttenuationFilter filterReliable( this ); + filterReliable.MakeReliable(); + + Vector vecWorldSpaceCenter = WorldSpaceCenter(); + + if (!m_bSoundPlaying) + { + if ( m_iszSoundStart != NULL_STRING ) + { + EmitSound_t ep; + ep.m_nChannel = CHAN_ITEM; + ep.m_pSoundName = STRING(m_iszSoundStart); + ep.m_flVolume = m_flVolume; + ep.m_SoundLevel = SNDLVL_NORM; + ep.m_pOrigin = &vecWorldSpaceCenter; + + EmitSound( filter, entindex(), ep ); + } + + if ( m_iszSoundMove != NULL_STRING ) + { + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = STRING(m_iszSoundMove); + ep.m_flVolume = m_flVolume; + ep.m_SoundLevel = SNDLVL_NORM; + ep.m_nPitch = (int)flpitch; + ep.m_pOrigin = &vecWorldSpaceCenter; + + EmitSound( filterReliable, entindex(), ep ); + } + + // We've just started moving. Delay the next move ping sound. + m_flNextMoveSoundTime = gpGlobals->curtime + RemapVal( flSpeedRatio, 0, 1, m_flMoveSoundMaxTime, m_flMoveSoundMinTime ); + + m_bSoundPlaying = true; + } + else + { + if ( m_iszSoundMove != NULL_STRING ) + { + // update pitch + EmitSound_t ep; + ep.m_nChannel = CHAN_STATIC; + ep.m_pSoundName = STRING(m_iszSoundMove); + ep.m_flVolume = m_flVolume; + ep.m_SoundLevel = SNDLVL_NORM; + ep.m_nPitch = (int)flpitch; + ep.m_nFlags = SND_CHANGE_PITCH; + ep.m_pOrigin = &vecWorldSpaceCenter; + + // In multiplayer, don't make this reliable + if ( g_pGameRules->IsMultiplayer() ) + { + EmitSound( filter, entindex(), ep ); + } + else + { + EmitSound( filterReliable, entindex(), ep ); + } + } + + if ( ( m_iszSoundMovePing != NULL_STRING ) && ( gpGlobals->curtime > m_flNextMoveSoundTime ) ) + { + EmitSound(STRING(m_iszSoundMovePing)); + m_flNextMoveSoundTime = gpGlobals->curtime + RemapVal( flSpeedRatio, 0, 1, m_flMoveSoundMaxTime, m_flMoveSoundMinTime ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pNode - +//----------------------------------------------------------------------------- +void CFuncTrackTrain::ArriveAtNode( CPathTrack *pNode ) +{ + // BUGBUG: This is wrong. We need to fire all targets between the one we've passed and the one + // we've switched to. + FirePassInputs( pNode, pNode->GetNext(), true ); + + // + // Disable train controls if this path track says to do so. + // + if ( pNode->HasSpawnFlags( SF_PATH_DISABLE_TRAIN ) ) + { + m_spawnflags |= SF_TRACKTRAIN_NOCONTROL; + } + + // + // Don't override the train speed if it's under user control. + // + if ( m_spawnflags & SF_TRACKTRAIN_NOCONTROL ) + { + // + // Don't copy speed from path track if it is 0 (uninitialized). + // + if ( pNode->m_flSpeed != 0 ) + { + SetSpeed( pNode->m_flSpeed ); + DevMsg( 2, "TrackTrain %s arrived at %s, speed to %4.2f\n", GetDebugName(), pNode->GetDebugName(), pNode->m_flSpeed ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Controls how the train accelerates as it moves along the path. +//----------------------------------------------------------------------------- +TrainVelocityType_t CFuncTrackTrain::GetTrainVelocityType() +{ + return m_eVelocityType; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pnext - +//----------------------------------------------------------------------------- +void CFuncTrackTrain::UpdateTrainVelocity( CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) +{ + switch ( GetTrainVelocityType() ) + { + case TrainVelocity_Instantaneous: + { + Vector velDesired = nextPos - GetLocalOrigin(); + VectorNormalize( velDesired ); + velDesired *= fabs( m_flSpeed ); + SetLocalVelocity( velDesired ); + break; + } + + case TrainVelocity_LinearBlend: + case TrainVelocity_EaseInEaseOut: + { + if ( m_bAccelToSpeed ) + { + float flPrevSpeed = m_flSpeed; + float flNextSpeed = m_flDesiredSpeed; + + if ( flPrevSpeed != flNextSpeed ) + { + float flSpeedChangeTime = ( abs(flNextSpeed) > abs(flPrevSpeed) ) ? m_flAccelSpeed : m_flDecelSpeed; + m_flSpeed = UTIL_Approach( m_flDesiredSpeed, m_flSpeed, flSpeedChangeTime * gpGlobals->frametime ); + } + } + else if ( pPrev && pNext ) + { + // Get the speed to blend from. + float flPrevSpeed = m_flSpeed; + if ( pPrev->m_flSpeed != 0 ) + { + flPrevSpeed = pPrev->m_flSpeed; + } + + // Get the speed to blend to. + float flNextSpeed = flPrevSpeed; + if ( pNext->m_flSpeed != 0 ) + { + flNextSpeed = pNext->m_flSpeed; + } + + // If they're different, do the blend. + if ( flPrevSpeed != flNextSpeed ) + { + Vector vecSegment = pNext->GetLocalOrigin() - pPrev->GetLocalOrigin(); + float flSegmentLen = vecSegment.Length(); + if ( flSegmentLen ) + { + Vector vecCurOffset = GetLocalOrigin() - pPrev->GetLocalOrigin(); + float p = vecCurOffset.Length() / flSegmentLen; + if ( GetTrainVelocityType() == TrainVelocity_EaseInEaseOut ) + { + p = SimpleSplineRemapVal( p, 0.0f, 1.0f, 0.0f, 1.0f ); + } + + m_flSpeed = m_dir * ( flPrevSpeed * ( 1 - p ) + flNextSpeed * p ); + } + } + else + { + m_flSpeed = m_dir * flPrevSpeed; + } + } + + Vector velDesired = nextPos - GetLocalOrigin(); + VectorNormalize( velDesired ); + velDesired *= fabs( m_flSpeed ); + SetLocalVelocity( velDesired ); + break; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Controls how the train blends angles as it moves along the path. +//----------------------------------------------------------------------------- +TrainOrientationType_t CFuncTrackTrain::GetTrainOrientationType() +{ +#ifdef HL1_DLL + return TrainOrientation_AtPathTracks; +#else + return m_eOrientationType; +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pnext - +//----------------------------------------------------------------------------- +void CFuncTrackTrain::UpdateTrainOrientation( CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) +{ + // FIXME: old way of doing fixed orienation trains, remove! + if ( HasSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ) ) + return; + + // Trains *can* work in local space, but only if all elements of the track share + // the same move parent as the train. + Assert( !pPrev || (pPrev->GetMoveParent() == GetMoveParent()) ); + + switch ( GetTrainOrientationType() ) + { + case TrainOrientation_Fixed: + { + // Fixed orientation. Do nothing. + break; + } + + case TrainOrientation_AtPathTracks: + { + UpdateOrientationAtPathTracks( pPrev, pNext, nextPos, flInterval ); + break; + } + + case TrainOrientation_EaseInEaseOut: + case TrainOrientation_LinearBlend: + { + UpdateOrientationBlend( GetTrainOrientationType(), pPrev, pNext, nextPos, flInterval ); + break; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Adjusts our angles as we hit each path track. This is for support of +// trains with wheels that round corners a la HL1 trains. +// FIXME: move into path_track, have the angles come back from LookAhead +//----------------------------------------------------------------------------- +void CFuncTrackTrain::UpdateOrientationAtPathTracks( CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) +{ + if ( !m_ppath ) + return; + + Vector nextFront = GetLocalOrigin(); + + CPathTrack *pNextNode = NULL; + + nextFront.z -= m_height; + if ( m_length > 0 ) + { + m_ppath->LookAhead( nextFront, IsDirForward() ? m_length : -m_length, 0, &pNextNode ); + } + else + { + m_ppath->LookAhead( nextFront, IsDirForward() ? 100 : -100, 0, &pNextNode ); + } + nextFront.z += m_height; + + Vector vecFaceDir = nextFront - GetLocalOrigin(); + if ( !IsDirForward() ) + { + vecFaceDir *= -1; + } + QAngle angles; + VectorAngles( vecFaceDir, angles ); + // !!! All of this crap has to be done to make the angles not wrap around, revisit this. + FixupAngles( angles ); + + // Wrapped with this bool so we don't affect old trains + if ( m_bManualSpeedChanges ) + { + if ( pNextNode && pNextNode->GetOrientationType() == TrackOrientation_FacePathAngles ) + { + angles = pNextNode->GetOrientation( IsDirForward() ); + } + } + + QAngle curAngles = GetLocalAngles(); + FixupAngles( curAngles ); + + if ( !pPrev || (vecFaceDir.x == 0 && vecFaceDir.y == 0) ) + angles = curAngles; + + DoUpdateOrientation( curAngles, angles, flInterval ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Blends our angles using one of two orientation blending types. +// ASSUMES that eOrientationType is either LinearBlend or EaseInEaseOut. +// FIXME: move into path_track, have the angles come back from LookAhead +//----------------------------------------------------------------------------- +void CFuncTrackTrain::UpdateOrientationBlend( TrainOrientationType_t eOrientationType, CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ) +{ + // Get the angles to blend from. + QAngle angPrev = pPrev->GetOrientation( IsDirForward() ); + FixupAngles( angPrev ); + + // Get the angles to blend to. + QAngle angNext; + if ( pNext ) + { + angNext = pNext->GetOrientation( IsDirForward() ); + FixupAngles( angNext ); + } + else + { + // At a dead end, just use the last path track's angles. + angNext = angPrev; + } + + if ( m_spawnflags & SF_TRACKTRAIN_NOPITCH ) + { + angNext[PITCH] = angPrev[PITCH]; + } + + // Calculate our parametric distance along the path segment from 0 to 1. + float p = 0; + if ( pPrev && ( angPrev != angNext ) ) + { + Vector vecSegment = pNext->GetLocalOrigin() - pPrev->GetLocalOrigin(); + float flSegmentLen = vecSegment.Length(); + if ( flSegmentLen ) + { + Vector vecCurOffset = GetLocalOrigin() - pPrev->GetLocalOrigin(); + p = vecCurOffset.Length() / flSegmentLen; + } + } + + if ( eOrientationType == TrainOrientation_EaseInEaseOut ) + { + p = SimpleSplineRemapVal( p, 0.0f, 1.0f, 0.0f, 1.0f ); + } + + //Msg( "UpdateOrientationFacePathAngles: %s->%s, p=%f, ", pPrev->GetDebugName(), pNext->GetDebugName(), p ); + + Quaternion qtPrev; + Quaternion qtNext; + + AngleQuaternion( angPrev, qtPrev ); + AngleQuaternion( angNext, qtNext ); + + QAngle angNew = angNext; + float flAngleDiff = QuaternionAngleDiff( qtPrev, qtNext ); + if ( flAngleDiff ) + { + Quaternion qtNew; + QuaternionSlerp( qtPrev, qtNext, p, qtNew ); + QuaternionAngles( qtNew, angNew ); + } + + if ( m_spawnflags & SF_TRACKTRAIN_NOPITCH ) + { + angNew[PITCH] = angPrev[PITCH]; + } + + DoUpdateOrientation( GetLocalAngles(), angNew, flInterval ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets our angular velocity to approach the target angles over the given interval. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::DoUpdateOrientation( const QAngle &curAngles, const QAngle &angles, float flInterval ) +{ + float vy, vx; + if ( !(m_spawnflags & SF_TRACKTRAIN_NOPITCH) ) + { + vx = UTIL_AngleDistance( angles.x, curAngles.x ); + } + else + { + vx = 0; + } + + vy = UTIL_AngleDistance( angles.y, curAngles.y ); + + // HACKHACK: Clamp really small angular deltas to avoid rotating movement on things + // that are close enough + if ( fabs(vx) < 0.1 ) + { + vx = 0; + } + if ( fabs(vy) < 0.1 ) + { + vy = 0; + } + + if ( flInterval == 0 ) + { + // Avoid dividing by zero + flInterval = 0.1; + } + + QAngle vecAngVel( vx / flInterval, vy / flInterval, GetLocalAngularVelocity().z ); + + if ( m_flBank != 0 ) + { + if ( vecAngVel.y < -5 ) + { + vecAngVel.z = UTIL_AngleDistance( UTIL_ApproachAngle( -m_flBank, curAngles.z, m_flBank*2 ), curAngles.z); + } + else if ( vecAngVel.y > 5 ) + { + vecAngVel.z = UTIL_AngleDistance( UTIL_ApproachAngle( m_flBank, curAngles.z, m_flBank*2 ), curAngles.z); + } + else + { + vecAngVel.z = UTIL_AngleDistance( UTIL_ApproachAngle( 0, curAngles.z, m_flBank*4 ), curAngles.z) * 4; + } + } + + SetLocalAngularVelocity( vecAngVel ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pTeleport - +//----------------------------------------------------------------------------- +void CFuncTrackTrain::TeleportToPathTrack( CPathTrack *pTeleport ) +{ + QAngle angCur = GetLocalAngles(); + + Vector nextPos = pTeleport->GetLocalOrigin(); + Vector look = nextPos; + pTeleport->LookAhead( look, m_length, 0 ); + + QAngle nextAngles; + if ( HasSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ) || ( look == nextPos ) ) + { + nextAngles = GetLocalAngles(); + } + else + { + nextAngles = pTeleport->GetOrientation( IsDirForward() ); + if ( HasSpawnFlags( SF_TRACKTRAIN_NOPITCH ) ) + { + nextAngles[PITCH] = angCur[PITCH]; + } + } + + Teleport( &pTeleport->GetLocalOrigin(), &nextAngles, NULL ); + SetLocalAngularVelocity( vec3_angle ); + + variant_t emptyVariant; + pTeleport->AcceptInput( "InTeleport", this, this, emptyVariant, 0 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Advances the train to the next path corner on the path. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::Next( void ) +{ + if ( !m_flSpeed ) + { + DevMsg( 2, "TRAIN(%s): Speed is 0\n", GetDebugName() ); + SoundStop(); + return; + } + + if ( !m_ppath ) + { + DevMsg( 2, "TRAIN(%s): Lost path\n", GetDebugName() ); + SoundStop(); + m_flSpeed = 0; + return; + } + + SoundUpdate(); + + // + // Based on our current position and speed, look ahead along our path and see + // where we should be in 0.1 seconds. + // + Vector nextPos = GetLocalOrigin(); + float flSpeed = m_flSpeed; + + nextPos.z -= m_height; + CPathTrack *pNextNext = NULL; + CPathTrack *pNext = m_ppath->LookAhead( nextPos, flSpeed * 0.1, 1, &pNextNext ); + //Assert( pNext != NULL ); + + // If we're moving towards a dead end, but our desired speed goes in the opposite direction + // this fixes us from stalling + if ( m_bManualSpeedChanges && ( ( flSpeed < 0 ) != ( m_flDesiredSpeed < 0 ) ) ) + { + if ( !pNext ) + pNext = m_ppath; + } + + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + if ( pNext != NULL ) + { + NDebugOverlay::Line( GetAbsOrigin(), pNext->GetAbsOrigin(), 255, 0, 0, true, 0.1 ); + NDebugOverlay::Line( pNext->GetAbsOrigin(), pNext->GetAbsOrigin() + Vector( 0,0,32), 255, 0, 0, true, 0.1 ); + NDebugOverlay::Box( pNext->GetAbsOrigin(), Vector( -8, -8, -8 ), Vector( 8, 8, 8 ), 255, 0, 0, 0, 0.1 ); + } + + if ( pNextNext != NULL ) + { + NDebugOverlay::Line( GetAbsOrigin(), pNextNext->GetAbsOrigin(), 0, 255, 0, true, 0.1 ); + NDebugOverlay::Line( pNextNext->GetAbsOrigin(), pNextNext->GetAbsOrigin() + Vector( 0,0,32), 0, 255, 0, true, 0.1 ); + NDebugOverlay::Box( pNextNext->GetAbsOrigin(), Vector( -8, -8, -8 ), Vector( 8, 8, 8 ), 0, 255, 0, 0, 0.1 ); + } + } + + nextPos.z += m_height; + + // Trains *can* work in local space, but only if all elements of the track share + // the same move parent as the train. + Assert( !pNext || (pNext->GetMoveParent() == GetMoveParent()) ); + + if ( pNext ) + { + UpdateTrainVelocity( pNext, pNextNext, nextPos, gpGlobals->frametime ); + UpdateTrainOrientation( pNext, pNextNext, nextPos, gpGlobals->frametime ); + + if ( pNext != m_ppath ) + { + // + // We have reached a new path track. Fire its OnPass output. + // + m_ppath = pNext; + ArriveAtNode( pNext ); +#ifdef HL1_DLL + m_bOnTrackChange = false; +#endif + + // + // See if we should teleport to the next path track. + // + CPathTrack *pTeleport = pNext->GetNext(); + if ( ( pTeleport != NULL ) && pTeleport->HasSpawnFlags( SF_PATH_TELEPORT ) ) + { + TeleportToPathTrack( pTeleport ); + } + } + + m_OnNext.FireOutput( pNext, this ); + + SetThink( &CFuncTrackTrain::Next ); + SetMoveDoneTime( 0.5 ); + SetNextThink( gpGlobals->curtime ); + SetMoveDone( NULL ); + } + else + { + // + // We've reached the end of the path, stop. + // + SoundStop(); + SetLocalVelocity(nextPos - GetLocalOrigin()); + SetLocalAngularVelocity( vec3_angle ); + float distance = GetLocalVelocity().Length(); + m_oldSpeed = m_flSpeed; + + m_flSpeed = 0; + + // Move to the dead end + + // Are we there yet? + if ( distance > 0 ) + { + // no, how long to get there? + float flTime = distance / fabs( m_oldSpeed ); + SetLocalVelocity( GetLocalVelocity() * (m_oldSpeed / distance) ); + SetMoveDone( &CFuncTrackTrain::DeadEnd ); + SetNextThink( TICK_NEVER_THINK ); + SetMoveDoneTime( flTime ); + } + else + { + DeadEnd(); + } + } +} + + +void CFuncTrackTrain::FirePassInputs( CPathTrack *pStart, CPathTrack *pEnd, bool forward ) +{ + CPathTrack *pCurrent = pStart; + + // swap if going backward + if ( !forward ) + { + pCurrent = pEnd; + pEnd = pStart; + } + variant_t emptyVariant; + + while ( pCurrent && pCurrent != pEnd ) + { + //Msg("Fired pass on %s\n", STRING(pCurrent->GetEntityName()) ); + pCurrent->AcceptInput( "InPass", this, this, emptyVariant, 0 ); + pCurrent = forward ? pCurrent->GetNext() : pCurrent->GetPrevious(); + } +} + + +void CFuncTrackTrain::DeadEnd( void ) +{ + // Fire the dead-end target if there is one + CPathTrack *pTrack, *pNext; + + pTrack = m_ppath; + + DevMsg( 2, "TRAIN(%s): Dead end ", GetDebugName() ); + // Find the dead end path node + // HACKHACK -- This is bugly, but the train can actually stop moving at a different node depending on it's speed + // so we have to traverse the list to it's end. + if ( pTrack ) + { + if ( m_oldSpeed < 0 ) + { + do + { + pNext = pTrack->ValidPath( pTrack->GetPrevious(), true ); + if ( pNext ) + pTrack = pNext; + } while ( pNext ); + } + else + { + do + { + pNext = pTrack->ValidPath( pTrack->GetNext(), true ); + if ( pNext ) + pTrack = pNext; + } while ( pNext ); + } + } + + SetLocalVelocity( vec3_origin ); + SetLocalAngularVelocity( vec3_angle ); + if ( pTrack ) + { + DevMsg( 2, "at %s\n", pTrack->GetDebugName() ); + variant_t emptyVariant; + pTrack->AcceptInput( "InPass", this, this, emptyVariant, 0 ); + } + else + { + DevMsg( 2, "\n" ); + } +} + + +void CFuncTrackTrain::SetControls( CBaseEntity *pControls ) +{ + Vector offset = pControls->GetLocalOrigin(); + + m_controlMins = pControls->WorldAlignMins() + offset; + m_controlMaxs = pControls->WorldAlignMaxs() + offset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if the entity's origin is within the controls region. +//----------------------------------------------------------------------------- +bool CFuncTrackTrain::OnControls( CBaseEntity *pTest ) +{ + Vector offset = pTest->GetLocalOrigin() - GetLocalOrigin(); + + if ( m_spawnflags & SF_TRACKTRAIN_NOCONTROL ) + return false; + + // Transform offset into local coordinates + VMatrix tmp = SetupMatrixAngles( GetLocalAngles() ); + // rotate into local space + Vector local = tmp.VMul3x3Transpose( offset ); + + /* + NDebugOverlay::Box( GetLocalOrigin(), m_controlMins, m_controlMaxs, + 255, 0, 0, 100, 5.0 ); + + NDebugOverlay::Box( GetLocalOrigin() + local, Vector(-5,-5,-5), Vector(5,5,5), + 0, 0, 255, 100, 5.0 ); + */ + + if ( local.x >= m_controlMins.x && local.y >= m_controlMins.y && local.z >= m_controlMins.z && + local.x <= m_controlMaxs.x && local.y <= m_controlMaxs.y && local.z <= m_controlMaxs.z ) + return true; + + return false; +} + + +void CFuncTrackTrain::Find( void ) +{ + m_ppath = (CPathTrack *)gEntList.FindEntityByName( NULL, m_target ); + if ( !m_ppath ) + return; + + if ( !FClassnameIs( m_ppath, "path_track" ) +#ifndef PORTAL //env_portal_path_track is a child of path_track and would like to get found + && !FClassnameIs( m_ppath, "env_portal_path_track" ) +#endif //#ifndef PORTAL + ) + { + Warning( "func_track_train must be on a path of path_track\n" ); + Assert(0); + m_ppath = NULL; + return; + } + + + + Vector nextPos = m_ppath->GetLocalOrigin(); + Vector look = nextPos; + m_ppath->LookAhead( look, m_length, 0 ); + nextPos.z += m_height; + look.z += m_height; + + QAngle nextAngles; + if ( HasSpawnFlags( SF_TRACKTRAIN_FIXED_ORIENTATION ) ) + { + nextAngles = GetLocalAngles(); + } + else + { + VectorAngles( look - nextPos, nextAngles ); + if ( HasSpawnFlags( SF_TRACKTRAIN_NOPITCH ) ) + { + nextAngles.x = 0; + } + } + + Teleport( &nextPos, &nextAngles, NULL ); + + ArriveAtNode( m_ppath ); + + if ( m_flSpeed != 0 ) + { + SetNextThink( gpGlobals->curtime + 0.1f ); + SetThink( &CFuncTrackTrain::Next ); + SoundUpdate(); + } +} + + +void CFuncTrackTrain::NearestPath( void ) +{ + CBaseEntity *pTrack = NULL; + CBaseEntity *pNearest = NULL; + float dist, closest; + + closest = 1024; + + for ( CEntitySphereQuery sphere( GetAbsOrigin(), 1024 ); ( pTrack = sphere.GetCurrentEntity() ) != NULL; sphere.NextEntity() ) + { + // filter out non-tracks + if ( !(pTrack->GetFlags() & (FL_CLIENT|FL_NPC)) && FClassnameIs( pTrack, "path_track" ) ) + { + dist = (GetAbsOrigin() - pTrack->GetAbsOrigin()).Length(); + if ( dist < closest ) + { + closest = dist; + pNearest = pTrack; + } + } + } + + if ( !pNearest ) + { + Msg( "Can't find a nearby track !!!\n" ); + SetThink(NULL); + return; + } + + DevMsg( 2, "TRAIN: %s, Nearest track is %s\n", GetDebugName(), pNearest->GetDebugName() ); + // If I'm closer to the next path_track on this path, then it's my real path + pTrack = ((CPathTrack *)pNearest)->GetNext(); + if ( pTrack ) + { + if ( (GetLocalOrigin() - pTrack->GetLocalOrigin()).Length() < (GetLocalOrigin() - pNearest->GetLocalOrigin()).Length() ) + pNearest = pTrack; + } + + m_ppath = (CPathTrack *)pNearest; + + if ( m_flSpeed != 0 ) + { + SetMoveDoneTime( 0.1 ); + SetMoveDone( &CFuncTrackTrain::Next ); + } +} + +void CFuncTrackTrain::OnRestore( void ) +{ + BaseClass::OnRestore(); + if ( !m_ppath +#ifdef HL1_DLL + && !m_bOnTrackChange +#endif + ) + { + NearestPath(); + SetThink( NULL ); + } +} + + +CFuncTrackTrain *CFuncTrackTrain::Instance( edict_t *pent ) +{ + CBaseEntity *pEntity = CBaseEntity::Instance( pent ); + if ( FClassnameIs( pEntity, "func_tracktrain" ) ) + return (CFuncTrackTrain *)pEntity; + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrackTrain::Spawn( void ) +{ + if ( m_maxSpeed == 0 ) + { + if ( m_flSpeed == 0 ) + { + m_maxSpeed = 100; + } + else + { + m_maxSpeed = m_flSpeed; + } + } + + if ( m_nMoveSoundMinPitch == 0 ) + { + m_nMoveSoundMinPitch = 60; + } + + if ( m_nMoveSoundMaxPitch == 0 ) + { + m_nMoveSoundMaxPitch = 200; + } + + SetLocalVelocity(vec3_origin); + SetLocalAngularVelocity( vec3_angle ); + + m_dir = 1; + + if ( !m_target ) + { + Msg("FuncTrackTrain '%s' has no target.\n", GetDebugName()); + } + + SetModel( STRING( GetModelName() ) ); + SetMoveType( MOVETYPE_PUSH ); + +#ifdef HL1_DLL + // BUGBUG: For now, just force this for testing. Remove if we want to tag all of the trains in the levels + SetSolid( SOLID_BSP ); +#else + SetSolid( HasSpawnFlags( SF_TRACKTRAIN_HL1TRAIN ) ? SOLID_BSP : SOLID_VPHYSICS ); + //SetSolid( SOLID_VPHYSICS ); +#endif + + if ( HasSpawnFlags( SF_TRACKTRAIN_UNBLOCKABLE_BY_PLAYER ) ) + { + AddFlag( FL_UNBLOCKABLE_BY_PLAYER ); + } + if ( m_spawnflags & SF_TRACKTRAIN_PASSABLE ) + { + AddSolidFlags( FSOLID_NOT_SOLID ); + } + + m_controlMins = CollisionProp()->OBBMins(); + m_controlMaxs = CollisionProp()->OBBMaxs(); + m_controlMaxs.z += 72; +// start trains on the next frame, to make sure their targets have had +// a chance to spawn/activate + SetThink( &CFuncTrackTrain::Find ); + SetNextThink( gpGlobals->curtime ); + Precache(); + + CreateVPhysics(); +} + + +bool CFuncTrackTrain::CreateVPhysics( void ) +{ + VPhysicsInitShadow( false, false ); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Precaches the train sounds. +//----------------------------------------------------------------------------- +void CFuncTrackTrain::Precache( void ) +{ + if (m_flVolume == 0.0) + { + m_flVolume = 1.0; + } + + if ( m_iszSoundMove != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_iszSoundMove ) ); + } + + if ( m_iszSoundMovePing != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_iszSoundMovePing ) ); + } + + if ( m_iszSoundStart != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_iszSoundStart ) ); + } + + if ( m_iszSoundStop != NULL_STRING ) + { + PrecacheScriptSound( STRING( m_iszSoundStop ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFuncTrackTrain::UpdateOnRemove() +{ + SoundStop(); + BaseClass::UpdateOnRemove(); +} + +void CFuncTrackTrain::MoveDone() +{ + m_lastBlockPos.Init(); + m_lastBlockTick = -1; + BaseClass::MoveDone(); +} + +int CFuncTrackTrain::OnTakeDamage( const CTakeDamageInfo &info ) +{ + if ( m_bDamageChild ) + { + if ( FirstMoveChild() ) + { + FirstMoveChild()->TakeDamage( info ); + } + + return 0; + } + else + { + return BaseClass::OnTakeDamage( info ); + } +} + + + +//----------------------------------------------------------------------------- +// Purpose: Defines the volume of space that the player must stand in to +// control the train +//----------------------------------------------------------------------------- +class CFuncTrainControls : public CBaseEntity +{ + DECLARE_CLASS( CFuncTrainControls, CBaseEntity ); +public: + void Spawn( void ); + void Find( void ); + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CFuncTrainControls ) + + // Function Pointers + DEFINE_FUNCTION( Find ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_traincontrols, CFuncTrainControls ); + + +void CFuncTrainControls::Find( void ) +{ + CBaseEntity *pTarget = NULL; + + do + { + pTarget = gEntList.FindEntityByName( pTarget, m_target ); + } while ( pTarget && !FClassnameIs(pTarget, "func_tracktrain") ); + + if ( !pTarget ) + { + Msg( "No train %s\n", STRING(m_target) ); + return; + } + + CFuncTrackTrain *ptrain = (CFuncTrackTrain*) pTarget; + ptrain->SetControls( this ); + + SetThink( NULL ); +} + + +void CFuncTrainControls::Spawn( void ) +{ + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); + AddEffects( EF_NODRAW ); + + Assert( GetParent() && "func_traincontrols needs parent to properly align to train" ); + + SetThink( &CFuncTrainControls::Find ); + SetNextThink( gpGlobals->curtime ); +} + + +#define SF_TRACK_ACTIVATETRAIN 0x00000001 +#define SF_TRACK_RELINK 0x00000002 +#define SF_TRACK_ROTMOVE 0x00000004 +#define SF_TRACK_STARTBOTTOM 0x00000008 +#define SF_TRACK_DONT_MOVE 0x00000010 + + +typedef enum { TRAIN_SAFE, TRAIN_BLOCKING, TRAIN_FOLLOWING } TRAIN_CODE; + + +//----------------------------------------------------------------------------- +// This entity is a rotating/moving platform that will carry a train to a new track. +// It must be larger in X-Y planar area than the train, since it must contain the +// train within these dimensions in order to operate when the train is near it. +//----------------------------------------------------------------------------- +class CFuncTrackChange : public CFuncPlatRot +{ + DECLARE_CLASS( CFuncTrackChange, CFuncPlatRot ); +public: + void Spawn( void ); + void Precache( void ); + +// virtual void Blocked( void ); + virtual void GoUp( void ); + virtual void GoDown( void ); + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void Find( void ); + TRAIN_CODE EvaluateTrain( CPathTrack *pcurrent ); + void UpdateTrain( QAngle &dest ); + virtual void HitBottom( void ); + virtual void HitTop( void ); + void Touch( CBaseEntity *pOther ); + virtual void UpdateAutoTargets( int toggleState ); + virtual bool IsTogglePlat( void ) { return true; } + + void DisableUse( void ) { m_use = 0; } + void EnableUse( void ) { m_use = 1; } + int UseEnabled( void ) { return m_use; } + + DECLARE_DATADESC(); + + CPathTrack *m_trackTop; + CPathTrack *m_trackBottom; + + CFuncTrackTrain *m_train; + + string_t m_trackTopName; + string_t m_trackBottomName; + string_t m_trainName; + TRAIN_CODE m_code; + int m_targetState; + int m_use; +}; + +LINK_ENTITY_TO_CLASS( func_trackchange, CFuncTrackChange ); + +BEGIN_DATADESC( CFuncTrackChange ) + + DEFINE_GLOBAL_FIELD( m_trackTop, FIELD_CLASSPTR ), + DEFINE_GLOBAL_FIELD( m_trackBottom, FIELD_CLASSPTR ), + DEFINE_GLOBAL_FIELD( m_train, FIELD_CLASSPTR ), + DEFINE_GLOBAL_KEYFIELD( m_trackTopName, FIELD_STRING, "toptrack" ), + DEFINE_GLOBAL_KEYFIELD( m_trackBottomName, FIELD_STRING, "bottomtrack" ), + DEFINE_GLOBAL_KEYFIELD( m_trainName, FIELD_STRING, "train" ), + DEFINE_FIELD( m_code, FIELD_INTEGER ), + DEFINE_FIELD( m_targetState, FIELD_INTEGER ), + DEFINE_FIELD( m_use, FIELD_INTEGER ), + + // Function Pointers + DEFINE_FUNCTION( Find ), + +END_DATADESC() + + +void CFuncTrackChange::Spawn( void ) +{ + Setup(); + if ( FBitSet( m_spawnflags, SF_TRACK_DONT_MOVE ) ) + m_vecPosition2.z = GetLocalOrigin().z; + + SetupRotation(); + + if ( FBitSet( m_spawnflags, SF_TRACK_STARTBOTTOM ) ) + { + UTIL_SetOrigin( this, m_vecPosition2); + m_toggle_state = TS_AT_BOTTOM; + SetLocalAngles( m_start ); + m_targetState = TS_AT_TOP; + } + else + { + UTIL_SetOrigin( this, m_vecPosition1); + m_toggle_state = TS_AT_TOP; + SetLocalAngles( m_end ); + m_targetState = TS_AT_BOTTOM; + } + + EnableUse(); + SetThink( &CFuncTrackChange::Find ); + SetNextThink( gpGlobals->curtime + 2 ); + Precache(); +} + + +void CFuncTrackChange::Precache( void ) +{ + BaseClass::Precache(); + + PrecacheScriptSound( "FuncTrackChange.Blocking" ); +} + + +// UNDONE: Filter touches before re-evaluating the train. +void CFuncTrackChange::Touch( CBaseEntity *pOther ) +{ +} + + +void CFuncTrackChange::Find( void ) +{ + // Find track entities + CBaseEntity *target; + + target = gEntList.FindEntityByName( NULL, m_trackTopName ); + if ( target ) + { + m_trackTop = (CPathTrack*) target; + target = gEntList.FindEntityByName( NULL, m_trackBottomName ); + if ( target ) + { + m_trackBottom = (CPathTrack*) target; + target = gEntList.FindEntityByName( NULL, m_trainName ); + if ( target ) + { + m_train = (CFuncTrackTrain *)gEntList.FindEntityByName( NULL, m_trainName ); + if ( !m_train ) + { + Warning( "Can't find train for track change! %s\n", STRING(m_trainName) ); + Assert(0); + return; + } + Vector center = WorldSpaceCenter(); + m_trackBottom = m_trackBottom->Nearest( center ); + m_trackTop = m_trackTop->Nearest( center ); + UpdateAutoTargets( m_toggle_state ); + SetThink( NULL ); + return; + } + else + { + Warning( "Can't find train for track change! %s\n", STRING(m_trainName) ); + Assert(0); + target = gEntList.FindEntityByName( NULL, m_trainName ); + } + } + else + { + Warning( "Can't find bottom track for track change! %s\n", STRING(m_trackBottomName) ); + Assert(0); + } + } + else + { + Warning( "Can't find top track for track change! %s\n", STRING(m_trackTopName) ); + Assert(0); + } +} + + +TRAIN_CODE CFuncTrackChange::EvaluateTrain( CPathTrack *pcurrent ) +{ + // Go ahead and work, we don't have anything to switch, so just be an elevator + if ( !pcurrent || !m_train ) + return TRAIN_SAFE; + + if ( m_train->m_ppath == pcurrent || (pcurrent->m_pprevious && m_train->m_ppath == pcurrent->m_pprevious) || + (pcurrent->m_pnext && m_train->m_ppath == pcurrent->m_pnext) ) + { + if ( m_train->m_flSpeed != 0 ) + return TRAIN_BLOCKING; + + Vector dist = GetLocalOrigin() - m_train->GetLocalOrigin(); + float length = dist.Length2D(); + if ( length < m_train->m_length ) // Empirically determined close distance + return TRAIN_FOLLOWING; + else if ( length > (150 + m_train->m_length) ) + return TRAIN_SAFE; + + return TRAIN_BLOCKING; + } + + return TRAIN_SAFE; +} + + +void CFuncTrackChange::UpdateTrain( QAngle &dest ) +{ + float time = GetMoveDoneTime(); + + m_train->SetAbsVelocity( GetAbsVelocity() ); + m_train->SetLocalAngularVelocity( GetLocalAngularVelocity() ); + m_train->SetMoveDoneTime( time ); + + // Attempt at getting the train to rotate properly around the origin of the trackchange + if ( time <= 0 ) + return; + + Vector offset = m_train->GetLocalOrigin() - GetLocalOrigin(); + QAngle delta = dest - GetLocalAngles(); + // Transform offset into local coordinates + Vector forward, right, up; + AngleVectorsTranspose( delta, &forward, &right, &up ); + Vector local; + local.x = DotProduct( offset, forward ); + local.y = DotProduct( offset, right ); + local.z = DotProduct( offset, up ); + + local = local - offset; + m_train->SetAbsVelocity( GetAbsVelocity() + (local * (1.0/time)) ); +} + + +void CFuncTrackChange::GoDown( void ) +{ + if ( m_code == TRAIN_BLOCKING ) + return; + + // HitBottom may get called during CFuncPlat::GoDown(), so set up for that + // before you call GoDown() + + UpdateAutoTargets( TS_GOING_DOWN ); + // If ROTMOVE, move & rotate + if ( FBitSet( m_spawnflags, SF_TRACK_DONT_MOVE ) ) + { + SetMoveDone( &CFuncTrackChange::CallHitBottom ); + m_toggle_state = TS_GOING_DOWN; + AngularMove( m_start, m_flSpeed ); + } + else + { + BaseClass::GoDown(); + SetMoveDone( &CFuncTrackChange::CallHitBottom ); + RotMove( m_start, GetMoveDoneTime() ); + } + // Otherwise, rotate first, move second + + // If the train is moving with the platform, update it + if ( m_code == TRAIN_FOLLOWING ) + { + UpdateTrain( m_start ); + m_train->m_ppath = NULL; +#ifdef HL1_DLL + m_train->m_bOnTrackChange = true; +#endif + } +} + + +// +// Platform is at bottom, now starts moving up +// +void CFuncTrackChange::GoUp( void ) +{ + if ( m_code == TRAIN_BLOCKING ) + return; + + // HitTop may get called during CFuncPlat::GoUp(), so set up for that + // before you call GoUp(); + + UpdateAutoTargets( TS_GOING_UP ); + if ( FBitSet( m_spawnflags, SF_TRACK_DONT_MOVE ) ) + { + m_toggle_state = TS_GOING_UP; + SetMoveDone( &CFuncTrackChange::CallHitTop ); + AngularMove( m_end, m_flSpeed ); + } + else + { + // If ROTMOVE, move & rotate + BaseClass::GoUp(); + SetMoveDone( &CFuncTrackChange::CallHitTop ); + RotMove( m_end, GetMoveDoneTime() ); + } + + // Otherwise, move first, rotate second + + // If the train is moving with the platform, update it + if ( m_code == TRAIN_FOLLOWING ) + { + UpdateTrain( m_end ); + m_train->m_ppath = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Normal track change +// Input : toggleState - +//----------------------------------------------------------------------------- +void CFuncTrackChange::UpdateAutoTargets( int toggleState ) +{ + if ( !m_trackTop || !m_trackBottom ) + return; + + if ( toggleState == TS_AT_TOP ) + { + m_trackTop->RemoveSpawnFlags( SF_PATH_DISABLED ); + } + else + { + m_trackTop->AddSpawnFlags( SF_PATH_DISABLED ); + } + + if ( toggleState == TS_AT_BOTTOM ) + { + m_trackBottom->RemoveSpawnFlags( SF_PATH_DISABLED ); + } + else + { + m_trackBottom->AddSpawnFlags( SF_PATH_DISABLED ); + } +} + + +void CFuncTrackChange::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( m_toggle_state != TS_AT_TOP && m_toggle_state != TS_AT_BOTTOM ) + return; + + // If train is in "safe" area, but not on the elevator, play alarm sound + if ( m_toggle_state == TS_AT_TOP ) + m_code = EvaluateTrain( m_trackTop ); + else if ( m_toggle_state == TS_AT_BOTTOM ) + m_code = EvaluateTrain( m_trackBottom ); + else + m_code = TRAIN_BLOCKING; + if ( m_code == TRAIN_BLOCKING ) + { + // Play alarm and return + EmitSound( "FuncTrackChange.Blocking" ); + return; + } + + // Otherwise, it's safe to move + // If at top, go down + // at bottom, go up + + DisableUse(); + if (m_toggle_state == TS_AT_TOP) + GoDown(); + else + GoUp(); +} + + +// +// Platform has hit bottom. Stops and waits forever. +// +void CFuncTrackChange::HitBottom( void ) +{ + BaseClass::HitBottom(); + if ( m_code == TRAIN_FOLLOWING ) + { +// UpdateTrain(); + m_train->SetTrack( m_trackBottom ); + } + SetMoveDone( NULL ); + SetMoveDoneTime( -1 ); + + UpdateAutoTargets( m_toggle_state ); + + EnableUse(); +} + + +// +// Platform has hit bottom. Stops and waits forever. +// +void CFuncTrackChange::HitTop( void ) +{ + BaseClass::HitTop(); + if ( m_code == TRAIN_FOLLOWING ) + { +// UpdateTrain(); + m_train->SetTrack( m_trackTop ); + } + + // Don't let the plat go back down + SetMoveDone( NULL ); + SetMoveDoneTime( -1 ); + UpdateAutoTargets( m_toggle_state ); + EnableUse(); +} + + +class CFuncTrackAuto : public CFuncTrackChange +{ + DECLARE_CLASS( CFuncTrackAuto, CFuncTrackChange ); +public: + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual void UpdateAutoTargets( int toggleState ); + void TriggerTrackChange( inputdata_t &inputdata ); + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CFuncTrackAuto ) + DEFINE_INPUTFUNC( FIELD_VOID, "Trigger", TriggerTrackChange ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( func_trackautochange, CFuncTrackAuto ); + + +// Auto track change +void CFuncTrackAuto::UpdateAutoTargets( int toggleState ) +{ + CPathTrack *pTarget, *pNextTarget; + + if ( !m_trackTop || !m_trackBottom ) + return; + + if ( m_targetState == TS_AT_TOP ) + { + pTarget = m_trackTop->GetNext(); + pNextTarget = m_trackBottom->GetNext(); + } + else + { + pTarget = m_trackBottom->GetNext(); + pNextTarget = m_trackTop->GetNext(); + } + if ( pTarget ) + { + pTarget->RemoveSpawnFlags( SF_PATH_DISABLED ); + if ( m_code == TRAIN_FOLLOWING && m_train && m_train->m_flSpeed == 0 ) + { + m_train->SetSpeed( pTarget->m_flSpeed ); + m_train->Use( this, this, USE_SET, 0 ); + } + } + + if ( pNextTarget ) + { + pNextTarget->AddSpawnFlags( SF_PATH_DISABLED ); + } +} + + +void CFuncTrackAuto::TriggerTrackChange ( inputdata_t &inputdata ) +{ + CPathTrack *pTarget; + + if ( !UseEnabled() ) + return; + + if ( m_toggle_state == TS_AT_TOP ) + pTarget = m_trackTop; + else if ( m_toggle_state == TS_AT_BOTTOM ) + pTarget = m_trackBottom; + else + pTarget = NULL; + + if ( inputdata.pActivator && FClassnameIs( inputdata.pActivator, "func_tracktrain" ) ) + { + m_code = EvaluateTrain( pTarget ); + // Safe to fire? + if ( m_code == TRAIN_FOLLOWING && m_toggle_state != m_targetState ) + { + DisableUse(); + if (m_toggle_state == TS_AT_TOP) + GoDown(); + else + GoUp(); + } + } + else + { + if ( pTarget ) + pTarget = pTarget->GetNext(); + if ( pTarget && m_train->m_ppath != pTarget && ShouldToggle( USE_TOGGLE, m_targetState ) ) + { + if ( m_targetState == TS_AT_TOP ) + m_targetState = TS_AT_BOTTOM; + else + m_targetState = TS_AT_TOP; + } + + UpdateAutoTargets( m_targetState ); + } +} + + +void CFuncTrackAuto::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CPathTrack *pTarget; + + if ( !UseEnabled() ) + return; + + if ( m_toggle_state == TS_AT_TOP ) + pTarget = m_trackTop; + else if ( m_toggle_state == TS_AT_BOTTOM ) + pTarget = m_trackBottom; + else + pTarget = NULL; + + if ( FClassnameIs( pActivator, "func_tracktrain" ) ) + { + m_code = EvaluateTrain( pTarget ); + // Safe to fire? + if ( m_code == TRAIN_FOLLOWING && m_toggle_state != m_targetState ) + { + DisableUse(); + if (m_toggle_state == TS_AT_TOP) + GoDown(); + else + GoUp(); + } + } + else + { + if ( pTarget ) + pTarget = pTarget->GetNext(); + if ( pTarget && m_train->m_ppath != pTarget && ShouldToggle( useType, m_targetState ) ) + { + if ( m_targetState == TS_AT_TOP ) + m_targetState = TS_AT_BOTTOM; + else + m_targetState = TS_AT_TOP; + } + + UpdateAutoTargets( m_targetState ); + } +} diff --git a/sp/src/game/server/trains.h b/sp/src/game/server/trains.h new file mode 100644 index 00000000..ef7e8b7e --- /dev/null +++ b/sp/src/game/server/trains.h @@ -0,0 +1,217 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef TRAINS_H +#define TRAINS_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "entityoutput.h" +#include "pathtrack.h" + + +// Spawnflags of CPathCorner +#define SF_CORNER_WAITFORTRIG 0x001 +#define SF_CORNER_TELEPORT 0x002 + +// Tracktrain spawn flags +#define SF_TRACKTRAIN_NOPITCH 0x0001 +#define SF_TRACKTRAIN_NOCONTROL 0x0002 +#define SF_TRACKTRAIN_FORWARDONLY 0x0004 +#define SF_TRACKTRAIN_PASSABLE 0x0008 +#define SF_TRACKTRAIN_FIXED_ORIENTATION 0x0010 +#define SF_TRACKTRAIN_HL1TRAIN 0x0080 +#define SF_TRACKTRAIN_USE_MAXSPEED_FOR_PITCH 0x0100 +#define SF_TRACKTRAIN_UNBLOCKABLE_BY_PLAYER 0x0200 + +#define TRAIN_ACTIVE 0x80 +#define TRAIN_NEW 0xc0 +#define TRAIN_OFF 0x00 +#define TRAIN_NEUTRAL 0x01 +#define TRAIN_SLOW 0x02 +#define TRAIN_MEDIUM 0x03 +#define TRAIN_FAST 0x04 +#define TRAIN_BACK 0x05 + + +enum TrainVelocityType_t +{ + TrainVelocity_Instantaneous = 0, + TrainVelocity_LinearBlend, + TrainVelocity_EaseInEaseOut, +}; + + +enum TrainOrientationType_t +{ + TrainOrientation_Fixed = 0, + TrainOrientation_AtPathTracks, + TrainOrientation_LinearBlend, + TrainOrientation_EaseInEaseOut, +}; + +class CFuncTrackTrain : public CBaseEntity +{ + DECLARE_CLASS( CFuncTrackTrain, CBaseEntity ); + DECLARE_SERVERCLASS(); + +public: + CFuncTrackTrain(); + + void Spawn( void ); + bool CreateVPhysics( void ); + void Precache( void ); + void UpdateOnRemove(); + void MoveDone(); + + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + void Blocked( CBaseEntity *pOther ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + virtual int DrawDebugTextOverlays(); + void DrawDebugGeometryOverlays(); + + void Next( void ); + void Find( void ); + void NearestPath( void ); + void DeadEnd( void ); + + void SetTrack( CPathTrack *track ) { m_ppath = track->Nearest(GetLocalOrigin()); } + void SetControls( CBaseEntity *pControls ); + bool OnControls( CBaseEntity *pControls ); + + void SoundStop( void ); + void SoundUpdate( void ); + + void Start( void ); + void Stop( void ); + + bool IsDirForward(); + void SetDirForward( bool bForward ); + void SetSpeed( float flSpeed, bool bAccel = false ); + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void SetSpeedDirAccel( float flNewSpeed ); + + // Input handlers + void InputSetSpeed( inputdata_t &inputdata ); + void InputSetSpeedDir( inputdata_t &inputdata ); + void InputSetSpeedReal( inputdata_t &inputdata ); + void InputStop( inputdata_t &inputdata ); + void InputResume( inputdata_t &inputdata ); + void InputReverse( inputdata_t &inputdata ); + void InputStartForward( inputdata_t &inputdata ); + void InputStartBackward( inputdata_t &inputdata ); + void InputToggle( inputdata_t &inputdata ); + void InputSetSpeedDirAccel( inputdata_t &inputdata ); + void InputTeleportToPathTrack( inputdata_t &inputdata ); + void InputSetSpeedForwardModifier( inputdata_t &inputdata ); + + static CFuncTrackTrain *Instance( edict_t *pent ); + +#ifdef TF_DLL + int UpdateTransmitState() + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } +#endif + + DECLARE_DATADESC(); + + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_DIRECTIONAL_USE | FCAP_USE_ONGROUND; } + + virtual void OnRestore( void ); + + float GetMaxSpeed() const { return m_maxSpeed; } + float GetCurrentSpeed() const { return m_flSpeed; } + float GetDesiredSpeed() const { return m_flDesiredSpeed;} + + virtual bool IsBaseTrain( void ) const { return true; } + + void SetSpeedForwardModifier( float flModifier ); + void SetBlockDamage( float flDamage ) { m_flBlockDamage = flDamage; } + void SetDamageChild( bool bDamageChild ) { m_bDamageChild = bDamageChild; } + +private: + + void ArriveAtNode( CPathTrack *pNode ); + void FirePassInputs( CPathTrack *pStart, CPathTrack *pEnd, bool forward ); + +public: + + // UNDONE: Add accessors? + CPathTrack *m_ppath; + float m_length; + +#ifdef HL1_DLL + bool m_bOnTrackChange; // we don't want to find a new node if we restore while + // riding on a func_trackchange +#endif + +private: + + TrainVelocityType_t GetTrainVelocityType(); + void UpdateTrainVelocity( CPathTrack *pnext, CPathTrack *pNextNext, const Vector &nextPos, float flInterval ); + + TrainOrientationType_t GetTrainOrientationType(); + void UpdateTrainOrientation( CPathTrack *pnext, CPathTrack *pNextNext, const Vector &nextPos, float flInterval ); + void UpdateOrientationAtPathTracks( CPathTrack *pnext, CPathTrack *pNextNext, const Vector &nextPos, float flInterval ); + void UpdateOrientationBlend( TrainOrientationType_t eOrientationType, CPathTrack *pPrev, CPathTrack *pNext, const Vector &nextPos, float flInterval ); + void DoUpdateOrientation( const QAngle &curAngles, const QAngle &angles, float flInterval ); + + void TeleportToPathTrack( CPathTrack *pTeleport ); + + + Vector m_controlMins; + Vector m_controlMaxs; + Vector m_lastBlockPos; // These are used to build a heuristic decision about being temporarily blocked by physics objects + int m_lastBlockTick; // ^^^^^^^ + float m_flVolume; + float m_flBank; + float m_oldSpeed; + float m_flBlockDamage; // Damage to inflict when blocked. + float m_height; + float m_maxSpeed; + float m_dir; + + + string_t m_iszSoundMove; // Looping sound to play while moving. Pitch shifted based on speed. + string_t m_iszSoundMovePing; // Ping sound to play while moving. Interval decreased based on speed. + string_t m_iszSoundStart; // Sound to play when starting to move. + string_t m_iszSoundStop; // Sound to play when stopping. + + float m_flMoveSoundMinTime; // The most often to play the move 'ping' sound (used at max speed) + float m_flMoveSoundMaxTime; // The least often to play the move 'ping' sound (used approaching zero speed) + float m_flNextMoveSoundTime; + + int m_nMoveSoundMinPitch; // The sound pitch to approach as we come to a stop + int m_nMoveSoundMaxPitch; // The sound pitch to approach as we approach our max speed (actually, it's hardcoded to 1000 in/sec) + + TrainOrientationType_t m_eOrientationType; + TrainVelocityType_t m_eVelocityType; + bool m_bSoundPlaying; + + COutputEvent m_OnStart,m_OnNext; + + bool m_bManualSpeedChanges; // set when we want to send entity IO to govern speed and obey our TrainVelocityType_t + float m_flDesiredSpeed; // target speed, when m_bManualSpeedChanges is set + float m_flSpeedChangeTime; + float m_flAccelSpeed; + float m_flDecelSpeed; + bool m_bAccelToSpeed; + + float m_flNextMPSoundTime; + + float m_flSpeedForwardModifier; + float m_flUnmodifiedDesiredSpeed; + + bool m_bDamageChild; +}; + + +#endif // TRAINS_H diff --git a/sp/src/game/server/trigger_area_capture.cpp b/sp/src/game/server/trigger_area_capture.cpp new file mode 100644 index 00000000..2839200b --- /dev/null +++ b/sp/src/game/server/trigger_area_capture.cpp @@ -0,0 +1,1232 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "cbase.h" +#include "team_train_watcher.h" +#include "trigger_area_capture.h" +#include "player.h" +#include "teamplay_gamerules.h" +#include "team.h" +#include "team_objectiveresource.h" +#include "team_control_point_master.h" +#include "teamplayroundbased_gamerules.h" + +extern ConVar mp_capstyle; +extern ConVar mp_blockstyle; +extern ConVar mp_capdeteriorate_time; + +IMPLEMENT_AUTO_LIST( ITriggerAreaCaptureAutoList ); + +BEGIN_DATADESC(CTriggerAreaCapture) + + // Touch functions + DEFINE_FUNCTION( CTriggerAreaCaptureShim::Touch ), + + // Think functions + DEFINE_THINKFUNC( CaptureThink ), + + // Keyfields + DEFINE_KEYFIELD( m_iszCapPointName, FIELD_STRING, "area_cap_point" ), + DEFINE_KEYFIELD( m_flCapTime, FIELD_FLOAT, "area_time_to_cap" ), + +// DEFINE_FIELD( m_iCapMode, FIELD_INTEGER ), +// DEFINE_FIELD( m_bCapturing, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_nCapturingTeam, FIELD_INTEGER ), +// DEFINE_FIELD( m_nOwningTeam, FIELD_INTEGER ), +// DEFINE_FIELD( m_nTeamInZone, FIELD_INTEGER ), +// DEFINE_FIELD( m_fTimeRemaining, FIELD_FLOAT ), +// DEFINE_FIELD( m_flLastReductionTime, FIELD_FLOAT ), +// DEFINE_FIELD( m_bBlocked, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_TeamData, CUtlVector < perteamdata_t > ), +// DEFINE_FIELD( m_Blockers, CUtlVector < blockers_t > ), +// DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_hPoint, CHandle < CTeamControlPoint > ), +// DEFINE_FIELD( m_bRequiresObject, FIELD_BOOLEAN ), +// DEFINE_FIELD( m_iCapAttemptNumber, FIELD_INTEGER ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "RoundSpawn", InputRoundSpawn ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetTeamCanCap", InputSetTeamCanCap ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetControlPoint", InputSetControlPoint ), + DEFINE_INPUTFUNC( FIELD_VOID, "CaptureCurrentCP", InputCaptureCurrentCP ), + + // Outputs + DEFINE_OUTPUT( m_OnStartTeam1, "OnStartTeam1" ), + DEFINE_OUTPUT( m_OnStartTeam2, "OnStartTeam2" ), + DEFINE_OUTPUT( m_OnBreakTeam1, "OnBreakTeam1" ), + DEFINE_OUTPUT( m_OnBreakTeam2, "OnBreakTeam2" ), + DEFINE_OUTPUT( m_OnCapTeam1, "OnCapTeam1" ), + DEFINE_OUTPUT( m_OnCapTeam2, "OnCapTeam2" ), + + DEFINE_OUTPUT( m_StartOutput, "OnStartCap" ), + DEFINE_OUTPUT( m_BreakOutput, "OnBreakCap" ), + DEFINE_OUTPUT( m_CapOutput, "OnEndCap" ), + + DEFINE_OUTPUT( m_OnNumCappersChanged, "OnNumCappersChanged" ), + DEFINE_OUTPUT( m_OnNumCappersChanged2, "OnNumCappersChanged2" ), + +END_DATADESC(); + +LINK_ENTITY_TO_CLASS( trigger_capture_area, CTriggerAreaCapture ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTriggerAreaCapture::CTriggerAreaCapture() +{ + m_TeamData.SetSize( GetNumberOfTeams() ); + m_bStartTouch = false; + m_hTrainWatcher = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::Spawn( void ) +{ + BaseClass::Spawn(); + + AddSpawnFlags( SF_TRIGGER_ALLOW_CLIENTS ); + + InitTrigger(); + + Precache(); + + SetTouch ( &CTriggerAreaCaptureShim::Touch ); + SetThink( &CTriggerAreaCapture::CaptureThink ); + SetNextThink( gpGlobals->curtime + AREA_THINK_TIME ); + + for ( int i = 0; i < m_TeamData.Count(); i++ ) + { + if ( m_TeamData[i].iNumRequiredToCap < 1 ) + { + m_TeamData[i].iNumRequiredToCap = 1; + } + + if ( m_TeamData[i].iNumRequiredToStartCap < 1 ) + { + m_TeamData[i].iNumRequiredToStartCap = 1; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTriggerAreaCapture::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( !Q_strncmp( szKeyName, "team_numcap_", 12 ) ) + { + int iTeam = atoi(szKeyName+12); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iNumRequiredToCap = atoi(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_cancap_", 12 ) ) + { + int iTeam = atoi(szKeyName+12); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].bCanCap = (atoi(szValue) != 0); + } + else if ( !Q_strncmp( szKeyName, "team_spawn_", 11 ) ) + { + int iTeam = atoi(szKeyName+11); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iSpawnAdjust = atoi(szValue); + } + else if ( !Q_strncmp( szKeyName, "team_startcap_", 14 ) ) + { + int iTeam = atoi(szKeyName+14); + Assert( iTeam >= 0 && iTeam < m_TeamData.Count() ); + + m_TeamData[iTeam].iNumRequiredToStartCap = atoi(szValue); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::Precache( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTriggerAreaCapture::IsActive( void ) +{ + return !m_bDisabled; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::StartTouch(CBaseEntity *pOther) +{ + BaseClass::StartTouch( pOther ); + + if ( PassesTriggerFilters(pOther) && m_hPoint ) + { + m_nOwningTeam = m_hPoint->GetOwner(); + + IGameEvent *event = gameeventmanager->CreateEvent( "controlpoint_starttouch" ); + if ( event ) + { + event->SetInt( "player", pOther->entindex() ); + event->SetInt( "area", m_hPoint->GetPointIndex() ); + gameeventmanager->FireEvent( event ); + } + + // Call capture think immediately to make it update our area's player counts. + // If we don't do this, the player can receive the above event telling him he's + // in a zone, but the objective resource still thinks he's not. + m_bStartTouch = true; + CaptureThink(); + m_bStartTouch = false; + + if ( m_bCapturing ) + { + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + float flRate = pMaster->GetPartialCapturePointRate(); + + if ( flRate > 0.0f ) + { + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer(pOther); + if ( pPlayer && pPlayer->GetTeamNumber() == m_nCapturingTeam ) + { + pPlayer->StartScoringEscortPoints( flRate ); + } + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::EndTouch(CBaseEntity *pOther) +{ + if ( IsTouching( pOther ) && m_hPoint ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "controlpoint_endtouch" ); + if ( event ) + { + event->SetInt( "player", pOther->entindex() ); + event->SetInt( "area", m_hPoint->GetPointIndex() ); + gameeventmanager->FireEvent( event ); + } + + // incase we leave but the area keeps capturing + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer(pOther); + if ( pPlayer ) + { + pPlayer->StopScoringEscortPoints(); + } + } + + BaseClass::EndTouch( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTriggerAreaCapture::CaptureModeScalesWithPlayers() const +{ + return mp_capstyle.GetBool(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::AreaTouch( CBaseEntity *pOther ) +{ + if ( !IsActive() ) + return; + if ( !PassesTriggerFilters(pOther) ) + return; + + // Don't cap areas unless the round is running + if ( !TeamplayGameRules()->PointsMayBeCaptured() ) + return; + + // dont touch for non-alive or non-players + if( !pOther->IsPlayer() || !pOther->IsAlive() ) + return; + + // make sure this point is in the round being played (if we're playing one) + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster && m_hPoint ) + { + if ( !pMaster->IsInRound( m_hPoint ) ) + { + return; + } + } + + if ( m_hPoint ) + { + m_nOwningTeam = m_hPoint->GetOwner(); + } + + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer(pOther); + Assert( pPlayer ); + + if ( pPlayer->GetTeamNumber() != m_nOwningTeam ) + { + if ( m_TeamData[ pPlayer->GetTeamNumber() ].bCanCap ) + { + DisplayCapHintTo( pPlayer ); + } + } +} + +ConVar mp_simulatemultiplecappers( "mp_simulatemultiplecappers", "1", FCVAR_CHEAT ); + +#define MAX_CAPTURE_TEAMS 8 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::CaptureThink( void ) +{ + SetNextThink( gpGlobals->curtime + AREA_THINK_TIME ); + + // make sure this point is in the round being played (if we're playing one) + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster && m_hPoint ) + { + if ( !pMaster->IsInRound( m_hPoint ) ) + { + return; + } + } + + if ( !TeamplayGameRules()->PointsMayBeCaptured() ) + { + // Points aren't allowed to be captured. If we were + // being captured, we need to clean up and reset. + if ( m_bCapturing ) + { + BreakCapture( false ); + UpdateNumPlayers(); + } + return; + } + + // go through our list of players + Assert( GetNumberOfTeams() <= MAX_CAPTURE_TEAMS ); + int iNumPlayers[MAX_CAPTURE_TEAMS]; + int iNumBlockablePlayers[MAX_CAPTURE_TEAMS]; // Players in the zone who can't cap, but can block / pause caps + CBaseMultiplayerPlayer *pFirstPlayerTouching[MAX_CAPTURE_TEAMS]; + for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ ) + { + iNumPlayers[i] = 0; + iNumBlockablePlayers[i] = 0; + pFirstPlayerTouching[i] = NULL; + } + + if ( m_hPoint ) + { + // Loop through the entities we're touching, and find players + for ( int i = 0; i < m_hTouchingEntities.Count(); i++ ) + { + CBaseEntity *ent = m_hTouchingEntities[i]; + if ( ent && ent->IsPlayer() ) + { + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer(ent); + if ( pPlayer->IsAlive() ) + { + int iTeam = pPlayer->GetTeamNumber(); + + // If a team's not allowed to cap a point, don't count players in it at all + if ( !TeamplayGameRules()->TeamMayCapturePoint( iTeam, m_hPoint->GetPointIndex() ) ) + continue; + + if ( !TeamplayGameRules()->PlayerMayCapturePoint( pPlayer, m_hPoint->GetPointIndex() ) ) + { + if ( TeamplayGameRules()->PlayerMayBlockPoint( pPlayer, m_hPoint->GetPointIndex() ) ) + { + if ( iNumPlayers[iTeam] == 0 && iNumBlockablePlayers[iTeam] == 0 ) + { + pFirstPlayerTouching[iTeam] = pPlayer; + } + + iNumBlockablePlayers[iTeam] += TeamplayGameRules()->GetCaptureValueForPlayer( pPlayer ); + } + continue; + } + + if ( iTeam >= FIRST_GAME_TEAM ) + { + if ( iNumPlayers[iTeam] == 0 && iNumBlockablePlayers[iTeam] == 0 ) + { + pFirstPlayerTouching[iTeam] = pPlayer; + } + + iNumPlayers[iTeam] += TeamplayGameRules()->GetCaptureValueForPlayer( pPlayer ); + } + } + } + } + } + + int iTeamsInZone = 0; + bool bUpdatePlayers = false; + m_nTeamInZone = TEAM_UNASSIGNED; + for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ ) + { + iNumPlayers[i] *= mp_simulatemultiplecappers.GetInt(); + + if ( m_TeamData[i].iNumTouching != iNumPlayers[i] ) + { + m_TeamData[i].iNumTouching = iNumPlayers[i]; + bUpdatePlayers = true; + } + m_TeamData[i].iBlockedTouching = m_TeamData[i].iNumTouching; + + if ( m_TeamData[i].iNumTouching ) + { + iTeamsInZone++; + + m_nTeamInZone = i; + } + } + + if ( iTeamsInZone > 1 ) + { + m_nTeamInZone = TEAM_UNASSIGNED; + } + else + { + // If we've got non-cappable, yet blockable players here for the team that's defending, they + // need to block the cap. This catches cases like the TF invulnerability, which needs to block + // caps, but isn't allowed to contribute to a cap. + for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ ) + { + if ( !iNumBlockablePlayers[i] || m_nTeamInZone == i ) + continue; + + iTeamsInZone++; + } + } + + UpdateTeamInZone(); + + bool bBlocked = false; + + // If the cap is being blocked, reset the number of players so the client + // knows to stop the capture as well. + if ( mp_blockstyle.GetInt() == 1 ) + { + if ( m_bCapturing && iTeamsInZone > 1 ) + { + bBlocked = true; + + for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ ) + { + iNumPlayers[i] = 0; + if ( m_TeamData[i].iNumTouching != iNumPlayers[i] ) + { + m_TeamData[i].iNumTouching = iNumPlayers[i]; + bUpdatePlayers = true; + } + } + } + } + + if ( bUpdatePlayers ) + { + UpdateNumPlayers( bBlocked ); + } + + // When a player blocks, tell them the cap index and attempt number + // only give successive blocks to them if the attempt number is different + if ( m_bCapturing ) + { + if ( m_hPoint ) + { + m_hPoint->SetLastContestedAt( gpGlobals->curtime ); + } + + // Calculate the amount of modification to the cap time + float flTimeDelta = gpGlobals->curtime - m_flLastReductionTime; + + float flReduction = flTimeDelta; + if ( CaptureModeScalesWithPlayers() ) + { + // Diminishing returns for successive players. + for ( int i = 1; i < m_TeamData[m_nTeamInZone].iNumTouching; i++ ) + { + flReduction += (flTimeDelta / (float)(i+1)); + } + } + m_flLastReductionTime = gpGlobals->curtime; + + //if more than one team is in the zone + if( iTeamsInZone > 1 ) + { + if ( !m_bBlocked ) + { + m_bBlocked = true; + UpdateBlocked(); + } + + // See if anyone gets credit for the block + float flPercentToGo = m_fTimeRemaining / m_flCapTime; + if ( CaptureModeScalesWithPlayers() ) + { + flPercentToGo = m_fTimeRemaining / ((m_flCapTime * 2) * m_TeamData[m_nCapturingTeam].iNumRequiredToCap); + } + + if ( ( flPercentToGo <= 0.5 || TeamplayGameRules()->PointsMayAlwaysBeBlocked() ) && m_hPoint ) + { + // find the first player that is not on the capturing team + // they have just broken a cap and should be rewarded + // tell the player the capture attempt number, for checking later + CBaseMultiplayerPlayer *pBlockingPlayer = NULL; + for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ ) + { + if ( m_nCapturingTeam == i ) + continue; + + if ( pFirstPlayerTouching[i] ) + { + pBlockingPlayer = pFirstPlayerTouching[i]; + break; + } + } + Assert( pBlockingPlayer ); + + if ( pBlockingPlayer ) + { + bool bRepeatBlocker = false; + for ( int i = m_Blockers.Count()-1; i >= 0; i-- ) + { + if ( m_Blockers[i].hPlayer != pBlockingPlayer ) + continue; + + // If this guy's was a blocker, but not valid now, remove him from the list + if ( m_Blockers[i].iCapAttemptNumber != m_iCapAttemptNumber || !IsTouching(m_Blockers[i].hPlayer) || + ( TeamplayGameRules()->PointsMayAlwaysBeBlocked() && m_Blockers[i].flNextBlockTime < gpGlobals->curtime && m_bStartTouch ) ) + { + m_Blockers.Remove(i); + continue; + } + + bRepeatBlocker = true; + break; + } + + if ( !bRepeatBlocker ) + { + m_hPoint->CaptureBlocked( pBlockingPlayer ); + + // Add this guy to our blocker list + int iNew = m_Blockers.AddToTail(); + m_Blockers[iNew].hPlayer = pBlockingPlayer; + m_Blockers[iNew].iCapAttemptNumber = m_iCapAttemptNumber; + m_Blockers[iNew].flNextBlockTime = gpGlobals->curtime + 10.0f; + } + } + } + + if ( mp_blockstyle.GetInt() == 0 ) + { + BreakCapture( false ); + } + return; + } + + if ( m_bBlocked ) + { + m_bBlocked = false; + UpdateBlocked(); + } + + float flTotalTimeToCap = m_flCapTime; + if ( CaptureModeScalesWithPlayers() ) + { + flTotalTimeToCap = ((m_flCapTime * 2) * m_TeamData[m_nCapturingTeam].iNumRequiredToCap); + } + + // Now remove the reduction amount after we've determined there's only 1 team in the area + if ( m_nCapturingTeam == m_nTeamInZone ) + { + SetCapTimeRemaining( m_fTimeRemaining - flReduction ); + } + else if ( m_nOwningTeam == TEAM_UNASSIGNED && m_nTeamInZone != TEAM_UNASSIGNED ) + { + SetCapTimeRemaining( m_fTimeRemaining + flReduction ); + } + else + { + // Caps deteriorate over time + if ( TeamplayRoundBasedRules() && m_hPoint && TeamplayRoundBasedRules()->TeamMayCapturePoint(m_nCapturingTeam,m_hPoint->GetPointIndex()) ) + { + float flDecreaseScale = CaptureModeScalesWithPlayers() ? mp_capdeteriorate_time.GetFloat() : flTotalTimeToCap; + float flDecrease = (flTotalTimeToCap / flDecreaseScale) * flTimeDelta; + if ( TeamplayRoundBasedRules() && TeamplayRoundBasedRules()->InOvertime() ) + { + flDecrease *= 6; + } + SetCapTimeRemaining( m_fTimeRemaining + flDecrease ); + } + else + { + SetCapTimeRemaining( flTotalTimeToCap ); + } + } + + /* + //if no-one is in the area + if( iTeamsInZone == 0 ) + { + BreakCapture( true ); + return; + } + + //if they've lost the number of players needed to cap + int iTeamMembersHere = m_TeamData[m_nCapturingTeam].iNumTouching + iNumBlockablePlayers[m_nCapturingTeam]; + if ( (iTeamMembersHere == 0 ) || (mp_capstyle.GetInt() == 0 && iTeamMembersHere < m_TeamData[m_nCapturingTeam].iNumRequiredToCap) ) + { + BreakCapture( true ); + return; + } + */ + + // if the cap is done + if ( m_fTimeRemaining <= 0 ) + { + EndCapture( m_nCapturingTeam ); + return; //we're done + } + else + { + // We may get several simultaneous CaptureThink calls from StartTouch if there are several players on the trigger + // when it is enabled (like in Raid mode). We haven't started reducing m_fTimeRemaining yet but the second call to CaptureThink + // from StartTouch has m_bCapturing set to true and we hit this condition and call BreakCapture right away. + // We put this check here to prevent calling BreakCapture from the StartTouch call to CaptureThink. If the capture should + // really be broken it will happen the next time the trigger thinks on its own. + if ( !m_bStartTouch ) + { + if ( m_fTimeRemaining >= flTotalTimeToCap ) + { + BreakCapture( false ); + return; + } + } + } + } + else + { + // If there are any teams in the zone that aren't the owner, try to start capping + if ( iTeamsInZone > 0 ) + { + for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ ) + { + if ( !m_TeamData[i].bCanCap || m_nOwningTeam == i ) + continue; + + if ( m_TeamData[i].iNumTouching == 0 ) + continue; + + if ( m_TeamData[i].iNumTouching < m_TeamData[i].iNumRequiredToStartCap ) + continue; + + if ( !CaptureModeScalesWithPlayers() && m_TeamData[i].iNumTouching < m_TeamData[i].iNumRequiredToCap ) + continue; + + StartCapture( i, CAPTURE_NORMAL ); + break; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::SetCapTimeRemaining( float flTime ) +{ + m_fTimeRemaining = flTime; + + float flCapPercentage = 0; + if ( m_nCapturingTeam ) + { + flCapPercentage = m_fTimeRemaining / m_flCapTime; + if ( CaptureModeScalesWithPlayers() ) + { + flCapPercentage = m_fTimeRemaining / ((m_flCapTime * 2) * m_TeamData[m_nCapturingTeam].iNumRequiredToCap); + } + } + + ObjectiveResource()->SetCPCapPercentage( m_hPoint->GetPointIndex(), flCapPercentage ); + + if ( m_hPoint ) + { + m_hPoint->UpdateCapPercentage(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::SetOwner( int team ) +{ + //break any current capturing + BreakCapture( false ); + + HandleRespawnTimeAdjustments( m_nOwningTeam, team ); + + //set the owner to the passed value + m_nOwningTeam = team; + + UpdateOwningTeam(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::ForceOwner( int team ) +{ + SetOwner( team ); + + if ( m_hPoint ) + { + m_hPoint->ForceOwner( team ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::HandleRespawnTimeAdjustments( int oldTeam, int newTeam ) +{ + if ( oldTeam > LAST_SHARED_TEAM ) + { + // reverse the adjust made when the old team captured this point (if we made one) + if ( m_TeamData[oldTeam].iSpawnAdjust != 0 ) + { + TeamplayRoundBasedRules()->AddTeamRespawnWaveTime( oldTeam, -m_TeamData[oldTeam].iSpawnAdjust ); + } + } + + if ( newTeam > LAST_SHARED_TEAM ) + { + if ( m_TeamData[newTeam].iSpawnAdjust != 0 ) + { + TeamplayRoundBasedRules()->AddTeamRespawnWaveTime( newTeam, m_TeamData[newTeam].iSpawnAdjust ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::StartCapture( int team, int capmode ) +{ + // Remap team to get first game team = 1 + switch ( team - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnStartTeam1.FireOutput( this, this ); + break; + case 2: + m_OnStartTeam2.FireOutput( this, this ); + break; + default: + Assert(0); + break; + } + + m_StartOutput.FireOutput(this,this); + + m_nCapturingTeam = team; + + OnStartCapture( m_nCapturingTeam ); + + UpdateNumPlayers(); + + if ( CaptureModeScalesWithPlayers() ) + { + SetCapTimeRemaining( ((m_flCapTime * 2) * m_TeamData[team].iNumRequiredToCap) ); + } + else + { + SetCapTimeRemaining( m_flCapTime ); + } + m_bCapturing = true; + m_bBlocked = false; + m_iCapMode = capmode; + + m_flLastReductionTime = gpGlobals->curtime; + + UpdateCappingTeam( m_nCapturingTeam ); + UpdateBlocked(); + + if( m_hPoint ) + { + int numcappers = 0; + int cappingplayers[MAX_AREA_CAPPERS]; + + GetNumCappingPlayers( m_nCapturingTeam, numcappers, cappingplayers ); + m_hPoint->CaptureStart( m_nCapturingTeam, numcappers, cappingplayers ); + } + + // tell all touching players to start racking up capture points + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + float flRate = pMaster->GetPartialCapturePointRate(); + + if ( flRate > 0.0f ) + { + // for each player touch + CTeam *pTeam = GetGlobalTeam( m_nCapturingTeam ); + if ( pTeam ) + { + for ( int i=0;iGetNumPlayers();i++ ) + { + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( pTeam->GetPlayer(i) ); + if ( pPlayer && IsTouching( pPlayer ) ) + { + pPlayer->StartScoringEscortPoints( flRate ); + } + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::GetNumCappingPlayers( int team, int &numcappers, int *cappingplayers ) +{ + numcappers = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseEntity *ent = UTIL_PlayerByIndex( i ); + if ( ent ) + { + CBaseMultiplayerPlayer *player = ToBaseMultiplayerPlayer(ent); + + if ( IsTouching( player ) && ( player->GetTeamNumber() == team ) ) // need to make sure disguised spies aren't included in the list of capping players + { + if ( numcappers < MAX_AREA_CAPPERS-1 ) + { + cappingplayers[numcappers] = i; + numcappers++; + } + } + } + } + + if ( numcappers < MAX_AREA_CAPPERS ) + { + cappingplayers[numcappers] = 0; //null terminate :) + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::EndCapture( int team ) +{ + IncrementCapAttemptNumber(); + + // Remap team to get first game team = 1 + switch ( team - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnCapTeam1.FireOutput( this, this ); + break; + case 2: + m_OnCapTeam2.FireOutput( this, this ); + break; + default: + Assert(0); + break; + } + + m_CapOutput.FireOutput(this,this); + + int numcappers = 0; + int cappingplayers[MAX_AREA_CAPPERS]; + + GetNumCappingPlayers( team, numcappers, cappingplayers ); + + // Handle this before we assign the new team as the owner of this area + HandleRespawnTimeAdjustments( m_nOwningTeam, team ); + + m_nOwningTeam = team; + m_bCapturing = false; + m_nCapturingTeam = TEAM_UNASSIGNED; + SetCapTimeRemaining( 0 ); + + //there may have been more than one capper, but only report this one. + //he hasn't gotten points yet, and his name will go in the cap string if its needed + //first capper gets name sent and points given by flag. + //other cappers get points manually above, no name in message + + //send the player in the cap string + if( m_hPoint ) + { + OnEndCapture( m_nOwningTeam ); + + UpdateOwningTeam(); + m_hPoint->SetOwner( m_nOwningTeam, true, numcappers, cappingplayers ); + m_hPoint->CaptureEnd(); + } + + SetNumCappers( 0 ); + + // tell all touching players to stop racking up capture points + CTeam *pTeam = GetGlobalTeam( m_nCapturingTeam ); + if ( pTeam ) + { + for ( int i=0;iGetNumPlayers();i++ ) + { + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( pTeam->GetPlayer(i) ); + if ( pPlayer && IsTouching( pPlayer ) ) + { + pPlayer->StopScoringEscortPoints(); + } + } + } + + // play any special cap sounds + if ( TeamplayRoundBasedRules() ) + { + TeamplayRoundBasedRules()->PlaySpecialCapSounds( m_nOwningTeam ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::BreakCapture( bool bNotEnoughPlayers ) +{ + if( m_bCapturing ) + { + // Remap team to get first game team = 1 + switch ( m_nCapturingTeam - FIRST_GAME_TEAM+1 ) + { + case 1: + m_OnBreakTeam1.FireOutput( this, this ); + break; + case 2: + m_OnBreakTeam2.FireOutput( this, this ); + break; + default: + Assert(0); + break; + } + + m_BreakOutput.FireOutput(this,this); + + m_bCapturing = false; + m_nCapturingTeam = TEAM_UNASSIGNED; + + UpdateCappingTeam( TEAM_UNASSIGNED ); + + if ( bNotEnoughPlayers ) + { + IncrementCapAttemptNumber(); + } + + SetCapTimeRemaining( 0 ); + + if( m_hPoint ) + { + m_hPoint->CaptureEnd(); + + // The point reverted to it's previous owner. + IGameEvent *event = gameeventmanager->CreateEvent( "teamplay_capture_broken" ); + if ( event ) + { + event->SetInt( "cp", m_hPoint->GetPointIndex() ); + event->SetString( "cpname", m_hPoint->GetName() ); + event->SetFloat( "time_remaining", m_fTimeRemaining ); + gameeventmanager->FireEvent( event ); + } + } + + SetNumCappers( 0 ); + + // tell all touching players to stop racking up capture points + CTeam *pTeam = GetGlobalTeam( m_nCapturingTeam ); + if ( pTeam ) + { + for ( int i=0;iGetNumPlayers();i++ ) + { + CBaseMultiplayerPlayer *pPlayer = ToBaseMultiplayerPlayer( pTeam->GetPlayer(i) ); + if ( pPlayer && IsTouching( pPlayer ) ) + { + pPlayer->StopScoringEscortPoints(); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::IncrementCapAttemptNumber( void ) +{ + m_iCapAttemptNumber++; + + m_Blockers.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::InputRoundSpawn( inputdata_t &inputdata ) +{ + // find the flag we're linked to + if( !m_hPoint ) + { + m_hPoint = dynamic_cast( gEntList.FindEntityByName(NULL, STRING(m_iszCapPointName) ) ); + + if ( m_hPoint ) + { + m_nOwningTeam = m_hPoint->GetOwner(); + + for ( int i = FIRST_GAME_TEAM; i < GetNumberOfTeams(); i++ ) + { + m_hPoint->SetCappersRequiredForTeam( i, m_TeamData[i].iNumRequiredToCap ); + + ObjectiveResource()->SetCPRequiredCappers( m_hPoint->GetPointIndex(), i, m_TeamData[i].iNumRequiredToCap ); + ObjectiveResource()->SetTeamCanCap( m_hPoint->GetPointIndex(), i, m_TeamData[i].bCanCap ); + + if ( CaptureModeScalesWithPlayers() ) + { + ObjectiveResource()->SetCPCapTime( m_hPoint->GetPointIndex(), i, (m_flCapTime * 2) * m_TeamData[i].iNumRequiredToCap ); + } + else + { + ObjectiveResource()->SetCPCapTime( m_hPoint->GetPointIndex(), i, m_flCapTime ); + } + + ObjectiveResource()->SetCPCapTimeScalesWithPlayers( m_hPoint->GetPointIndex(), CaptureModeScalesWithPlayers() ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::InputSetTeamCanCap( inputdata_t &inputdata ) +{ + // Get the interaction name & target + char parseString[255]; + Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); + + char *pszParam = strtok(parseString," "); + if ( pszParam && pszParam[0] ) + { + int iTeam = atoi( pszParam ); + pszParam = strtok(NULL," "); + + if ( pszParam && pszParam[0] ) + { + bool bCanCap = (atoi(pszParam) != 0); + + if ( iTeam >= 0 && iTeam < GetNumberOfTeams() ) + { + m_TeamData[iTeam].bCanCap = bCanCap; + if ( m_hPoint ) + { + ObjectiveResource()->SetTeamCanCap( m_hPoint->GetPointIndex(), iTeam, m_TeamData[iTeam].bCanCap ); + } + return; + } + } + } + + Warning("%s(%s) received SetTeamCanCap input with invalid format. Format should be: .\n", GetClassname(), GetDebugName() ); +} + +void CTriggerAreaCapture::InputCaptureCurrentCP( inputdata_t &inputdata ) +{ + if ( m_bCapturing ) + { + EndCapture( m_nCapturingTeam ); + } +} + +void CTriggerAreaCapture::InputSetControlPoint( inputdata_t &inputdata ) +{ + BreakCapture( false ); // clear the capping for the previous point, forces us to recalc on the new one + + char parseString[255]; + Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); + + m_iszCapPointName = MAKE_STRING( parseString ); + m_hPoint = NULL; // force a reset of this + InputRoundSpawn( inputdata ); + + // force everyone touching to re-touch so the hud gets set up properly + for ( int i = 0; i < m_hTouchingEntities.Count(); i++ ) + { + CBaseEntity *ent = m_hTouchingEntities[i]; + if ( ent && ent->IsPlayer() ) + { + EndTouch( ent ); + StartTouch( ent ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Check if this player's death causes a block +// return FALSE if the player is not in this area +// return TRUE otherwise ( eg player is in area, but his death does not cause break ) +//----------------------------------------------------------------------------- +bool CTriggerAreaCapture::CheckIfDeathCausesBlock( CBaseMultiplayerPlayer *pVictim, CBaseMultiplayerPlayer *pKiller ) +{ + if ( !pVictim || !pKiller ) + return false; + + // make sure this player is in this area + if ( !IsTouching( pVictim ) ) + return false; + + // Teamkills shouldn't give a block reward + if ( pVictim->GetTeamNumber() == pKiller->GetTeamNumber() ) + return true; + + // return if the area is not being capped + if ( !m_bCapturing ) + return true; + + int iTeam = pVictim->GetTeamNumber(); + + // return if this player's team is not capping the area + if ( iTeam != m_nCapturingTeam ) + return true; + + // break early incase we kill multiple people in the same frame + bool bBreakCap = false; + if ( CaptureModeScalesWithPlayers() ) + { + bBreakCap = ( m_TeamData[m_nCapturingTeam].iBlockedTouching - 1 ) <= 0; + } + else + { + bBreakCap = ( m_TeamData[m_nCapturingTeam].iBlockedTouching - 1 < m_TeamData[m_nCapturingTeam].iNumRequiredToCap ); + } + + if ( bBreakCap ) + { + m_hPoint->CaptureBlocked( pKiller ); + //BreakCapture( true ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::UpdateNumPlayers( bool bBlocked /*= false */ ) +{ + if( !m_hPoint ) + return; + + int index = m_hPoint->GetPointIndex(); + for ( int i = 0; i < m_TeamData.Count(); i++ ) + { + if ( i >= FIRST_GAME_TEAM && i == m_nCapturingTeam ) + { + SetNumCappers( m_TeamData[i].iNumTouching, bBlocked ); + } + + ObjectiveResource()->SetNumPlayers( index, i, m_TeamData[i].iNumTouching ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::UpdateOwningTeam( void ) +{ + if ( m_hPoint ) + { + ObjectiveResource()->SetOwningTeam( m_hPoint->GetPointIndex(), m_nOwningTeam ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::UpdateCappingTeam( int iTeam ) +{ + if ( m_hPoint ) + { + ObjectiveResource()->SetCappingTeam( m_hPoint->GetPointIndex(), iTeam ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::UpdateTeamInZone( void ) +{ + if ( m_hPoint ) + { + ObjectiveResource()->SetTeamInZone( m_hPoint->GetPointIndex(), m_nTeamInZone ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::UpdateBlocked( void ) +{ + if ( m_hPoint ) + { + ObjectiveResource()->SetCapBlocked( m_hPoint->GetPointIndex(), m_bBlocked ); + m_hPoint->CaptureInterrupted( m_bBlocked ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerAreaCapture::SetNumCappers( int nNumCappers, bool bBlocked /* = false */ ) +{ + m_OnNumCappersChanged.Set( nNumCappers, this, this ); + + // m_OnNumCappersChanged2 sets -1 for a blocked cart (for movement decisions on hills) + if ( bBlocked ) + { + nNumCappers = -1; + } + + m_OnNumCappersChanged2.Set( nNumCappers, this, this ); + + if ( m_hTrainWatcher.Get() ) + { + m_hTrainWatcher->SetNumTrainCappers( nNumCappers, this ); + } +} \ No newline at end of file diff --git a/sp/src/game/server/trigger_area_capture.h b/sp/src/game/server/trigger_area_capture.h new file mode 100644 index 00000000..90e0ed0a --- /dev/null +++ b/sp/src/game/server/trigger_area_capture.h @@ -0,0 +1,190 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef TRIGGER_AREA_CAPTURE_H +#define TRIGGER_AREA_CAPTURE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "basemultiplayerplayer.h" +#include "triggers.h" +#include "team_control_point.h" + +class CTeamTrainWatcher; + +#define AREA_ATTEND_TIME 0.7f + +#define AREA_THINK_TIME 0.1f + +#define CAPTURE_NORMAL 0 +#define CAPTURE_CATCHUP_ALIVEPLAYERS 1 + +#define MAX_CLIENT_AREAS 128 +#define MAX_AREA_CAPPERS 9 + +//----------------------------------------------------------------------------- +// Purpose: An area entity that players must remain in in order to active another entity +// Triggers are fired on start of capture, on end of capture and on broken capture +// Can either be capped by both teams at once, or just by one +// Time to capture and number of people required to capture are both passed by the mapper +//----------------------------------------------------------------------------- +// This class is to get around the fact that DEFINE_FUNCTION doesn't like multiple inheritance +class CTriggerAreaCaptureShim : public CBaseTrigger +{ + virtual void AreaTouch( CBaseEntity *pOther ) = 0; +public: + void Touch( CBaseEntity *pOther ) { return AreaTouch( pOther ) ; } +}; + +DECLARE_AUTO_LIST( ITriggerAreaCaptureAutoList ); + +class CTriggerAreaCapture : public CTriggerAreaCaptureShim, public ITriggerAreaCaptureAutoList +{ + DECLARE_CLASS( CTriggerAreaCapture, CTriggerAreaCaptureShim ); +public: + CTriggerAreaCapture(); + + // Derived, game-specific area triggers must override these functions +public: + // Display a hint about capturing zones to the player + virtual void DisplayCapHintTo( CBaseMultiplayerPlayer *pPlayer ) { return; } + + // A team has finished capturing the zone. + virtual void OnEndCapture( int iTeam ) { return; } + virtual void OnStartCapture( int iTeam ) { return; } + +public: + virtual void Spawn( void ); + virtual void Precache( void ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + + bool IsActive( void ); + bool CheckIfDeathCausesBlock( CBaseMultiplayerPlayer *pVictim, CBaseMultiplayerPlayer *pKiller ); + + void UpdateNumPlayers( bool bBlocked = false ); + void UpdateOwningTeam( void ); + void UpdateCappingTeam( int iTeam ); + void UpdateTeamInZone( void ); + void UpdateBlocked( void ); + + void ForceOwner( int team ); // by the control_point_round to force an owner of this point (so we can play a specific round) + + bool TeamCanCap( int iTeam ){ return m_TeamData[iTeam].bCanCap; } + CHandle GetControlPoint( void ){ return m_hPoint; } + + int GetOwningTeam( void ) { return m_nOwningTeam; } + + bool IsBlocked( void ) { return m_bBlocked; } + + void SetTrainWatcher( CTeamTrainWatcher *pTrainWatcher ){ m_hTrainWatcher = pTrainWatcher; } // used for train watchers that control train movement + CTeamTrainWatcher *GetTrainWatcher( void ) const { return m_hTrainWatcher; } + + virtual void StartTouch(CBaseEntity *pOther) OVERRIDE; + virtual void EndTouch(CBaseEntity *pOther) OVERRIDE; + + float GetCapTime() const { return m_flCapTime; } + +protected: + + virtual bool CaptureModeScalesWithPlayers() const; + +private: + virtual void AreaTouch( CBaseEntity *pOther ) OVERRIDE; + void CaptureThink( void ); + + void StartCapture( int team, int capmode ); + void EndCapture( int team ); + void BreakCapture( bool bNotEnoughPlayers ); + void IncrementCapAttemptNumber( void ); + void SwitchCapture( int team ); + void SendNumPlayers( void ); + + void SetOwner( int team ); //sets the owner of this point - useful for resetting all to -1 + + void InputRoundSpawn( inputdata_t &inputdata ); + void InputCaptureCurrentCP( inputdata_t &inputdata ); + void InputSetTeamCanCap( inputdata_t &inputdata ); + void InputSetControlPoint( inputdata_t &inputdata ); + + void SetCapTimeRemaining( float flTime ); + + void HandleRespawnTimeAdjustments( int oldTeam, int newTeam ); + void GetNumCappingPlayers( int team, int &numcappers, int *cappingplayers ); + + void SetNumCappers( int nNumCappers, bool bBlocked = false ); + +private: + int m_iCapMode; //which capture mode we're in + bool m_bCapturing; + int m_nCapturingTeam; //the team that is capturing this point + int m_nOwningTeam; //the team that has captured this point + int m_nTeamInZone; //if there's one team in the zone, this is it. + float m_flCapTime; //the total time it takes to capture the area, in seconds + float m_fTimeRemaining; //the time left in the capture + float m_flLastReductionTime; + bool m_bBlocked; + + struct perteamdata_t + { + perteamdata_t() + { + iNumRequiredToCap = 0; + iNumTouching = 0; + iBlockedTouching = 0; + bCanCap = false; + iSpawnAdjust = 0; + iNumRequiredToStartCap = 0; + } + + int iNumRequiredToCap; + int iNumTouching; + int iBlockedTouching; // Number of capping players on the cap while it's being blocked + bool bCanCap; + int iSpawnAdjust; + int iNumRequiredToStartCap; + }; + CUtlVector m_TeamData; + + struct blockers_t + { + CHandle hPlayer; + int iCapAttemptNumber; + float flNextBlockTime; + }; + CUtlVector m_Blockers; + + bool m_bActive; + + COutputEvent m_OnStartTeam1; + COutputEvent m_OnStartTeam2; + COutputEvent m_OnBreakTeam1; + COutputEvent m_OnBreakTeam2; + COutputEvent m_OnCapTeam1; + COutputEvent m_OnCapTeam2; + + COutputEvent m_StartOutput; + COutputEvent m_BreakOutput; + COutputEvent m_CapOutput; + + COutputInt m_OnNumCappersChanged; + COutputInt m_OnNumCappersChanged2; + + CHandle m_hPoint; //the capture point that we are linked to! + + bool m_bRequiresObject; + + string_t m_iszCapPointName; //name of the cap point that we're linked to + + int m_iCapAttemptNumber; // number used to keep track of discrete cap attempts, for block tracking + bool m_bStartTouch; + + CHandle m_hTrainWatcher; // used for train watchers that control train movement + + DECLARE_DATADESC(); +}; + +#endif // TRIGGER_AREA_CAPTURE_H diff --git a/sp/src/game/server/trigger_portal.cpp b/sp/src/game/server/trigger_portal.cpp new file mode 100644 index 00000000..17508047 --- /dev/null +++ b/sp/src/game/server/trigger_portal.cpp @@ -0,0 +1,343 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity which teleports touched entities and reorients their physics +// +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" +#include "triggers.h" +#include "modelentities.h" +#include "saverestore_utlvector.h" +#include "player_pickup.h" +#include "vphysics/friction.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define TRIGGER_DISABLED_THINK "PortalDisabledThink" + +ConVar portal_debug( "portal_debug", "0", FCVAR_CHEAT, "Turn on debugging for portal connections." ); + +////////////////////////////////////////////////////////////////////////// +// CTriggerPortal +// Moves touched entity to a target location, changing the model's orientation +// to match the exit target. It differs from CTriggerTeleport in that it +// reorients physics and has inputs to enable/disable its function. +////////////////////////////////////////////////////////////////////////// +class CTriggerPortal : public CBaseTrigger +{ +public: + DECLARE_DATADESC(); + DECLARE_CLASS( CTriggerPortal, CBaseTrigger ); + DECLARE_SERVERCLASS(); + + virtual void Spawn( void ); + virtual void Activate(); + + void Touch( CBaseEntity *pOther ); + void EndTouch(CBaseEntity *pOther); + void DisableForIncomingEntity( CBaseEntity *pEntity ); + bool IsTouchingPortal( CBaseEntity *pEntity ); + + void DisabledThink( void ); + + // TEMP: Since brushes have no directionality, give this wall a forward face specified in hammer + QAngle m_qFaceAngles; + +private: + string_t m_strRemotePortal; + CNetworkHandle( CTriggerPortal, m_hRemotePortal ); + CUtlVector m_hDisabledForEntities; + + // Input for setting remote portal entity (for teleporting to it) + void SetRemotePortal ( const char* strRemotePortalName ); + void InputSetRemotePortal ( inputdata_t &inputdata ); + +}; + +LINK_ENTITY_TO_CLASS( trigger_portal, CTriggerPortal ); + +BEGIN_DATADESC( CTriggerPortal ) + DEFINE_KEYFIELD( m_strRemotePortal, FIELD_STRING, "RemotePortal" ), + + DEFINE_FIELD( m_hRemotePortal, FIELD_EHANDLE ), + DEFINE_UTLVECTOR( m_hDisabledForEntities, FIELD_EHANDLE ), + + // TEMP: Only keep this field while portals are still brushes + DEFINE_FIELD( m_qFaceAngles, FIELD_VECTOR ), + + DEFINE_THINKFUNC( DisabledThink ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetRemotePortal", InputSetRemotePortal ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CTriggerPortal, DT_TriggerPortal ) + SendPropEHandle(SENDINFO(m_hRemotePortal)), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerPortal::Spawn( void ) +{ + BaseClass::Spawn(); + + InitTrigger(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : - +//----------------------------------------------------------------------------- +void CTriggerPortal::Activate() +{ + BaseClass::Activate(); + + m_qFaceAngles = this->GetAbsAngles(); + + // keep the remote portal's pointer at activate time to avoid redundant FindEntity calls + if ( m_strRemotePortal != NULL_STRING ) + { + SetRemotePortal( STRING(m_strRemotePortal) ); + m_strRemotePortal = NULL_STRING; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CTriggerPortal::InputSetRemotePortal(inputdata_t &inputdata ) +{ + SetRemotePortal( inputdata.value.String() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : strRemotePortalName - +//----------------------------------------------------------------------------- +void CTriggerPortal::SetRemotePortal(const char *strRemotePortalName ) +{ + m_hRemotePortal = dynamic_cast (gEntList.FindEntityByName( NULL, strRemotePortalName, NULL, NULL, NULL )); + if ( m_hRemotePortal == NULL ) + { + Warning ( "trigger_portal: Cannot find remote portal entity named %s\n", strRemotePortalName ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pOther - +//----------------------------------------------------------------------------- +void CTriggerPortal::EndTouch(CBaseEntity *pOther) +{ + BaseClass::EndTouch(pOther); + + if ( portal_debug.GetBool() ) + { + Msg("%s ENDTOUCH: for %s\n", GetDebugName(), pOther->GetDebugName() ); + } + + EHANDLE hHandle; + hHandle = pOther; + m_hDisabledForEntities.FindAndRemove( hHandle ); +} + +//----------------------------------------------------------------------------- +// Purpose: Upon touching a non-filtered entity, CTriggerPortal teleports them to it's +// remote portal location. +// Input : *pOther - +//----------------------------------------------------------------------------- +void CTriggerPortal::Touch( CBaseEntity *pOther ) +{ + // If we are enabled, and allowed to react to the touched entity + if ( PassesTriggerFilters(pOther) ) + { + // If we somehow lost our pointer to the remote portal, get a new one + if ( m_hRemotePortal == NULL ) + { + Disable(); + return; + } + + bool bDebug = portal_debug.GetBool(); + if ( bDebug ) + { + Msg("%s TOUCH: for %s\n", GetDebugName(), pOther->GetDebugName() ); + } + + // Don't touch entities that came through us and haven't left us yet. + EHANDLE hHandle; + hHandle = pOther; + if ( m_hDisabledForEntities.Find(hHandle) != m_hDisabledForEntities.InvalidIndex() ) + { + Msg(" IGNORED\n", GetDebugName(), pOther->GetDebugName() ); + return; + } + + Pickup_ForcePlayerToDropThisObject( pOther ); + + // de-ground this entity + pOther->SetGroundEntity( NULL ); + + // Build a this --> remote transformation + VMatrix matMyModelToWorld, matMyInverse; + matMyModelToWorld = this->EntityToWorldTransform(); + MatrixInverseGeneral ( matMyModelToWorld, matMyInverse ); + + // Teleport our object + VMatrix matRemotePortalTransform = m_hRemotePortal->EntityToWorldTransform(); + Vector ptNewOrigin, vLook, vRight, vUp, vNewLook; + pOther->GetVectors( &vLook, &vRight, &vUp ); + + // Move origin + ptNewOrigin = matMyInverse * pOther->GetAbsOrigin(); + ptNewOrigin = matRemotePortalTransform * Vector( ptNewOrigin.x, -ptNewOrigin.y, ptNewOrigin.z ); + + // Re-aim camera + vNewLook = matMyInverse.ApplyRotation( vLook ); + vNewLook = matRemotePortalTransform.ApplyRotation( Vector( -vNewLook.x, -vNewLook.y, vNewLook.z ) ); + + // Reorient the physics + Vector vVelocity, vOldVelocity; + pOther->GetVelocity( &vOldVelocity ); + vVelocity = matMyInverse.ApplyRotation( vOldVelocity ); + vVelocity = matRemotePortalTransform.ApplyRotation( Vector( -vVelocity.x, -vVelocity.y, vVelocity.z ) ); + + QAngle qNewAngles; + VectorAngles( vNewLook, qNewAngles ); + + if ( pOther->IsPlayer() ) + { + ((CBasePlayer*)pOther)->SnapEyeAngles(qNewAngles); + } + + Vector vecOldPos = pOther->WorldSpaceCenter(); + if ( bDebug ) + { + NDebugOverlay::Box( pOther->GetAbsOrigin(), pOther->WorldAlignMins(), pOther->WorldAlignMaxs(), 255,0,0, 8, 20 ); + NDebugOverlay::Axis( pOther->GetAbsOrigin(), pOther->GetAbsAngles(), 10.0f, true, 50 ); + } + + // place player at the new destination + CTriggerPortal *pPortal = m_hRemotePortal.Get(); + pPortal->DisableForIncomingEntity( pOther ); + pOther->Teleport( &ptNewOrigin, &qNewAngles, &vVelocity ); + + if ( bDebug ) + { + NDebugOverlay::Box( pOther->GetAbsOrigin(), pOther->WorldAlignMins(), pOther->WorldAlignMaxs(), 0,255,0, 8, 20 ); + NDebugOverlay::Line( vecOldPos, pOther->WorldSpaceCenter(), 0,255,0, true, 20 ); + NDebugOverlay::Axis( pOther->GetAbsOrigin(), pOther->GetAbsAngles(), 10.0f, true, 50 ); + + Msg("%s TELEPORTED: %s\n", GetDebugName(), pOther->GetDebugName() ); + } + + // test collision on the new teleport location + Vector vMin, vMax, vCenter; + pOther->CollisionProp()->WorldSpaceAABB( &vMin, &vMax ); + vCenter = (vMin + vMax) * 0.5f; + vMin -= vCenter; + vMax -= vCenter; + + Vector vStart, vEnd; + vStart = ptNewOrigin; + vEnd = ptNewOrigin; + + Ray_t ray; + ray.Init( vStart, vEnd, vMin, vMax ); + trace_t tr; + pPortal->TestCollision( ray, pOther->PhysicsSolidMaskForEntity(), tr ); + + // Teleportation caused us to hit something, deal with it. + if ( tr.DidHit() ) + { + + } + + + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerPortal::DisableForIncomingEntity( CBaseEntity *pEntity ) +{ + EHANDLE hHandle; + hHandle = pEntity; + Assert( m_hDisabledForEntities.Find(hHandle) == m_hDisabledForEntities.InvalidIndex() ); + m_hDisabledForEntities.AddToTail( hHandle ); + + // Start thinking, and remove the other as soon as it's not touching me. + // Needs to be done in addition to EndTouch, because entities may move fast + // enough through the portal to come out not touching the other portal. + SetContextThink( DisabledThink, gpGlobals->curtime + 0.1, TRIGGER_DISABLED_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerPortal::DisabledThink( void ) +{ + // If we've got no disabled entities left, we're done + if ( !m_hDisabledForEntities.Count() ) + { + SetContextThink( NULL, gpGlobals->curtime, TRIGGER_DISABLED_THINK ); + return; + } + + for ( int i = m_hDisabledForEntities.Count()-1; i >= 0; i-- ) + { + CBaseEntity *pEntity = m_hDisabledForEntities[i]; + if ( !pEntity || !IsTouchingPortal(pEntity) ) + { + m_hDisabledForEntities.Remove(i); + } + } + + SetContextThink( DisabledThink, gpGlobals->curtime + 0.1, TRIGGER_DISABLED_THINK ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTriggerPortal::IsTouchingPortal( CBaseEntity *pEntity ) +{ + // First, check the touchlinks. This will find non-vphysics entities touching us + touchlink_t *root = ( touchlink_t * )GetDataObject( TOUCHLINK ); + if ( root ) + { + for ( touchlink_t *link = root->nextLink; link != root; link = link->nextLink ) + { + CBaseEntity *pTouch = link->entityTouched; + if ( pTouch == pEntity ) + return true; + } + } + + // Then check the friction snapshot. This will find vphysics objects touching us. + IPhysicsObject *pPhysics = VPhysicsGetObject(); + if ( !pPhysics ) + return false; + + IPhysicsFrictionSnapshot *pSnapshot = pPhysics->CreateFrictionSnapshot(); + bool bFound = false; + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject( 1 ); + if ( ((CBaseEntity *)pOther->GetGameData()) == pEntity ) + { + bFound = true; + break; + } + + pSnapshot->NextFrictionData(); + } + pPhysics->DestroyFrictionSnapshot( pSnapshot ); + + return bFound; +} \ No newline at end of file diff --git a/sp/src/game/server/triggers.cpp b/sp/src/game/server/triggers.cpp new file mode 100644 index 00000000..03e23cb7 --- /dev/null +++ b/sp/src/game/server/triggers.cpp @@ -0,0 +1,5606 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Spawn and use functions for editor-placed triggers. +// +//===========================================================================// + +#include "cbase.h" +#include "ai_basenpc.h" +#include "player.h" +#include "saverestore.h" +#include "gamerules.h" +#include "entityapi.h" +#include "entitylist.h" +#include "ndebugoverlay.h" +#include "globalstate.h" +#include "filters.h" +#include "vstdlib/random.h" +#include "triggers.h" +#include "saverestoretypes.h" +#include "hierarchy.h" +#include "bspfile.h" +#include "saverestore_utlvector.h" +#include "physics_saverestore.h" +#include "te_effect_dispatch.h" +#include "ammodef.h" +#include "iservervehicle.h" +#include "movevars_shared.h" +#include "physics_prop_ragdoll.h" +#include "props.h" +#include "RagdollBoogie.h" +#include "EntityParticleTrail.h" +#include "in_buttons.h" +#include "ai_behavior_follow.h" +#include "ai_behavior_lead.h" +#include "gameinterface.h" + +#ifdef HL2_DLL +#include "hl2_player.h" +#endif + +#ifdef MAPBASE +#include "ai_hint.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define DEBUG_TRANSITIONS_VERBOSE 2 +ConVar g_debug_transitions( "g_debug_transitions", "0", FCVAR_NONE, "Set to 1 and restart the map to be warned if the map has no trigger_transition volumes. Set to 2 to see a dump of all entities & associated results during a transition." ); + +// Global list of triggers that care about weapon fire +// Doesn't need saving, the triggers re-add themselves on restore. +CUtlVector< CHandle > g_hWeaponFireTriggers; + +extern CServerGameDLL g_ServerGameDLL; +extern bool g_fGameOver; +ConVar showtriggers( "showtriggers", "0", FCVAR_CHEAT, "Shows trigger brushes" ); + +bool IsTriggerClass( CBaseEntity *pEntity ); + +// Command to dynamically toggle trigger visibility +void Cmd_ShowtriggersToggle_f( const CCommand &args ) +{ + // Loop through the entities in the game and make visible anything derived from CBaseTrigger + CBaseEntity *pEntity = gEntList.FirstEnt(); + while ( pEntity ) + { + if ( IsTriggerClass(pEntity) ) + { + // If a classname is specified, only show triggles of that type + if ( args.ArgC() > 1 ) + { + const char *sClassname = args[1]; + if ( sClassname && sClassname[0] ) + { + if ( !FClassnameIs( pEntity, sClassname ) ) + { + pEntity = gEntList.NextEnt( pEntity ); + continue; + } + } + } + + if ( pEntity->IsEffectActive( EF_NODRAW ) ) + { + pEntity->RemoveEffects( EF_NODRAW ); + } + else + { + pEntity->AddEffects( EF_NODRAW ); + } + } + + pEntity = gEntList.NextEnt( pEntity ); + } +} + +static ConCommand showtriggers_toggle( "showtriggers_toggle", Cmd_ShowtriggersToggle_f, "Toggle show triggers", FCVAR_CHEAT ); + +// Global Savedata for base trigger +BEGIN_DATADESC( CBaseTrigger ) + + // Keyfields + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + DEFINE_FIELD( m_hFilter, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_UTLVECTOR( m_hTouchingEntities, FIELD_EHANDLE ), + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_flWait, FIELD_FLOAT, "wait" ), + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_sMaster, FIELD_STRING, "master" ), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "TouchTest", InputTouchTest ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartTouch", InputStartTouch ), + DEFINE_INPUTFUNC( FIELD_VOID, "EndTouch", InputEndTouch ), + + // Outputs + DEFINE_OUTPUT( m_OnStartTouch, "OnStartTouch"), + DEFINE_OUTPUT( m_OnStartTouchAll, "OnStartTouchAll"), + DEFINE_OUTPUT( m_OnEndTouch, "OnEndTouch"), + DEFINE_OUTPUT( m_OnEndTouchAll, "OnEndTouchAll"), + DEFINE_OUTPUT( m_OnTouching, "OnTouching" ), + DEFINE_OUTPUT( m_OnNotTouching, "OnNotTouching" ), + +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT + +BEGIN_ENT_SCRIPTDESC( CBaseTrigger, CBaseEntity, "Trigger entity" ) + DEFINE_SCRIPTFUNC( Enable, "" ) + DEFINE_SCRIPTFUNC( Disable, "" ) + DEFINE_SCRIPTFUNC( TouchTest, "" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptIsTouching, "IsTouching", "Checks whether the passed entity is touching the trigger." ) + + DEFINE_SCRIPTFUNC( UsesFilter, "Returns true if this trigger uses a filter." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptPassesTriggerFilters, "PassesTriggerFilters", "Returns whether a target entity satisfies the trigger's spawnflags, filter, etc." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetTouchedEntityOfType, "GetTouchedEntityOfType", "Gets the first touching entity which matches the specified class." ) + + DEFINE_SCRIPTFUNC( PointIsWithin, "Checks if the given vector is within the trigger's volume." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetTouchingEntities, "GetTouchingEntities", "Gets all entities touching this trigger (and satisfying its criteria). This function copies them to a table with a maximum number of elements." ) +END_SCRIPTDESC(); + +#endif // MAPBASE_VSCRIPT + +LINK_ENTITY_TO_CLASS( trigger, CBaseTrigger ); + + +CBaseTrigger::CBaseTrigger() +{ + AddEFlags( EFL_USE_PARTITION_WHEN_NOT_SOLID ); +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler to turn on this trigger. +//------------------------------------------------------------------------------ +void CBaseTrigger::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler to turn off this trigger. +//------------------------------------------------------------------------------ +void CBaseTrigger::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +void CBaseTrigger::InputTouchTest( inputdata_t &inputdata ) +{ + TouchTest(); +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CBaseTrigger::Spawn() +{ + if ( HasSpawnFlags( SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS ) || HasSpawnFlags( SF_TRIGGER_ONLY_NPCS_IN_VEHICLES ) ) + { + // Automatically set this trigger to work with NPC's. + AddSpawnFlags( SF_TRIGGER_ALLOW_NPCS ); + } + + if ( HasSpawnFlags( SF_TRIGGER_ONLY_CLIENTS_IN_VEHICLES ) ) + { + AddSpawnFlags( SF_TRIGGER_ALLOW_CLIENTS ); + } + + if ( HasSpawnFlags( SF_TRIGGER_ONLY_CLIENTS_OUT_OF_VEHICLES ) ) + { + AddSpawnFlags( SF_TRIGGER_ALLOW_CLIENTS ); + } + + BaseClass::Spawn(); +} + + +//------------------------------------------------------------------------------ +// Cleanup +//------------------------------------------------------------------------------ +void CBaseTrigger::UpdateOnRemove( void ) +{ + if ( VPhysicsGetObject()) + { + VPhysicsGetObject()->RemoveTrigger(); + } + + BaseClass::UpdateOnRemove(); +} + +//------------------------------------------------------------------------------ +// Purpose: Turns on this trigger. +//------------------------------------------------------------------------------ +void CBaseTrigger::Enable( void ) +{ + m_bDisabled = false; + + if ( VPhysicsGetObject()) + { + VPhysicsGetObject()->EnableCollisions( true ); + } + + if (!IsSolidFlagSet( FSOLID_TRIGGER )) + { + AddSolidFlags( FSOLID_TRIGGER ); + PhysicsTouchTriggers(); + } +} + + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +void CBaseTrigger::Activate( void ) +{ + // Get a handle to my filter entity if there is one + if (m_iFilterName != NULL_STRING) + { + m_hFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iFilterName )); + } + + BaseClass::Activate(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called after player becomes active in the game +//----------------------------------------------------------------------------- +void CBaseTrigger::PostClientActive( void ) +{ + BaseClass::PostClientActive(); + + if ( !m_bDisabled ) + { + PhysicsTouchTriggers(); + } +} + +//------------------------------------------------------------------------------ +// Purpose: Turns off this trigger. +//------------------------------------------------------------------------------ +void CBaseTrigger::Disable( void ) +{ + m_bDisabled = true; + + if ( VPhysicsGetObject()) + { + VPhysicsGetObject()->EnableCollisions( false ); + } + + if (IsSolidFlagSet(FSOLID_TRIGGER)) + { + RemoveSolidFlags( FSOLID_TRIGGER ); + PhysicsTouchTriggers(); + } +} +//------------------------------------------------------------------------------ +// Purpose: Tests to see if anything is touching this trigger. +//------------------------------------------------------------------------------ +void CBaseTrigger::TouchTest( void ) +{ + // If the trigger is disabled don't test to see if anything is touching it. + if ( !m_bDisabled ) + { + if ( m_hTouchingEntities.Count() !=0 ) + { +#ifdef MAPBASE + m_OnTouching.FireOutput( m_hTouchingEntities[0], this ); +#else + m_OnTouching.FireOutput( this, this ); +#endif + } + else + { + m_OnNotTouching.FireOutput( this, this ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CBaseTrigger::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + // -------------- + // Print Target + // -------------- + char tempstr[255]; + if (IsSolidFlagSet(FSOLID_TRIGGER)) + { + Q_strncpy(tempstr,"State: Enabled",sizeof(tempstr)); + } + else + { + Q_strncpy(tempstr,"State: Disabled",sizeof(tempstr)); + } + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if the specified point is within this zone +//----------------------------------------------------------------------------- +bool CBaseTrigger::PointIsWithin( const Vector &vecPoint ) +{ + Ray_t ray; + trace_t tr; + ICollideable *pCollide = CollisionProp(); + ray.Init( vecPoint, vecPoint ); + enginetrace->ClipRayToCollideable( ray, MASK_ALL, pCollide, &tr ); + return ( tr.startsolid ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseTrigger::InitTrigger( ) +{ + SetSolid( GetParent() ? SOLID_VPHYSICS : SOLID_BSP ); + AddSolidFlags( FSOLID_NOT_SOLID ); + if (m_bDisabled) + { + RemoveSolidFlags( FSOLID_TRIGGER ); + } + else + { + AddSolidFlags( FSOLID_TRIGGER ); + } + + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); // set size and link into world + if ( showtriggers.GetInt() == 0 ) + { + AddEffects( EF_NODRAW ); + } + + m_hTouchingEntities.Purge(); + + if ( HasSpawnFlags( SF_TRIG_TOUCH_DEBRIS ) ) + { + CollisionProp()->AddSolidFlags( FSOLID_TRIGGER_TOUCH_DEBRIS ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if this entity passes the filter criteria, false if not. +// Input : pOther - The entity to be filtered. +//----------------------------------------------------------------------------- +bool CBaseTrigger::PassesTriggerFilters(CBaseEntity *pOther) +{ + // First test spawn flag filters + if ( HasSpawnFlags(SF_TRIGGER_ALLOW_ALL) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_CLIENTS) && (pOther->GetFlags() & FL_CLIENT)) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_NPCS) && (pOther->GetFlags() & FL_NPC)) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_PUSHABLES) && FClassnameIs(pOther, "func_pushable")) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_PHYSICS) && pOther->GetMoveType() == MOVETYPE_VPHYSICS) +#ifdef MAPBASE + || + (HasSpawnFlags(SF_TRIGGER_ALLOW_ITEMS) && pOther->GetMoveType() == MOVETYPE_FLYGRAVITY) +#endif +#if defined( HL2_EPISODIC ) || defined( TF_DLL ) + || + ( HasSpawnFlags(SF_TRIG_TOUCH_DEBRIS) && + (pOther->GetCollisionGroup() == COLLISION_GROUP_DEBRIS || + pOther->GetCollisionGroup() == COLLISION_GROUP_DEBRIS_TRIGGER || + pOther->GetCollisionGroup() == COLLISION_GROUP_INTERACTIVE_DEBRIS) + ) +#endif + ) + { + if ( pOther->GetFlags() & FL_NPC ) + { + CAI_BaseNPC *pNPC = pOther->MyNPCPointer(); + + if ( HasSpawnFlags( SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS ) ) + { + if ( !pNPC || !pNPC->IsPlayerAlly() ) + { + return false; + } + } + + if ( HasSpawnFlags( SF_TRIGGER_ONLY_NPCS_IN_VEHICLES ) ) + { + if ( !pNPC || !pNPC->IsInAVehicle() ) + return false; + } + } + + bool bOtherIsPlayer = pOther->IsPlayer(); + + if ( bOtherIsPlayer ) + { + CBasePlayer *pPlayer = (CBasePlayer*)pOther; + if ( !pPlayer->IsAlive() ) + return false; + + if ( HasSpawnFlags(SF_TRIGGER_ONLY_CLIENTS_IN_VEHICLES) ) + { + if ( !pPlayer->IsInAVehicle() ) + return false; + + // Make sure we're also not exiting the vehicle at the moment + IServerVehicle *pVehicleServer = pPlayer->GetVehicle(); + if ( pVehicleServer == NULL ) + return false; + + if ( pVehicleServer->IsPassengerExiting() ) + return false; + } + + if ( HasSpawnFlags(SF_TRIGGER_ONLY_CLIENTS_OUT_OF_VEHICLES) ) + { + if ( pPlayer->IsInAVehicle() ) + return false; + } + + if ( HasSpawnFlags( SF_TRIGGER_DISALLOW_BOTS ) ) + { + if ( pPlayer->IsFakeClient() ) + return false; + } + } + + CBaseFilter *pFilter = m_hFilter.Get(); + return (!pFilter) ? true : pFilter->PassesFilter( this, pOther ); + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Called to simulate what happens when an entity touches the trigger. +// Input : pOther - The entity that is touching us. +//----------------------------------------------------------------------------- +void CBaseTrigger::InputStartTouch( inputdata_t &inputdata ) +{ + //Pretend we just touched the trigger. + StartTouch( inputdata.pCaller ); +} +//----------------------------------------------------------------------------- +// Purpose: Called to simulate what happens when an entity leaves the trigger. +// Input : pOther - The entity that is touching us. +//----------------------------------------------------------------------------- +void CBaseTrigger::InputEndTouch( inputdata_t &inputdata ) +{ + //And... pretend we left the trigger. + EndTouch( inputdata.pCaller ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called when an entity starts touching us. +// Input : pOther - The entity that is touching us. +//----------------------------------------------------------------------------- +void CBaseTrigger::StartTouch(CBaseEntity *pOther) +{ + if (PassesTriggerFilters(pOther) ) + { + EHANDLE hOther; + hOther = pOther; + + bool bAdded = false; + if ( m_hTouchingEntities.Find( hOther ) == m_hTouchingEntities.InvalidIndex() ) + { + m_hTouchingEntities.AddToTail( hOther ); + bAdded = true; + } + + m_OnStartTouch.FireOutput(pOther, this); + + if ( bAdded && ( m_hTouchingEntities.Count() == 1 ) ) + { + // First entity to touch us that passes our filters + m_OnStartTouchAll.FireOutput( pOther, this ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when an entity stops touching us. +// Input : pOther - The entity that was touching us. +//----------------------------------------------------------------------------- +void CBaseTrigger::EndTouch(CBaseEntity *pOther) +{ + if ( IsTouching( pOther ) ) + { + EHANDLE hOther; + hOther = pOther; + m_hTouchingEntities.FindAndRemove( hOther ); + + //FIXME: Without this, triggers fire their EndTouch outputs when they are disabled! + //if ( !m_bDisabled ) + //{ + m_OnEndTouch.FireOutput(pOther, this); + //} + + // If there are no more entities touching this trigger, fire the lost all touches + // Loop through the touching entities backwards. Clean out old ones, and look for existing + bool bFoundOtherTouchee = false; + int iSize = m_hTouchingEntities.Count(); + for ( int i = iSize-1; i >= 0; i-- ) + { + EHANDLE hOther; + hOther = m_hTouchingEntities[i]; + + if ( !hOther ) + { + m_hTouchingEntities.Remove( i ); + } + else if ( hOther->IsPlayer() && !hOther->IsAlive() ) + { +#ifdef STAGING_ONLY + AssertMsg( 0, CFmtStr( "Dead player [%s] is still touching this trigger at [%f %f %f]", hOther->GetEntityName().ToCStr(), XYZ( hOther->GetAbsOrigin() ) ) ); + Warning( "Dead player [%s] is still touching this trigger at [%f %f %f]", hOther->GetEntityName().ToCStr(), XYZ( hOther->GetAbsOrigin() ) ); +#endif + m_hTouchingEntities.Remove( i ); + } + else + { + bFoundOtherTouchee = true; + } + } + + //FIXME: Without this, triggers fire their EndTouch outputs when they are disabled! + // Didn't find one? + if ( !bFoundOtherTouchee /*&& !m_bDisabled*/ ) + { + m_OnEndTouchAll.FireOutput(pOther, this); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if the specified entity is touching us +//----------------------------------------------------------------------------- +bool CBaseTrigger::IsTouching( CBaseEntity *pOther ) +{ + EHANDLE hOther; + hOther = pOther; + return ( m_hTouchingEntities.Find( hOther ) != m_hTouchingEntities.InvalidIndex() ); +} + +#ifdef MAPBASE_VSCRIPT +bool CBaseTrigger::ScriptIsTouching( HSCRIPT hOther ) +{ + CBaseEntity *pOther = ToEnt(hOther); + if ( !pOther ) + return false; + + EHANDLE eOther; + eOther = pOther; + return ( m_hTouchingEntities.Find( eOther ) != m_hTouchingEntities.InvalidIndex() ); +} +#endif // MAPBASE_VSCRIPT + +//----------------------------------------------------------------------------- +// Purpose: Return a pointer to the first entity of the specified type being touched by this trigger +//----------------------------------------------------------------------------- +CBaseEntity *CBaseTrigger::GetTouchedEntityOfType( const char *sClassName ) +{ + int iCount = m_hTouchingEntities.Count(); + for ( int i = 0; i < iCount; i++ ) + { + CBaseEntity *pEntity = m_hTouchingEntities[i]; + if ( FClassnameIs( pEntity, sClassName ) ) + return pEntity; + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Toggles this trigger between enabled and disabled. +//----------------------------------------------------------------------------- +void CBaseTrigger::InputToggle( inputdata_t &inputdata ) +{ + if (IsSolidFlagSet( FSOLID_TRIGGER )) + { + RemoveSolidFlags(FSOLID_TRIGGER); + } + else + { + AddSolidFlags(FSOLID_TRIGGER); + } + + PhysicsTouchTriggers(); +} + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +// Purpose: Copies touching entities to a script table +//----------------------------------------------------------------------------- +void CBaseTrigger::ScriptGetTouchingEntities( HSCRIPT hTable ) +{ + for (int i = 0; i < m_hTouchingEntities.Count(); i++) + { + g_pScriptVM->ArrayAppend( hTable, ToHScript( m_hTouchingEntities[i] ) ); + } +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: Removes anything that touches it. If the trigger has a targetname, +// firing it will toggle state. +//----------------------------------------------------------------------------- +class CTriggerRemove : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerRemove, CBaseTrigger ); + + void Spawn( void ); + void Touch( CBaseEntity *pOther ); + + DECLARE_DATADESC(); + + // Outputs + COutputEvent m_OnRemove; +}; + +BEGIN_DATADESC( CTriggerRemove ) + + // Outputs + DEFINE_OUTPUT( m_OnRemove, "OnRemove" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( trigger_remove, CTriggerRemove ); + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerRemove::Spawn( void ) +{ + BaseClass::Spawn(); + InitTrigger(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Trigger hurt that causes radiation will do a radius check and set +// the player's geiger counter level according to distance from center +// of trigger. +//----------------------------------------------------------------------------- +void CTriggerRemove::Touch( CBaseEntity *pOther ) +{ + if (!PassesTriggerFilters(pOther)) + return; + + UTIL_Remove( pOther ); +} + + +BEGIN_DATADESC( CTriggerHurt ) + + // Function Pointers + DEFINE_FUNCTION( RadiationThink ), + DEFINE_FUNCTION( HurtThink ), + + // Fields + DEFINE_FIELD( m_flOriginalDamage, FIELD_FLOAT ), + DEFINE_KEYFIELD( m_flDamage, FIELD_FLOAT, "damage" ), + DEFINE_KEYFIELD( m_flDamageCap, FIELD_FLOAT, "damagecap" ), + DEFINE_KEYFIELD( m_bitsDamageInflict, FIELD_INTEGER, "damagetype" ), + DEFINE_KEYFIELD( m_damageModel, FIELD_INTEGER, "damagemodel" ), + DEFINE_KEYFIELD( m_bNoDmgForce, FIELD_BOOLEAN, "nodmgforce" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_flHurtRate, FIELD_FLOAT, "hurtrate" ), +#endif + + DEFINE_FIELD( m_flLastDmgTime, FIELD_TIME ), + DEFINE_FIELD( m_flDmgResetTime, FIELD_TIME ), + DEFINE_UTLVECTOR( m_hurtEntities, FIELD_EHANDLE ), + + // Inputs + DEFINE_INPUT( m_flDamage, FIELD_FLOAT, "SetDamage" ), + + // Outputs + DEFINE_OUTPUT( m_OnHurt, "OnHurt" ), + DEFINE_OUTPUT( m_OnHurtPlayer, "OnHurtPlayer" ), + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( trigger_hurt, CTriggerHurt ); + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been handled. +//----------------------------------------------------------------------------- +void CTriggerHurt::Spawn( void ) +{ + BaseClass::Spawn(); + + InitTrigger(); + + m_flOriginalDamage = m_flDamage; + + SetNextThink( TICK_NEVER_THINK ); + SetThink( NULL ); + if (m_bitsDamageInflict & DMG_RADIATION) + { + SetThink ( &CTriggerHurt::RadiationThink ); + SetNextThink( gpGlobals->curtime + random->RandomFloat(0.0, 0.5) ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Trigger hurt that causes radiation will do a radius check and set +// the player's geiger counter level according to distance from center +// of trigger. +//----------------------------------------------------------------------------- +void CTriggerHurt::RadiationThink( void ) +{ + // check to see if a player is in pvs + // if not, continue + Vector vecSurroundMins, vecSurroundMaxs; + CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); + CBasePlayer *pPlayer = static_cast(UTIL_FindClientInPVS( vecSurroundMins, vecSurroundMaxs )); + + if (pPlayer) + { + // get range to player; + float flRange = CollisionProp()->CalcDistanceFromPoint( pPlayer->WorldSpaceCenter() ); + flRange *= 3.0f; + pPlayer->NotifyNearbyRadiationSource(flRange); + } + + float dt = gpGlobals->curtime - m_flLastDmgTime; +#ifdef MAPBASE + if ( dt >= m_flHurtRate ) +#else + if ( dt >= 0.5 ) +#endif + { + HurtAllTouchers( dt ); + } + + SetNextThink( gpGlobals->curtime + 0.25 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: When touched, a hurt trigger does m_flDamage points of damage each half-second. +// Input : pOther - The entity that is touching us. +//----------------------------------------------------------------------------- +bool CTriggerHurt::HurtEntity( CBaseEntity *pOther, float damage ) +{ + if ( !pOther->m_takedamage || !PassesTriggerFilters(pOther) ) + return false; + + if ( damage < 0 ) + { + pOther->TakeHealth( -damage, m_bitsDamageInflict ); + } + else + { + // The damage position is the nearest point on the damaged entity + // to the trigger's center. Not perfect, but better than nothing. + Vector vecCenter = CollisionProp()->WorldSpaceCenter(); + + Vector vecDamagePos; + pOther->CollisionProp()->CalcNearestPoint( vecCenter, &vecDamagePos ); + + CTakeDamageInfo info( this, this, damage, m_bitsDamageInflict ); + info.SetDamagePosition( vecDamagePos ); + if ( !m_bNoDmgForce ) + { + GuessDamageForce( &info, ( vecDamagePos - vecCenter ), vecDamagePos ); + } + else + { + info.SetDamageForce( vec3_origin ); + } + + pOther->TakeDamage( info ); + } + + if (pOther->IsPlayer()) + { + m_OnHurtPlayer.FireOutput(pOther, this); + } + else + { + m_OnHurt.FireOutput(pOther, this); + } + m_hurtEntities.AddToTail( EHANDLE(pOther) ); + //NDebugOverlay::Box( pOther->GetAbsOrigin(), pOther->WorldAlignMins(), pOther->WorldAlignMaxs(), 255,0,0,0,0.5 ); + return true; +} + +void CTriggerHurt::HurtThink() +{ + // if I hurt anyone, think again + if ( HurtAllTouchers( 0.5 ) <= 0 ) + { + SetThink(NULL); + } + else + { +#ifdef MAPBASE + SetNextThink( gpGlobals->curtime + m_flHurtRate ); +#else + SetNextThink( gpGlobals->curtime + 0.5f ); +#endif + } +} + +void CTriggerHurt::EndTouch( CBaseEntity *pOther ) +{ + if (PassesTriggerFilters(pOther)) + { + EHANDLE hOther; + hOther = pOther; + + // if this guy has never taken damage, hurt him now + if ( !m_hurtEntities.HasElement( hOther ) ) + { + HurtEntity( pOther, m_flDamage * 0.5 ); + } + } + BaseClass::EndTouch( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: called from RadiationThink() as well as HurtThink() +// This function applies damage to any entities currently touching the +// trigger +// Input : dt - time since last call +// Output : int - number of entities actually hurt +//----------------------------------------------------------------------------- +#define TRIGGER_HURT_FORGIVE_TIME 3.0f // time in seconds +int CTriggerHurt::HurtAllTouchers( float dt ) +{ + int hurtCount = 0; + // half second worth of damage + float fldmg = m_flDamage * dt; + m_flLastDmgTime = gpGlobals->curtime; + + m_hurtEntities.RemoveAll(); + + touchlink_t *root = ( touchlink_t * )GetDataObject( TOUCHLINK ); + if ( root ) + { + for ( touchlink_t *link = root->nextLink; link != root; link = link->nextLink ) + { + CBaseEntity *pTouch = link->entityTouched; + if ( pTouch ) + { + if ( HurtEntity( pTouch, fldmg ) ) + { + hurtCount++; + } + } + } + } + + if( m_damageModel == DAMAGEMODEL_DOUBLE_FORGIVENESS ) + { + if( hurtCount == 0 ) + { + if( gpGlobals->curtime > m_flDmgResetTime ) + { + // Didn't hurt anyone. Reset the damage if it's time. (hence, the forgiveness) + m_flDamage = m_flOriginalDamage; + } + } + else + { + // Hurt someone! double the damage + m_flDamage *= 2.0f; + + if( m_flDamage > m_flDamageCap ) + { + // Clamp + m_flDamage = m_flDamageCap; + } + + // Now, put the damage reset time into the future. The forgive time is how long the trigger + // must go without harming anyone in order that its accumulated damage be reset to the amount + // set by the level designer. This is a stop-gap for an exploit where players could hop through + // slime and barely take any damage because the trigger would reset damage anytime there was no + // one in the trigger when this function was called. (sjb) + m_flDmgResetTime = gpGlobals->curtime + TRIGGER_HURT_FORGIVE_TIME; + } + } + + return hurtCount; +} + +void CTriggerHurt::Touch( CBaseEntity *pOther ) +{ + if ( m_pfnThink == NULL ) + { + SetThink( &CTriggerHurt::HurtThink ); + SetNextThink( gpGlobals->curtime ); + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTriggerHurt::KeyValue( const char *szKeyName, const char *szValue ) +{ + // Additional OR flags + if (FStrEq( szKeyName, "damageor" ) || FStrEq( szKeyName, "damagepresets" )) + { + m_bitsDamageInflict |= atoi(szValue); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} +#endif + + +// ################################################################################## +// >> TriggerMultiple +// ################################################################################## +LINK_ENTITY_TO_CLASS( trigger_multiple, CTriggerMultiple ); + + +BEGIN_DATADESC( CTriggerMultiple ) + + // Function Pointers + DEFINE_FUNCTION(MultiTouch), + DEFINE_FUNCTION(MultiWaitOver ), + + // Outputs + DEFINE_OUTPUT(m_OnTrigger, "OnTrigger") + +END_DATADESC() + + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been handled. +//----------------------------------------------------------------------------- +void CTriggerMultiple::Spawn( void ) +{ + BaseClass::Spawn(); + + InitTrigger(); + + if (m_flWait == 0) + { + m_flWait = 0.2; + } + + ASSERTSZ(m_iHealth == 0, "trigger_multiple with health"); + SetTouch( &CTriggerMultiple::MultiTouch ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Touch function. Activates the trigger. +// Input : pOther - The thing that touched us. +//----------------------------------------------------------------------------- +void CTriggerMultiple::MultiTouch(CBaseEntity *pOther) +{ + if (PassesTriggerFilters(pOther)) + { + ActivateMultiTrigger( pOther ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pActivator - +//----------------------------------------------------------------------------- +void CTriggerMultiple::ActivateMultiTrigger(CBaseEntity *pActivator) +{ + if (GetNextThink() > gpGlobals->curtime) + return; // still waiting for reset time + + m_hActivator = pActivator; + + m_OnTrigger.FireOutput(m_hActivator, this); + + if (m_flWait > 0) + { + SetThink( &CTriggerMultiple::MultiWaitOver ); + SetNextThink( gpGlobals->curtime + m_flWait ); + } + else + { + // we can't just remove (self) here, because this is a touch function + // called while C code is looping through area links... + SetTouch( NULL ); + SetNextThink( gpGlobals->curtime + 0.1f ); + SetThink( &CTriggerMultiple::SUB_Remove ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: The wait time has passed, so set back up for another activation +//----------------------------------------------------------------------------- +void CTriggerMultiple::MultiWaitOver( void ) +{ + SetThink( NULL ); +} + +// ################################################################################## +// >> TriggerOnce +// ################################################################################## +class CTriggerOnce : public CTriggerMultiple +{ + DECLARE_CLASS( CTriggerOnce, CTriggerMultiple ); +public: + + void Spawn( void ); +}; + +LINK_ENTITY_TO_CLASS( trigger_once, CTriggerOnce ); + +void CTriggerOnce::Spawn( void ) +{ + BaseClass::Spawn(); + + m_flWait = -1; +} + + +// ################################################################################## +// >> TriggerLook +// +// Triggers once when player is looking at m_target +// +// ################################################################################## +#define SF_TRIGGERLOOK_FIREONCE 128 +#define SF_TRIGGERLOOK_USEVELOCITY 256 + +class CTriggerLook : public CTriggerOnce +{ + DECLARE_CLASS( CTriggerLook, CTriggerOnce ); +public: + +#ifdef MAPBASE + CUtlVector m_hLookTargets; +#else + EHANDLE m_hLookTarget; +#endif + float m_flFieldOfView; + float m_flLookTime; // How long must I look for + float m_flLookTimeTotal; // How long have I looked + float m_flLookTimeLast; // When did I last look + float m_flTimeoutDuration; // Number of seconds after start touch to fire anyway + bool m_bTimeoutFired; // True if the OnTimeout output fired since the last StartTouch. + EHANDLE m_hActivator; // The entity that triggered us. +#ifdef MAPBASE + bool m_bUseLOS; // Makes lookers use LOS calculations in addition to viewcone calculations + bool m_bUseLookEntityAsCaller; // Fires OnTrigger with the seen entity +#endif + + void Spawn( void ); + void Touch( CBaseEntity *pOther ); + void StartTouch(CBaseEntity *pOther); + void EndTouch( CBaseEntity *pOther ); + int DrawDebugTextOverlays(void); + + DECLARE_DATADESC(); + +private: + +#ifdef MAPBASE + void Trigger(CBaseEntity *pActivator, bool bTimeout, CBaseEntity *pCaller = NULL); +#else + void Trigger(CBaseEntity *pActivator, bool bTimeout); +#endif + void TimeoutThink(); + + COutputEvent m_OnTimeout; +}; + +LINK_ENTITY_TO_CLASS( trigger_look, CTriggerLook ); +BEGIN_DATADESC( CTriggerLook ) + +#ifdef MAPBASE + DEFINE_UTLVECTOR( m_hLookTargets, FIELD_EHANDLE ), +#else + DEFINE_FIELD( m_hLookTarget, FIELD_EHANDLE ), +#endif + DEFINE_FIELD( m_flLookTimeTotal, FIELD_FLOAT ), + DEFINE_FIELD( m_flLookTimeLast, FIELD_TIME ), + DEFINE_KEYFIELD( m_flTimeoutDuration, FIELD_FLOAT, "timeout" ), + DEFINE_FIELD( m_bTimeoutFired, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bUseLOS, FIELD_BOOLEAN, "UseLOS" ), + DEFINE_KEYFIELD( m_bUseLookEntityAsCaller, FIELD_BOOLEAN, "LookEntityCaller" ), +#endif + + DEFINE_OUTPUT( m_OnTimeout, "OnTimeout" ), + + DEFINE_FUNCTION( TimeoutThink ), + + // Inputs + DEFINE_INPUT( m_flFieldOfView, FIELD_FLOAT, "FieldOfView" ), + DEFINE_INPUT( m_flLookTime, FIELD_FLOAT, "LookTime" ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerLook::Spawn( void ) +{ +#ifndef MAPBASE + m_hLookTarget = NULL; +#endif + m_flLookTimeTotal = -1; + m_bTimeoutFired = false; + + BaseClass::Spawn(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pOther - +//----------------------------------------------------------------------------- +void CTriggerLook::StartTouch(CBaseEntity *pOther) +{ + BaseClass::StartTouch(pOther); + + if (pOther->IsPlayer() && m_flTimeoutDuration) + { + m_bTimeoutFired = false; + m_hActivator = pOther; + SetThink(&CTriggerLook::TimeoutThink); + SetNextThink(gpGlobals->curtime + m_flTimeoutDuration); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerLook::TimeoutThink(void) +{ + Trigger(m_hActivator, true); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerLook::EndTouch(CBaseEntity *pOther) +{ + BaseClass::EndTouch(pOther); + + if (pOther->IsPlayer()) + { + SetThink(NULL); + SetNextThink( TICK_NEVER_THINK ); + + m_flLookTimeTotal = -1; + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerLook::Touch(CBaseEntity *pOther) +{ + // Don't fire the OnTrigger if we've already fired the OnTimeout. This will be + // reset in OnEndTouch. + if (m_bTimeoutFired) + return; + + // -------------------------------- + // Make sure we have a look target + // -------------------------------- +#ifdef MAPBASE + if (m_hLookTargets.Count() <= 0) + { + CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_target, this, m_hActivator, this ); + while (pEntity) + { + m_hLookTargets.AddToTail(pEntity); + pEntity = gEntList.FindEntityByName( pEntity, m_target, this, m_hActivator, this ); + } + } +#else + if (m_hLookTarget == NULL) + { + m_hLookTarget = GetNextTarget(); + if (m_hLookTarget == NULL) + { + return; + } + } +#endif + + // This is designed for single player only + // so we'll always have the same player + if (pOther->IsPlayer()) + { + // ---------------------------------------- + // Check that toucher is facing the target + // ---------------------------------------- + Vector vLookDir; + if ( HasSpawnFlags( SF_TRIGGERLOOK_USEVELOCITY ) ) + { + vLookDir = pOther->GetAbsVelocity(); + if ( vLookDir == vec3_origin ) + { + // See if they're in a vehicle + CBasePlayer *pPlayer = (CBasePlayer *)pOther; + if ( pPlayer->IsInAVehicle() ) + { + vLookDir = pPlayer->GetVehicle()->GetVehicleEnt()->GetSmoothedVelocity(); + } + } + VectorNormalize( vLookDir ); + } + else + { + vLookDir = ((CBaseCombatCharacter*)pOther)->EyeDirection3D( ); + } + +#ifdef MAPBASE + // Check if the player is looking at any of the entities, even if they turn to look at another entity candidate. + // This is how we're doing support for multiple entities without redesigning trigger_look. + EHANDLE hLookingAtEntity = NULL; + for (int i = 0; i < m_hLookTargets.Count(); i++) + { + if (!m_hLookTargets[i]) + continue; + + Vector vTargetDir = m_hLookTargets[i]->GetAbsOrigin() - pOther->EyePosition(); + VectorNormalize(vTargetDir); + + float fDotPr = DotProduct(vLookDir,vTargetDir); + if (fDotPr > m_flFieldOfView && (!m_bUseLOS || pOther->FVisible(pOther))) + { + hLookingAtEntity = m_hLookTargets[i]; + break; + } + } + + if (hLookingAtEntity != NULL) + { + // Is it the first time I'm looking? + if (m_flLookTimeTotal == -1) + { + m_flLookTimeLast = gpGlobals->curtime; + m_flLookTimeTotal = 0; + } + else + { + m_flLookTimeTotal += gpGlobals->curtime - m_flLookTimeLast; + m_flLookTimeLast = gpGlobals->curtime; + } + + if (m_flLookTimeTotal >= m_flLookTime) + { + Trigger(pOther, false, hLookingAtEntity); + } + } + else + { + m_flLookTimeTotal = -1; + } +#else + Vector vTargetDir = m_hLookTarget->GetAbsOrigin() - pOther->EyePosition(); + VectorNormalize(vTargetDir); + + float fDotPr = DotProduct(vLookDir,vTargetDir); + if (fDotPr > m_flFieldOfView) + { + // Is it the first time I'm looking? + if (m_flLookTimeTotal == -1) + { + m_flLookTimeLast = gpGlobals->curtime; + m_flLookTimeTotal = 0; + } + else + { + m_flLookTimeTotal += gpGlobals->curtime - m_flLookTimeLast; + m_flLookTimeLast = gpGlobals->curtime; + } + + if (m_flLookTimeTotal >= m_flLookTime) + { + Trigger(pOther, false); + } + } + else + { + m_flLookTimeTotal = -1; + } +#endif + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Called when the trigger is fired by look logic or timeout. +//----------------------------------------------------------------------------- +#ifdef MAPBASE +void CTriggerLook::Trigger(CBaseEntity *pActivator, bool bTimeout, CBaseEntity *pCaller) +#else +void CTriggerLook::Trigger(CBaseEntity *pActivator, bool bTimeout) +#endif +{ + if (bTimeout) + { + // Fired due to timeout (player never looked at the target). + m_OnTimeout.FireOutput(pActivator, this); + + // Don't fire the OnTrigger for this toucher. + m_bTimeoutFired = true; + } + else + { + // Fire because the player looked at the target. +#ifdef MAPBASE + m_OnTrigger.FireOutput(pActivator, m_bUseLookEntityAsCaller ? pCaller : this); +#else + m_OnTrigger.FireOutput(pActivator, this); +#endif + m_flLookTimeTotal = -1; + + // Cancel the timeout think. + SetThink(NULL); + SetNextThink( TICK_NEVER_THINK ); + } + + if (HasSpawnFlags(SF_TRIGGERLOOK_FIREONCE)) + { + SetThink(&CTriggerLook::SUB_Remove); + SetNextThink(gpGlobals->curtime); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CTriggerLook::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + // ---------------- + // Print Look time + // ---------------- + char tempstr[255]; + Q_snprintf(tempstr,sizeof(tempstr),"Time: %3.2f",m_flLookTime - MAX(0,m_flLookTimeTotal)); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + + +// ################################################################################## +// >> TriggerVolume +// ################################################################################## +class CTriggerVolume : public CPointEntity // Derive from point entity so this doesn't move across levels +{ +public: + DECLARE_CLASS( CTriggerVolume, CPointEntity ); + + void Spawn( void ); +}; + +LINK_ENTITY_TO_CLASS( trigger_transition, CTriggerVolume ); + +// Define space that travels across a level transition +void CTriggerVolume::Spawn( void ) +{ + SetSolid( SOLID_BSP ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); // set size and link into world + if ( showtriggers.GetInt() == 0 ) + { + AddEffects( EF_NODRAW ); + } +} + +#define SF_CHANGELEVEL_NOTOUCH 0x0002 +#define SF_CHANGELEVEL_CHAPTER 0x0004 + +#define cchMapNameMost 32 + +enum +{ + TRANSITION_VOLUME_SCREENED_OUT = 0, + TRANSITION_VOLUME_NOT_FOUND = 1, + TRANSITION_VOLUME_PASSED = 2, +}; + + +//------------------------------------------------------------------------------ +// Reesponsible for changing levels when the player touches it +//------------------------------------------------------------------------------ +class CChangeLevel : public CBaseTrigger +{ + DECLARE_DATADESC(); + +public: + DECLARE_CLASS( CChangeLevel, CBaseTrigger ); + + void Spawn( void ); + void Activate( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + static int ChangeList( levellist_t *pLevelList, int maxList ); + +private: + void TouchChangeLevel( CBaseEntity *pOther ); + void ChangeLevelNow( CBaseEntity *pActivator ); + + void InputChangeLevel( inputdata_t &inputdata ); + + bool IsEntityInTransition( CBaseEntity *pEntity ); + void NotifyEntitiesOutOfTransition(); + + void WarnAboutActiveLead( void ); + + static CBaseEntity *FindLandmark( const char *pLandmarkName ); + static int AddTransitionToList( levellist_t *pLevelList, int listCount, const char *pMapName, const char *pLandmarkName, edict_t *pentLandmark ); + static int InTransitionVolume( CBaseEntity *pEntity, const char *pVolumeName ); + + // Builds the list of entities to save when moving across a transition + static int BuildChangeLevelList( levellist_t *pLevelList, int maxList ); + + // Builds the list of entities to bring across a particular transition + static int BuildEntityTransitionList( CBaseEntity *pLandmarkEntity, const char *pLandmarkName, CBaseEntity **ppEntList, int *pEntityFlags, int nMaxList ); + + // Adds a single entity to the transition list, if appropriate. Returns the new count + static int AddEntityToTransitionList( CBaseEntity *pEntity, int flags, int nCount, CBaseEntity **ppEntList, int *pEntityFlags ); + + // Adds in all entities depended on by entities near the transition + static int AddDependentEntities( int nCount, CBaseEntity **ppEntList, int *pEntityFlags, int nMaxList ); + + // Figures out save flags for the entity + static int ComputeEntitySaveFlags( CBaseEntity *pEntity ); + +private: + char m_szMapName[cchMapNameMost]; // trigger_changelevel only: next map + char m_szLandmarkName[cchMapNameMost]; // trigger_changelevel only: landmark on next map + bool m_bTouched; + + // Outputs + COutputEvent m_OnChangeLevel; +}; + + +LINK_ENTITY_TO_CLASS( trigger_changelevel, CChangeLevel ); + +// Global Savedata for changelevel trigger +BEGIN_DATADESC( CChangeLevel ) + + DEFINE_AUTO_ARRAY( m_szMapName, FIELD_CHARACTER ), + DEFINE_AUTO_ARRAY( m_szLandmarkName, FIELD_CHARACTER ), +// DEFINE_FIELD( m_touchTime, FIELD_TIME ), // don't save +// DEFINE_FIELD( m_bTouched, FIELD_BOOLEAN ), + + // Function Pointers + DEFINE_FUNCTION( TouchChangeLevel ), + + DEFINE_INPUTFUNC( FIELD_VOID, "ChangeLevel", InputChangeLevel ), + + // Outputs + DEFINE_OUTPUT( m_OnChangeLevel, "OnChangeLevel"), + +END_DATADESC() + + +// +// Cache user-entity-field values until spawn is called. +// + +bool CChangeLevel::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "map")) + { + if (strlen(szValue) >= cchMapNameMost) + { + Warning( "Map name '%s' too long (32 chars)\n", szValue ); + Assert(0); + } + Q_strncpy(m_szMapName, szValue, sizeof(m_szMapName)); + } + else if (FStrEq(szKeyName, "landmark")) + { + if (strlen(szValue) >= cchMapNameMost) + { + Warning( "Landmark name '%s' too long (32 chars)\n", szValue ); + Assert(0); + } + + Q_strncpy(m_szLandmarkName, szValue, sizeof( m_szLandmarkName )); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + + +void CChangeLevel::Spawn( void ) +{ + if ( FStrEq( m_szMapName, "" ) ) + { + Msg( "a trigger_changelevel doesn't have a map" ); + } + + if ( FStrEq( m_szLandmarkName, "" ) ) + { + Msg( "trigger_changelevel to %s doesn't have a landmark", m_szMapName ); + } + + InitTrigger(); + + if ( !HasSpawnFlags(SF_CHANGELEVEL_NOTOUCH) ) + { + SetTouch( &CChangeLevel::TouchChangeLevel ); + } + +// Msg( "TRANSITION: %s (%s)\n", m_szMapName, m_szLandmarkName ); +} + +void CChangeLevel::Activate( void ) +{ + BaseClass::Activate(); + + if ( gpGlobals->eLoadType == MapLoad_NewGame ) + { + if ( HasSpawnFlags( SF_CHANGELEVEL_CHAPTER ) ) + { + VPhysicsInitStatic(); + RemoveSolidFlags( FSOLID_NOT_SOLID | FSOLID_TRIGGER ); + SetTouch( NULL ); + return; + } + } + + // Level transitions will bust if they are in solid + CBaseEntity *pLandmark = FindLandmark( m_szLandmarkName ); + if ( pLandmark ) + { + int clusterIndex = engine->GetClusterForOrigin( pLandmark->GetAbsOrigin() ); + if ( clusterIndex < 0 ) + { + Warning( "trigger_changelevel to map %s has a landmark embedded in solid!\n" + "This will break level transitions!\n", m_szMapName ); + } + + if ( g_debug_transitions.GetInt() ) + { + if ( !gEntList.FindEntityByClassname( NULL, "trigger_transition" ) ) + { + Warning( "Map has no trigger_transition volumes for landmark %s\n", m_szLandmarkName ); + } + } + } + + m_bTouched = false; +} + + +static char st_szNextMap[cchMapNameMost]; +static char st_szNextSpot[cchMapNameMost]; + +// Used to show debug for only the transition volume we're currently in +static int g_iDebuggingTransition = 0; + +CBaseEntity *CChangeLevel::FindLandmark( const char *pLandmarkName ) +{ + CBaseEntity *pentLandmark; + + pentLandmark = gEntList.FindEntityByName( NULL, pLandmarkName ); + while ( pentLandmark ) + { + // Found the landmark + if ( FClassnameIs( pentLandmark, "info_landmark" ) ) + return pentLandmark; + else + pentLandmark = gEntList.FindEntityByName( pentLandmark, pLandmarkName ); + } + Warning( "Can't find landmark %s\n", pLandmarkName ); + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Allows level transitions to be triggered by buttons, etc. +//----------------------------------------------------------------------------- +void CChangeLevel::InputChangeLevel( inputdata_t &inputdata ) +{ + // Ignore changelevel transitions if the player's dead or attempting a challenge + if ( gpGlobals->maxClients == 1 ) + { + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer && ( !pPlayer->IsAlive() || pPlayer->GetBonusChallenge() > 0 ) ) + return; + } + + ChangeLevelNow( inputdata.pActivator ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Performs the level change and fires targets. +// Input : pActivator - +//----------------------------------------------------------------------------- +bool CChangeLevel::IsEntityInTransition( CBaseEntity *pEntity ) +{ + int transitionState = InTransitionVolume(pEntity, m_szLandmarkName); + if ( transitionState == TRANSITION_VOLUME_SCREENED_OUT ) + { + return false; + } + + // look for a landmark entity + CBaseEntity *pLandmark = FindLandmark( m_szLandmarkName ); + + if ( !pLandmark ) + return false; + + // Check to make sure it's also in the PVS of landmark + byte pvs[MAX_MAP_CLUSTERS/8]; + int clusterIndex = engine->GetClusterForOrigin( pLandmark->GetAbsOrigin() ); + engine->GetPVSForCluster( clusterIndex, sizeof(pvs), pvs ); + Vector vecSurroundMins, vecSurroundMaxs; + pEntity->CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); + + return engine->CheckBoxInPVS( vecSurroundMins, vecSurroundMaxs, pvs, sizeof( pvs ) ); +} + +void CChangeLevel::NotifyEntitiesOutOfTransition() +{ + CBaseEntity *pEnt = gEntList.FirstEnt(); + while ( pEnt ) + { + // Found the landmark + if ( pEnt->ObjectCaps() & FCAP_NOTIFY_ON_TRANSITION ) + { + variant_t emptyVariant; + if ( !(pEnt->ObjectCaps() & (FCAP_ACROSS_TRANSITION|FCAP_FORCE_TRANSITION)) || !IsEntityInTransition( pEnt ) ) + { + pEnt->AcceptInput( "OutsideTransition", this, this, emptyVariant, 0 ); + } + else + { + pEnt->AcceptInput( "InsideTransition", this, this, emptyVariant, 0 ); + } + } + pEnt = gEntList.NextEnt( pEnt ); + } +} + +//------------------------------------------------------------------------------ +// Purpose : Checks all spawned AIs and prints a warning if any are actively leading +// Input : +// Output : +//------------------------------------------------------------------------------ +void CChangeLevel::WarnAboutActiveLead( void ) +{ + int i; + CAI_BaseNPC * ai; + CAI_BehaviorBase * behavior; + + for ( i = 0; i < g_AI_Manager.NumAIs(); i++ ) + { + ai = g_AI_Manager.AccessAIs()[i]; + behavior = ai->GetRunningBehavior(); + if ( behavior ) + { + if ( dynamic_cast( behavior ) ) + { + Warning( "Entity '%s' is still actively leading\n", STRING( ai->GetEntityName() ) ); + } + } + } +} + +void CChangeLevel::ChangeLevelNow( CBaseEntity *pActivator ) +{ + CBaseEntity *pLandmark; + levellist_t levels[16]; + + Assert(!FStrEq(m_szMapName, "")); + + // Don't work in deathmatch + if ( g_pGameRules->IsDeathmatch() ) + return; + + // Some people are firing these multiple times in a frame, disable + if ( m_bTouched ) + return; + + m_bTouched = true; + + CBaseEntity *pPlayer = (pActivator && pActivator->IsPlayer()) ? pActivator : UTIL_GetLocalPlayer(); + + int transitionState = InTransitionVolume(pPlayer, m_szLandmarkName); + if ( transitionState == TRANSITION_VOLUME_SCREENED_OUT ) + { + DevMsg( 2, "Player isn't in the transition volume %s, aborting\n", m_szLandmarkName ); + return; + } + + // look for a landmark entity + pLandmark = FindLandmark( m_szLandmarkName ); + + if ( !pLandmark ) + return; + + // no transition volumes, check PVS of landmark + if ( transitionState == TRANSITION_VOLUME_NOT_FOUND ) + { + byte pvs[MAX_MAP_CLUSTERS/8]; + int clusterIndex = engine->GetClusterForOrigin( pLandmark->GetAbsOrigin() ); + engine->GetPVSForCluster( clusterIndex, sizeof(pvs), pvs ); + if ( pPlayer ) + { + Vector vecSurroundMins, vecSurroundMaxs; + pPlayer->CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); + bool playerInPVS = engine->CheckBoxInPVS( vecSurroundMins, vecSurroundMaxs, pvs, sizeof( pvs ) ); + + //Assert( playerInPVS ); + if ( !playerInPVS ) + { + Warning( "Player isn't in the landmark's (%s) PVS, aborting\n", m_szLandmarkName ); +#ifndef HL1_DLL + // HL1 works even with these errors! + return; +#endif + } + } + } + + WarnAboutActiveLead(); + + g_iDebuggingTransition = 0; + st_szNextSpot[0] = 0; // Init landmark to NULL + Q_strncpy(st_szNextSpot, m_szLandmarkName,sizeof(st_szNextSpot)); + // This object will get removed in the call to engine->ChangeLevel, copy the params into "safe" memory + Q_strncpy(st_szNextMap, m_szMapName, sizeof(st_szNextMap)); + + m_hActivator = pActivator; + + m_OnChangeLevel.FireOutput(pActivator, this); + + NotifyEntitiesOutOfTransition(); + + +//// Msg( "Level touches %d levels\n", ChangeList( levels, 16 ) ); + if ( g_debug_transitions.GetInt() ) + { + Msg( "CHANGE LEVEL: %s %s\n", st_szNextMap, st_szNextSpot ); + } + + // If we're debugging, don't actually change level + if ( g_debug_transitions.GetInt() == 0 ) + { + engine->ChangeLevel( st_szNextMap, st_szNextSpot ); + } + else + { + // Build a change list so we can see what would be transitioning + CSaveRestoreData *pSaveData = SaveInit( 0 ); + if ( pSaveData ) + { + g_pGameSaveRestoreBlockSet->PreSave( pSaveData ); + pSaveData->levelInfo.connectionCount = BuildChangeList( pSaveData->levelInfo.levelList, MAX_LEVEL_CONNECTIONS ); + g_pGameSaveRestoreBlockSet->PostSave(); + } + + SetTouch( NULL ); + } +} + +// +// GLOBALS ASSUMED SET: st_szNextMap +// +void CChangeLevel::TouchChangeLevel( CBaseEntity *pOther ) +{ + CBasePlayer *pPlayer = ToBasePlayer(pOther); + if ( !pPlayer ) + return; + + if( pPlayer->IsSinglePlayerGameEnding() ) + { + // Some semblance of deceleration, but allow player to fall normally. + // Also, disable controls. + Vector vecVelocity = pPlayer->GetAbsVelocity(); + vecVelocity.x *= 0.5f; + vecVelocity.y *= 0.5f; + pPlayer->SetAbsVelocity( vecVelocity ); + pPlayer->AddFlag( FL_FROZEN ); + return; + } + + if ( !pPlayer->IsInAVehicle() && pPlayer->GetMoveType() == MOVETYPE_NOCLIP ) + { + DevMsg("In level transition: %s %s\n", st_szNextMap, st_szNextSpot ); + return; + } + + ChangeLevelNow( pOther ); +} + + +// Add a transition to the list, but ignore duplicates +// (a designer may have placed multiple trigger_changelevels with the same landmark) +int CChangeLevel::AddTransitionToList( levellist_t *pLevelList, int listCount, const char *pMapName, const char *pLandmarkName, edict_t *pentLandmark ) +{ + int i; + + if ( !pLevelList || !pMapName || !pLandmarkName || !pentLandmark ) + return 0; + + // Ignore changelevels to the level we're ready in. Mapmakers love to do this! + if ( stricmp( pMapName, STRING(gpGlobals->mapname) ) == 0 ) + return 0; + + for ( i = 0; i < listCount; i++ ) + { + if ( pLevelList[i].pentLandmark == pentLandmark && stricmp( pLevelList[i].mapName, pMapName ) == 0 ) + return 0; + } + Q_strncpy( pLevelList[listCount].mapName, pMapName, sizeof(pLevelList[listCount].mapName) ); + Q_strncpy( pLevelList[listCount].landmarkName, pLandmarkName, sizeof(pLevelList[listCount].landmarkName) ); + pLevelList[listCount].pentLandmark = pentLandmark; + + CBaseEntity *ent = CBaseEntity::Instance( pentLandmark ); + Assert( ent ); + + pLevelList[listCount].vecLandmarkOrigin = ent->GetAbsOrigin(); + + return 1; +} + +int BuildChangeList( levellist_t *pLevelList, int maxList ) +{ + return CChangeLevel::ChangeList( pLevelList, maxList ); +} + +struct collidelist_t +{ + const CPhysCollide *pCollide; + Vector origin; + QAngle angles; +}; + + +// NOTE: This routine is relatively slow. If you need to use it for per-frame work, consider that fact. +// UNDONE: Expand this to the full matrix of solid types on each side and move into enginetrace +#ifdef MAPBASE // Other files may use this +bool TestEntityTriggerIntersection_Accurate( CBaseEntity *pTrigger, CBaseEntity *pEntity ) +#else +static bool TestEntityTriggerIntersection_Accurate( CBaseEntity *pTrigger, CBaseEntity *pEntity ) +#endif +{ + Assert( pTrigger->GetSolid() == SOLID_BSP ); + + if ( pTrigger->Intersects( pEntity ) ) // It touches one, it's in the volume + { + switch ( pEntity->GetSolid() ) + { + case SOLID_BBOX: + { + ICollideable *pCollide = pTrigger->CollisionProp(); + Ray_t ray; + trace_t tr; + ray.Init( pEntity->GetAbsOrigin(), pEntity->GetAbsOrigin(), pEntity->WorldAlignMins(), pEntity->WorldAlignMaxs() ); + enginetrace->ClipRayToCollideable( ray, MASK_ALL, pCollide, &tr ); + + if ( tr.startsolid ) + return true; + } + break; + case SOLID_BSP: + case SOLID_VPHYSICS: + { + CPhysCollide *pTriggerCollide = modelinfo->GetVCollide( pTrigger->GetModelIndex() )->solids[0]; + Assert( pTriggerCollide ); + + CUtlVector collideList; + IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; + int physicsCount = pEntity->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); + if ( physicsCount ) + { + for ( int i = 0; i < physicsCount; i++ ) + { + const CPhysCollide *pCollide = pList[i]->GetCollide(); + if ( pCollide ) + { + collidelist_t element; + element.pCollide = pCollide; + pList[i]->GetPosition( &element.origin, &element.angles ); + collideList.AddToTail( element ); + } + } + } + else + { + vcollide_t *pVCollide = modelinfo->GetVCollide( pEntity->GetModelIndex() ); + if ( pVCollide && pVCollide->solidCount ) + { + collidelist_t element; + element.pCollide = pVCollide->solids[0]; + element.origin = pEntity->GetAbsOrigin(); + element.angles = pEntity->GetAbsAngles(); + collideList.AddToTail( element ); + } + } + for ( int i = collideList.Count()-1; i >= 0; --i ) + { + const collidelist_t &element = collideList[i]; + trace_t tr; + physcollision->TraceCollide( element.origin, element.origin, element.pCollide, element.angles, pTriggerCollide, pTrigger->GetAbsOrigin(), pTrigger->GetAbsAngles(), &tr ); + if ( tr.startsolid ) + return true; + } + } + break; + + default: + return true; + } + } + return false; +} + +int CChangeLevel::InTransitionVolume( CBaseEntity *pEntity, const char *pVolumeName ) +{ + CBaseEntity *pVolume; + + if ( pEntity->ObjectCaps() & FCAP_FORCE_TRANSITION ) + return TRANSITION_VOLUME_PASSED; + + // If you're following another entity, follow it through the transition (weapons follow the player) + pEntity = pEntity->GetRootMoveParent(); + + int inVolume = TRANSITION_VOLUME_NOT_FOUND; // Unless we find a trigger_transition, everything is in the volume + + pVolume = gEntList.FindEntityByName( NULL, pVolumeName ); + while ( pVolume ) + { + if ( pVolume && FClassnameIs( pVolume, "trigger_transition" ) ) + { + if ( TestEntityTriggerIntersection_Accurate(pVolume, pEntity ) ) // It touches one, it's in the volume + return TRANSITION_VOLUME_PASSED; + + inVolume = TRANSITION_VOLUME_SCREENED_OUT; // Found a trigger_transition, but I don't intersect it -- if I don't find another, don't go! + } + pVolume = gEntList.FindEntityByName( pVolume, pVolumeName ); + } + return inVolume; +} + + +//------------------------------------------------------------------------------ +// Builds the list of entities to save when moving across a transition +//------------------------------------------------------------------------------ +int CChangeLevel::BuildChangeLevelList( levellist_t *pLevelList, int maxList ) +{ + int nCount = 0; + + CBaseEntity *pentChangelevel = gEntList.FindEntityByClassname( NULL, "trigger_changelevel" ); + while ( pentChangelevel ) + { + CChangeLevel *pTrigger = dynamic_cast(pentChangelevel); + if ( pTrigger ) + { + // Find the corresponding landmark + CBaseEntity *pentLandmark = FindLandmark( pTrigger->m_szLandmarkName ); + if ( pentLandmark ) + { + // Build a list of unique transitions + if ( AddTransitionToList( pLevelList, nCount, pTrigger->m_szMapName, pTrigger->m_szLandmarkName, pentLandmark->edict() ) ) + { + ++nCount; + if ( nCount >= maxList ) // FULL!! + break; + } + } + } + pentChangelevel = gEntList.FindEntityByClassname( pentChangelevel, "trigger_changelevel" ); + } + + return nCount; +} + + +//------------------------------------------------------------------------------ +// Adds a single entity to the transition list, if appropriate. Returns the new count +//------------------------------------------------------------------------------ +int CChangeLevel::ComputeEntitySaveFlags( CBaseEntity *pEntity ) +{ + if ( g_iDebuggingTransition == DEBUG_TRANSITIONS_VERBOSE ) + { + Msg( "Trying %s (%s): ", pEntity->GetClassname(), pEntity->GetDebugName() ); + } + + int caps = pEntity->ObjectCaps(); + if ( caps & FCAP_DONT_SAVE ) + { + if ( g_iDebuggingTransition == DEBUG_TRANSITIONS_VERBOSE ) + { + Msg( "IGNORED due to being marked \"Don't save\".\n" ); + } + return 0; + } + + // If this entity can be moved or is global, mark it + int flags = 0; + if ( caps & FCAP_ACROSS_TRANSITION ) + { + flags |= FENTTABLE_MOVEABLE; + } + if ( pEntity->m_iGlobalname != NULL_STRING && !pEntity->IsDormant() ) + { + flags |= FENTTABLE_GLOBAL; + } + + if ( g_iDebuggingTransition == DEBUG_TRANSITIONS_VERBOSE && !flags ) + { + Msg( "IGNORED, no across_transition flag & no globalname\n" ); + } + + return flags; +} + + +//------------------------------------------------------------------------------ +// Adds a single entity to the transition list, if appropriate. Returns the new count +//------------------------------------------------------------------------------ +inline int CChangeLevel::AddEntityToTransitionList( CBaseEntity *pEntity, int flags, int nCount, CBaseEntity **ppEntList, int *pEntityFlags ) +{ + ppEntList[ nCount ] = pEntity; + pEntityFlags[ nCount ] = flags; + ++nCount; + + // If we're debugging, make it visible + if ( g_iDebuggingTransition ) + { + if ( g_iDebuggingTransition == DEBUG_TRANSITIONS_VERBOSE ) + { + // In verbose mode we've already printed out what the entity is + Msg("ADDED.\n"); + } + else + { + // In non-verbose mode, we just print this line + Msg( "ADDED %s (%s) to transition.\n", pEntity->GetClassname(), pEntity->GetDebugName() ); + } + + pEntity->m_debugOverlays |= (OVERLAY_BBOX_BIT | OVERLAY_NAME_BIT); + } + + return nCount; +} + + +//------------------------------------------------------------------------------ +// Builds the list of entities to bring across a particular transition +//------------------------------------------------------------------------------ +int CChangeLevel::BuildEntityTransitionList( CBaseEntity *pLandmarkEntity, const char *pLandmarkName, + CBaseEntity **ppEntList, int *pEntityFlags, int nMaxList ) +{ + int iEntity = 0; + + // Only show debug for the transition to the level we're going to + if ( g_debug_transitions.GetInt() && pLandmarkEntity->NameMatches(st_szNextSpot) ) + { + g_iDebuggingTransition = g_debug_transitions.GetInt(); + + // Show us where the landmark entity is + pLandmarkEntity->m_debugOverlays |= (OVERLAY_PIVOT_BIT | OVERLAY_BBOX_BIT | OVERLAY_NAME_BIT); + } + else + { + g_iDebuggingTransition = 0; + } + + // Follow the linked list of entities in the PVS of the transition landmark + CBaseEntity *pEntity = NULL; + while ( (pEntity = UTIL_EntitiesInPVS( pLandmarkEntity, pEntity)) != NULL ) + { + int flags = ComputeEntitySaveFlags( pEntity ); + if ( !flags ) + continue; + + // Check to make sure the entity isn't screened out by a trigger_transition + if ( !InTransitionVolume( pEntity, pLandmarkName ) ) + { + if ( g_iDebuggingTransition == DEBUG_TRANSITIONS_VERBOSE ) + { + Msg( "IGNORED, outside transition volume.\n" ); + } + continue; + } + + if ( iEntity >= nMaxList ) + { + Warning( "Too many entities across a transition!\n" ); + Assert( 0 ); + return iEntity; + } + + iEntity = AddEntityToTransitionList( pEntity, flags, iEntity, ppEntList, pEntityFlags ); + } + + return iEntity; +} + + +//------------------------------------------------------------------------------ +// Tests bits in a bitfield +//------------------------------------------------------------------------------ +static inline bool IsBitSet( char *pBuf, int nBit ) +{ + return (pBuf[ nBit >> 3 ] & ( 1 << (nBit & 0x7) )) != 0; +} + +static inline void Set( char *pBuf, int nBit ) +{ + pBuf[ nBit >> 3 ] |= 1 << (nBit & 0x7); +} + + +//------------------------------------------------------------------------------ +// Adds in all entities depended on by entities near the transition +//------------------------------------------------------------------------------ +#define MAX_ENTITY_BYTE_COUNT (NUM_ENT_ENTRIES >> 3) +int CChangeLevel::AddDependentEntities( int nCount, CBaseEntity **ppEntList, int *pEntityFlags, int nMaxList ) +{ + char pEntitiesSaved[MAX_ENTITY_BYTE_COUNT]; + memset( pEntitiesSaved, 0, MAX_ENTITY_BYTE_COUNT * sizeof(char) ); + + // Populate the initial bitfield + int i; + for ( i = 0; i < nCount; ++i ) + { + // NOTE: Must use GetEntryIndex because we're saving non-networked entities + int nEntIndex = ppEntList[i]->GetRefEHandle().GetEntryIndex(); + + // We shouldn't already have this entity in the list! + Assert( !IsBitSet( pEntitiesSaved, nEntIndex ) ); + + // Mark the entity as being in the list + Set( pEntitiesSaved, nEntIndex ); + } + + IEntitySaveUtils *pSaveUtils = GetEntitySaveUtils(); + + // Iterate over entities whose dependencies we've not yet processed + // NOTE: nCount will change value during this loop in AddEntityToTransitionList + for ( i = 0; i < nCount; ++i ) + { + CBaseEntity *pEntity = ppEntList[i]; + + // Find dependencies in the hash. + int nDepCount = pSaveUtils->GetEntityDependencyCount( pEntity ); + if ( !nDepCount ) + continue; + + CBaseEntity **ppDependentEntities = (CBaseEntity**)stackalloc( nDepCount * sizeof(CBaseEntity*) ); + pSaveUtils->GetEntityDependencies( pEntity, nDepCount, ppDependentEntities ); + for ( int j = 0; j < nDepCount; ++j ) + { + CBaseEntity *pDependent = ppDependentEntities[j]; + if ( !pDependent ) + continue; + + // NOTE: Must use GetEntryIndex because we're saving non-networked entities + int nEntIndex = pDependent->GetRefEHandle().GetEntryIndex(); + + // Don't re-add it if it's already in the list + if ( IsBitSet( pEntitiesSaved, nEntIndex ) ) + continue; + + // Mark the entity as being in the list + Set( pEntitiesSaved, nEntIndex ); + + int flags = ComputeEntitySaveFlags( pEntity ); + if ( flags ) + { + if ( nCount >= nMaxList ) + { + Warning( "Too many entities across a transition!\n" ); + Assert( 0 ); + return false; + } + + if ( g_debug_transitions.GetInt() ) + { + Msg( "ADDED DEPENDANCY: %s (%s)\n", pEntity->GetClassname(), pEntity->GetDebugName() ); + } + + nCount = AddEntityToTransitionList( pEntity, flags, nCount, ppEntList, pEntityFlags ); + } + else + { + Warning("Warning!! Save dependency is linked to an entity that doesn't want to be saved!\n"); + } + } + } + + return nCount; +} + + +//------------------------------------------------------------------------------ +// This builds the list of all transitions on this level and which entities +// are in their PVS's and can / should be moved across. +//------------------------------------------------------------------------------ + +// We can only ever move 512 entities across a transition +#define MAX_ENTITY 512 + +// FIXME: This has grown into a complicated beast. Can we make this more elegant? +int CChangeLevel::ChangeList( levellist_t *pLevelList, int maxList ) +{ + // Find all of the possible level changes on this BSP + int count = BuildChangeLevelList( pLevelList, maxList ); + + if ( !gpGlobals->pSaveData || ( static_cast(gpGlobals->pSaveData)->NumEntities() == 0 ) ) + return count; + + CSave saveHelper( static_cast(gpGlobals->pSaveData) ); + + // For each level change, find nearby entities and save them + int i; + for ( i = 0; i < count; i++ ) + { + CBaseEntity *pEntList[ MAX_ENTITY ]; + int entityFlags[ MAX_ENTITY ]; + + // First, figure out which entities are near the transition + CBaseEntity *pLandmarkEntity = CBaseEntity::Instance( pLevelList[i].pentLandmark ); + int iEntity = BuildEntityTransitionList( pLandmarkEntity, pLevelList[i].landmarkName, pEntList, entityFlags, MAX_ENTITY ); + + // FIXME: Activate if we have a dependency problem on level transition + // Next, add in all entities depended on by entities near the transition +// iEntity = AddDependentEntities( iEntity, pEntList, entityFlags, MAX_ENTITY ); + + int j; + for ( j = 0; j < iEntity; j++ ) + { + // Mark entity table with 1<IsSolid() || (pOther->GetMoveType() == MOVETYPE_PUSH || pOther->GetMoveType() == MOVETYPE_NONE ) ) + return; + + if (!PassesTriggerFilters(pOther)) + return; + + // FIXME: If something is hierarchically attached, should we try to push the parent? + if (pOther->GetMoveParent()) + return; + + // Transform the push dir into global space + Vector vecAbsDir; + VectorRotate( m_vecPushDir, EntityToWorldTransform(), vecAbsDir ); + + // Instant trigger, just transfer velocity and remove + if (HasSpawnFlags(SF_TRIG_PUSH_ONCE)) + { + pOther->ApplyAbsVelocityImpulse( m_flPushSpeed * vecAbsDir ); + + if ( vecAbsDir.z > 0 ) + { + pOther->SetGroundEntity( NULL ); + } + UTIL_Remove( this ); + return; + } + + switch( pOther->GetMoveType() ) + { + case MOVETYPE_NONE: + case MOVETYPE_PUSH: + case MOVETYPE_NOCLIP: + break; + + case MOVETYPE_VPHYSICS: + { + IPhysicsObject *pPhys = pOther->VPhysicsGetObject(); + if ( pPhys ) + { + // UNDONE: Assume the velocity is for a 100kg object, scale with mass + pPhys->ApplyForceCenter( m_flPushSpeed * vecAbsDir * 100.0f * gpGlobals->frametime ); + return; + } + } + break; + + default: + { +#if defined( HL2_DLL ) + // HACK HACK HL2 players on ladders will only be disengaged if the sf is set, otherwise no push occurs. + if ( pOther->IsPlayer() && + pOther->GetMoveType() == MOVETYPE_LADDER ) + { + if ( !HasSpawnFlags(SF_TRIG_PUSH_AFFECT_PLAYER_ON_LADDER) ) + { + // Ignore the push + return; + } + } +#endif + + Vector vecPush = (m_flPushSpeed * vecAbsDir); + if ( pOther->GetFlags() & FL_BASEVELOCITY ) + { + vecPush = vecPush + pOther->GetBaseVelocity(); + } + if ( vecPush.z > 0 && (pOther->GetFlags() & FL_ONGROUND) ) + { + pOther->SetGroundEntity( NULL ); + Vector origin = pOther->GetAbsOrigin(); + origin.z += 1.0f; + pOther->SetAbsOrigin( origin ); + } + +#ifdef HL1_DLL + // Apply the z velocity as a force so it counteracts gravity properly + Vector vecImpulse( 0, 0, vecPush.z * 0.025 );//magic hack number + + pOther->ApplyAbsVelocityImpulse( vecImpulse ); + + // apply x, y as a base velocity so we travel at constant speed on conveyors + vecPush.z = 0; +#endif + + pOther->SetBaseVelocity( vecPush ); + pOther->AddFlag( FL_BASEVELOCITY ); + } + break; + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerPush::InputSetSpeed( inputdata_t &inputdata ) +{ + m_flSpeed = inputdata.value.Float(); + + // Need to update push speed/alternative ticks + Activate(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerPush::InputSetPushDir( inputdata_t &inputdata ) +{ + inputdata.value.Vector3D( m_vecPushDir ); + + // Convert pushdir from angles to a vector + Vector vecAbsDir; + QAngle angPushDir = QAngle( m_vecPushDir.x, m_vecPushDir.y, m_vecPushDir.z ); + AngleVectors( angPushDir, &vecAbsDir ); + + // Transform the vector into entity space + VectorIRotate( vecAbsDir, EntityToWorldTransform(), m_vecPushDir ); +} +#endif + + +//----------------------------------------------------------------------------- +// Teleport trigger +//----------------------------------------------------------------------------- +const int SF_TELEPORT_PRESERVE_ANGLES = 0x20; // Preserve angles even when a local landmark is not specified + +class CTriggerTeleport : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerTeleport, CBaseTrigger ); + + void Spawn( void ); + void Touch( CBaseEntity *pOther ); + + string_t m_iLandmark; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( trigger_teleport, CTriggerTeleport ); + +BEGIN_DATADESC( CTriggerTeleport ) + + DEFINE_KEYFIELD( m_iLandmark, FIELD_STRING, "landmark" ), + +END_DATADESC() + + + +void CTriggerTeleport::Spawn( void ) +{ + InitTrigger(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Teleports the entity that touched us to the location of our target, +// setting the toucher's angles to our target's angles if they are a +// player. +// +// If a landmark was specified, the toucher is offset from the target +// by their initial offset from the landmark and their angles are +// left alone. +// +// Input : pOther - The entity that touched us. +//----------------------------------------------------------------------------- +void CTriggerTeleport::Touch( CBaseEntity *pOther ) +{ + CBaseEntity *pentTarget = NULL; + + if (!PassesTriggerFilters(pOther)) + { + return; + } + + // The activator and caller are the same + pentTarget = gEntList.FindEntityByName( pentTarget, m_target, NULL, pOther, pOther ); + if (!pentTarget) + { + return; + } + + // + // If a landmark was specified, offset the player relative to the landmark. + // + CBaseEntity *pentLandmark = NULL; + Vector vecLandmarkOffset(0, 0, 0); + if (m_iLandmark != NULL_STRING) + { + // The activator and caller are the same + pentLandmark = gEntList.FindEntityByName(pentLandmark, m_iLandmark, NULL, pOther, pOther ); + if (pentLandmark) + { + vecLandmarkOffset = pOther->GetAbsOrigin() - pentLandmark->GetAbsOrigin(); + } + } + + pOther->SetGroundEntity( NULL ); + + Vector tmp = pentTarget->GetAbsOrigin(); + + if (!pentLandmark && pOther->IsPlayer()) + { + // make origin adjustments in case the teleportee is a player. (origin in center, not at feet) + tmp.z -= pOther->WorldAlignMins().z; + } + + // + // Only modify the toucher's angles and zero their velocity if no landmark was specified. + // + const QAngle *pAngles = NULL; + Vector *pVelocity = NULL; + +#ifdef HL1_DLL + Vector vecZero(0,0,0); +#endif + + if (!pentLandmark && !HasSpawnFlags(SF_TELEPORT_PRESERVE_ANGLES) ) + { + pAngles = &pentTarget->GetAbsAngles(); + +#ifdef HL1_DLL + pVelocity = &vecZero; +#else + pVelocity = NULL; //BUGBUG - This does not set the player's velocity to zero!!! +#endif + } + + tmp += vecLandmarkOffset; + pOther->Teleport( &tmp, pAngles, pVelocity ); +} + + +LINK_ENTITY_TO_CLASS( info_teleport_destination, CPointEntity ); + + +//----------------------------------------------------------------------------- +// Teleport Relative trigger +//----------------------------------------------------------------------------- +class CTriggerTeleportRelative : public CBaseTrigger +{ +public: + DECLARE_CLASS(CTriggerTeleportRelative, CBaseTrigger); + + virtual void Spawn( void ) OVERRIDE; + virtual void Touch( CBaseEntity *pOther ) OVERRIDE; + + Vector m_TeleportOffset; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( trigger_teleport_relative, CTriggerTeleportRelative ); +BEGIN_DATADESC( CTriggerTeleportRelative ) + DEFINE_KEYFIELD( m_TeleportOffset, FIELD_VECTOR, "teleportoffset" ) +END_DATADESC() + + +void CTriggerTeleportRelative::Spawn( void ) +{ + InitTrigger(); +} + +void CTriggerTeleportRelative::Touch( CBaseEntity *pOther ) +{ + if ( !PassesTriggerFilters(pOther) ) + { + return; + } + + const Vector finalPos = m_TeleportOffset + WorldSpaceCenter(); + const Vector *momentum = &vec3_origin; + + pOther->Teleport( &finalPos, NULL, momentum ); +} + +//----------------------------------------------------------------------------- +// Purpose: Saves the game when the player touches the trigger. Can be enabled or disabled +//----------------------------------------------------------------------------- +class CTriggerToggleSave : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerToggleSave, CBaseTrigger ); + + void Spawn( void ); + void Touch( CBaseEntity *pOther ); + + void InputEnable( inputdata_t &inputdata ) + { + m_bDisabled = false; + } + + void InputDisable( inputdata_t &inputdata ) + { + m_bDisabled = true; + } + + bool m_bDisabled; // Initial state + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CTriggerToggleSave ) + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( trigger_togglesave, CTriggerToggleSave ); + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been set. +//----------------------------------------------------------------------------- +void CTriggerToggleSave::Spawn( void ) +{ + if ( g_pGameRules->IsDeathmatch() ) + { + UTIL_Remove( this ); + return; + } + + InitTrigger(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Performs the autosave when the player touches us. +// Input : pOther - +//----------------------------------------------------------------------------- +void CTriggerToggleSave::Touch( CBaseEntity *pOther ) +{ + if( m_bDisabled ) + return; + + // Only save on clients + if ( !pOther->IsPlayer() ) + return; + + // Can be re-enabled + m_bDisabled = true; + + engine->ServerCommand( "autosave\n" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Saves the game when the player touches the trigger. +//----------------------------------------------------------------------------- +class CTriggerSave : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerSave, CBaseTrigger ); + + void Spawn( void ); + void Touch( CBaseEntity *pOther ); + DECLARE_DATADESC(); + + bool m_bForceNewLevelUnit; + float m_fDangerousTimer; + int m_minHitPoints; +}; + + +BEGIN_DATADESC( CTriggerSave ) + + DEFINE_KEYFIELD( m_bForceNewLevelUnit, FIELD_BOOLEAN, "NewLevelUnit" ), + DEFINE_KEYFIELD( m_minHitPoints, FIELD_INTEGER, "MinimumHitPoints" ), + DEFINE_KEYFIELD( m_fDangerousTimer, FIELD_FLOAT, "DangerousTimer" ), + +END_DATADESC() +LINK_ENTITY_TO_CLASS( trigger_autosave, CTriggerSave ); + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been set. +//----------------------------------------------------------------------------- +void CTriggerSave::Spawn( void ) +{ + if ( g_pGameRules->IsDeathmatch() ) + { + UTIL_Remove( this ); + return; + } + + InitTrigger(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Performs the autosave when the player touches us. +// Input : pOther - +//----------------------------------------------------------------------------- +void CTriggerSave::Touch( CBaseEntity *pOther ) +{ + // Only save on clients + if ( !pOther->IsPlayer() ) + return; + + if ( m_fDangerousTimer != 0.0f ) + { + if ( g_ServerGameDLL.m_fAutoSaveDangerousTime != 0.0f && g_ServerGameDLL.m_fAutoSaveDangerousTime >= gpGlobals->curtime ) + { + // A previous dangerous auto save was waiting to become safe + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + + if ( pPlayer->GetDeathTime() == 0.0f || pPlayer->GetDeathTime() > gpGlobals->curtime ) + { + // The player isn't dead, so make the dangerous auto save safe + engine->ServerCommand( "autosavedangerousissafe\n" ); + } + } + } + + // this is a one-way transition - there is no way to return to the previous map. + if ( m_bForceNewLevelUnit ) + { + engine->ClearSaveDir(); + } + UTIL_Remove( this ); + + if ( m_fDangerousTimer != 0.0f ) + { + // There's a dangerous timer. Save if we have enough hitpoints. + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + + if (pPlayer && pPlayer->GetHealth() >= m_minHitPoints) + { + engine->ServerCommand( "autosavedangerous\n" ); + g_ServerGameDLL.m_fAutoSaveDangerousTime = gpGlobals->curtime + m_fDangerousTimer; + } + } + else + { + engine->ServerCommand( "autosave\n" ); + } +} + + +class CTriggerGravity : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerGravity, CBaseTrigger ); + DECLARE_DATADESC(); + + void Spawn( void ); + void GravityTouch( CBaseEntity *pOther ); +}; +LINK_ENTITY_TO_CLASS( trigger_gravity, CTriggerGravity ); + +BEGIN_DATADESC( CTriggerGravity ) + + // Function Pointers + DEFINE_FUNCTION(GravityTouch), + +END_DATADESC() + +void CTriggerGravity::Spawn( void ) +{ + BaseClass::Spawn(); + InitTrigger(); + SetTouch( &CTriggerGravity::GravityTouch ); +} + +void CTriggerGravity::GravityTouch( CBaseEntity *pOther ) +{ + // Only save on clients + if ( !pOther->IsPlayer() ) + return; + + pOther->SetGravity( GetGravity() ); +} + + +// this is a really bad idea. +class CAI_ChangeTarget : public CBaseEntity +{ +public: + DECLARE_CLASS( CAI_ChangeTarget, CBaseEntity ); + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + + int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + DECLARE_DATADESC(); + +private: + string_t m_iszNewTarget; +}; +LINK_ENTITY_TO_CLASS( ai_changetarget, CAI_ChangeTarget ); + +BEGIN_DATADESC( CAI_ChangeTarget ) + + DEFINE_KEYFIELD( m_iszNewTarget, FIELD_STRING, "m_iszNewTarget" ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + +END_DATADESC() + + +void CAI_ChangeTarget::InputActivate( inputdata_t &inputdata ) +{ + CBaseEntity *pTarget = NULL; + + while ((pTarget = gEntList.FindEntityByName( pTarget, m_target, NULL, inputdata.pActivator, inputdata.pCaller )) != NULL) + { + pTarget->m_target = m_iszNewTarget; + CAI_BaseNPC *pNPC = pTarget->MyNPCPointer( ); + if (pNPC) + { + pNPC->SetGoalEnt( NULL ); + } + } +} + + + + + + + + +//----------------------------------------------------------------------------- +// Purpose: Change an NPC's hint group to something new +//----------------------------------------------------------------------------- +class CAI_ChangeHintGroup : public CBaseEntity +{ +public: + DECLARE_CLASS( CAI_ChangeHintGroup, CBaseEntity ); + + int ObjectCaps( void ) { return BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +private: + CAI_BaseNPC *FindQualifiedNPC( CAI_BaseNPC *pPrev, CBaseEntity *pActivator, CBaseEntity *pCaller ); + + int m_iSearchType; + string_t m_strSearchName; + string_t m_strNewHintGroup; + float m_flRadius; + bool m_bHintGroupNavLimiting; +#ifdef MAPBASE + bool m_bChangeHints; +#endif +}; +LINK_ENTITY_TO_CLASS( ai_changehintgroup, CAI_ChangeHintGroup ); + +BEGIN_DATADESC( CAI_ChangeHintGroup ) + + DEFINE_KEYFIELD( m_iSearchType, FIELD_INTEGER, "SearchType" ), + DEFINE_KEYFIELD( m_strSearchName, FIELD_STRING, "SearchName" ), + DEFINE_KEYFIELD( m_strNewHintGroup, FIELD_STRING, "NewHintGroup" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "Radius" ), + DEFINE_KEYFIELD( m_bHintGroupNavLimiting, FIELD_BOOLEAN, "hintlimiting" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bChangeHints, FIELD_BOOLEAN, "changehints" ), +#endif + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + +END_DATADESC() + +CAI_BaseNPC *CAI_ChangeHintGroup::FindQualifiedNPC( CAI_BaseNPC *pPrev, CBaseEntity *pActivator, CBaseEntity *pCaller ) +{ + CBaseEntity *pEntity = pPrev; + CAI_BaseNPC *pResult = NULL; + const char *pszSearchName = STRING(m_strSearchName); + while ( !pResult ) + { + // Find a candidate + switch ( m_iSearchType ) + { + case 0: + { + pEntity = gEntList.FindEntityByNameWithin( pEntity, pszSearchName, GetLocalOrigin(), m_flRadius, NULL, pActivator, pCaller ); + break; + } + + case 1: + { + pEntity = gEntList.FindEntityByClassnameWithin( pEntity, pszSearchName, GetLocalOrigin(), m_flRadius ); + break; + } + + case 2: + { + pEntity = gEntList.FindEntityInSphere( pEntity, GetLocalOrigin(), ( m_flRadius != 0.0 ) ? m_flRadius : FLT_MAX ); + break; + } + } + + if ( !pEntity ) + return NULL; + + // Qualify + pResult = pEntity->MyNPCPointer(); + if ( pResult && m_iSearchType == 2 && (!FStrEq( STRING(pResult->GetHintGroup()), pszSearchName ) ) ) + { + pResult = NULL; + } + } + + return pResult; +} + +void CAI_ChangeHintGroup::InputActivate( inputdata_t &inputdata ) +{ + CAI_BaseNPC *pTarget = NULL; + + while((pTarget = FindQualifiedNPC( pTarget, inputdata.pActivator, inputdata.pCaller )) != NULL) + { + pTarget->SetHintGroup( m_strNewHintGroup, m_bHintGroupNavLimiting ); + } + +#ifdef MAPBASE + if (m_bChangeHints) + { + AIHintIter_t iter; + CAI_Hint *pHint = CAI_HintManager::GetFirstHint( &iter ); + while ( pHint != NULL ) + { + if ((GetAbsOrigin() - pHint->GetAbsOrigin()).Length() < m_flRadius) + { + bool bValid = false; + switch (m_iSearchType) + { + case 0: { bValid = pHint->NameMatches(STRING(m_strSearchName)); } break; + case 1: { bValid = pHint->ClassMatches(STRING(m_strSearchName)); } break; + + // These should be pooled, so if they're the same hintgroup, they should point to the same string... + case 2: { bValid = pHint->GetGroup() == m_strSearchName; } break; + } + + if (bValid) + pHint->SetGroup(m_strNewHintGroup); + } + + // Move to the next + pHint = CAI_HintManager::GetNextHint( &iter ); + } + } +#endif +} + + + + +#define SF_CAMERA_PLAYER_POSITION 1 +#define SF_CAMERA_PLAYER_TARGET 2 +#define SF_CAMERA_PLAYER_TAKECONTROL 4 +#define SF_CAMERA_PLAYER_INFINITE_WAIT 8 +#define SF_CAMERA_PLAYER_SNAP_TO 16 +#define SF_CAMERA_PLAYER_NOT_SOLID 32 +#define SF_CAMERA_PLAYER_INTERRUPT 64 +#ifdef MAPBASE +#define SF_CAMERA_PLAYER_SETFOV 128 +#define SF_CAMERA_PLAYER_NEW_BEHAVIOR 256 // In case anyone or anything relied on the broken features +#endif + + +#if HL2_EPISODIC +const float CTriggerCamera::kflPosInterpTime = 2.0f; +#endif + +LINK_ENTITY_TO_CLASS( point_viewcontrol, CTriggerCamera ); + +BEGIN_DATADESC( CTriggerCamera ) + + DEFINE_FIELD( m_hPlayer, FIELD_EHANDLE ), + DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_pPath, FIELD_CLASSPTR ), + DEFINE_FIELD( m_sPath, FIELD_STRING ), + DEFINE_FIELD( m_flWait, FIELD_FLOAT ), + DEFINE_FIELD( m_flReturnTime, FIELD_TIME ), + DEFINE_FIELD( m_flStopTime, FIELD_TIME ), + DEFINE_FIELD( m_moveDistance, FIELD_FLOAT ), + DEFINE_FIELD( m_targetSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_initialSpeed, FIELD_FLOAT ), + DEFINE_FIELD( m_acceleration, FIELD_FLOAT ), + DEFINE_FIELD( m_deceleration, FIELD_FLOAT ), + DEFINE_FIELD( m_state, FIELD_INTEGER ), + DEFINE_FIELD( m_vecMoveDir, FIELD_VECTOR ), + DEFINE_KEYFIELD( m_iszTargetAttachment, FIELD_STRING, "targetattachment" ), + DEFINE_FIELD( m_iAttachmentIndex, FIELD_INTEGER ), + DEFINE_FIELD( m_bSnapToGoal, FIELD_BOOLEAN ), +#if HL2_EPISODIC + DEFINE_KEYFIELD( m_bInterpolatePosition, FIELD_BOOLEAN, "interpolatepositiontoplayer" ), + DEFINE_FIELD( m_vStartPos, FIELD_VECTOR ), + DEFINE_FIELD( m_vEndPos, FIELD_VECTOR ), + DEFINE_FIELD( m_flInterpStartTime, FIELD_TIME ), +#endif + DEFINE_FIELD( m_nPlayerButtons, FIELD_INTEGER ), + DEFINE_FIELD( m_nOldTakeDamage, FIELD_INTEGER ), + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_fov, FIELD_FLOAT, "fov" ), + DEFINE_KEYFIELD( m_fovSpeed, FIELD_FLOAT, "fov_rate" ), + + DEFINE_KEYFIELD( m_bDontSetPlayerView, FIELD_BOOLEAN, "DontSetPlayerView" ), +#endif + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFOV", InputSetFOV ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetFOVRate", InputSetFOVRate ), +#endif + + // Function Pointers +#ifdef MAPBASE + DEFINE_FUNCTION( MoveThink ), +#endif + DEFINE_FUNCTION( FollowTarget ), + DEFINE_OUTPUT( m_OnEndFollow, "OnEndFollow" ), +#ifdef MAPBASE + DEFINE_OUTPUT( m_OnStartFollow, "OnStartFollow" ), +#endif + +END_DATADESC() + +// VScript: publish class and select members to script language +BEGIN_ENT_SCRIPTDESC( CTriggerCamera, CBaseEntity, "Server-side camera entity" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetFov, "GetFov", "get camera's current fov setting as integer" ) + DEFINE_SCRIPTFUNC_NAMED( ScriptSetFov, "SetFov", "set camera's current fov in integer degrees and fov change rate as float" ) +END_SCRIPTDESC(); + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTriggerCamera::CTriggerCamera() +{ + m_fov = 90; + m_fovSpeed = 1; +} + +//------------------------------------------------------------------------------ +// Cleanup +//------------------------------------------------------------------------------ +void CTriggerCamera::UpdateOnRemove() +{ + if (m_state == USE_ON && HasSpawnFlags(SF_CAMERA_PLAYER_NEW_BEHAVIOR)) + { + Disable(); + } + + BaseClass::UpdateOnRemove(); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerCamera::Spawn( void ) +{ + BaseClass::Spawn(); + + SetMoveType( MOVETYPE_NOCLIP ); + SetSolid( SOLID_NONE ); // Remove model & collisions + SetRenderColorA( 0 ); // The engine won't draw this model if this is set to 0 and blending is on + m_nRenderMode = kRenderTransTexture; + + m_state = USE_OFF; + + m_initialSpeed = m_flSpeed; + + if ( m_acceleration == 0 ) + m_acceleration = 500; + + if ( m_deceleration == 0 ) + m_deceleration = 500; + + DispatchUpdateTransmitState(); +} + +int CTriggerCamera::UpdateTransmitState() +{ + // always tranmit if currently used by a monitor + if ( m_state == USE_ON ) + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + else + { + return SetTransmitState( FL_EDICT_DONTSEND ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTriggerCamera::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "wait")) + { + m_flWait = atof(szValue); + } + else if (FStrEq(szKeyName, "moveto")) + { + m_sPath = AllocPooledString( szValue ); + } + else if (FStrEq(szKeyName, "acceleration")) + { + m_acceleration = atof( szValue ); + } + else if (FStrEq(szKeyName, "deceleration")) + { + m_deceleration = atof( szValue ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler to turn on this trigger. +//------------------------------------------------------------------------------ +void CTriggerCamera::InputEnable( inputdata_t &inputdata ) +{ + m_hPlayer = inputdata.pActivator; + Enable(); +} + + +//------------------------------------------------------------------------------ +// Purpose: Input handler to turn off this trigger. +//------------------------------------------------------------------------------ +void CTriggerCamera::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +#ifdef MAPBASE +//------------------------------------------------------------------------------ +// Purpose: Input handler to set FOV. +//------------------------------------------------------------------------------ +void CTriggerCamera::InputSetFOV( inputdata_t &inputdata ) +{ + m_fov = inputdata.value.Float(); + + if ( m_state == USE_ON && m_hPlayer ) + { + ((CBasePlayer*)m_hPlayer.Get())->SetFOV( this, m_fov, m_fovSpeed ); + } +} + +//------------------------------------------------------------------------------ +// Purpose: Input handler to set FOV rate. +//------------------------------------------------------------------------------ +void CTriggerCamera::InputSetFOVRate( inputdata_t &inputdata ) +{ + m_fovSpeed = inputdata.value.Float(); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerCamera::Enable( void ) +{ + m_state = USE_ON; + + if ( !m_hPlayer || !m_hPlayer->IsPlayer() ) + { + m_hPlayer = UTIL_GetLocalPlayer(); + } + + if ( !m_hPlayer ) + { + DispatchUpdateTransmitState(); + return; + } + + Assert( m_hPlayer->IsPlayer() ); + CBasePlayer *pPlayer = NULL; + + if ( m_hPlayer->IsPlayer() ) + { + pPlayer = ((CBasePlayer*)m_hPlayer.Get()); + } + else + { + Warning("CTriggerCamera could not find a player!\n"); + return; + } + + // if the player was already under control of a similar trigger, disable the previous trigger. + { + CBaseEntity *pPrevViewControl = pPlayer->GetViewEntity(); + if (pPrevViewControl && pPrevViewControl != pPlayer) + { + CTriggerCamera *pOtherCamera = dynamic_cast(pPrevViewControl); + if ( pOtherCamera ) + { + if ( pOtherCamera == this ) + { + // what the hell do you think you are doing? + Warning("Viewcontrol %s was enabled twice in a row!\n", GetDebugName()); + return; + } + else + { + pOtherCamera->Disable(); + } + } + } + } + + + m_nPlayerButtons = pPlayer->m_nButtons; + + + // Make the player invulnerable while under control of the camera. This will prevent situations where the player dies while under camera control but cannot restart their game due to disabled player inputs. + m_nOldTakeDamage = m_hPlayer->m_takedamage; + m_hPlayer->m_takedamage = DAMAGE_NO; + + if ( HasSpawnFlags( SF_CAMERA_PLAYER_NOT_SOLID ) ) + { + m_hPlayer->AddSolidFlags( FSOLID_NOT_SOLID ); + } + + m_flReturnTime = gpGlobals->curtime + m_flWait; + m_flSpeed = m_initialSpeed; + m_targetSpeed = m_initialSpeed; + + // this pertains to view angles, not translation. + if ( HasSpawnFlags( SF_CAMERA_PLAYER_SNAP_TO ) ) + { + m_bSnapToGoal = true; + } + +#ifdef MAPBASE + if ( HasSpawnFlags( SF_CAMERA_PLAYER_SETFOV ) ) + { + if ( pPlayer ) + { + if ( pPlayer->GetFOVOwner() && (/*FClassnameIs( pPlayer->GetFOVOwner(), "point_viewcontrol_multiplayer" ) ||*/ FClassnameIs( pPlayer->GetFOVOwner(), "point_viewcontrol" )) ) + { + pPlayer->ClearZoomOwner(); + } + pPlayer->SetFOV( this, m_fov, m_fovSpeed ); + } + } +#endif + + if ( HasSpawnFlags(SF_CAMERA_PLAYER_TARGET ) ) + { + m_hTarget = m_hPlayer; + } + else + { + m_hTarget = GetNextTarget(); + } + + // If we don't have a target, ignore the attachment / etc + if ( m_hTarget ) + { + m_iAttachmentIndex = 0; + if ( m_iszTargetAttachment != NULL_STRING ) + { + if ( !m_hTarget->GetBaseAnimating() ) + { + Warning("%s tried to target an attachment (%s) on target %s, which has no model.\n", GetClassname(), STRING(m_iszTargetAttachment), STRING(m_hTarget->GetEntityName()) ); + } + else + { + m_iAttachmentIndex = m_hTarget->GetBaseAnimating()->LookupAttachment( STRING(m_iszTargetAttachment) ); + if ( m_iAttachmentIndex <= 0 ) + { + Warning("%s could not find attachment %s on target %s.\n", GetClassname(), STRING(m_iszTargetAttachment), STRING(m_hTarget->GetEntityName()) ); + } + } + } + } + + if (HasSpawnFlags(SF_CAMERA_PLAYER_TAKECONTROL ) ) + { + ((CBasePlayer*)m_hPlayer.Get())->EnableControl(FALSE); + } + + if ( m_sPath != NULL_STRING ) + { + m_pPath = gEntList.FindEntityByName( NULL, m_sPath, NULL, m_hPlayer ); + } + else + { + m_pPath = NULL; + } + + m_flStopTime = gpGlobals->curtime; + if ( m_pPath ) + { + if ( m_pPath->m_flSpeed != 0 ) + m_targetSpeed = m_pPath->m_flSpeed; + + m_flStopTime += m_pPath->GetDelay(); + } + + + // copy over player information. If we're interpolating from + // the player position, do something more elaborate. +#if HL2_EPISODIC + if (m_bInterpolatePosition) + { + // initialize the values we'll spline between + m_vStartPos = m_hPlayer->EyePosition(); + m_vEndPos = GetAbsOrigin(); + m_flInterpStartTime = gpGlobals->curtime; + UTIL_SetOrigin( this, m_hPlayer->EyePosition() ); + SetLocalAngles( QAngle( m_hPlayer->GetLocalAngles().x, m_hPlayer->GetLocalAngles().y, 0 ) ); + + SetAbsVelocity( vec3_origin ); + } + else +#endif + if (HasSpawnFlags(SF_CAMERA_PLAYER_POSITION ) ) + { + UTIL_SetOrigin( this, m_hPlayer->EyePosition() ); + SetLocalAngles( QAngle( m_hPlayer->GetLocalAngles().x, m_hPlayer->GetLocalAngles().y, 0 ) ); + SetAbsVelocity( m_hPlayer->GetAbsVelocity() ); + } + else + { + SetAbsVelocity( vec3_origin ); + } + + +#ifdef MAPBASE + if (!m_bDontSetPlayerView) +#endif + pPlayer->SetViewEntity( this ); + + // Hide the player's viewmodel + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->AddEffects( EF_NODRAW ); + } + + // Only track if we have a target + if ( m_hTarget ) + { + // follow the player down + SetThink( &CTriggerCamera::FollowTarget ); + SetNextThink( gpGlobals->curtime ); + } +#ifdef MAPBASE + else if (m_pPath && HasSpawnFlags(SF_CAMERA_PLAYER_NEW_BEHAVIOR)) + { + // Move if we have a path + SetThink( &CTriggerCamera::MoveThink ); + SetNextThink( gpGlobals->curtime ); + } +#endif + +#ifdef MAPBASE + m_OnStartFollow.FireOutput( pPlayer, this ); +#endif + + m_moveDistance = 0; + Move(); + + DispatchUpdateTransmitState(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerCamera::Disable( void ) +{ +#ifdef MAPBASE + if ( m_hPlayer ) + { + CBasePlayer *pBasePlayer = (CBasePlayer*)m_hPlayer.Get(); + + if ( pBasePlayer->IsAlive() ) + { + if ( HasSpawnFlags( SF_CAMERA_PLAYER_NOT_SOLID ) ) + { + pBasePlayer->RemoveSolidFlags( FSOLID_NOT_SOLID ); + } + + if (!m_bDontSetPlayerView) + pBasePlayer->SetViewEntity( NULL ); + + pBasePlayer->EnableControl(TRUE); + pBasePlayer->m_Local.m_bDrawViewmodel = true; + } + + if ( HasSpawnFlags( SF_CAMERA_PLAYER_SETFOV ) ) + { + pBasePlayer->SetFOV( this, 0, m_fovSpeed ); + } + + //return the player to previous takedamage state + m_hPlayer->m_takedamage = m_nOldTakeDamage; + } +#else + if ( m_hPlayer && m_hPlayer->IsAlive() ) + { + if ( HasSpawnFlags( SF_CAMERA_PLAYER_NOT_SOLID ) ) + { + m_hPlayer->RemoveSolidFlags( FSOLID_NOT_SOLID ); + } + + ((CBasePlayer*)m_hPlayer.Get())->SetViewEntity( m_hPlayer ); + ((CBasePlayer*)m_hPlayer.Get())->EnableControl(TRUE); + + // Restore the player's viewmodel + if ( ((CBasePlayer*)m_hPlayer.Get())->GetActiveWeapon() ) + { + ((CBasePlayer*)m_hPlayer.Get())->GetActiveWeapon()->RemoveEffects( EF_NODRAW ); + } + //return the player to previous takedamage state + m_hPlayer->m_takedamage = m_nOldTakeDamage; + } +#endif + + m_state = USE_OFF; + m_flReturnTime = gpGlobals->curtime; + SetThink( NULL ); + + m_OnEndFollow.FireOutput(this, this); // dvsents2: what is the best name for this output? + SetLocalAngularVelocity( vec3_angle ); + + DispatchUpdateTransmitState(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerCamera::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + if ( !ShouldToggle( useType, m_state ) ) + return; + + // Toggle state + if ( m_state != USE_OFF ) + { + Disable(); + } + else + { + m_hPlayer = pActivator; + Enable(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerCamera::FollowTarget( ) +{ + if (m_hPlayer == NULL) + return; + + if ( m_hTarget == NULL ) + { + Disable(); + return; + } + + if ( !HasSpawnFlags(SF_CAMERA_PLAYER_INFINITE_WAIT) && (!m_hTarget || m_flReturnTime < gpGlobals->curtime) ) + { + Disable(); + return; + } + + QAngle vecGoal; + if ( m_iAttachmentIndex ) + { + Vector vecOrigin; + m_hTarget->GetBaseAnimating()->GetAttachment( m_iAttachmentIndex, vecOrigin ); + VectorAngles( vecOrigin - GetAbsOrigin(), vecGoal ); + } + else + { + if ( m_hTarget ) + { + VectorAngles( m_hTarget->GetAbsOrigin() - GetAbsOrigin(), vecGoal ); + } + else + { + // Use the viewcontroller's angles + vecGoal = GetAbsAngles(); + } + } + + // Should we just snap to the goal angles? + if ( m_bSnapToGoal ) + { + SetAbsAngles( vecGoal ); + m_bSnapToGoal = false; + } + else + { + // UNDONE: Can't we just use UTIL_AngleDiff here? + QAngle angles = GetLocalAngles(); + + if (angles.y > 360) + angles.y -= 360; + + if (angles.y < 0) + angles.y += 360; + + SetLocalAngles( angles ); + + float dx = vecGoal.x - GetLocalAngles().x; + float dy = vecGoal.y - GetLocalAngles().y; + + if (dx < -180) + dx += 360; + if (dx > 180) + dx = dx - 360; + + if (dy < -180) + dy += 360; + if (dy > 180) + dy = dy - 360; + + QAngle vecAngVel; + vecAngVel.Init( dx * 40 * gpGlobals->frametime, dy * 40 * gpGlobals->frametime, GetLocalAngularVelocity().z ); + SetLocalAngularVelocity(vecAngVel); + } + + if (!HasSpawnFlags(SF_CAMERA_PLAYER_TAKECONTROL)) + { + SetAbsVelocity( GetAbsVelocity() * 0.8 ); + if (GetAbsVelocity().Length( ) < 10.0) + { + SetAbsVelocity( vec3_origin ); + } + } + + SetNextThink( gpGlobals->curtime ); + + Move(); +} + +void CTriggerCamera::StartCameraShot( const char *pszShotType, CBaseEntity *pSceneEntity, CBaseEntity *pActor1, CBaseEntity *pActor2, float duration ) +{ + // called from SceneEntity in response to a CChoreoEvent::CAMERA sent from a VCD. + // talk to vscript, start a camera move + + HSCRIPT hStartCameraShot = NULL; + + // switch to this camera + // Enable(); + + // get script module associated with this ent, lookup function in module + if( m_iszVScripts != NULL_STRING ) + { + hStartCameraShot = m_ScriptScope.LookupFunction( "ScriptStartCameraShot" ); + } + + // call the script function to begin the camera move + if ( hStartCameraShot ) + { + g_pScriptVM->Call( hStartCameraShot, m_ScriptScope, true, NULL, pszShotType, ToHScript(pSceneEntity), ToHScript(pActor1), ToHScript(pActor2), duration ); + g_pScriptVM->ReleaseFunction( hStartCameraShot ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: vscript callback to get the player's fov +//----------------------------------------------------------------------------- +int CTriggerCamera::ScriptGetFov(void) +{ + if (m_hPlayer) + { + CBasePlayer* pBasePlayer = (CBasePlayer*)m_hPlayer.Get(); + int iFOV = pBasePlayer->GetFOV(); + return iFOV; + } + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: vscript callback to slam the player's fov +//----------------------------------------------------------------------------- +void CTriggerCamera::ScriptSetFov(int iFOV, float fovSpeed) +{ +#ifdef MAPBASE + m_fov = iFOV; + m_fovSpeed = fovSpeed; + + if ( m_state == USE_ON && m_hPlayer ) + { +#else + if ( m_hPlayer ) + { + m_fov = iFOV; + m_fovSpeed = fovSpeed; +#endif + + CBasePlayer* pBasePlayer = (CBasePlayer*)m_hPlayer.Get(); + pBasePlayer->SetFOV(this, iFOV, fovSpeed); + } +} + +#ifdef MAPBASE +void CTriggerCamera::MoveThink() +{ + Move(); + SetNextThink( gpGlobals->curtime ); +} +#endif + +void CTriggerCamera::Move() +{ + if ( HasSpawnFlags( SF_CAMERA_PLAYER_INTERRUPT ) ) + { + if ( m_hPlayer ) + { + CBasePlayer *pPlayer = ToBasePlayer( m_hPlayer ); + + if ( pPlayer ) + { + int buttonsChanged = m_nPlayerButtons ^ pPlayer->m_nButtons; + + if ( buttonsChanged && pPlayer->m_nButtons ) + { + Disable(); + return; + } + + m_nPlayerButtons = pPlayer->m_nButtons; + } + } + } + + // In vanilla HL2, the camera is either on a path, or doesn't move. In episodic + // we add the capacity for interpolation to the start point. +#if HL2_EPISODIC + if (m_pPath) +#else + // Not moving on a path, return + if (!m_pPath) + return; +#endif + { + // Subtract movement from the previous frame + m_moveDistance -= m_flSpeed * gpGlobals->frametime; + + // Have we moved enough to reach the target? + if ( m_moveDistance <= 0 ) + { + variant_t emptyVariant; + m_pPath->AcceptInput( "InPass", this, this, emptyVariant, 0 ); + // Time to go to the next target + m_pPath = m_pPath->GetNextTarget(); + + // Set up next corner + if ( !m_pPath ) + { + SetAbsVelocity( vec3_origin ); + } + else + { + if ( m_pPath->m_flSpeed != 0 ) + m_targetSpeed = m_pPath->m_flSpeed; + + m_vecMoveDir = m_pPath->GetLocalOrigin() - GetLocalOrigin(); + m_moveDistance = VectorNormalize( m_vecMoveDir ); + m_flStopTime = gpGlobals->curtime + m_pPath->GetDelay(); + } + } + + if ( m_flStopTime > gpGlobals->curtime ) + m_flSpeed = UTIL_Approach( 0, m_flSpeed, m_deceleration * gpGlobals->frametime ); + else + m_flSpeed = UTIL_Approach( m_targetSpeed, m_flSpeed, m_acceleration * gpGlobals->frametime ); + + float fraction = 2 * gpGlobals->frametime; + SetAbsVelocity( ((m_vecMoveDir * m_flSpeed) * fraction) + (GetAbsVelocity() * (1-fraction)) ); + } +#if HL2_EPISODIC + else if (m_bInterpolatePosition) + { + // get the interpolation parameter [0..1] + float tt = (gpGlobals->curtime - m_flInterpStartTime) / kflPosInterpTime; + if (tt >= 1.0f) + { + // we're there, we're done + UTIL_SetOrigin( this, m_vEndPos ); + SetAbsVelocity( vec3_origin ); + + m_bInterpolatePosition = false; + } + else + { + Assert(tt >= 0); + + Vector nextPos = ( (m_vEndPos - m_vStartPos) * SimpleSpline(tt) ) + m_vStartPos; + // rather than stomping origin, set the velocity so that we get there in the proper time + Vector desiredVel = (nextPos - GetAbsOrigin()) * (1.0f / gpGlobals->frametime); + SetAbsVelocity( desiredVel ); + } + } +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts/stops cd audio tracks +//----------------------------------------------------------------------------- +class CTriggerCDAudio : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerCDAudio, CBaseTrigger ); + + void Spawn( void ); + + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void PlayTrack( void ); + void Touch ( CBaseEntity *pOther ); +}; + +LINK_ENTITY_TO_CLASS( trigger_cdaudio, CTriggerCDAudio ); + + +//----------------------------------------------------------------------------- +// Purpose: Changes tracks or stops CD when player touches +// Input : pOther - The entity that touched us. +//----------------------------------------------------------------------------- +void CTriggerCDAudio::Touch ( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() ) + { + return; + } + + PlayTrack(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerCDAudio::Spawn( void ) +{ + BaseClass::Spawn(); + InitTrigger(); +} + + +void CTriggerCDAudio::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + PlayTrack(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Issues a client command to play a given CD track. Called from +// trigger_cdaudio and target_cdaudio. +// Input : iTrack - Track number to play. +//----------------------------------------------------------------------------- +static void PlayCDTrack( int iTrack ) +{ + edict_t *pClient; + + // manually find the single player. + pClient = engine->PEntityOfEntIndex( 1 ); + + Assert(gpGlobals->maxClients == 1); + + // Can't play if the client is not connected! + if ( !pClient ) + return; + + // UNDONE: Move this to engine sound + if ( iTrack < -1 || iTrack > 30 ) + { + Warning( "TriggerCDAudio - Track %d out of range\n", iTrack ); + return; + } + + if ( iTrack == -1 ) + { + engine->ClientCommand ( pClient, "cd pause\n"); + } + else + { + engine->ClientCommand ( pClient, "cd play %3d\n", iTrack); + } +} + + +// only plays for ONE client, so only use in single play! +void CTriggerCDAudio::PlayTrack( void ) +{ + PlayCDTrack( (int)m_iHealth ); + + SetTouch( NULL ); + UTIL_Remove( this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Measures the proximity to a specified entity of any entities within +// the trigger, provided they are within a given radius of the specified +// entity. The nearest entity distance is output as a number from [0 - 1]. +//----------------------------------------------------------------------------- +class CTriggerProximity : public CBaseTrigger +{ +public: + + DECLARE_CLASS( CTriggerProximity, CBaseTrigger ); + + virtual void Spawn(void); + virtual void Activate(void); + virtual void StartTouch(CBaseEntity *pOther); + virtual void EndTouch(CBaseEntity *pOther); + + void MeasureThink(void); + +protected: + + EHANDLE m_hMeasureTarget; + string_t m_iszMeasureTarget; // The entity from which we measure proximities. + float m_fRadius; // The radius around the measure target that we measure within. + int m_nTouchers; // Number of entities touching us. + + // Outputs + COutputFloat m_NearestEntityDistance; + + DECLARE_DATADESC(); +}; + + +BEGIN_DATADESC( CTriggerProximity ) + + // Functions + DEFINE_FUNCTION(MeasureThink), + + // Keys + DEFINE_KEYFIELD(m_iszMeasureTarget, FIELD_STRING, "measuretarget"), + DEFINE_FIELD( m_hMeasureTarget, FIELD_EHANDLE ), + DEFINE_KEYFIELD(m_fRadius, FIELD_FLOAT, "radius"), + DEFINE_FIELD( m_nTouchers, FIELD_INTEGER ), + + // Outputs + DEFINE_OUTPUT(m_NearestEntityDistance, "NearestEntityDistance"), + +END_DATADESC() + + + +LINK_ENTITY_TO_CLASS(trigger_proximity, CTriggerProximity); +LINK_ENTITY_TO_CLASS(logic_proximity, CPointEntity); + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been handled. +//----------------------------------------------------------------------------- +void CTriggerProximity::Spawn(void) +{ + // Avoid divide by zero in MeasureThink! + if (m_fRadius == 0) + { + m_fRadius = 32; + } + + InitTrigger(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Called after all entities have spawned and after a load game. +// Finds the reference point from which to measure. +//----------------------------------------------------------------------------- +void CTriggerProximity::Activate(void) +{ + BaseClass::Activate(); + m_hMeasureTarget = gEntList.FindEntityByName(NULL, m_iszMeasureTarget ); + + // + // Disable our Touch function if we were given a bad measure target. + // + if ((m_hMeasureTarget == NULL) || (m_hMeasureTarget->edict() == NULL)) + { + Warning( "TriggerProximity - Missing measure target or measure target with no origin!\n"); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Decrements the touch count and cancels the think if the count reaches +// zero. +// Input : pOther - +//----------------------------------------------------------------------------- +void CTriggerProximity::StartTouch(CBaseEntity *pOther) +{ + BaseClass::StartTouch( pOther ); + + if ( PassesTriggerFilters( pOther ) ) + { + m_nTouchers++; + + SetThink( &CTriggerProximity::MeasureThink ); + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Decrements the touch count and cancels the think if the count reaches +// zero. +// Input : pOther - +//----------------------------------------------------------------------------- +void CTriggerProximity::EndTouch(CBaseEntity *pOther) +{ + BaseClass::EndTouch( pOther ); + + if ( PassesTriggerFilters( pOther ) ) + { + m_nTouchers--; + + if ( m_nTouchers == 0 ) + { + SetThink( NULL ); + SetNextThink( TICK_NEVER_THINK ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Think function called every frame as long as we have entities touching +// us that we care about. Finds the closest entity to the measure +// target and outputs the distance as a normalized value from [0..1]. +//----------------------------------------------------------------------------- +void CTriggerProximity::MeasureThink( void ) +{ + if ( ( m_hMeasureTarget == NULL ) || ( m_hMeasureTarget->edict() == NULL ) ) + { + SetThink(NULL); + SetNextThink( TICK_NEVER_THINK ); + return; + } + + // + // Traverse our list of touchers and find the entity that is closest to the + // measure target. + // + float fMinDistance = m_fRadius + 100; + CBaseEntity *pNearestEntity = NULL; + + touchlink_t *root = ( touchlink_t * )GetDataObject( TOUCHLINK ); + if ( root ) + { + touchlink_t *pLink = root->nextLink; + while ( pLink != root ) + { + CBaseEntity *pEntity = pLink->entityTouched; + + // If this is an entity that we care about, check its distance. + if ( ( pEntity != NULL ) && PassesTriggerFilters( pEntity ) ) + { + float flDistance = (pEntity->GetLocalOrigin() - m_hMeasureTarget->GetLocalOrigin()).Length(); + if (flDistance < fMinDistance) + { + fMinDistance = flDistance; + pNearestEntity = pEntity; + } + } + + pLink = pLink->nextLink; + } + } + + // Update our output with the nearest entity distance, normalized to [0..1]. + if ( fMinDistance <= m_fRadius ) + { + fMinDistance /= m_fRadius; + if ( fMinDistance != m_NearestEntityDistance.Get() ) + { + m_NearestEntityDistance.Set( fMinDistance, pNearestEntity, this ); + } + } + + SetNextThink( gpGlobals->curtime ); +} + + +// ################################################################################## +// >> TriggerWind +// +// Blows physics objects in the trigger +// +// ################################################################################## + +#define MAX_WIND_CHANGE 5.0f + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +class CPhysicsWind : public IMotionEvent +{ + DECLARE_SIMPLE_DATADESC(); + +public: + simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) + { + // If we have no windspeed, we're not doing anything + if ( !m_flWindSpeed ) + return IMotionEvent::SIM_NOTHING; + + // Get a cosine modulated noise between 5 and 20 that is object specific + int nNoiseMod = 5+(int)pObject%15; // + + // Turn wind yaw direction into a vector and add noise + QAngle vWindAngle = vec3_angle; + vWindAngle[1] = m_nWindYaw+(30*cos(nNoiseMod * gpGlobals->curtime + nNoiseMod)); + Vector vWind; + AngleVectors(vWindAngle,&vWind); + + // Add lift with noise + vWind.z = 1.1 + (1.0 * sin(nNoiseMod * gpGlobals->curtime + nNoiseMod)); + + linear = 3*vWind*m_flWindSpeed; + angular = vec3_origin; + return IMotionEvent::SIM_GLOBAL_FORCE; + } + + int m_nWindYaw; + float m_flWindSpeed; +}; + +BEGIN_SIMPLE_DATADESC( CPhysicsWind ) + + DEFINE_FIELD( m_nWindYaw, FIELD_INTEGER ), + DEFINE_FIELD( m_flWindSpeed, FIELD_FLOAT ), + +END_DATADESC() + + +extern short g_sModelIndexSmoke; +extern float GetFloorZ(const Vector &origin); +#define WIND_THINK_CONTEXT "WindThinkContext" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CTriggerWind : public CBaseVPhysicsTrigger +{ + DECLARE_CLASS( CTriggerWind, CBaseVPhysicsTrigger ); +public: + DECLARE_DATADESC(); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void OnRestore(); + void UpdateOnRemove(); + bool CreateVPhysics(); + void StartTouch( CBaseEntity *pOther ); + void EndTouch( CBaseEntity *pOther ); + void WindThink( void ); + int DrawDebugTextOverlays( void ); + + // Input handlers + void InputEnable( inputdata_t &inputdata ); + void InputSetSpeed( inputdata_t &inputdata ); + +private: + int m_nSpeedBase; // base line for how hard the wind blows + int m_nSpeedNoise; // noise added to wind speed +/- + int m_nSpeedCurrent;// current wind speed + int m_nSpeedTarget; // wind speed I'm approaching + + int m_nDirBase; // base line for direction the wind blows (yaw) + int m_nDirNoise; // noise added to wind direction + int m_nDirCurrent; // the current wind direction + int m_nDirTarget; // wind direction I'm approaching + + int m_nHoldBase; // base line for how long to wait before changing wind + int m_nHoldNoise; // noise added to how long to wait before changing wind + + bool m_bSwitch; // when does wind change + + IPhysicsMotionController* m_pWindController; + CPhysicsWind m_WindCallback; + +}; + +LINK_ENTITY_TO_CLASS( trigger_wind, CTriggerWind ); + +BEGIN_DATADESC( CTriggerWind ) + + DEFINE_FIELD( m_nSpeedCurrent, FIELD_INTEGER), + DEFINE_FIELD( m_nSpeedTarget, FIELD_INTEGER), + DEFINE_FIELD( m_nDirBase, FIELD_INTEGER), + DEFINE_FIELD( m_nDirCurrent, FIELD_INTEGER), + DEFINE_FIELD( m_nDirTarget, FIELD_INTEGER), + DEFINE_FIELD( m_bSwitch, FIELD_BOOLEAN), + + DEFINE_FIELD( m_nSpeedBase, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_nSpeedNoise, FIELD_INTEGER, "SpeedNoise"), + DEFINE_KEYFIELD( m_nDirNoise, FIELD_INTEGER, "DirectionNoise"), + DEFINE_KEYFIELD( m_nHoldBase, FIELD_INTEGER, "HoldTime"), + DEFINE_KEYFIELD( m_nHoldNoise, FIELD_INTEGER, "HoldNoise"), + + DEFINE_PHYSPTR( m_pWindController ), + DEFINE_EMBEDDED( m_WindCallback ), + + DEFINE_FUNCTION( WindThink ), + + DEFINE_INPUTFUNC( FIELD_INTEGER, "SetSpeed", InputSetSpeed ), + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerWind::Spawn( void ) +{ + m_bSwitch = true; + m_nDirBase = GetLocalAngles().y; + + BaseClass::Spawn(); + + m_nSpeedCurrent = m_nSpeedBase; + m_nDirCurrent = m_nDirBase; + + SetContextThink( &CTriggerWind::WindThink, gpGlobals->curtime, WIND_THINK_CONTEXT ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CTriggerWind::KeyValue( const char *szKeyName, const char *szValue ) +{ + // Done here to avoid collision with CBaseEntity's speed key + if ( FStrEq(szKeyName, "Speed") ) + { + m_nSpeedBase = atoi( szValue ); + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + +//------------------------------------------------------------------------------ +// Create VPhysics +//------------------------------------------------------------------------------ +bool CTriggerWind::CreateVPhysics() +{ + BaseClass::CreateVPhysics(); + + m_pWindController = physenv->CreateMotionController( &m_WindCallback ); + return true; +} + +//------------------------------------------------------------------------------ +// Cleanup +//------------------------------------------------------------------------------ +void CTriggerWind::UpdateOnRemove() +{ + if ( m_pWindController ) + { + physenv->DestroyMotionController( m_pWindController ); + m_pWindController = NULL; + } + + BaseClass::UpdateOnRemove(); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerWind::OnRestore() +{ + BaseClass::OnRestore(); + if ( m_pWindController ) + { + m_pWindController->SetEventHandler( &m_WindCallback ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerWind::StartTouch(CBaseEntity *pOther) +{ + if ( !PassesTriggerFilters(pOther) ) + return; + if ( pOther->IsPlayer() ) + return; + + IPhysicsObject *pPhys = pOther->VPhysicsGetObject(); + if ( pPhys) + { + m_pWindController->AttachObject( pPhys, false ); + pPhys->Wake(); + } +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerWind::EndTouch(CBaseEntity *pOther) +{ + if ( !PassesTriggerFilters(pOther) ) + return; + if ( pOther->IsPlayer() ) + return; + + IPhysicsObject *pPhys = pOther->VPhysicsGetObject(); + if ( pPhys && m_pWindController ) + { + m_pWindController->DetachObject( pPhys ); + } +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerWind::InputEnable( inputdata_t &inputdata ) +{ + BaseClass::InputEnable( inputdata ); + SetContextThink( &CTriggerWind::WindThink, gpGlobals->curtime + 0.1f, WIND_THINK_CONTEXT ); +} + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerWind::WindThink( void ) +{ + // By default... + SetContextThink( &CTriggerWind::WindThink, gpGlobals->curtime + 0.1, WIND_THINK_CONTEXT ); + + // Is it time to change the wind? + if (m_bSwitch) + { + m_bSwitch = false; + + // Set new target direction and speed + m_nSpeedTarget = m_nSpeedBase + random->RandomInt( -m_nSpeedNoise, m_nSpeedNoise ); + m_nDirTarget = UTIL_AngleMod( m_nDirBase + random->RandomInt(-m_nDirNoise, m_nDirNoise) ); + } + else + { + bool bDone = true; + // either ramp up, or sleep till change + if (abs(m_nSpeedTarget - m_nSpeedCurrent) > MAX_WIND_CHANGE) + { + m_nSpeedCurrent += (m_nSpeedTarget > m_nSpeedCurrent) ? MAX_WIND_CHANGE : -MAX_WIND_CHANGE; + bDone = false; + } + + if (abs(m_nDirTarget - m_nDirCurrent) > MAX_WIND_CHANGE) + { + + m_nDirCurrent = UTIL_ApproachAngle( m_nDirTarget, m_nDirCurrent, MAX_WIND_CHANGE ); + bDone = false; + } + + if (bDone) + { + m_nSpeedCurrent = m_nSpeedTarget; + SetContextThink( &CTriggerWind::WindThink, m_nHoldBase + random->RandomFloat(-m_nHoldNoise,m_nHoldNoise), WIND_THINK_CONTEXT ); + m_bSwitch = true; + } + } + + // If we're starting to blow, where we weren't before, wake up all our objects + if ( m_nSpeedCurrent ) + { + m_pWindController->WakeObjects(); + } + + // store the wind data in the controller callback + m_WindCallback.m_nWindYaw = m_nDirCurrent; + if ( m_bDisabled ) + { + m_WindCallback.m_flWindSpeed = 0; + } + else + { + m_WindCallback.m_flWindSpeed = m_nSpeedCurrent; + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerWind::InputSetSpeed( inputdata_t &inputdata ) +{ + // Set new speed and mark to switch + m_nSpeedBase = inputdata.value.Int(); + m_bSwitch = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CTriggerWind::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + // -------------- + // Print Target + // -------------- + char tempstr[255]; + Q_snprintf(tempstr,sizeof(tempstr),"Dir: %i (%i)",m_nDirCurrent,m_nDirTarget); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf(tempstr,sizeof(tempstr),"Speed: %i (%i)",m_nSpeedCurrent,m_nSpeedTarget); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + + +// ################################################################################## +// >> TriggerImpact +// +// Blows physics objects in the trigger +// +// ################################################################################## +#define TRIGGERIMPACT_VIEWKICK_SCALE 0.1 + +class CTriggerImpact : public CTriggerMultiple +{ + DECLARE_CLASS( CTriggerImpact, CTriggerMultiple ); +public: + DECLARE_DATADESC(); + + float m_flMagnitude; + float m_flNoise; + float m_flViewkick; + + void Spawn( void ); + void StartTouch( CBaseEntity *pOther ); + + // Inputs + void InputSetMagnitude( inputdata_t &inputdata ); + void InputImpact( inputdata_t &inputdata ); + + // Outputs + COutputVector m_pOutputForce; // Output force in case anyone else wants to use it + + // Debug + int DrawDebugTextOverlays(void); +}; + +LINK_ENTITY_TO_CLASS( trigger_impact, CTriggerImpact ); + +BEGIN_DATADESC( CTriggerImpact ) + + DEFINE_KEYFIELD( m_flMagnitude, FIELD_FLOAT, "Magnitude"), + DEFINE_KEYFIELD( m_flNoise, FIELD_FLOAT, "Noise"), + DEFINE_KEYFIELD( m_flViewkick, FIELD_FLOAT, "Viewkick"), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Impact", InputImpact ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMagnitude", InputSetMagnitude ), + + // Outputs + DEFINE_OUTPUT(m_pOutputForce, "ImpactForce"), + + // Function Pointers + DEFINE_FUNCTION( Disable ), + + +END_DATADESC() + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerImpact::Spawn( void ) +{ + // Clamp date in case user made an error + m_flNoise = clamp(m_flNoise,0.f,1.f); + m_flViewkick = clamp(m_flViewkick,0.f,1.f); + + // Always start disabled + m_bDisabled = true; + BaseClass::Spawn(); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerImpact::InputImpact( inputdata_t &inputdata ) +{ + // Output the force vector in case anyone else wants to use it + Vector vDir; + AngleVectors( GetLocalAngles(),&vDir ); + m_pOutputForce.Set( m_flMagnitude * vDir, inputdata.pActivator, inputdata.pCaller); + + // Enable long enough to throw objects inside me + Enable(); + SetNextThink( gpGlobals->curtime + 0.1f ); + SetThink(&CTriggerImpact::Disable); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerImpact::StartTouch(CBaseEntity *pOther) +{ + //If the entity is valid and has physics, hit it + if ( ( pOther != NULL ) && ( pOther->VPhysicsGetObject() != NULL ) ) + { + Vector vDir; + AngleVectors( GetLocalAngles(),&vDir ); + vDir += RandomVector(-m_flNoise,m_flNoise); + pOther->VPhysicsGetObject()->ApplyForceCenter( m_flMagnitude * vDir ); + } + + // If the player, so a view kick + if (pOther->IsPlayer() && fabs(m_flMagnitude)>0 ) + { + Vector vDir; + AngleVectors( GetLocalAngles(),&vDir ); + + float flPunch = -m_flViewkick*m_flMagnitude*TRIGGERIMPACT_VIEWKICK_SCALE; + pOther->ViewPunch( QAngle( vDir.y * flPunch, 0, vDir.x * flPunch ) ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CTriggerImpact::InputSetMagnitude( inputdata_t &inputdata ) +{ + m_flMagnitude = inputdata.value.Float(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CTriggerImpact::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[255]; + Q_snprintf(tempstr,sizeof(tempstr),"Magnitude: %3.2f",m_flMagnitude); + EntityText(text_offset,tempstr,0); + text_offset++; + } + return text_offset; +} + +//----------------------------------------------------------------------------- +// Purpose: Disables auto movement on players that touch it +//----------------------------------------------------------------------------- + +const int SF_TRIGGER_MOVE_AUTODISABLE = 0x80; // disable auto movement +const int SF_TRIGGER_AUTO_DUCK = 0x800; // Duck automatically +#ifdef MAPBASE +const int SF_TRIGGER_AUTO_WALK = 0x1000; +const int SF_TRIGGER_DISABLE_JUMP = 0x2000; +#endif + +class CTriggerPlayerMovement : public CBaseTrigger +{ + DECLARE_CLASS( CTriggerPlayerMovement, CBaseTrigger ); +public: + + void Spawn( void ); + void StartTouch( CBaseEntity *pOther ); + void EndTouch( CBaseEntity *pOther ); + + DECLARE_DATADESC(); + +}; + +BEGIN_DATADESC( CTriggerPlayerMovement ) + +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( trigger_playermovement, CTriggerPlayerMovement ); + + +//----------------------------------------------------------------------------- +// Purpose: Called when spawning, after keyvalues have been handled. +//----------------------------------------------------------------------------- +void CTriggerPlayerMovement::Spawn( void ) +{ + if( HasSpawnFlags( SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS ) ) + { + // @Note (toml 01-07-04): fix up spawn flag collision coding error. Remove at some point once all maps fixed up please! + DevMsg("*** trigger_playermovement using obsolete spawnflag. Remove and reset with new value for \"Disable auto player movement\"\n" ); + RemoveSpawnFlags(SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS); + AddSpawnFlags(SF_TRIGGER_MOVE_AUTODISABLE); + } + BaseClass::Spawn(); + + InitTrigger(); +} + + +// UNDONE: This will not support a player touching more than one of these +// UNDONE: Do we care? If so, ref count automovement in the player? +void CTriggerPlayerMovement::StartTouch( CBaseEntity *pOther ) +{ + if (!PassesTriggerFilters(pOther)) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pOther ); + + if ( !pPlayer ) + return; + + if ( HasSpawnFlags( SF_TRIGGER_AUTO_DUCK ) ) + { + pPlayer->ForceButtons( IN_DUCK ); + } + +#ifdef MAPBASE + if ( HasSpawnFlags( SF_TRIGGER_AUTO_WALK ) ) + { + pPlayer->ForceButtons( IN_WALK ); + } + + if ( HasSpawnFlags( SF_TRIGGER_DISABLE_JUMP ) ) + { + pPlayer->DisableButtons( IN_JUMP ); + } +#endif + + // UNDONE: Currently this is the only operation this trigger can do + if ( HasSpawnFlags(SF_TRIGGER_MOVE_AUTODISABLE) ) + { + pPlayer->m_Local.m_bAllowAutoMovement = false; + } +} + +void CTriggerPlayerMovement::EndTouch( CBaseEntity *pOther ) +{ + if (!PassesTriggerFilters(pOther)) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pOther ); + + if ( !pPlayer ) + return; + + if ( HasSpawnFlags( SF_TRIGGER_AUTO_DUCK ) ) + { + pPlayer->UnforceButtons( IN_DUCK ); + } + +#ifdef MAPBASE + if ( HasSpawnFlags( SF_TRIGGER_AUTO_WALK ) ) + { + pPlayer->UnforceButtons( IN_WALK ); + } + + if ( HasSpawnFlags( SF_TRIGGER_DISABLE_JUMP ) ) + { + pPlayer->EnableButtons( IN_JUMP ); + } +#endif + + if ( HasSpawnFlags(SF_TRIGGER_MOVE_AUTODISABLE) ) + { + pPlayer->m_Local.m_bAllowAutoMovement = true; + } +} + +//------------------------------------------------------------------------------ +// Base VPhysics trigger implementation +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +// Save/load +//------------------------------------------------------------------------------ +BEGIN_DATADESC( CBaseVPhysicsTrigger ) + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_KEYFIELD( m_iFilterName, FIELD_STRING, "filtername" ), + DEFINE_FIELD( m_hFilter, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), +END_DATADESC() + +//------------------------------------------------------------------------------ +// Spawn +//------------------------------------------------------------------------------ +void CBaseVPhysicsTrigger::Spawn() +{ + Precache(); + + SetSolid( SOLID_VPHYSICS ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + // NOTE: Don't make yourself FSOLID_TRIGGER here or you'll get game + // collisions AND vphysics collisions. You don't want any game collisions + // so just use FSOLID_NOT_SOLID + + SetMoveType( MOVETYPE_NONE ); + SetModel( STRING( GetModelName() ) ); // set size and link into world + if ( showtriggers.GetInt() == 0 ) + { + AddEffects( EF_NODRAW ); + } + + CreateVPhysics(); +} + +//------------------------------------------------------------------------------ +// Create VPhysics +//------------------------------------------------------------------------------ +bool CBaseVPhysicsTrigger::CreateVPhysics() +{ + IPhysicsObject *pPhysics; + if ( !HasSpawnFlags( SF_VPHYSICS_MOTION_MOVEABLE ) ) + { + pPhysics = VPhysicsInitStatic(); + } + else + { + pPhysics = VPhysicsInitShadow( false, false ); + } + + pPhysics->BecomeTrigger(); + return true; +} + +//------------------------------------------------------------------------------ +// Cleanup +//------------------------------------------------------------------------------ +void CBaseVPhysicsTrigger::UpdateOnRemove() +{ + if ( VPhysicsGetObject()) + { + VPhysicsGetObject()->RemoveTrigger(); + } + + BaseClass::UpdateOnRemove(); +} + +//------------------------------------------------------------------------------ +// Activate +//------------------------------------------------------------------------------ +void CBaseVPhysicsTrigger::Activate( void ) +{ + // Get a handle to my filter entity if there is one + if (m_iFilterName != NULL_STRING) + { + m_hFilter = dynamic_cast(gEntList.FindEntityByName( NULL, m_iFilterName )); + } + + BaseClass::Activate(); +} + +//------------------------------------------------------------------------------ +// Inputs +//------------------------------------------------------------------------------ +void CBaseVPhysicsTrigger::InputToggle( inputdata_t &inputdata ) +{ + if ( m_bDisabled ) + { + InputEnable( inputdata ); + } + else + { + InputDisable( inputdata ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseVPhysicsTrigger::InputEnable( inputdata_t &inputdata ) +{ + if ( m_bDisabled ) + { + m_bDisabled = false; + if ( VPhysicsGetObject()) + { + VPhysicsGetObject()->EnableCollisions( true ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseVPhysicsTrigger::InputDisable( inputdata_t &inputdata ) +{ + if ( !m_bDisabled ) + { + m_bDisabled = true; + if ( VPhysicsGetObject()) + { + VPhysicsGetObject()->EnableCollisions( false ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseVPhysicsTrigger::StartTouch( CBaseEntity *pOther ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseVPhysicsTrigger::EndTouch( CBaseEntity *pOther ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseVPhysicsTrigger::PassesTriggerFilters( CBaseEntity *pOther ) +{ +#ifdef MAPBASE + if ( !pOther->VPhysicsGetObject() ) +#else + if ( pOther->GetMoveType() != MOVETYPE_VPHYSICS && !pOther->IsPlayer() ) +#endif + return false; + +#ifdef MAPBASE + // Copied and pasted code from CBaseTrigger::PassesTriggerFilters(). + if ( HasSpawnFlags(SF_TRIGGER_ALLOW_ALL) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_CLIENTS) && (pOther->GetFlags() & FL_CLIENT)) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_NPCS) && (pOther->GetFlags() & FL_NPC)) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_PUSHABLES) && FClassnameIs(pOther, "func_pushable")) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_PHYSICS) && pOther->GetMoveType() == MOVETYPE_VPHYSICS) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_ITEMS) && pOther->GetMoveType() == MOVETYPE_FLYGRAVITY) +#if defined( HL2_EPISODIC ) || defined( TF_DLL ) + || + ( HasSpawnFlags(SF_TRIG_TOUCH_DEBRIS) && + (pOther->GetCollisionGroup() == COLLISION_GROUP_DEBRIS || + pOther->GetCollisionGroup() == COLLISION_GROUP_DEBRIS_TRIGGER || + pOther->GetCollisionGroup() == COLLISION_GROUP_INTERACTIVE_DEBRIS) + ) +#endif + ) + { + if ( pOther->GetFlags() & FL_NPC ) + { + CAI_BaseNPC *pNPC = pOther->MyNPCPointer(); + + if ( HasSpawnFlags( SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS ) ) + { + if ( !pNPC || !pNPC->IsPlayerAlly() ) + { + return false; + } + } + + if ( HasSpawnFlags( SF_TRIGGER_ONLY_NPCS_IN_VEHICLES ) ) + { + if ( !pNPC || !pNPC->IsInAVehicle() ) + return false; + } + } + + bool bOtherIsPlayer = pOther->IsPlayer(); + + if ( bOtherIsPlayer ) + { + CBasePlayer *pPlayer = (CBasePlayer*)pOther; + if ( !pPlayer->IsAlive() ) + return false; + + if ( HasSpawnFlags(SF_TRIGGER_ONLY_CLIENTS_IN_VEHICLES) ) + { + if ( !pPlayer->IsInAVehicle() ) + return false; + + // Make sure we're also not exiting the vehicle at the moment + IServerVehicle *pVehicleServer = pPlayer->GetVehicle(); + if ( pVehicleServer == NULL ) + return false; + + if ( pVehicleServer->IsPassengerExiting() ) + return false; + } + + if ( HasSpawnFlags(SF_TRIGGER_ONLY_CLIENTS_OUT_OF_VEHICLES) ) + { + if ( pPlayer->IsInAVehicle() ) + return false; + } + + if ( HasSpawnFlags( SF_TRIGGER_DISALLOW_BOTS ) ) + { + if ( pPlayer->IsFakeClient() ) + return false; + } + } + + CBaseFilter *pFilter = m_hFilter.Get(); + return (!pFilter) ? true : pFilter->PassesFilter( this, pOther ); + } +#else + // First test spawn flag filters + if ( HasSpawnFlags(SF_TRIGGER_ALLOW_ALL) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_CLIENTS) && (pOther->GetFlags() & FL_CLIENT)) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_NPCS) && (pOther->GetFlags() & FL_NPC)) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_PUSHABLES) && FClassnameIs(pOther, "func_pushable")) || + (HasSpawnFlags(SF_TRIGGER_ALLOW_PHYSICS) && pOther->GetMoveType() == MOVETYPE_VPHYSICS)) + { + bool bOtherIsPlayer = pOther->IsPlayer(); + if( HasSpawnFlags(SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS) && !bOtherIsPlayer ) + { + CAI_BaseNPC *pNPC = pOther->MyNPCPointer(); + + if( !pNPC || !pNPC->IsPlayerAlly() ) + { + return false; + } + } + + if ( HasSpawnFlags(SF_TRIGGER_ONLY_CLIENTS_IN_VEHICLES) && bOtherIsPlayer ) + { + if ( !((CBasePlayer*)pOther)->IsInAVehicle() ) + return false; + } + + if ( HasSpawnFlags(SF_TRIGGER_ONLY_CLIENTS_OUT_OF_VEHICLES) && bOtherIsPlayer ) + { + if ( ((CBasePlayer*)pOther)->IsInAVehicle() ) + return false; + } + + CBaseFilter *pFilter = m_hFilter.Get(); + return (!pFilter) ? true : pFilter->PassesFilter( this, pOther ); + } +#endif + return false; +} + +//===================================================================================================================== +//----------------------------------------------------------------------------- +// Purpose: VPhysics trigger that changes the motion of vphysics objects that touch it +//----------------------------------------------------------------------------- +class CTriggerVPhysicsMotion : public CBaseVPhysicsTrigger, public IMotionEvent +{ + DECLARE_CLASS( CTriggerVPhysicsMotion, CBaseVPhysicsTrigger ); + +public: + void Spawn(); + void Precache(); + virtual void UpdateOnRemove(); + bool CreateVPhysics(); + void OnRestore(); + + // UNDONE: Pass trigger event in or change Start/EndTouch. Add ITriggerVPhysics perhaps? + // BUGBUG: If a player touches two of these, his movement will screw up. + // BUGBUG: If a player uses crouch/uncrouch it will generate touch events and clear the motioncontroller flag + void StartTouch( CBaseEntity *pOther ); + void EndTouch( CBaseEntity *pOther ); + + void InputSetVelocityLimitTime( inputdata_t &inputdata ); + + float LinearLimit(); + + inline bool HasGravityScale() { return m_gravityScale != 1.0 ? true : false; } + inline bool HasAirDensity() { return m_addAirDensity != 0 ? true : false; } + inline bool HasLinearLimit() { return LinearLimit() != 0.0f; } + inline bool HasLinearScale() { return m_linearScale != 1.0 ? true : false; } + inline bool HasAngularLimit() { return m_angularLimit != 0 ? true : false; } + inline bool HasAngularScale() { return m_angularScale != 1.0 ? true : false; } + inline bool HasLinearForce() { return m_linearForce != 0.0 ? true : false; } + + DECLARE_DATADESC(); + + virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); + +private: + IPhysicsMotionController *m_pController; + +#ifndef _XBOX + EntityParticleTrailInfo_t m_ParticleTrail; +#endif //!_XBOX + + float m_gravityScale; + float m_addAirDensity; + float m_linearLimit; + float m_linearLimitDelta; + float m_linearLimitTime; + float m_linearLimitStart; + float m_linearLimitStartTime; + float m_linearScale; + float m_angularLimit; + float m_angularScale; + float m_linearForce; + QAngle m_linearForceAngles; +}; + + +//------------------------------------------------------------------------------ +// Save/load +//------------------------------------------------------------------------------ +BEGIN_DATADESC( CTriggerVPhysicsMotion ) + DEFINE_PHYSPTR( m_pController ), +#ifndef _XBOX + DEFINE_EMBEDDED( m_ParticleTrail ), +#endif //!_XBOX + DEFINE_INPUT( m_gravityScale, FIELD_FLOAT, "SetGravityScale" ), + DEFINE_INPUT( m_addAirDensity, FIELD_FLOAT, "SetAdditionalAirDensity" ), + DEFINE_INPUT( m_linearLimit, FIELD_FLOAT, "SetVelocityLimit" ), + DEFINE_INPUT( m_linearLimitDelta, FIELD_FLOAT, "SetVelocityLimitDelta" ), + DEFINE_FIELD( m_linearLimitTime, FIELD_FLOAT ), + DEFINE_FIELD( m_linearLimitStart, FIELD_TIME ), + DEFINE_FIELD( m_linearLimitStartTime, FIELD_TIME ), + DEFINE_INPUT( m_linearScale, FIELD_FLOAT, "SetVelocityScale" ), + DEFINE_INPUT( m_angularLimit, FIELD_FLOAT, "SetAngVelocityLimit" ), + DEFINE_INPUT( m_angularScale, FIELD_FLOAT, "SetAngVelocityScale" ), + DEFINE_INPUT( m_linearForce, FIELD_FLOAT, "SetLinearForce" ), + DEFINE_INPUT( m_linearForceAngles, FIELD_VECTOR, "SetLinearForceAngles" ), + + DEFINE_INPUTFUNC( FIELD_STRING, "SetVelocityLimitTime", InputSetVelocityLimitTime ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( trigger_vphysics_motion, CTriggerVPhysicsMotion ); + + +//------------------------------------------------------------------------------ +// Spawn +//------------------------------------------------------------------------------ +void CTriggerVPhysicsMotion::Spawn() +{ + Precache(); + + BaseClass::Spawn(); +} + +//------------------------------------------------------------------------------ +// Precache +//------------------------------------------------------------------------------ +void CTriggerVPhysicsMotion::Precache() +{ +#ifndef _XBOX + if ( m_ParticleTrail.m_strMaterialName != NULL_STRING ) + { + PrecacheMaterial( STRING(m_ParticleTrail.m_strMaterialName) ); + } +#endif //!_XBOX +} + +//------------------------------------------------------------------------------ +// Create VPhysics +//------------------------------------------------------------------------------ +float CTriggerVPhysicsMotion::LinearLimit() +{ + if ( m_linearLimitTime == 0.0f ) + return m_linearLimit; + + float dt = gpGlobals->curtime - m_linearLimitStartTime; + if ( dt >= m_linearLimitTime ) + { + m_linearLimitTime = 0.0; + return m_linearLimit; + } + + dt /= m_linearLimitTime; + float flLimit = RemapVal( dt, 0.0f, 1.0f, m_linearLimitStart, m_linearLimit ); + return flLimit; +} + + +//------------------------------------------------------------------------------ +// Create VPhysics +//------------------------------------------------------------------------------ +bool CTriggerVPhysicsMotion::CreateVPhysics() +{ + m_pController = physenv->CreateMotionController( this ); + BaseClass::CreateVPhysics(); + + return true; +} + + +//------------------------------------------------------------------------------ +// Cleanup +//------------------------------------------------------------------------------ +void CTriggerVPhysicsMotion::UpdateOnRemove() +{ + if ( m_pController ) + { + physenv->DestroyMotionController( m_pController ); + m_pController = NULL; + } + + BaseClass::UpdateOnRemove(); +} + + +//------------------------------------------------------------------------------ +// Restore +//------------------------------------------------------------------------------ +void CTriggerVPhysicsMotion::OnRestore() +{ + BaseClass::OnRestore(); + if ( m_pController ) + { + m_pController->SetEventHandler( this ); + } +} + +//------------------------------------------------------------------------------ +// Start/End Touch +//------------------------------------------------------------------------------ +// UNDONE: Pass trigger event in or change Start/EndTouch. Add ITriggerVPhysics perhaps? +// BUGBUG: If a player touches two of these, his movement will screw up. +// BUGBUG: If a player uses crouch/uncrouch it will generate touch events and clear the motioncontroller flag +void CTriggerVPhysicsMotion::StartTouch( CBaseEntity *pOther ) +{ + BaseClass::StartTouch( pOther ); + + if ( !PassesTriggerFilters(pOther) ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pOther ); + if ( pPlayer ) + { + pPlayer->SetPhysicsFlag( PFLAG_VPHYSICS_MOTIONCONTROLLER, true ); + pPlayer->m_Local.m_bSlowMovement = true; + } + + triggerevent_t event; + PhysGetTriggerEvent( &event, this ); + if ( event.pObject ) + { + // these all get done again on save/load, so check + m_pController->AttachObject( event.pObject, true ); + } + + // Don't show these particles on the XBox +#ifndef _XBOX + if ( m_ParticleTrail.m_strMaterialName != NULL_STRING ) + { + CEntityParticleTrail::Create( pOther, m_ParticleTrail, this ); + } +#endif + + if ( pOther->GetBaseAnimating() && pOther->GetBaseAnimating()->IsRagdoll() ) + { + CRagdollBoogie::IncrementSuppressionCount( pOther ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerVPhysicsMotion::EndTouch( CBaseEntity *pOther ) +{ + BaseClass::EndTouch( pOther ); + + if ( !PassesTriggerFilters(pOther) ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pOther ); + if ( pPlayer ) + { + pPlayer->SetPhysicsFlag( PFLAG_VPHYSICS_MOTIONCONTROLLER, false ); + pPlayer->m_Local.m_bSlowMovement = false; + } + triggerevent_t event; + PhysGetTriggerEvent( &event, this ); + if ( event.pObject && m_pController ) + { + m_pController->DetachObject( event.pObject ); + } + +#ifndef _XBOX + if ( m_ParticleTrail.m_strMaterialName != NULL_STRING ) + { + CEntityParticleTrail::Destroy( pOther, m_ParticleTrail ); + } +#endif //!_XBOX + + if ( pOther->GetBaseAnimating() && pOther->GetBaseAnimating()->IsRagdoll() ) + { + CRagdollBoogie::DecrementSuppressionCount( pOther ); + } +} + + +//------------------------------------------------------------------------------ +// Inputs +//------------------------------------------------------------------------------ +void CTriggerVPhysicsMotion::InputSetVelocityLimitTime( inputdata_t &inputdata ) +{ + m_linearLimitStart = LinearLimit(); + m_linearLimitStartTime = gpGlobals->curtime; + + float args[2]; + UTIL_StringToFloatArray( args, 2, inputdata.value.String() ); + m_linearLimit = args[0]; + m_linearLimitTime = args[1]; +} + +//------------------------------------------------------------------------------ +// Apply the forces to the entity +//------------------------------------------------------------------------------ +IMotionEvent::simresult_e CTriggerVPhysicsMotion::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + if ( m_bDisabled ) + return SIM_NOTHING; + + linear.Init(); + angular.Init(); + + if ( HasGravityScale() ) + { + // assume object already has 1.0 gravities applied to it, so apply the additional amount + linear.z -= (m_gravityScale-1) * GetCurrentGravity(); + } + + if ( HasLinearForce() ) + { + Vector vecForceDir; + AngleVectors( m_linearForceAngles, &vecForceDir ); + VectorMA( linear, m_linearForce, vecForceDir, linear ); + } + + if ( HasAirDensity() || HasLinearLimit() || HasLinearScale() || HasAngularLimit() || HasAngularScale() ) + { + Vector vel; + AngularImpulse angVel; + pObject->GetVelocity( &vel, &angVel ); + vel += linear * deltaTime; // account for gravity scale + + Vector unitVel = vel; + Vector unitAngVel = angVel; + + float speed = VectorNormalize( unitVel ); + float angSpeed = VectorNormalize( unitAngVel ); + + float speedScale = 0.0; + float angSpeedScale = 0.0; + + if ( HasAirDensity() ) + { + float linearDrag = -0.5 * m_addAirDensity * pObject->CalculateLinearDrag( unitVel ) * deltaTime; + if ( linearDrag < -1 ) + { + linearDrag = -1; + } + speedScale += linearDrag / deltaTime; + float angDrag = -0.5 * m_addAirDensity * pObject->CalculateAngularDrag( unitAngVel ) * deltaTime; + if ( angDrag < -1 ) + { + angDrag = -1; + } + angSpeedScale += angDrag / deltaTime; + } + + if ( HasLinearLimit() && speed > m_linearLimit ) + { + float flDeltaVel = (LinearLimit() - speed) / deltaTime; + if ( m_linearLimitDelta != 0.0f ) + { + float flMaxDeltaVel = -m_linearLimitDelta / deltaTime; + if ( flDeltaVel < flMaxDeltaVel ) + { + flDeltaVel = flMaxDeltaVel; + } + } + VectorMA( linear, flDeltaVel, unitVel, linear ); + } + if ( HasAngularLimit() && angSpeed > m_angularLimit ) + { + angular += ((m_angularLimit - angSpeed)/deltaTime) * unitAngVel; + } + if ( HasLinearScale() ) + { + speedScale = ( (speedScale+1) * m_linearScale ) - 1; + } + if ( HasAngularScale() ) + { + angSpeedScale = ( (angSpeedScale+1) * m_angularScale ) - 1; + } + linear += vel * speedScale; + angular += angVel * angSpeedScale; + } + + return SIM_GLOBAL_ACCELERATION; +} + +class CServerRagdollTrigger : public CBaseTrigger +{ + DECLARE_CLASS( CServerRagdollTrigger, CBaseTrigger ); + +public: + + virtual void StartTouch( CBaseEntity *pOther ); + virtual void EndTouch( CBaseEntity *pOther ); + virtual void Spawn( void ); + +}; + +LINK_ENTITY_TO_CLASS( trigger_serverragdoll, CServerRagdollTrigger ); + +void CServerRagdollTrigger::Spawn( void ) +{ + BaseClass::Spawn(); + +#ifdef MAPBASE + // This didn't use PassesTriggerFilters() before, so a trigger_serverragdoll could work regardless of flags. + // Because of this, using trigger filters now will break existing trigger_serverragdolls that functioned without the right flags ticked. + // NPCs are going to be using this in almost all circumstances, so SF_TRIGGER_ALLOW_NPCS is pretty much the main flag of concern. + AddSpawnFlags(SF_TRIGGER_ALLOW_NPCS); +#endif + + InitTrigger(); +} + +void CServerRagdollTrigger::StartTouch(CBaseEntity *pOther) +{ + BaseClass::StartTouch( pOther ); + + if ( pOther->IsPlayer() ) + return; + +#ifdef MAPBASE + // This means base class didn't accept it (trigger filters) + if (m_hTouchingEntities.Find(pOther) == m_hTouchingEntities.InvalidIndex()) + return; +#endif + + CBaseCombatCharacter *pCombatChar = pOther->MyCombatCharacterPointer(); + + if ( pCombatChar ) + { +#ifdef MAPBASE + // The mapper or some other force might've changed it themselves. + // Pretend it never touched us... + if (pCombatChar->m_bForceServerRagdoll == true) + { + BaseClass::EndTouch(pOther); + return; + } +#endif + pCombatChar->m_bForceServerRagdoll = true; + } +} + +void CServerRagdollTrigger::EndTouch(CBaseEntity *pOther) +{ + BaseClass::EndTouch( pOther ); + + if ( pOther->IsPlayer() ) + return; + + CBaseCombatCharacter *pCombatChar = pOther->MyCombatCharacterPointer(); + + if ( pCombatChar ) + { + pCombatChar->m_bForceServerRagdoll = false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: A trigger that adds impulse to touching entities +//----------------------------------------------------------------------------- +class CTriggerApplyImpulse : public CBaseTrigger +{ +public: + DECLARE_CLASS( CTriggerApplyImpulse, CBaseTrigger ); + DECLARE_DATADESC(); + + CTriggerApplyImpulse(); + + void Spawn( void ); + + void InputApplyImpulse( inputdata_t& ); + +private: + Vector m_vecImpulseDir; + float m_flForce; +}; + + +BEGIN_DATADESC( CTriggerApplyImpulse ) + DEFINE_KEYFIELD( m_vecImpulseDir, FIELD_VECTOR, "impulse_dir" ), + DEFINE_KEYFIELD( m_flForce, FIELD_FLOAT, "force" ), + DEFINE_INPUTFUNC( FIELD_VOID, "ApplyImpulse", InputApplyImpulse ), +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( trigger_apply_impulse, CTriggerApplyImpulse ); + + +CTriggerApplyImpulse::CTriggerApplyImpulse() +{ + m_flForce = 300.f; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerApplyImpulse::Spawn() +{ + // Convert pushdir from angles to a vector + Vector vecAbsDir; + QAngle angPushDir = QAngle(m_vecImpulseDir.x, m_vecImpulseDir.y, m_vecImpulseDir.z); + AngleVectors(angPushDir, &vecAbsDir); + + // Transform the vector into entity space + VectorIRotate( vecAbsDir, EntityToWorldTransform(), m_vecImpulseDir ); + + BaseClass::Spawn(); + + InitTrigger(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTriggerApplyImpulse::InputApplyImpulse( inputdata_t& ) +{ + Vector vecImpulse = m_flForce * m_vecImpulseDir; + FOR_EACH_VEC( m_hTouchingEntities, i ) + { + if ( m_hTouchingEntities[i] ) + { + m_hTouchingEntities[i]->ApplyAbsVelocityImpulse( vecImpulse ); + } + } +} + +#ifdef MAPBASE +class CTriggerFall : public CBaseTrigger +{ + DECLARE_CLASS( CTriggerFall, CBaseTrigger ); + +public: + + virtual void StartTouch( CBaseEntity *pOther ); + virtual void EndTouch( CBaseEntity *pOther ); + virtual void Spawn( void ); + + bool m_bStayLethal; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( trigger_fall, CTriggerFall ); + +BEGIN_DATADESC( CTriggerFall ) + + DEFINE_KEYFIELD( m_bStayLethal, FIELD_BOOLEAN, "StayLethal" ), + +END_DATADESC() + +void CTriggerFall::Spawn( void ) +{ + BaseClass::Spawn(); + + InitTrigger(); +} + +void CTriggerFall::StartTouch(CBaseEntity *pOther) +{ + BaseClass::StartTouch( pOther ); + + if ( !pOther->IsPlayer() ) + return; + + static_cast(pOther)->m_bInTriggerFall = true; +} + +void CTriggerFall::EndTouch(CBaseEntity *pOther) +{ + BaseClass::EndTouch( pOther ); + + if ( !pOther->IsPlayer() || m_bStayLethal ) + return; + + static_cast(pOther)->m_bInTriggerFall = false; +} + + + +class CTriggerWorld : public CTriggerMultiple +{ + DECLARE_CLASS( CTriggerWorld, CTriggerMultiple ); + +public: + + virtual bool PassesTriggerFilters(CBaseEntity *pOther); +}; + +LINK_ENTITY_TO_CLASS( trigger_world, CTriggerWorld ); + +bool CTriggerWorld::PassesTriggerFilters( CBaseEntity *pOther ) +{ + return pOther->IsWorld(); +} +#endif + +#ifdef HL1_DLL +//---------------------------------------------------------------------------------- +// func_friction +//---------------------------------------------------------------------------------- +class CFrictionModifier : public CBaseTrigger +{ + DECLARE_CLASS( CFrictionModifier, CBaseTrigger ); + +public: + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + virtual void StartTouch(CBaseEntity *pOther); + virtual void EndTouch(CBaseEntity *pOther); + + virtual int ObjectCaps( void ) { return CBaseEntity::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; } + float m_frictionFraction; + + DECLARE_DATADESC(); + +}; + +LINK_ENTITY_TO_CLASS( func_friction, CFrictionModifier ); + +BEGIN_DATADESC( CFrictionModifier ) + DEFINE_FIELD( m_frictionFraction, FIELD_FLOAT ), +END_DATADESC() + +// Modify an entity's friction +void CFrictionModifier::Spawn( void ) +{ + BaseClass::Spawn(); + + InitTrigger(); +} + +// Sets toucher's friction to m_frictionFraction (1.0 = normal friction) +bool CFrictionModifier::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "modifier")) + { + m_frictionFraction = atof(szValue) / 100.0; + } + else + { + BaseClass::KeyValue( szKeyName, szValue ); + } + return true; +} + +void CFrictionModifier::StartTouch( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() ) // ignore player + { + pOther->SetFriction( m_frictionFraction ); + } +} + +void CFrictionModifier::EndTouch( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() ) // ignore player + { + pOther->SetFriction( 1.0f ); + } +} + +#endif //HL1_DLL + +bool IsTriggerClass( CBaseEntity *pEntity ) +{ + if ( NULL != dynamic_cast(pEntity) ) + return true; + + if ( NULL != dynamic_cast(pEntity) ) + return true; + + if ( NULL != dynamic_cast(pEntity) ) + return true; + + return false; +} diff --git a/sp/src/game/server/triggers.h b/sp/src/game/server/triggers.h new file mode 100644 index 00000000..4aacad13 --- /dev/null +++ b/sp/src/game/server/triggers.h @@ -0,0 +1,354 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef TRIGGERS_H +#define TRIGGERS_H +#ifdef _WIN32 +#pragma once +#endif + +#ifdef MAPBASE +#include "baseentity.h" +#else +#include "basetoggle.h" +#endif +#include "entityoutput.h" + +// +// Spawnflags +// + +enum +{ + SF_TRIGGER_ALLOW_CLIENTS = 0x01, // Players can fire this trigger + SF_TRIGGER_ALLOW_NPCS = 0x02, // NPCS can fire this trigger + SF_TRIGGER_ALLOW_PUSHABLES = 0x04, // Pushables can fire this trigger + SF_TRIGGER_ALLOW_PHYSICS = 0x08, // Physics objects can fire this trigger + SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS = 0x10, // *if* NPCs can fire this trigger, this flag means only player allies do so + SF_TRIGGER_ONLY_CLIENTS_IN_VEHICLES = 0x20, // *if* Players can fire this trigger, this flag means only players inside vehicles can + SF_TRIGGER_ALLOW_ALL = 0x40, // Everything can fire this trigger EXCEPT DEBRIS! + SF_TRIGGER_ONLY_CLIENTS_OUT_OF_VEHICLES = 0x200, // *if* Players can fire this trigger, this flag means only players outside vehicles can + SF_TRIG_PUSH_ONCE = 0x80, // trigger_push removes itself after firing once + SF_TRIG_PUSH_AFFECT_PLAYER_ON_LADDER = 0x100, // if pushed object is player on a ladder, then this disengages them from the ladder (HL2only) + SF_TRIG_TOUCH_DEBRIS = 0x400, // Will touch physics debris objects + SF_TRIGGER_ONLY_NPCS_IN_VEHICLES = 0X800, // *if* NPCs can fire this trigger, only NPCs in vehicles do so (respects player ally flag too) + SF_TRIGGER_DISALLOW_BOTS = 0x1000, // Bots are not allowed to fire this trigger +#ifdef MAPBASE + SF_TRIGGER_ALLOW_ITEMS = 0x2000, // MOVETYPE_FLYGRAVITY (Weapons, items, flares, etc.) can fire this trigger +#endif +}; + +// DVS TODO: get rid of CBaseToggle +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +#ifdef MAPBASE +#define CBaseToggle CBaseEntity +#endif +class CBaseTrigger : public CBaseToggle +{ + DECLARE_CLASS( CBaseTrigger, CBaseToggle ); +#ifdef MAPBASE +#undef CBaseToggle +#endif +public: + CBaseTrigger(); + + void Activate( void ); + virtual void PostClientActive( void ); + void InitTrigger( void ); + + void Enable( void ); + void Disable( void ); + void Spawn( void ); + void UpdateOnRemove( void ); + void TouchTest( void ); + + // Input handlers + virtual void InputEnable( inputdata_t &inputdata ); + virtual void InputDisable( inputdata_t &inputdata ); + virtual void InputToggle( inputdata_t &inputdata ); + virtual void InputTouchTest ( inputdata_t &inputdata ); + + virtual void InputStartTouch( inputdata_t &inputdata ); + virtual void InputEndTouch( inputdata_t &inputdata ); + + virtual bool UsesFilter( void ){ return ( m_hFilter.Get() != NULL ); } + virtual bool PassesTriggerFilters(CBaseEntity *pOther); + virtual void StartTouch(CBaseEntity *pOther); + virtual void EndTouch(CBaseEntity *pOther); + bool IsTouching( CBaseEntity *pOther ); +#ifdef MAPBASE_VSCRIPT + bool ScriptIsTouching( HSCRIPT hOther ); +#endif + + CBaseEntity *GetTouchedEntityOfType( const char *sClassName ); + + int DrawDebugTextOverlays(void); + + // by default, triggers don't deal with TraceAttack + void TraceAttack(CBaseEntity *pAttacker, float flDamage, const Vector &vecDir, trace_t *ptr, int bitsDamageType) {} + + bool PointIsWithin( const Vector &vecPoint ); + +#ifdef MAPBASE_VSCRIPT + bool ScriptPassesTriggerFilters( HSCRIPT hOther ) { return ToEnt(hOther) ? PassesTriggerFilters( ToEnt(hOther) ) : NULL; } + HSCRIPT ScriptGetTouchedEntityOfType( const char *sClassName ) { return ToHScript( GetTouchedEntityOfType(sClassName) ); } + + void ScriptGetTouchingEntities( HSCRIPT hTable ); +#endif + + bool m_bDisabled; + string_t m_iFilterName; + CHandle m_hFilter; + +protected: + + // Outputs + COutputEvent m_OnStartTouch; + COutputEvent m_OnStartTouchAll; + COutputEvent m_OnEndTouch; + COutputEvent m_OnEndTouchAll; + COutputEvent m_OnTouching; + COutputEvent m_OnNotTouching; + + // Entities currently being touched by this trigger + CUtlVector< EHANDLE > m_hTouchingEntities; + +#ifdef MAPBASE + // We don't descend from CBaseToggle anymore. These have to be defined here now. + EHANDLE m_hActivator; + float m_flWait; + string_t m_sMaster; // If this button has a master switch, this is the targetname. + // A master switch must be of the multisource type. If all + // of the switches in the multisource have been triggered, then + // the button will be allowed to operate. Otherwise, it will be + // deactivated. + + virtual float GetDelay( void ) { return m_flWait; } +#endif + + DECLARE_DATADESC(); +#ifdef MAPBASE_VSCRIPT + DECLARE_ENT_SCRIPTDESC(); +#endif +}; + +//----------------------------------------------------------------------------- +// Purpose: Variable sized repeatable trigger. Must be targeted at one or more entities. +// If "delay" is set, the trigger waits some time after activating before firing. +// "wait" : Seconds between triggerings. (.2 default/minimum) +//----------------------------------------------------------------------------- +class CTriggerMultiple : public CBaseTrigger +{ + DECLARE_CLASS( CTriggerMultiple, CBaseTrigger ); +public: + void Spawn( void ); + void MultiTouch( CBaseEntity *pOther ); + void MultiWaitOver( void ); + void ActivateMultiTrigger(CBaseEntity *pActivator); + + DECLARE_DATADESC(); + + // Outputs + COutputEvent m_OnTrigger; +}; + +// Global list of triggers that care about weapon fire +extern CUtlVector< CHandle > g_hWeaponFireTriggers; + + +//------------------------------------------------------------------------------ +// Base VPhysics trigger implementation +// NOTE: This uses vphysics to compute touch events. It doesn't do a per-frame Touch call, so the +// Entity I/O is different from a regular trigger +//------------------------------------------------------------------------------ +#define SF_VPHYSICS_MOTION_MOVEABLE 0x1000 + +class CBaseVPhysicsTrigger : public CBaseEntity +{ + DECLARE_CLASS( CBaseVPhysicsTrigger , CBaseEntity ); + +public: + DECLARE_DATADESC(); + + virtual void Spawn(); + virtual void UpdateOnRemove(); + virtual bool CreateVPhysics(); + virtual void Activate( void ); + virtual bool PassesTriggerFilters(CBaseEntity *pOther); + + // UNDONE: Pass trigger event in or change Start/EndTouch. Add ITriggerVPhysics perhaps? + // BUGBUG: If a player touches two of these, his movement will screw up. + // BUGBUG: If a player uses crouch/uncrouch it will generate touch events and clear the motioncontroller flag + virtual void StartTouch( CBaseEntity *pOther ); + virtual void EndTouch( CBaseEntity *pOther ); + + void InputToggle( inputdata_t &inputdata ); + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + + +protected: + bool m_bDisabled; + string_t m_iFilterName; + CHandle m_hFilter; +}; + +//----------------------------------------------------------------------------- +// Purpose: Hurts anything that touches it. If the trigger has a targetname, +// firing it will toggle state. +//----------------------------------------------------------------------------- +class CTriggerHurt : public CBaseTrigger +{ +public: + CTriggerHurt() + { + // This field came along after levels were built so the field defaults to 20 here in the constructor. + m_flDamageCap = 20.0f; +#ifdef MAPBASE + // Uh, same here. + m_flHurtRate = 0.5f; +#endif + } + + DECLARE_CLASS( CTriggerHurt, CBaseTrigger ); + + void Spawn( void ); + void RadiationThink( void ); + void HurtThink( void ); + void Touch( CBaseEntity *pOther ); + void EndTouch( CBaseEntity *pOther ); + bool HurtEntity( CBaseEntity *pOther, float damage ); + int HurtAllTouchers( float dt ); + +#ifdef MAPBASE + bool KeyValue( const char *szKeyName, const char *szValue ); +#endif + + DECLARE_DATADESC(); + + float m_flOriginalDamage; // Damage as specified by the level designer. + float m_flDamage; // Damage per second. + float m_flDamageCap; // Maximum damage per second. + float m_flLastDmgTime; // Time that we last applied damage. + float m_flDmgResetTime; // For forgiveness, the time to reset the counter that accumulates damage. + int m_bitsDamageInflict; // DMG_ damage type that the door or tigger does + int m_damageModel; + bool m_bNoDmgForce; // Should damage from this trigger impart force on what it's hurting +#ifdef MAPBASE + float m_flHurtRate; +#endif + + enum + { + DAMAGEMODEL_NORMAL = 0, + DAMAGEMODEL_DOUBLE_FORGIVENESS, + }; + + // Outputs + COutputEvent m_OnHurt; + COutputEvent m_OnHurtPlayer; + + CUtlVector m_hurtEntities; +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CTriggerCamera : public CBaseEntity +{ +public: + DECLARE_CLASS( CTriggerCamera, CBaseEntity ); + // script description + DECLARE_ENT_SCRIPTDESC(); + +#ifdef MAPBASE + CTriggerCamera(); + + void UpdateOnRemove(); +#endif + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + void Enable( void ); + void Disable( void ); + + void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + void FollowTarget( void ); + void StartCameraShot( const char *pszShotType, CBaseEntity *pSceneEntity, CBaseEntity *pActor1, CBaseEntity *pActor2, float duration ); + int ScriptGetFov(void); + void ScriptSetFov(int iFOV, float rate); +#ifdef MAPBASE + void MoveThink( void ); +#endif + void Move(void); + + // Always transmit to clients so they know where to move the view to + virtual int UpdateTransmitState(); + + DECLARE_DATADESC(); + + // Input handlers + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + +#ifdef MAPBASE + void InputSetFOV( inputdata_t &inputdata ); + void InputSetFOVRate( inputdata_t &inputdata ); +#endif + +private: + EHANDLE m_hPlayer; + EHANDLE m_hTarget; + + // used for moving the camera along a path (rail rides) + CBaseEntity *m_pPath; + string_t m_sPath; + float m_flWait; + float m_flReturnTime; + float m_flStopTime; + float m_moveDistance; + float m_targetSpeed; + float m_initialSpeed; + float m_acceleration; + float m_deceleration; + int m_state; + Vector m_vecMoveDir; + +#ifdef MAPBASE + float m_fov; + float m_fovSpeed; + + bool m_bDontSetPlayerView; +#endif + + string_t m_iszTargetAttachment; + int m_iAttachmentIndex; + bool m_bSnapToGoal; + +#if HL2_EPISODIC + bool m_bInterpolatePosition; + + // these are interpolation vars used for interpolating the camera over time + Vector m_vStartPos, m_vEndPos; + float m_flInterpStartTime; + + const static float kflPosInterpTime; // seconds +#endif + + int m_nPlayerButtons; + int m_nOldTakeDamage; + +private: + COutputEvent m_OnEndFollow; +#ifdef MAPBASE + COutputEvent m_OnStartFollow; +#endif +}; + +#endif // TRIGGERS_H diff --git a/sp/src/game/server/util.cpp b/sp/src/game/server/util.cpp new file mode 100644 index 00000000..8a97695a --- /dev/null +++ b/sp/src/game/server/util.cpp @@ -0,0 +1,3502 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Utility code. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "saverestore.h" +#include "globalstate.h" +#include +#include "shake.h" +#include "decals.h" +#include "player.h" +#include "gamerules.h" +#include "entitylist.h" +#include "bspfile.h" +#include "mathlib/mathlib.h" +#include "IEffects.h" +#include "vstdlib/random.h" +#include "soundflags.h" +#include "ispatialpartition.h" +#include "igamesystem.h" +#include "saverestoretypes.h" +#include "checksum_crc.h" +#include "hierarchy.h" +#include "iservervehicle.h" +#include "te_effect_dispatch.h" +#include "utldict.h" +#include "collisionutils.h" +#include "movevars_shared.h" +#include "inetchannelinfo.h" +#include "tier0/vprof.h" +#include "ndebugoverlay.h" +#include "engine/ivdebugoverlay.h" +#include "datacache/imdlcache.h" +#include "util.h" +#include "cdll_int.h" +#ifdef MAPBASE +#include "fmtstr.h" +#endif + +#ifdef PORTAL +#include "PortalSimulation.h" +//#include "Portal_PhysicsEnvironmentMgr.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud +extern short g_sModelIndexBloodDrop; // (in combatweapon.cpp) holds the sprite index for the initial blood +extern short g_sModelIndexBloodSpray; // (in combatweapon.cpp) holds the sprite index for splattered blood + +#ifdef DEBUG +void DBG_AssertFunction( bool fExpr, const char *szExpr, const char *szFile, int szLine, const char *szMessage ) +{ + if (fExpr) + return; + Warning("ASSERT FAILED:\n %s \n(%s@%d)\n%s", szExpr, szFile, szLine, szMessage ? szMessage : ""); +} +#endif // DEBUG + + +//----------------------------------------------------------------------------- +// Entity creation factory +//----------------------------------------------------------------------------- +class CEntityFactoryDictionary : public IEntityFactoryDictionary +{ +public: + CEntityFactoryDictionary(); + + virtual void InstallFactory( IEntityFactory *pFactory, const char *pClassName ); + virtual IServerNetworkable *Create( const char *pClassName ); + virtual void Destroy( const char *pClassName, IServerNetworkable *pNetworkable ); + virtual const char *GetCannonicalName( const char *pClassName ); + void ReportEntitySizes(); + +private: + IEntityFactory *FindFactory( const char *pClassName ); +public: + CUtlDict< IEntityFactory *, unsigned short > m_Factories; +}; + +//----------------------------------------------------------------------------- +// Singleton accessor +//----------------------------------------------------------------------------- +IEntityFactoryDictionary *EntityFactoryDictionary() +{ + static CEntityFactoryDictionary s_EntityFactory; + return &s_EntityFactory; +} + +void DumpEntityFactories_f() +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CEntityFactoryDictionary *dict = ( CEntityFactoryDictionary * )EntityFactoryDictionary(); + if ( dict ) + { + for ( int i = dict->m_Factories.First(); i != dict->m_Factories.InvalidIndex(); i = dict->m_Factories.Next( i ) ) + { + Warning( "%s\n", dict->m_Factories.GetElementName( i ) ); + } + } +} + +static ConCommand dumpentityfactories( "dumpentityfactories", DumpEntityFactories_f, "Lists all entity factory names.", FCVAR_GAMEDLL ); + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +CON_COMMAND( dump_entity_sizes, "Print sizeof(entclass)" ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + ((CEntityFactoryDictionary*)EntityFactoryDictionary())->ReportEntitySizes(); +} + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CEntityFactoryDictionary::CEntityFactoryDictionary() : m_Factories( true, 0, 128 ) +{ +} + + +//----------------------------------------------------------------------------- +// Finds a new factory +//----------------------------------------------------------------------------- +IEntityFactory *CEntityFactoryDictionary::FindFactory( const char *pClassName ) +{ + unsigned short nIndex = m_Factories.Find( pClassName ); + if ( nIndex == m_Factories.InvalidIndex() ) + return NULL; + return m_Factories[nIndex]; +} + + +//----------------------------------------------------------------------------- +// Install a new factory +//----------------------------------------------------------------------------- +void CEntityFactoryDictionary::InstallFactory( IEntityFactory *pFactory, const char *pClassName ) +{ + Assert( FindFactory( pClassName ) == NULL ); + m_Factories.Insert( pClassName, pFactory ); +} + + +//----------------------------------------------------------------------------- +// Instantiate something using a factory +//----------------------------------------------------------------------------- +IServerNetworkable *CEntityFactoryDictionary::Create( const char *pClassName ) +{ + IEntityFactory *pFactory = FindFactory( pClassName ); + if ( !pFactory ) + { + Warning("Attempted to create unknown entity type %s!\n", pClassName ); + return NULL; + } +#if defined(TRACK_ENTITY_MEMORY) && defined(USE_MEM_DEBUG) + MEM_ALLOC_CREDIT_( m_Factories.GetElementName( m_Factories.Find( pClassName ) ) ); +#endif + return pFactory->Create( pClassName ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +const char *CEntityFactoryDictionary::GetCannonicalName( const char *pClassName ) +{ + return m_Factories.GetElementName( m_Factories.Find( pClassName ) ); +} + +//----------------------------------------------------------------------------- +// Destroy a networkable +//----------------------------------------------------------------------------- +void CEntityFactoryDictionary::Destroy( const char *pClassName, IServerNetworkable *pNetworkable ) +{ + IEntityFactory *pFactory = FindFactory( pClassName ); + if ( !pFactory ) + { + Warning("Attempted to destroy unknown entity type %s!\n", pClassName ); + return; + } + + pFactory->Destroy( pNetworkable ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CEntityFactoryDictionary::ReportEntitySizes() +{ + for ( int i = m_Factories.First(); i != m_Factories.InvalidIndex(); i = m_Factories.Next( i ) ) + { + Msg( " %s: %d", m_Factories.GetElementName( i ), m_Factories[i]->GetEntitySize() ); + } +} + +#ifdef MAPBASE +int EntityFactory_AutoComplete( const char *cmdname, CUtlVector< CUtlString > &commands, CUtlRBTree< CUtlString > &symbols, char *substring, int checklen = 0 ) +{ + CEntityFactoryDictionary *pFactoryDict = (CEntityFactoryDictionary*)EntityFactoryDictionary(); + for ( int i = pFactoryDict->m_Factories.First(); i != pFactoryDict->m_Factories.InvalidIndex(); i = pFactoryDict->m_Factories.Next( i ) ) + { + const char *name = pFactoryDict->m_Factories.GetElementName( i ); + if (Q_strnicmp(name, substring, checklen)) + continue; + + CUtlString sym = name; + int idx = symbols.Find(sym); + if (idx == symbols.InvalidIndex()) + { + symbols.Insert(sym); + } + + // Too many + if (symbols.Count() >= COMMAND_COMPLETION_MAXITEMS) + break; + } + + // Now fill in the results + for (int i = symbols.FirstInorder(); i != symbols.InvalidIndex(); i = symbols.NextInorder(i)) + { + const char *name = symbols[i].String(); + + char buf[512]; + Q_strncpy(buf, name, sizeof(buf)); + Q_strlower(buf); + + CUtlString command; + command = CFmtStr("%s %s", cmdname, buf); + commands.AddToTail(command); + } + + return symbols.Count(); +} +#endif + + +//----------------------------------------------------------------------------- +// class CFlaggedEntitiesEnum +//----------------------------------------------------------------------------- + +CFlaggedEntitiesEnum::CFlaggedEntitiesEnum( CBaseEntity **pList, int listMax, int flagMask ) +{ + m_pList = pList; + m_listMax = listMax; + m_flagMask = flagMask; + m_count = 0; +} + +bool CFlaggedEntitiesEnum::AddToList( CBaseEntity *pEntity ) +{ + if ( m_count >= m_listMax ) + { + AssertMsgOnce( 0, "reached enumerated list limit. Increase limit, decrease radius, or make it so entity flags will work for you" ); + return false; + } + m_pList[m_count] = pEntity; + m_count++; + return true; +} + +IterationRetval_t CFlaggedEntitiesEnum::EnumElement( IHandleEntity *pHandleEntity ) +{ + CBaseEntity *pEntity = gEntList.GetBaseEntity( pHandleEntity->GetRefEHandle() ); + if ( pEntity ) + { + if ( m_flagMask && !(pEntity->GetFlags() & m_flagMask) ) // Does it meet the criteria? + return ITERATION_CONTINUE; + + if ( !AddToList( pEntity ) ) + return ITERATION_STOP; + } + + return ITERATION_CONTINUE; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int UTIL_PrecacheDecal( const char *name, bool preload ) +{ + // If this is out of order, make sure to warn. + if ( !CBaseEntity::IsPrecacheAllowed() ) + { + if ( !engine->IsDecalPrecached( name ) ) + { + Assert( !"UTIL_PrecacheDecal: too late" ); + + Warning( "Late precache of %s\n", name ); + } + } + + return engine->PrecacheDecal( name, preload ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float UTIL_GetSimulationInterval() +{ + if ( CBaseEntity::IsSimulatingOnAlternateTicks() ) + return ( TICK_INTERVAL * 2.0 ); + return TICK_INTERVAL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int UTIL_EntitiesInBox( const Vector &mins, const Vector &maxs, CFlaggedEntitiesEnum *pEnum ) +{ + partition->EnumerateElementsInBox( PARTITION_ENGINE_NON_STATIC_EDICTS, mins, maxs, false, pEnum ); + return pEnum->GetCount(); +} + +int UTIL_EntitiesAlongRay( const Ray_t &ray, CFlaggedEntitiesEnum *pEnum ) +{ + partition->EnumerateElementsAlongRay( PARTITION_ENGINE_NON_STATIC_EDICTS, ray, false, pEnum ); + return pEnum->GetCount(); +} + +int UTIL_EntitiesInSphere( const Vector ¢er, float radius, CFlaggedEntitiesEnum *pEnum ) +{ + partition->EnumerateElementsInSphere( PARTITION_ENGINE_NON_STATIC_EDICTS, center, radius, false, pEnum ); + return pEnum->GetCount(); +} + +#ifdef MAPBASE +int UTIL_EntitiesAtPoint( const Vector &point, CFlaggedEntitiesEnum *pEnum ) +{ + partition->EnumerateElementsAtPoint( PARTITION_ENGINE_NON_STATIC_EDICTS, point, false, pEnum ); + return pEnum->GetCount(); +} +#endif + +CEntitySphereQuery::CEntitySphereQuery( const Vector ¢er, float radius, int flagMask ) +{ + m_listIndex = 0; + m_listCount = UTIL_EntitiesInSphere( m_pList, ARRAYSIZE(m_pList), center, radius, flagMask ); +} + +CBaseEntity *CEntitySphereQuery::GetCurrentEntity() +{ + if ( m_listIndex < m_listCount ) + return m_pList[m_listIndex]; + return NULL; +} + + +//----------------------------------------------------------------------------- +// Simple trace filter +//----------------------------------------------------------------------------- +class CTracePassFilter : public CTraceFilter +{ +public: + CTracePassFilter( IHandleEntity *pPassEnt ) : m_pPassEnt( pPassEnt ) {} + + bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + if ( !StandardFilterRules( pHandleEntity, contentsMask ) ) + return false; + + if (!PassServerEntityFilter( pHandleEntity, m_pPassEnt )) + return false; + + return true; + } + +private: + IHandleEntity *m_pPassEnt; +}; + + +//----------------------------------------------------------------------------- +// Drops an entity onto the floor +//----------------------------------------------------------------------------- +int UTIL_DropToFloor( CBaseEntity *pEntity, unsigned int mask, CBaseEntity *pIgnore ) +{ + // Assume no ground + pEntity->SetGroundEntity( NULL ); + + Assert( pEntity ); + + trace_t trace; + +#ifndef HL2MP + // HACK: is this really the only sure way to detect crossing a terrain boundry? + UTIL_TraceEntity( pEntity, pEntity->GetAbsOrigin(), pEntity->GetAbsOrigin(), mask, pIgnore, pEntity->GetCollisionGroup(), &trace ); + if (trace.fraction == 0.0) + return -1; +#endif // HL2MP + + UTIL_TraceEntity( pEntity, pEntity->GetAbsOrigin(), pEntity->GetAbsOrigin() - Vector(0,0,256), mask, pIgnore, pEntity->GetCollisionGroup(), &trace ); + + if (trace.allsolid) + return -1; + + if (trace.fraction == 1) + return 0; + + pEntity->SetAbsOrigin( trace.endpos ); + pEntity->SetGroundEntity( trace.m_pEnt ); + + return 1; +} + +//----------------------------------------------------------------------------- +// Returns false if any part of the bottom of the entity is off an edge that +// is not a staircase. +//----------------------------------------------------------------------------- +bool UTIL_CheckBottom( CBaseEntity *pEntity, ITraceFilter *pTraceFilter, float flStepSize ) +{ + Vector mins, maxs, start, stop; + trace_t trace; + int x, y; + float mid, bottom; + + Assert( pEntity ); + + CTracePassFilter traceFilter(pEntity); + if ( !pTraceFilter ) + { + pTraceFilter = &traceFilter; + } + + unsigned int mask = pEntity->PhysicsSolidMaskForEntity(); + + VectorAdd (pEntity->GetAbsOrigin(), pEntity->WorldAlignMins(), mins); + VectorAdd (pEntity->GetAbsOrigin(), pEntity->WorldAlignMaxs(), maxs); + + // if all of the points under the corners are solid world, don't bother + // with the tougher checks + // the corners must be within 16 of the midpoint + start[2] = mins[2] - 1; + for (x=0 ; x<=1 ; x++) + { + for (y=0 ; y<=1 ; y++) + { + start[0] = x ? maxs[0] : mins[0]; + start[1] = y ? maxs[1] : mins[1]; + if (enginetrace->GetPointContents(start) != CONTENTS_SOLID) + goto realcheck; + } + } + return true; // we got out easy + +realcheck: + // check it for real... + start[2] = mins[2] + flStepSize; // seems to help going up/down slopes. + + // the midpoint must be within 16 of the bottom + start[0] = stop[0] = (mins[0] + maxs[0])*0.5; + start[1] = stop[1] = (mins[1] + maxs[1])*0.5; + stop[2] = start[2] - 2*flStepSize; + + UTIL_TraceLine( start, stop, mask, pTraceFilter, &trace ); + + if (trace.fraction == 1.0) + return false; + mid = bottom = trace.endpos[2]; + + // the corners must be within 16 of the midpoint + for (x=0 ; x<=1 ; x++) + { + for (y=0 ; y<=1 ; y++) + { + start[0] = stop[0] = x ? maxs[0] : mins[0]; + start[1] = stop[1] = y ? maxs[1] : mins[1]; + + UTIL_TraceLine( start, stop, mask, pTraceFilter, &trace ); + + if (trace.fraction != 1.0 && trace.endpos[2] > bottom) + bottom = trace.endpos[2]; + if (trace.fraction == 1.0 || mid - trace.endpos[2] > flStepSize) + return false; + } + } + return true; +} + + + +bool g_bDisableEhandleAccess = false; +bool g_bReceivedChainedUpdateOnRemove = false; +//----------------------------------------------------------------------------- +// Purpose: Sets the entity up for deletion. Entity will not actually be deleted +// until the next frame, so there can be no pointer errors. +// Input : *oldObj - object to delete +//----------------------------------------------------------------------------- +void UTIL_Remove( IServerNetworkable *oldObj ) +{ + CServerNetworkProperty* pProp = static_cast( oldObj ); + if ( !pProp || pProp->IsMarkedForDeletion() ) + return; + + if ( PhysIsInCallback() ) + { + // This assert means that someone is deleting an entity inside a callback. That isn't supported so + // this code will defer the deletion of that object until the end of the current physics simulation frame + // Since this is hidden from the calling code it's preferred to call PhysCallbackRemove() directly from the caller + // in case the deferred delete will have unwanted results (like continuing to receive callbacks). That will make it + // obvious why the unwanted results are happening so the caller can handle them appropriately. (some callbacks can be masked + // or the calling entity can be flagged to filter them in most cases) + Assert(0); + PhysCallbackRemove(oldObj); + return; + } + + // mark it for deletion + pProp->MarkForDeletion( ); + + CBaseEntity *pBaseEnt = oldObj->GetBaseEntity(); + if ( pBaseEnt ) + { +#ifdef PORTAL //make sure entities are in the primary physics environment for the portal mod, this code should be safe even if the entity is in neither extra environment + CPortalSimulator::Pre_UTIL_Remove( pBaseEnt ); +#endif + g_bReceivedChainedUpdateOnRemove = false; + pBaseEnt->UpdateOnRemove(); + + Assert( g_bReceivedChainedUpdateOnRemove ); + + // clear oldObj targetname / other flags now + pBaseEnt->SetName( NULL_STRING ); + +#ifdef PORTAL + CPortalSimulator::Post_UTIL_Remove( pBaseEnt ); +#endif + } + + gEntList.AddToDeleteList( oldObj ); +} + +void UTIL_Remove( CBaseEntity *oldObj ) +{ + if ( !oldObj ) + return; + UTIL_Remove( oldObj->NetworkProp() ); +} + +static int s_RemoveImmediateSemaphore = 0; +void UTIL_DisableRemoveImmediate() +{ + s_RemoveImmediateSemaphore++; +} +void UTIL_EnableRemoveImmediate() +{ + s_RemoveImmediateSemaphore--; + Assert(s_RemoveImmediateSemaphore>=0); +} +//----------------------------------------------------------------------------- +// Purpose: deletes an entity, without any delay. WARNING! Only use this when sure +// no pointers rely on this entity. +// Input : *oldObj - the entity to delete +//----------------------------------------------------------------------------- +void UTIL_RemoveImmediate( CBaseEntity *oldObj ) +{ + // valid pointer or already removed? + if ( !oldObj || oldObj->IsEFlagSet(EFL_KILLME) ) + return; + + if ( s_RemoveImmediateSemaphore ) + { + UTIL_Remove(oldObj); + return; + } + +#ifdef PORTAL //make sure entities are in the primary physics environment for the portal mod, this code should be safe even if the entity is in neither extra environment + CPortalSimulator::Pre_UTIL_Remove( oldObj ); +#endif + + oldObj->AddEFlags( EFL_KILLME ); // Make sure to ignore further calls into here or UTIL_Remove. + + g_bReceivedChainedUpdateOnRemove = false; + oldObj->UpdateOnRemove(); + Assert( g_bReceivedChainedUpdateOnRemove ); + + // Entities shouldn't reference other entities in their destructors + // that type of code should only occur in an UpdateOnRemove call + g_bDisableEhandleAccess = true; + delete oldObj; + g_bDisableEhandleAccess = false; + +#ifdef PORTAL + CPortalSimulator::Post_UTIL_Remove( oldObj ); +#endif +} + + +// returns a CBaseEntity pointer to a player by index. Only returns if the player is spawned and connected +// otherwise returns NULL +// Index is 1 based +CBasePlayer *UTIL_PlayerByIndex( int playerIndex ) +{ + CBasePlayer *pPlayer = NULL; + + if ( playerIndex > 0 && playerIndex <= gpGlobals->maxClients ) + { + edict_t *pPlayerEdict = INDEXENT( playerIndex ); + if ( pPlayerEdict && !pPlayerEdict->IsFree() ) + { + pPlayer = (CBasePlayer*)GetContainingEntity( pPlayerEdict ); + } + } + + return pPlayer; +} + +CBasePlayer* UTIL_PlayerByName( const char *name ) +{ + if ( !name || !name[0] ) + return NULL; + + for (int i = 1; i<=gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( !pPlayer ) + continue; + + if ( !pPlayer->IsConnected() ) + continue; + + if ( Q_stricmp( pPlayer->GetPlayerName(), name ) == 0 ) + { + return pPlayer; + } + } + + return NULL; +} + +CBasePlayer* UTIL_PlayerByUserId( int userID ) +{ + for (int i = 1; i<=gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + + if ( !pPlayer ) + continue; + + if ( !pPlayer->IsConnected() ) + continue; + + if ( engine->GetPlayerUserId(pPlayer->edict()) == userID ) + { + return pPlayer; + } + } + + return NULL; +} + +// +// Return the local player. +// If this is a multiplayer game, return NULL. +// +CBasePlayer *UTIL_GetLocalPlayer( void ) +{ + if ( gpGlobals->maxClients > 1 ) + { + if ( developer.GetBool() ) + { + Assert( !"UTIL_GetLocalPlayer" ); + +#ifdef DEBUG + Warning( "UTIL_GetLocalPlayer() called in multiplayer game.\n" ); +#endif + } + + return NULL; + } + + return UTIL_PlayerByIndex( 1 ); +} + +// +// Get the local player on a listen server - this is for multiplayer use only +// +CBasePlayer *UTIL_GetListenServerHost( void ) +{ + // no "local player" if this is a dedicated server or a single player game + if (engine->IsDedicatedServer()) + { + Assert( !"UTIL_GetListenServerHost" ); + Warning( "UTIL_GetListenServerHost() called from a dedicated server or single-player game.\n" ); + return NULL; + } + + return UTIL_PlayerByIndex( 1 ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if the command was issued by the listenserver host, or by the dedicated server, via rcon or the server console. + * This is valid during ConCommand execution. + */ +bool UTIL_IsCommandIssuedByServerAdmin( void ) +{ + int issuingPlayerIndex = UTIL_GetCommandClientIndex(); + + if ( engine->IsDedicatedServer() && issuingPlayerIndex > 0 ) + return false; + +#if defined( REPLAY_ENABLED ) + // entity 1 is replay? + player_info_t pi; + bool bPlayerIsReplay = engine->GetPlayerInfo( 1, &pi ) && pi.isreplay; +#else + bool bPlayerIsReplay = false; +#endif + + if ( bPlayerIsReplay ) + { + if ( issuingPlayerIndex > 2 ) + return false; + } + else if ( issuingPlayerIndex > 1 ) + { + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns a CBaseEntity pointer by entindex. Index is 1 based. + */ +CBaseEntity *UTIL_EntityByIndex( int entityIndex ) +{ + CBaseEntity *entity = NULL; + + if ( entityIndex > 0 ) + { + edict_t *edict = INDEXENT( entityIndex ); + if ( edict && !edict->IsFree() ) + { + entity = GetContainingEntity( edict ); + } + } + + return entity; +} + + +int ENTINDEX( CBaseEntity *pEnt ) +{ + // This works just like ENTINDEX for edicts. + if ( pEnt ) + return pEnt->entindex(); + else + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : playerIndex - +// ping - +// packetloss - +//----------------------------------------------------------------------------- +void UTIL_GetPlayerConnectionInfo( int playerIndex, int& ping, int &packetloss ) +{ + CBasePlayer *player = UTIL_PlayerByIndex( playerIndex ); + + INetChannelInfo *nci = engine->GetPlayerNetInfo(playerIndex); + + if ( nci && player && !player->IsBot() ) + { + float latency = nci->GetAvgLatency( FLOW_OUTGOING ); // in seconds + + // that should be the correct latency, we assume that cmdrate is higher + // then updaterate, what is the case for default settings + const char * szCmdRate = engine->GetClientConVarValue( playerIndex, "cl_cmdrate" ); + + int nCmdRate = MAX( 1, Q_atoi( szCmdRate ) ); + latency -= (0.5f/nCmdRate) + TICKS_TO_TIME( 1.0f ); // correct latency + + // in GoldSrc we had a different, not fixed tickrate. so we have to adjust + // Source pings by half a tick to match the old GoldSrc pings. + latency -= TICKS_TO_TIME( 0.5f ); + + ping = latency * 1000.0f; // as msecs + ping = clamp( ping, 5, 1000 ); // set bounds, dont show pings under 5 msecs + + packetloss = 100.0f * nci->GetAvgLoss( FLOW_INCOMING ); // loss in percentage + packetloss = clamp( packetloss, 0, 100 ); + } + else + { + ping = 0; + packetloss = 0; + } +} + +static unsigned short FixedUnsigned16( float value, float scale ) +{ + int output; + + output = value * scale; + if ( output < 0 ) + output = 0; + if ( output > 0xFFFF ) + output = 0xFFFF; + + return (unsigned short)output; +} + + +//----------------------------------------------------------------------------- +// Compute shake amplitude +//----------------------------------------------------------------------------- +inline float ComputeShakeAmplitude( const Vector ¢er, const Vector &shakePt, float amplitude, float radius ) +{ + if ( radius <= 0 ) + return amplitude; + + float localAmplitude = -1; + Vector delta = center - shakePt; + float distance = delta.Length(); + + if ( distance <= radius ) + { + // Make the amplitude fall off over distance + float flPerc = 1.0 - (distance / radius); + localAmplitude = amplitude * flPerc; + } + + return localAmplitude; +} + + +//----------------------------------------------------------------------------- +// Transmits the actual shake event +//----------------------------------------------------------------------------- +inline void TransmitShakeEvent( CBasePlayer *pPlayer, float localAmplitude, float frequency, float duration, ShakeCommand_t eCommand ) +{ + if (( localAmplitude > 0 ) || ( eCommand == SHAKE_STOP )) + { + if ( eCommand == SHAKE_STOP ) + localAmplitude = 0; + + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + UserMessageBegin( user, "Shake" ); + WRITE_BYTE( eCommand ); // shake command (SHAKE_START, STOP, FREQUENCY, AMPLITUDE) + WRITE_FLOAT( localAmplitude ); // shake magnitude/amplitude + WRITE_FLOAT( frequency ); // shake noise frequency + WRITE_FLOAT( duration ); // shake lasts this long + MessageEnd(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Shake the screen of all clients within radius. +// radius == 0, shake all clients +// UNDONE: Fix falloff model (disabled)? +// UNDONE: Affect user controls? +// Input : center - Center of screen shake, radius is measured from here. +// amplitude - Amplitude of shake +// frequency - +// duration - duration of shake in seconds. +// radius - Radius of effect, 0 shakes all clients. +// command - One of the following values: +// SHAKE_START - starts the screen shake for all players within the radius +// SHAKE_STOP - stops the screen shake for all players within the radius +// SHAKE_AMPLITUDE - modifies the amplitude of the screen shake +// for all players within the radius +// SHAKE_FREQUENCY - modifies the frequency of the screen shake +// for all players within the radius +// bAirShake - if this is false, then it will only shake players standing on the ground. +//----------------------------------------------------------------------------- +const float MAX_SHAKE_AMPLITUDE = 16.0f; +void UTIL_ScreenShake( const Vector ¢er, float amplitude, float frequency, float duration, float radius, ShakeCommand_t eCommand, bool bAirShake ) +{ + int i; + float localAmplitude; + + if ( amplitude > MAX_SHAKE_AMPLITUDE ) + { + amplitude = MAX_SHAKE_AMPLITUDE; + } + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseEntity *pPlayer = UTIL_PlayerByIndex( i ); + + // + // Only start shakes for players that are on the ground unless doing an air shake. + // + if ( !pPlayer || (!bAirShake && (eCommand == SHAKE_START) && !(pPlayer->GetFlags() & FL_ONGROUND)) ) + { + continue; + } + + localAmplitude = ComputeShakeAmplitude( center, pPlayer->WorldSpaceCenter(), amplitude, radius ); + + // This happens if the player is outside the radius, in which case we should ignore + // all commands + if (localAmplitude < 0) + continue; + + TransmitShakeEvent( (CBasePlayer *)pPlayer, localAmplitude, frequency, duration, eCommand ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Shake an object and all players on or near it +//----------------------------------------------------------------------------- +void UTIL_ScreenShakeObject( CBaseEntity *pEnt, const Vector ¢er, float amplitude, float frequency, float duration, float radius, ShakeCommand_t eCommand, bool bAirShake ) +{ + int i; + float localAmplitude; + + CBaseEntity *pHighestParent = pEnt->GetRootMoveParent(); + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseEntity *pPlayer = UTIL_PlayerByIndex( i ); + if (!pPlayer) + continue; + + // Shake the object, or anything hierarchically attached to it at maximum amplitude + localAmplitude = 0; + if (pHighestParent == pPlayer->GetRootMoveParent()) + { + localAmplitude = amplitude; + } + else if ((pPlayer->GetFlags() & FL_ONGROUND) && (pPlayer->GetGroundEntity()->GetRootMoveParent() == pHighestParent)) + { + // If the player is standing on the object, use maximum amplitude + localAmplitude = amplitude; + } + else + { + // Only shake players that are on the ground. + if ( !bAirShake && !(pPlayer->GetFlags() & FL_ONGROUND) ) + { + continue; + } + + if ( radius > 0 ) + { + localAmplitude = ComputeShakeAmplitude( center, pPlayer->WorldSpaceCenter(), amplitude, radius ); + } + else + { + // If using a 0 radius, apply to everyone with no falloff + localAmplitude = amplitude; + } + + // This happens if the player is outside the radius, + // in which case we should ignore all commands + if (localAmplitude < 0) + continue; + } + + TransmitShakeEvent( (CBasePlayer *)pPlayer, localAmplitude, frequency, duration, eCommand ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Punches the view of all clients within radius. +// If radius is 0, punches all clients. +// Input : center - Center of punch, radius is measured from here. +// radius - Radius of effect, 0 punches all clients. +// bInAir - if this is false, then it will only punch players standing on the ground. +//----------------------------------------------------------------------------- +void UTIL_ViewPunch( const Vector ¢er, QAngle angPunch, float radius, bool bInAir ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseEntity *pPlayer = UTIL_PlayerByIndex( i ); + + // + // Only apply the punch to players that are on the ground unless doing an air punch. + // + if ( !pPlayer || (!bInAir && !(pPlayer->GetFlags() & FL_ONGROUND)) ) + { + continue; + } + + QAngle angTemp = angPunch; + + if ( radius > 0 ) + { + Vector delta = center - pPlayer->GetAbsOrigin(); + float distance = delta.Length(); + + if ( distance <= radius ) + { + // Make the punch amplitude fall off over distance. + float flPerc = 1.0 - (distance / radius); + angTemp *= flPerc; + } + else + { + continue; + } + } + + pPlayer->ViewPunch( angTemp ); + } +} + + +void UTIL_ScreenFadeBuild( ScreenFade_t &fade, const color32 &color, float fadeTime, float fadeHold, int flags ) +{ + fade.duration = FixedUnsigned16( fadeTime, 1<IsNetClient() ) + return; + + CSingleUserRecipientFilter user( (CBasePlayer *)pEntity ); + user.MakeReliable(); + + UserMessageBegin( user, "Fade" ); // use the magic #1 for "one client" + WRITE_SHORT( fade.duration ); // fade lasts this long + WRITE_SHORT( fade.holdTime ); // fade lasts this long + WRITE_SHORT( fade.fadeFlags ); // fade type (in / out) + WRITE_BYTE( fade.r ); // fade red + WRITE_BYTE( fade.g ); // fade green + WRITE_BYTE( fade.b ); // fade blue + WRITE_BYTE( fade.a ); // fade blue + MessageEnd(); +} + + +void UTIL_ScreenFadeAll( const color32 &color, float fadeTime, float fadeHold, int flags ) +{ + int i; + ScreenFade_t fade; + + + UTIL_ScreenFadeBuild( fade, color, fadeTime, fadeHold, flags ); + + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseEntity *pPlayer = UTIL_PlayerByIndex( i ); + + UTIL_ScreenFadeWrite( fade, pPlayer ); + } +} + + +void UTIL_ScreenFade( CBaseEntity *pEntity, const color32 &color, float fadeTime, float fadeHold, int flags ) +{ + ScreenFade_t fade; + + UTIL_ScreenFadeBuild( fade, color, fadeTime, fadeHold, flags ); + UTIL_ScreenFadeWrite( fade, pEntity ); +} + + +void UTIL_HudMessage( CBasePlayer *pToPlayer, const hudtextparms_t &textparms, const char *pMessage, const char *pszFont, bool bAutobreak ) +{ + CRecipientFilter filter; + + if( pToPlayer ) + { + filter.AddRecipient( pToPlayer ); + } + else + { + filter.AddAllPlayers(); + } + + filter.MakeReliable(); + + UserMessageBegin( filter, "HudMsg" ); + WRITE_BYTE ( textparms.channel & 0xFF ); + WRITE_FLOAT( textparms.x ); + WRITE_FLOAT( textparms.y ); + WRITE_BYTE ( textparms.r1 ); + WRITE_BYTE ( textparms.g1 ); + WRITE_BYTE ( textparms.b1 ); + WRITE_BYTE ( textparms.a1 ); + WRITE_BYTE ( textparms.r2 ); + WRITE_BYTE ( textparms.g2 ); + WRITE_BYTE ( textparms.b2 ); + WRITE_BYTE ( textparms.a2 ); + WRITE_BYTE ( textparms.effect ); + WRITE_FLOAT( textparms.fadeinTime ); + WRITE_FLOAT( textparms.fadeoutTime ); + WRITE_FLOAT( textparms.holdTime ); + WRITE_FLOAT( textparms.fxTime ); + WRITE_STRING( pMessage ); +#ifdef MAPBASE + WRITE_STRING( pszFont ); + if (bAutobreak) + { + WRITE_BYTE ( Q_strlen( pMessage ) ); + } +#endif + MessageEnd(); +} + +void UTIL_HudMessageAll( const hudtextparms_t &textparms, const char *pMessage, const char *pszFont, bool bAutobreak ) +{ + UTIL_HudMessage( NULL, textparms, pMessage, pszFont, bAutobreak ); +} + +void UTIL_HudHintText( CBaseEntity *pEntity, const char *pMessage ) +{ + if ( !pEntity ) + return; + + CSingleUserRecipientFilter user( (CBasePlayer *)pEntity ); + user.MakeReliable(); + UserMessageBegin( user, "KeyHintText" ); + WRITE_BYTE( 1 ); // one string + WRITE_STRING( pMessage ); + MessageEnd(); +} + +void UTIL_ClientPrintFilter( IRecipientFilter& filter, int msg_dest, const char *msg_name, const char *param1, const char *param2, const char *param3, const char *param4 ) +{ + UserMessageBegin( filter, "TextMsg" ); + WRITE_BYTE( msg_dest ); + WRITE_STRING( msg_name ); + + if ( param1 ) + WRITE_STRING( param1 ); + else + WRITE_STRING( "" ); + + if ( param2 ) + WRITE_STRING( param2 ); + else + WRITE_STRING( "" ); + + if ( param3 ) + WRITE_STRING( param3 ); + else + WRITE_STRING( "" ); + + if ( param4 ) + WRITE_STRING( param4 ); + else + WRITE_STRING( "" ); + + MessageEnd(); +} + +void UTIL_ClientPrintAll( int msg_dest, const char *msg_name, const char *param1, const char *param2, const char *param3, const char *param4 ) +{ + CReliableBroadcastRecipientFilter filter; + + UTIL_ClientPrintFilter( filter, msg_dest, msg_name, param1, param2, param3, param4 ); +} + +void ClientPrint( CBasePlayer *player, int msg_dest, const char *msg_name, const char *param1, const char *param2, const char *param3, const char *param4 ) +{ + if ( !player ) + return; + + CSingleUserRecipientFilter user( player ); + user.MakeReliable(); + + UTIL_ClientPrintFilter( user, msg_dest, msg_name, param1, param2, param3, param4 ); +} + +void UTIL_SayTextFilter( IRecipientFilter& filter, const char *pText, CBasePlayer *pPlayer, bool bChat ) +{ + UserMessageBegin( filter, "SayText" ); + if ( pPlayer ) + { + WRITE_BYTE( pPlayer->entindex() ); + } + else + { + WRITE_BYTE( 0 ); // world, dedicated server says + } + WRITE_STRING( pText ); + WRITE_BYTE( bChat ); + MessageEnd(); +} + +void UTIL_SayText2Filter( IRecipientFilter& filter, CBasePlayer *pEntity, bool bChat, const char *msg_name, const char *param1, const char *param2, const char *param3, const char *param4 ) +{ + UserMessageBegin( filter, "SayText2" ); + if ( pEntity ) + { + WRITE_BYTE( pEntity->entindex() ); + } + else + { + WRITE_BYTE( 0 ); // world, dedicated server says + } + + WRITE_BYTE( bChat ); + + WRITE_STRING( msg_name ); + + if ( param1 ) + WRITE_STRING( param1 ); + else + WRITE_STRING( "" ); + + if ( param2 ) + WRITE_STRING( param2 ); + else + WRITE_STRING( "" ); + + if ( param3 ) + WRITE_STRING( param3 ); + else + WRITE_STRING( "" ); + + if ( param4 ) + WRITE_STRING( param4 ); + else + WRITE_STRING( "" ); + + MessageEnd(); +} + +void UTIL_SayText( const char *pText, CBasePlayer *pToPlayer ) +{ + if ( !pToPlayer->IsNetClient() ) + return; + + CSingleUserRecipientFilter user( pToPlayer ); + user.MakeReliable(); + + UTIL_SayTextFilter( user, pText, pToPlayer, false ); +} + +void UTIL_SayTextAll( const char *pText, CBasePlayer *pPlayer, bool bChat ) +{ + CReliableBroadcastRecipientFilter filter; + UTIL_SayTextFilter( filter, pText, pPlayer, bChat ); +} + +void UTIL_ShowMessage( const char *pString, CBasePlayer *pPlayer ) +{ + CRecipientFilter filter; + + if ( pPlayer ) + { + filter.AddRecipient( pPlayer ); + } + else + { + filter.AddAllPlayers(); + } + + filter.MakeReliable(); + + UserMessageBegin( filter, "HudText" ); + WRITE_STRING( pString ); + MessageEnd(); +} + + +void UTIL_ShowMessageAll( const char *pString ) +{ + UTIL_ShowMessage( pString, NULL ); +} + +// So we always return a valid surface +static csurface_t g_NullSurface = { "**empty**", 0 }; + +void UTIL_SetTrace(trace_t& trace, const Ray_t &ray, edict_t *ent, float fraction, + int hitgroup, unsigned int contents, const Vector& normal, float intercept ) +{ + trace.startsolid = (fraction == 0.0f); + trace.fraction = fraction; + VectorCopy( ray.m_Start, trace.startpos ); + VectorMA( ray.m_Start, fraction, ray.m_Delta, trace.endpos ); + VectorCopy( normal, trace.plane.normal ); + trace.plane.dist = intercept; + trace.m_pEnt = CBaseEntity::Instance( ent ); + trace.hitgroup = hitgroup; + trace.surface = g_NullSurface; + trace.contents = contents; +} + +void UTIL_ClearTrace( trace_t &trace ) +{ + memset( &trace, 0, sizeof(trace)); + trace.fraction = 1.f; + trace.fractionleftsolid = 0; + trace.surface = g_NullSurface; +} + + + +//----------------------------------------------------------------------------- +// Sets the entity size +//----------------------------------------------------------------------------- +static void SetMinMaxSize (CBaseEntity *pEnt, const Vector& mins, const Vector& maxs ) +{ + for ( int i=0 ; i<3 ; i++ ) + { + if ( mins[i] > maxs[i] ) + { + Error( "%s: backwards mins/maxs", ( pEnt ) ? pEnt->GetDebugName() : "" ); + } + } + + Assert( pEnt ); + + pEnt->SetCollisionBounds( mins, maxs ); +} + + +//----------------------------------------------------------------------------- +// Sets the model size +//----------------------------------------------------------------------------- +void UTIL_SetSize( CBaseEntity *pEnt, const Vector &vecMin, const Vector &vecMax ) +{ + SetMinMaxSize (pEnt, vecMin, vecMax); +} + + +//----------------------------------------------------------------------------- +// Sets the model to be associated with an entity +//----------------------------------------------------------------------------- +void UTIL_SetModel( CBaseEntity *pEntity, const char *pModelName ) +{ + // check to see if model was properly precached + int i = modelinfo->GetModelIndex( pModelName ); + if ( i == -1 ) + { +#if defined(MAPBASE) && !defined(_DEBUG) + // Throwing a program-terminating error might be a little too much since we could just precache it here. + // If we're not in debug mode, just let it off with a nice warning. + if (int newi = CBaseEntity::PrecacheModel(pModelName)) + { + i = newi; + Warning("%s was not precached\n", pModelName); + } +#else + Error("%i/%s - %s: UTIL_SetModel: not precached: %s\n", pEntity->entindex(), + STRING( pEntity->GetEntityName() ), + pEntity->GetClassname(), pModelName); +#endif + } + + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + if ( pAnimating ) + { + pAnimating->m_nForceBone = 0; + } + + pEntity->SetModelName( AllocPooledString( pModelName ) ); + pEntity->SetModelIndex( i ) ; + SetMinMaxSize(pEntity, vec3_origin, vec3_origin); + pEntity->SetCollisionBoundsFromModel(); +} + + +void UTIL_SetOrigin( CBaseEntity *entity, const Vector &vecOrigin, bool bFireTriggers ) +{ + entity->SetLocalOrigin( vecOrigin ); + if ( bFireTriggers ) + { + entity->PhysicsTouchTriggers(); + } +} + + +void UTIL_ParticleEffect( const Vector &vecOrigin, const Vector &vecDirection, ULONG ulColor, ULONG ulCount ) +{ + Msg( "UTIL_ParticleEffect: Disabled\n" ); +} + +void UTIL_Smoke( const Vector &origin, const float scale, const float framerate ) +{ + g_pEffects->Smoke( origin, g_sModelIndexSmoke, scale, framerate ); +} + +// snaps a vector to the nearest axis vector (if within epsilon) +void UTIL_SnapDirectionToAxis( Vector &direction, float epsilon ) +{ + float proj = 1 - epsilon; + for ( int i = 0; i < 3; i ++ ) + { + if ( fabs(direction[i]) > proj ) + { + // snap to axis unit vector + if ( direction[i] < 0 ) + direction[i] = -1.0f; + else + direction[i] = 1.0f; + direction[(i+1)%3] = 0; + direction[(i+2)%3] = 0; + return; + } + } +} + +char *UTIL_VarArgs( const char *format, ... ) +{ + va_list argptr; + static char string[1024]; + + va_start (argptr, format); + Q_vsnprintf(string, sizeof(string), format,argptr); + va_end (argptr); + + return string; +} + +bool UTIL_IsMasterTriggered(string_t sMaster, CBaseEntity *pActivator) +{ + if (sMaster != NULL_STRING) + { + CBaseEntity *pMaster = gEntList.FindEntityByName( NULL, sMaster, NULL, pActivator ); + + if ( pMaster && (pMaster->ObjectCaps() & FCAP_MASTER) ) + { + return pMaster->IsTriggered( pActivator ); + } + + Warning( "Master was null or not a master!\n"); + } + + // if this isn't a master entity, just say yes. + return true; +} + +void UTIL_BloodStream( const Vector &origin, const Vector &direction, int color, int amount ) +{ + if ( !UTIL_ShouldShowBlood( color ) ) + return; + + if ( g_Language.GetInt() == LANGUAGE_GERMAN && color == BLOOD_COLOR_RED ) + color = 0; + + CPVSFilter filter( origin ); + te->BloodStream( filter, 0.0, &origin, &direction, 247, 63, 14, 255, MIN( amount, 255 ) ); +} + + +Vector UTIL_RandomBloodVector( void ) +{ + Vector direction; + + direction.x = random->RandomFloat ( -1, 1 ); + direction.y = random->RandomFloat ( -1, 1 ); + direction.z = random->RandomFloat ( 0, 1 ); + + return direction; +} + + +//------------------------------------------------------------------------------ +// Purpose : Creates both an decal and any associated impact effects (such +// as flecks) for the given iDamageType and the trace's end position +// Input : +// Output : +//------------------------------------------------------------------------------ +void UTIL_ImpactTrace( trace_t *pTrace, int iDamageType, const char *pCustomImpactName ) +{ + CBaseEntity *pEntity = pTrace->m_pEnt; + + // Is the entity valid, is the surface sky? + if ( !pEntity || !UTIL_IsValidEntity( pEntity ) || (pTrace->surface.flags & SURF_SKY) ) + return; + + if ( pTrace->fraction == 1.0 ) + return; + + pEntity->ImpactTrace( pTrace, iDamageType, pCustomImpactName ); +} + +/* +============== +UTIL_PlayerDecalTrace + +A player is trying to apply his custom decal for the spray can. +Tell connected clients to display it, or use the default spray can decal +if the custom can't be loaded. +============== +*/ +void UTIL_PlayerDecalTrace( trace_t *pTrace, int playernum ) +{ + if (pTrace->fraction == 1.0) + return; + + CBroadcastRecipientFilter filter; + + te->PlayerDecal( filter, 0.0, + &pTrace->endpos, playernum, pTrace->m_pEnt->entindex() ); +} + +bool UTIL_TeamsMatch( const char *pTeamName1, const char *pTeamName2 ) +{ + // Everyone matches unless it's teamplay + if ( !g_pGameRules->IsTeamplay() ) + return true; + + // Both on a team? + if ( *pTeamName1 != 0 && *pTeamName2 != 0 ) + { + if ( !stricmp( pTeamName1, pTeamName2 ) ) // Same Team? + return true; + } + + return false; +} + + +void UTIL_AxisStringToPointPoint( Vector &start, Vector &end, const char *pString ) +{ + char tmpstr[256]; + + Q_strncpy( tmpstr, pString, sizeof(tmpstr) ); + char *pVec = strtok( tmpstr, "," ); + int i = 0; + while ( pVec != NULL && *pVec ) + { + if ( i == 0 ) + { + UTIL_StringToVector( start.Base(), pVec ); + i++; + } + else + { + UTIL_StringToVector( end.Base(), pVec ); + } + pVec = strtok( NULL, "," ); + } +} + +void UTIL_AxisStringToPointDir( Vector &start, Vector &dir, const char *pString ) +{ + Vector end; + UTIL_AxisStringToPointPoint( start, end, pString ); + dir = end - start; + VectorNormalize(dir); +} + +void UTIL_AxisStringToUnitDir( Vector &dir, const char *pString ) +{ + Vector start; + UTIL_AxisStringToPointDir( start, dir, pString ); +} + +/* +================================================== +UTIL_ClipPunchAngleOffset +================================================== +*/ + +void UTIL_ClipPunchAngleOffset( QAngle &in, const QAngle &punch, const QAngle &clip ) +{ + QAngle final = in + punch; + + //Clip each component + for ( int i = 0; i < 3; i++ ) + { + if ( final[i] > clip[i] ) + { + final[i] = clip[i]; + } + else if ( final[i] < -clip[i] ) + { + final[i] = -clip[i]; + } + + //Return the result + in[i] = final[i] - punch[i]; + } +} + +float UTIL_WaterLevel( const Vector &position, float minz, float maxz ) +{ + Vector midUp = position; + midUp.z = minz; + + if ( !(UTIL_PointContents(midUp) & MASK_WATER) ) + return minz; + + midUp.z = maxz; + if ( UTIL_PointContents(midUp) & MASK_WATER ) + return maxz; + + float diff = maxz - minz; + while (diff > 1.0) + { + midUp.z = minz + diff/2.0; + if ( UTIL_PointContents(midUp) & MASK_WATER ) + { + minz = midUp.z; + } + else + { + maxz = midUp.z; + } + diff = maxz - minz; + } + + return midUp.z; +} + + +//----------------------------------------------------------------------------- +// Like UTIL_WaterLevel, but *way* less expensive. +// I didn't replace UTIL_WaterLevel everywhere to avoid breaking anything. +//----------------------------------------------------------------------------- +class CWaterTraceFilter : public CTraceFilter +{ +public: + bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + CBaseEntity *pCollide = EntityFromEntityHandle( pHandleEntity ); + + // Static prop case... + if ( !pCollide ) + return false; + + // Only impact water stuff... + if ( pCollide->GetSolidFlags() & FSOLID_VOLUME_CONTENTS ) + return true; + + return false; + } +}; + +float UTIL_FindWaterSurface( const Vector &position, float minz, float maxz ) +{ + Vector vecStart, vecEnd; + vecStart.Init( position.x, position.y, maxz ); + vecEnd.Init( position.x, position.y, minz ); + + Ray_t ray; + trace_t tr; + CWaterTraceFilter waterTraceFilter; + ray.Init( vecStart, vecEnd ); + enginetrace->TraceRay( ray, MASK_WATER, &waterTraceFilter, &tr ); + + return tr.endpos.z; +} + + +extern short g_sModelIndexBubbles;// holds the index for the bubbles model + +void UTIL_Bubbles( const Vector& mins, const Vector& maxs, int count ) +{ + Vector mid = (mins + maxs) * 0.5; + + float flHeight = UTIL_WaterLevel( mid, mid.z, mid.z + 1024 ); + flHeight = flHeight - mins.z; + + CPASFilter filter( mid ); + + te->Bubbles( filter, 0.0, + &mins, &maxs, flHeight, g_sModelIndexBubbles, count, 8.0 ); +} + +void UTIL_BubbleTrail( const Vector& from, const Vector& to, int count ) +{ + // Find water surface will return from.z if the from point is above water + float flStartHeight = UTIL_FindWaterSurface( from, from.z, from.z + 256 ); + flStartHeight = flStartHeight - from.z; + + float flEndHeight = UTIL_FindWaterSurface( to, to.z, to.z + 256 ); + flEndHeight = flEndHeight - to.z; + + if ( ( flStartHeight == 0 ) && ( flEndHeight == 0 ) ) + return; + + float flWaterZ = flStartHeight + from.z; + + const Vector *pFrom = &from; + const Vector *pTo = &to; + Vector vecWaterPoint; + if ( ( flStartHeight == 0 ) || ( flEndHeight == 0 ) ) + { + if ( flStartHeight == 0 ) + { + flWaterZ = flEndHeight + to.z; + } + + float t = IntersectRayWithAAPlane( from, to, 2, 1.0f, flWaterZ ); + Assert( (t >= -1e-3f) && ( t <= 1.0f ) ); + VectorLerp( from, to, t, vecWaterPoint ); + if ( flStartHeight == 0 ) + { + pFrom = &vecWaterPoint; + + // Reduce the count by the actual length + count = (int)( count * ( 1.0f - t ) ); + } + else + { + pTo = &vecWaterPoint; + + // Reduce the count by the actual length + count = (int)( count * t ); + } + } + + CBroadcastRecipientFilter filter; + te->BubbleTrail( filter, 0.0, pFrom, pTo, flWaterZ, g_sModelIndexBubbles, count, 8.0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : Start - +// End - +// ModelIndex - +// FrameStart - +// FrameRate - +// Life - +// Width - +// Noise - +// Red - +// Green - +// Brightness - +// Speed - +//----------------------------------------------------------------------------- +void UTIL_Beam( Vector &Start, Vector &End, int nModelIndex, int nHaloIndex, unsigned char FrameStart, unsigned char FrameRate, + float Life, unsigned char Width, unsigned char EndWidth, unsigned char FadeLength, unsigned char Noise, unsigned char Red, unsigned char Green, + unsigned char Blue, unsigned char Brightness, unsigned char Speed) +{ + CBroadcastRecipientFilter filter; + + te->BeamPoints( filter, 0.0, + &Start, + &End, + nModelIndex, + nHaloIndex, + FrameStart, + FrameRate, + Life, + Width, + EndWidth, + FadeLength, + Noise, + Red, + Green, + Blue, + Brightness, + Speed ); +} + +bool UTIL_IsValidEntity( CBaseEntity *pEnt ) +{ + edict_t *pEdict = pEnt->edict(); + if ( !pEdict || pEdict->IsFree() ) + return false; + return true; +} + + +#define PRECACHE_OTHER_ONCE +// UNDONE: Do we need this to avoid doing too much of this? Measure startup times and see +#if defined( PRECACHE_OTHER_ONCE ) + +#include "utlsymbol.h" +class CPrecacheOtherList : public CAutoGameSystem +{ +public: + CPrecacheOtherList( char const *name ) : CAutoGameSystem( name ) + { + } + virtual void LevelInitPreEntity(); + virtual void LevelShutdownPostEntity(); + + bool AddOrMarkPrecached( const char *pClassname ); + +private: + CUtlSymbolTable m_list; +}; + +void CPrecacheOtherList::LevelInitPreEntity() +{ + m_list.RemoveAll(); +} + +void CPrecacheOtherList::LevelShutdownPostEntity() +{ + m_list.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: mark or add +// Input : *pEntity - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPrecacheOtherList::AddOrMarkPrecached( const char *pClassname ) +{ + CUtlSymbol sym = m_list.Find( pClassname ); + if ( sym.IsValid() ) + return false; + + m_list.AddString( pClassname ); + return true; +} + +CPrecacheOtherList g_PrecacheOtherList( "CPrecacheOtherList" ); +#endif + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *szClassname - +// *modelName - +//----------------------------------------------------------------------------- +void UTIL_PrecacheOther( const char *szClassname, const char *modelName ) +{ +#if defined( PRECACHE_OTHER_ONCE ) + // already done this one?, if not, mark as done + if ( !g_PrecacheOtherList.AddOrMarkPrecached( szClassname ) ) + return; +#endif + + CBaseEntity *pEntity = CreateEntityByName( szClassname ); + if ( !pEntity ) + { + Warning( "NULL Ent in UTIL_PrecacheOther\n" ); + return; + } + + // If we have a specified model, set it before calling precache + if ( modelName && modelName[0] ) + { + pEntity->SetModelName( AllocPooledString( modelName ) ); + } + + if (pEntity) + pEntity->Precache( ); + + UTIL_RemoveImmediate( pEntity ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Tests whether this entity exists in the dictionary and if it does, precaches it. (as opposed to complaining when it's missing) +// Input : *szClassname - +// *modelName - +//----------------------------------------------------------------------------- +bool UTIL_TestPrecacheOther( const char *szClassname, const char *modelName ) +{ +#if defined( PRECACHE_OTHER_ONCE ) + // already done this one?, if not, mark as done + if ( !g_PrecacheOtherList.AddOrMarkPrecached( szClassname ) ) + return true; +#endif + + // If we can't create it, it probably does not exist + CBaseEntity *pEntity = CreateEntityByName( szClassname ); + if (!pEntity) + return false; + + // If we have a specified model, set it before calling precache + if ( modelName && modelName[0] ) + { + pEntity->SetModelName( AllocPooledString( modelName ) ); + } + + if (pEntity) + pEntity->Precache( ); + + UTIL_RemoveImmediate( pEntity ); + + return true; +} +#endif + +//========================================================= +// UTIL_LogPrintf - Prints a logged message to console. +// Preceded by LOG: ( timestamp ) < message > +//========================================================= +void UTIL_LogPrintf( const char *fmt, ... ) +{ + va_list argptr; + char tempString[1024]; + + va_start ( argptr, fmt ); + Q_vsnprintf( tempString, sizeof(tempString), fmt, argptr ); + va_end ( argptr ); + + // Print to server console + engine->LogPrint( tempString ); +} + +//========================================================= +// UTIL_DotPoints - returns the dot product of a line from +// src to check and vecdir. +//========================================================= +float UTIL_DotPoints ( const Vector &vecSrc, const Vector &vecCheck, const Vector &vecDir ) +{ + Vector2D vec2LOS; + + vec2LOS = ( vecCheck - vecSrc ).AsVector2D(); + Vector2DNormalize( vec2LOS ); + + return DotProduct2D(vec2LOS, vecDir.AsVector2D()); +} + + +//========================================================= +// UTIL_StripToken - for redundant keynames +//========================================================= +void UTIL_StripToken( const char *pKey, char *pDest ) +{ + int i = 0; + + while ( pKey[i] && pKey[i] != '#' ) + { + pDest[i] = pKey[i]; + i++; + } + pDest[i] = 0; +} + + +// computes gravity scale for an absolute gravity. Pass the result into CBaseEntity::SetGravity() +float UTIL_ScaleForGravity( float desiredGravity ) +{ + float worldGravity = GetCurrentGravity(); + return worldGravity > 0 ? desiredGravity / worldGravity : 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Implemented for mathlib.c error handling +// Input : *error - +//----------------------------------------------------------------------------- +extern "C" void Sys_Error( char *error, ... ) +{ + va_list argptr; + char string[1024]; + + va_start( argptr, error ); + Q_vsnprintf( string, sizeof(string), error, argptr ); + va_end( argptr ); + + Warning( "%s", string ); + Assert(0); +} + + +//----------------------------------------------------------------------------- +// Purpose: Spawns an entity into the game, initializing it with the map ent data block +// Input : *pEntity - the newly created entity +// *mapData - pointer a block of entity map data +// Output : -1 if the entity was not successfully created; 0 on success +//----------------------------------------------------------------------------- +int DispatchSpawn( CBaseEntity *pEntity, bool bRunVScripts ) +{ + if ( pEntity ) + { + MDLCACHE_CRITICAL_SECTION(); + + // keep a smart pointer that will now if the object gets deleted + EHANDLE pEntSafe; + pEntSafe = pEntity; + + // Initialize these or entities who don't link to the world won't have anything in here + // is this necessary? + //pEntity->SetAbsMins( pEntity->GetOrigin() - Vector(1,1,1) ); + //pEntity->SetAbsMaxs( pEntity->GetOrigin() + Vector(1,1,1) ); + + if (bRunVScripts) + { + pEntity->RunVScripts(); + pEntity->RunPrecacheScripts(); + } + +#if defined(TRACK_ENTITY_MEMORY) && defined(USE_MEM_DEBUG) + const char *pszClassname = NULL; + int iClassname = ((CEntityFactoryDictionary*)EntityFactoryDictionary())->m_Factories.Find( pEntity->GetClassname() ); + if ( iClassname != ((CEntityFactoryDictionary*)EntityFactoryDictionary())->m_Factories.InvalidIndex() ) + pszClassname = ((CEntityFactoryDictionary*)EntityFactoryDictionary())->m_Factories.GetElementName( iClassname ); + if ( pszClassname ) + { + MemAlloc_PushAllocDbgInfo( pszClassname, __LINE__ ); + } +#endif + bool bAsyncAnims = mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, false ); + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + if (!pAnimating) + { + pEntity->Spawn(); + } + else + { + // Don't allow the PVS check to skip animation setup during spawning + pAnimating->SetBoneCacheFlags( BCF_IS_IN_SPAWN ); + pEntity->Spawn(); + if ( pEntSafe != NULL ) + pAnimating->ClearBoneCacheFlags( BCF_IS_IN_SPAWN ); + } + mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, bAsyncAnims ); + +#if defined(TRACK_ENTITY_MEMORY) && defined(USE_MEM_DEBUG) + if ( pszClassname ) + { + MemAlloc_PopAllocDbgInfo(); + } +#endif + // Try to get the pointer again, in case the spawn function deleted the entity. + // UNDONE: Spawn() should really return a code to ask that the entity be deleted, but + // that would touch too much code for me to do that right now. + + if ( pEntSafe == NULL || pEntity->IsMarkedForDeletion() ) + return -1; + + if ( pEntity->m_iGlobalname != NULL_STRING ) + { + // Handle global stuff here + int globalIndex = GlobalEntity_GetIndex( pEntity->m_iGlobalname ); + if ( globalIndex >= 0 ) + { + // Already dead? delete + if ( GlobalEntity_GetState(globalIndex) == GLOBAL_DEAD ) + { + pEntity->Remove(); + return -1; + } + else if ( !FStrEq(STRING(gpGlobals->mapname), GlobalEntity_GetMap(globalIndex)) ) + { + pEntity->MakeDormant(); // Hasn't been moved to this level yet, wait but stay alive + } + // In this level & not dead, continue on as normal + } + else + { + // Spawned entities default to 'On' + GlobalEntity_Add( pEntity->m_iGlobalname, gpGlobals->mapname, GLOBAL_ON ); +// Msg( "Added global entity %s (%s)\n", pEntity->GetClassname(), STRING(pEntity->m_iGlobalname) ); + } + } + + gEntList.NotifySpawn( pEntity ); + + if( bRunVScripts ) + { + pEntity->RunOnPostSpawnScripts(); + } + } + + return 0; +} + +// UNDONE: This could be a better test - can we run the absbox through the bsp and see +// if it contains any solid space? or would that eliminate some entities we want to keep? +int UTIL_EntityInSolid( CBaseEntity *ent ) +{ + Vector point; + + CBaseEntity *pParent = ent->GetMoveParent(); + // HACKHACK -- If you're attached to a client, always go through + if ( pParent ) + { + if ( pParent->IsPlayer() ) + return 0; + + ent = ent->GetRootMoveParent(); + } + + point = ent->WorldSpaceCenter(); + return ( enginetrace->GetPointContents( point ) & MASK_SOLID ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Initialize the matrix from an entity +// Input : *pEntity - +//----------------------------------------------------------------------------- +void EntityMatrix::InitFromEntity( CBaseEntity *pEntity, int iAttachment ) +{ + if ( !pEntity ) + { + Identity(); + return; + } + + // Get an attachment's matrix? + if ( iAttachment != 0 ) + { + CBaseAnimating *pAnimating = pEntity->GetBaseAnimating(); + if ( pAnimating && pAnimating->GetModelPtr() ) + { + Vector vOrigin; + QAngle vAngles; + if ( pAnimating->GetAttachment( iAttachment, vOrigin, vAngles ) ) + { + ((VMatrix *)this)->SetupMatrixOrgAngles( vOrigin, vAngles ); + return; + } + } + } + + ((VMatrix *)this)->SetupMatrixOrgAngles( pEntity->GetAbsOrigin(), pEntity->GetAbsAngles() ); +} + + +void EntityMatrix::InitFromEntityLocal( CBaseEntity *entity ) +{ + if ( !entity || !entity->edict() ) + { + Identity(); + return; + } + ((VMatrix *)this)->SetupMatrixOrgAngles( entity->GetLocalOrigin(), entity->GetLocalAngles() ); +} + +//================================================== +// Purpose: +// Input: +// Output: +//================================================== + +void UTIL_ValidateSoundName( string_t &name, const char *defaultStr ) +{ + if ( ( !name || + strlen( (char*) STRING( name ) ) < 1 ) || + !Q_stricmp( (char *)STRING(name), "0" ) ) + { + name = AllocPooledString( defaultStr ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Slightly modified strtok. Does not modify the input string. Does +// not skip over more than one separator at a time. This allows parsing +// strings where tokens between separators may or may not be present: +// +// Door01,,,0 would be parsed as "Door01" "" "" "0" +// Door01,Open,,0 would be parsed as "Door01" "Open" "" "0" +// +// Input : token - Returns with a token, or zero length if the token was missing. +// str - String to parse. +// sep - Character to use as separator. UNDONE: allow multiple separator chars +// Output : Returns a pointer to the next token to be parsed. +//----------------------------------------------------------------------------- +const char *nexttoken(char *token, const char *str, char sep, size_t tokenLen) +{ + if ((str == NULL) || (*str == '\0')) + { + if(tokenLen) + { + *token = '\0'; + } + return(NULL); + } + + // + // Copy everything up to the first separator into the return buffer. + // Do not include separators in the return buffer. + // + while ((*str != sep) && (*str != '\0') && (tokenLen > 1)) + { + *token++ = *str++; + tokenLen--; + } + + // + // If token is to big for return buffer, skip rest of token. + // + while ((*str != sep) && (*str != '\0')) + { + str++; + } + + if(tokenLen) + { + *token = '\0'; + tokenLen--; + } + + // + // Advance the pointer unless we hit the end of the input string. + // + if (*str == '\0') + { + return(str); + } + + return(++str); +} + +//----------------------------------------------------------------------------- +// Purpose: Helper for UTIL_FindClientInPVS +// Input : check - last checked client +// Output : static int UTIL_GetNewCheckClient +//----------------------------------------------------------------------------- +// FIXME: include bspfile.h here? +class CCheckClient : public CAutoGameSystem +{ +public: + CCheckClient( char const *name ) : CAutoGameSystem( name ) + { + } + + void LevelInitPreEntity() + { + m_checkCluster = -1; + m_lastcheck = 1; + m_lastchecktime = -1; + m_bClientPVSIsExpanded = false; + } + + byte m_checkPVS[MAX_MAP_LEAFS/8]; + byte m_checkVisibilityPVS[MAX_MAP_LEAFS/8]; + int m_checkCluster; + int m_lastcheck; + float m_lastchecktime; + bool m_bClientPVSIsExpanded; +}; + +CCheckClient g_CheckClient( "CCheckClient" ); + + +static int UTIL_GetNewCheckClient( int check ) +{ + int i; + edict_t *ent; + Vector org; + +// cycle to the next one + + if (check < 1) + check = 1; + if (check > gpGlobals->maxClients) + check = gpGlobals->maxClients; + + if (check == gpGlobals->maxClients) + i = 1; + else + i = check + 1; + + for ( ; ; i++) + { + if ( i > gpGlobals->maxClients ) + { + i = 1; + } + + ent = engine->PEntityOfEntIndex( i ); + if ( !ent ) + continue; + + // Looped but didn't find anything else + if ( i == check ) + break; + + if ( !ent->GetUnknown() ) + continue; + + CBaseEntity *entity = GetContainingEntity( ent ); + if ( !entity ) + continue; + + if ( entity->GetFlags() & FL_NOTARGET ) + continue; + + // anything that is a client, or has a client as an enemy + break; + } + + if ( i != check ) + { + memset( g_CheckClient.m_checkVisibilityPVS, 0, sizeof(g_CheckClient.m_checkVisibilityPVS) ); + g_CheckClient.m_bClientPVSIsExpanded = false; + } + + if ( ent ) + { + // get the PVS for the entity + CBaseEntity *pce = GetContainingEntity( ent ); + if ( !pce ) + return i; + + org = pce->EyePosition(); + + int clusterIndex = engine->GetClusterForOrigin( org ); + if ( clusterIndex != g_CheckClient.m_checkCluster ) + { + g_CheckClient.m_checkCluster = clusterIndex; + engine->GetPVSForCluster( clusterIndex, sizeof(g_CheckClient.m_checkPVS), g_CheckClient.m_checkPVS ); + } + } + + return i; +} + + +//----------------------------------------------------------------------------- +// Gets the current check client.... +//----------------------------------------------------------------------------- +static edict_t *UTIL_GetCurrentCheckClient() +{ + edict_t *ent; + + // find a new check if on a new frame + float delta = gpGlobals->curtime - g_CheckClient.m_lastchecktime; + if ( delta >= 0.1 || delta < 0 ) + { + g_CheckClient.m_lastcheck = UTIL_GetNewCheckClient( g_CheckClient.m_lastcheck ); + g_CheckClient.m_lastchecktime = gpGlobals->curtime; + } + + // return check if it might be visible + ent = engine->PEntityOfEntIndex( g_CheckClient.m_lastcheck ); + + // Allow dead clients -- JAY + // Our monsters know the difference, and this function gates alot of behavior + // It's annoying to die and see monsters stop thinking because you're no longer + // "in" their PVS + if ( !ent || ent->IsFree() || !ent->GetUnknown()) + { + return NULL; + } + + return ent; +} + +void UTIL_SetClientVisibilityPVS( edict_t *pClient, const unsigned char *pvs, int pvssize ) +{ + if ( pClient == UTIL_GetCurrentCheckClient() ) + { + Assert( pvssize <= sizeof(g_CheckClient.m_checkVisibilityPVS) ); + + g_CheckClient.m_bClientPVSIsExpanded = false; + + unsigned *pFrom = (unsigned *)pvs; + unsigned *pMask = (unsigned *)g_CheckClient.m_checkPVS; + unsigned *pTo = (unsigned *)g_CheckClient.m_checkVisibilityPVS; + + int limit = pvssize / 4; + int i; + + for ( i = 0; i < limit; i++ ) + { + pTo[i] = pFrom[i] & ~pMask[i]; + + if ( pFrom[i] ) + { + g_CheckClient.m_bClientPVSIsExpanded = true; + } + } + + int remainder = pvssize % 4; + for ( i = 0; i < remainder; i++ ) + { + ((unsigned char *)&pTo[limit])[i] = ((unsigned char *)&pFrom[limit])[i] & !((unsigned char *)&pMask[limit])[i]; + + if ( ((unsigned char *)&pFrom[limit])[i] != 0) + { + g_CheckClient.m_bClientPVSIsExpanded = true; + } + } + } +} + +bool UTIL_ClientPVSIsExpanded() +{ + return g_CheckClient.m_bClientPVSIsExpanded; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns a client (or object that has a client enemy) that would be a valid target. +// If there are more than one valid options, they are cycled each frame +// If (self.origin + self.viewofs) is not in the PVS of the current target, it is not returned at all. +// Input : *pEdict - +// Output : edict_t* +//----------------------------------------------------------------------------- +CBaseEntity *UTIL_FindClientInPVS( const Vector &vecBoxMins, const Vector &vecBoxMaxs ) +{ + edict_t *ent = UTIL_GetCurrentCheckClient(); + if ( !ent ) + { + return NULL; + } + + if ( !engine->CheckBoxInPVS( vecBoxMins, vecBoxMaxs, g_CheckClient.m_checkPVS, sizeof( g_CheckClient.m_checkPVS ) ) ) + { + return NULL; + } + + // might be able to see it + return GetContainingEntity( ent ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a client (or object that has a client enemy) that would be a valid target. +// If there are more than one valid options, they are cycled each frame +// If (self.origin + self.viewofs) is not in the PVS of the current target, it is not returned at all. +// Input : *pEdict - +// Output : edict_t* +//----------------------------------------------------------------------------- +ConVar sv_strict_notarget( "sv_strict_notarget", "0", 0, "If set, notarget will cause entities to never think they are in the pvs" ); + +static edict_t *UTIL_FindClientInPVSGuts(edict_t *pEdict, unsigned char *pvs, unsigned pvssize ) +{ + Vector view; + + edict_t *ent = UTIL_GetCurrentCheckClient(); + if ( !ent ) + { + return NULL; + } + + CBaseEntity *pPlayerEntity = GetContainingEntity( ent ); + if( (!pPlayerEntity || (pPlayerEntity->GetFlags() & FL_NOTARGET)) && sv_strict_notarget.GetBool() ) + { + return NULL; + } + // if current entity can't possibly see the check entity, return 0 + // UNDONE: Build a box for this and do it over that box + // UNDONE: Use CM_BoxLeafnums() + CBaseEntity *pe = GetContainingEntity( pEdict ); + if ( pe ) + { + view = pe->EyePosition(); + + if ( !engine->CheckOriginInPVS( view, pvs, pvssize ) ) + { + return NULL; + } + } + + // might be able to see it + return ent; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a client that could see the entity directly +//----------------------------------------------------------------------------- + +edict_t *UTIL_FindClientInPVS(edict_t *pEdict) +{ + return UTIL_FindClientInPVSGuts( pEdict, g_CheckClient.m_checkPVS, sizeof( g_CheckClient.m_checkPVS ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a client that could see the entity, including through a camera +//----------------------------------------------------------------------------- +edict_t *UTIL_FindClientInVisibilityPVS( edict_t *pEdict ) +{ + return UTIL_FindClientInPVSGuts( pEdict, g_CheckClient.m_checkVisibilityPVS, sizeof( g_CheckClient.m_checkVisibilityPVS ) ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: Returns a chain of entities within the PVS of another entity (client) +// starting_ent is the ent currently at in the list +// a starting_ent of NULL signifies the beginning of a search +// Input : *pplayer - +// *starting_ent - +// Output : edict_t +//----------------------------------------------------------------------------- +CBaseEntity *UTIL_EntitiesInPVS( CBaseEntity *pPVSEntity, CBaseEntity *pStartingEntity ) +{ + Vector org; + static byte pvs[ MAX_MAP_CLUSTERS/8 ]; + static Vector lastOrg( 0, 0, 0 ); + static int lastCluster = -1; + + if ( !pPVSEntity ) + return NULL; + + // NOTE: These used to be caching code here to prevent this from + // being called over+over which breaks when you go back + forth + // across level transitions + // So, we'll always get the PVS each time we start a new EntitiesInPVS iteration. + // Given that weapon_binocs + leveltransition code is the only current clients + // of this, this seems safe. + if ( !pStartingEntity ) + { + org = pPVSEntity->EyePosition(); + int clusterIndex = engine->GetClusterForOrigin( org ); + Assert( clusterIndex >= 0 ); + engine->GetPVSForCluster( clusterIndex, sizeof(pvs), pvs ); + } + + for ( CBaseEntity *pEntity = gEntList.NextEnt(pStartingEntity); pEntity; pEntity = gEntList.NextEnt(pEntity) ) + { + // Only return attached ents. + if ( !pEntity->edict() ) + continue; + + CBaseEntity *pParent = pEntity->GetRootMoveParent(); + + Vector vecSurroundMins, vecSurroundMaxs; + pParent->CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); + if ( !engine->CheckBoxInPVS( vecSurroundMins, vecSurroundMaxs, pvs, sizeof( pvs ) ) ) + continue; + + return pEntity; + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the predicted postion of an entity of a certain number of seconds +// Use this function with caution, it has great potential for annoying the player, especially +// if used for target firing predition +// Input : *pTarget - target entity to predict +// timeDelta - amount of time to predict ahead (in seconds) +// &vecPredictedPosition - output +//----------------------------------------------------------------------------- +void UTIL_PredictedPosition( CBaseEntity *pTarget, float flTimeDelta, Vector *vecPredictedPosition ) +{ + if ( ( pTarget == NULL ) || ( vecPredictedPosition == NULL ) ) + return; + + Vector vecPredictedVel; + + //FIXME: Should we look at groundspeed or velocity for non-clients?? + + //Get the proper velocity to predict with + CBasePlayer *pPlayer = ToBasePlayer( pTarget ); + + //Player works differently than other entities + if ( pPlayer != NULL ) + { + if ( pPlayer->IsInAVehicle() ) + { + //Calculate the predicted position in this vehicle + vecPredictedVel = pPlayer->GetVehicleEntity()->GetSmoothedVelocity(); + } + else + { + //Get the player's stored velocity + vecPredictedVel = pPlayer->GetSmoothedVelocity(); + } + } + else + { + // See if we're a combat character in a vehicle + CBaseCombatCharacter *pCCTarget = pTarget->MyCombatCharacterPointer(); + if ( pCCTarget != NULL && pCCTarget->IsInAVehicle() ) + { + //Calculate the predicted position in this vehicle + vecPredictedVel = pCCTarget->GetVehicleEntity()->GetSmoothedVelocity(); + } + else + { + // See if we're an animating entity + CBaseAnimating *pAnimating = dynamic_cast(pTarget); + if ( pAnimating != NULL ) + { + vecPredictedVel = pAnimating->GetGroundSpeedVelocity(); +#ifdef MAPBASE + if (vecPredictedVel.IsZero()) + vecPredictedVel = pAnimating->GetSmoothedVelocity(); +#endif + } + else + { + // Otherwise we're a vanilla entity + vecPredictedVel = pTarget->GetSmoothedVelocity(); + } + } + } + + //Get the result + (*vecPredictedPosition) = pTarget->GetAbsOrigin() + ( vecPredictedVel * flTimeDelta ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Same as above, except you don't have to use the absolute origin and can use your own position to predict from. +//----------------------------------------------------------------------------- +void UTIL_PredictedPosition( CBaseEntity *pTarget, const Vector &vecActualPosition, float flTimeDelta, Vector *vecPredictedPosition ) +{ + if ( ( pTarget == NULL ) || ( vecPredictedPosition == NULL ) ) + return; + + Vector vecPredictedVel; + CBasePlayer *pPlayer = ToBasePlayer( pTarget ); + if ( pPlayer != NULL ) + { + if ( pPlayer->IsInAVehicle() ) + vecPredictedVel = pPlayer->GetVehicleEntity()->GetSmoothedVelocity(); + else + vecPredictedVel = pPlayer->GetSmoothedVelocity(); + } + else + { + CBaseCombatCharacter *pCCTarget = pTarget->MyCombatCharacterPointer(); + if ( pCCTarget != NULL && pCCTarget->IsInAVehicle() ) + vecPredictedVel = pCCTarget->GetVehicleEntity()->GetSmoothedVelocity(); + else + { + CBaseAnimating *pAnimating = dynamic_cast(pTarget); + if ( pAnimating != NULL ) + { + vecPredictedVel = pAnimating->GetGroundSpeedVelocity(); + if (vecPredictedVel.IsZero()) + vecPredictedVel = pAnimating->GetSmoothedVelocity(); + } + else + vecPredictedVel = pTarget->GetSmoothedVelocity(); + } + } + + // Get the result + (*vecPredictedPosition) = vecActualPosition + ( vecPredictedVel * flTimeDelta ); +} + +//----------------------------------------------------------------------------- +// Purpose: Predicts angles through angular velocity instead of predicting origin through regular velocity. +//----------------------------------------------------------------------------- +void UTIL_PredictedAngles( CBaseEntity *pTarget, const QAngle &angActualAngles, float flTimeDelta, QAngle *angPredictedAngles ) +{ + if ( ( pTarget == NULL ) || ( angPredictedAngles == NULL ) ) + return; + + QAngle angPredictedVel; + CBasePlayer *pPlayer = ToBasePlayer( pTarget ); + if ( pPlayer != NULL ) + { + if ( pPlayer->IsInAVehicle() ) + angPredictedVel = pPlayer->GetVehicleEntity()->GetLocalAngularVelocity(); + else + angPredictedVel = pPlayer->GetLocalAngularVelocity(); + } + else + { + CBaseCombatCharacter *pCCTarget = pTarget->MyCombatCharacterPointer(); + if ( pCCTarget != NULL && pCCTarget->IsInAVehicle() ) + angPredictedVel = pCCTarget->GetVehicleEntity()->GetLocalAngularVelocity(); + else + { + angPredictedVel = pTarget->GetLocalAngularVelocity(); + } + } + + // Get the result + (*angPredictedAngles) = angActualAngles + ( angPredictedVel * flTimeDelta ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Points the destination entity at the target entity +// Input : *pDest - entity to be pointed at the target +// *pTarget - target to point at +//----------------------------------------------------------------------------- +bool UTIL_PointAtEntity( CBaseEntity *pDest, CBaseEntity *pTarget ) +{ + if ( ( pDest == NULL ) || ( pTarget == NULL ) ) + { + return false; + } + + Vector dir = (pTarget->GetAbsOrigin() - pDest->GetAbsOrigin()); + + VectorNormalize( dir ); + + //Store off as angles + QAngle angles; + VectorAngles( dir, angles ); + pDest->SetLocalAngles( angles ); + pDest->SetAbsAngles( angles ); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Points the destination entity at the target entity by name +// Input : *pDest - entity to be pointed at the target +// strTarget - name of entity to target (will only choose the first!) +//----------------------------------------------------------------------------- +void UTIL_PointAtNamedEntity( CBaseEntity *pDest, string_t strTarget ) +{ + //Attempt to find the entity + if ( !UTIL_PointAtEntity( pDest, gEntList.FindEntityByName( NULL, strTarget ) ) ) + { + DevMsg( 1, "%s (%s) was unable to point at an entity named: %s\n", pDest->GetClassname(), pDest->GetDebugName(), STRING( strTarget ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Copy the pose parameter values from one entity to the other +// Input : *pSourceEntity - entity to copy from +// *pDestEntity - entity to copy to +//----------------------------------------------------------------------------- +bool UTIL_TransferPoseParameters( CBaseEntity *pSourceEntity, CBaseEntity *pDestEntity ) +{ + CBaseAnimating *pSourceBaseAnimating = dynamic_cast( pSourceEntity ); + CBaseAnimating *pDestBaseAnimating = dynamic_cast( pDestEntity ); + + if ( !pSourceBaseAnimating || !pDestBaseAnimating ) + return false; + + for ( int iPose = 0; iPose < MAXSTUDIOPOSEPARAM; ++iPose ) + { + pDestBaseAnimating->SetPoseParameter( iPose, pSourceBaseAnimating->GetPoseParameter( iPose ) ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Make a muzzle flash appear +// Input : &origin - position of the muzzle flash +// &angles - angles of the fire direction +// scale - scale of the muzzle flash +// type - type of muzzle flash +//----------------------------------------------------------------------------- +void UTIL_MuzzleFlash( const Vector &origin, const QAngle &angles, int scale, int type ) +{ + CPASFilter filter( origin ); + + te->MuzzleFlash( filter, 0.0f, origin, angles, scale, type ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : vStartPos - start of the line +// vEndPos - end of the line +// vPoint - point to find nearest point to on specified line +// clampEnds - clamps returned points to being on the line segment specified +// Output : Vector - nearest point on the specified line +//----------------------------------------------------------------------------- +Vector UTIL_PointOnLineNearestPoint(const Vector& vStartPos, const Vector& vEndPos, const Vector& vPoint, bool clampEnds ) +{ + Vector vEndToStart = (vEndPos - vStartPos); + Vector vOrgToStart = (vPoint - vStartPos); + float fNumerator = DotProduct(vEndToStart,vOrgToStart); + float fDenominator = vEndToStart.Length() * vOrgToStart.Length(); + float fIntersectDist = vOrgToStart.Length()*(fNumerator/fDenominator); + float flLineLength = VectorNormalize( vEndToStart ); + + if ( clampEnds ) + { + fIntersectDist = clamp( fIntersectDist, 0.0f, flLineLength ); + } + + Vector vIntersectPos = vStartPos + vEndToStart * fIntersectDist; + + return vIntersectPos; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +AngularImpulse WorldToLocalRotation( const VMatrix &localToWorld, const Vector &worldAxis, float rotation ) +{ + // fix axes of rotation to match axes of vector + Vector rot = worldAxis * rotation; + // since the matrix maps local to world, do a transpose rotation to get world to local + AngularImpulse ang = localToWorld.VMul3x3Transpose( rot ); + + return ang; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *filename - +// *pLength - +// Output : byte +//----------------------------------------------------------------------------- +byte *UTIL_LoadFileForMe( const char *filename, int *pLength ) +{ + void *buffer = NULL; + + int length = filesystem->ReadFileEx( filename, "GAME", &buffer, true, true ); + + if ( pLength ) + { + *pLength = length; + } + + return (byte *)buffer; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *buffer - +//----------------------------------------------------------------------------- +void UTIL_FreeFile( byte *buffer ) +{ + filesystem->FreeOptimalReadBuffer( buffer ); +} + +//----------------------------------------------------------------------------- +// Purpose: Determines whether an entity is within a certain angular tolerance to viewer +// Input : *pEntity - entity which is the "viewer" +// vecPosition - position to test against +// flTolerance - tolerance (as dot-product) +// *pflDot - if not NULL, holds the +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool UTIL_IsFacingWithinTolerance( CBaseEntity *pViewer, const Vector &vecPosition, float flDotTolerance, float *pflDot /*= NULL*/ ) +{ + if ( pflDot ) + { + *pflDot = 0.0f; + } + + // Required elements + if ( pViewer == NULL ) + return false; + + Vector forward; + pViewer->GetVectors( &forward, NULL, NULL ); + + Vector dir = vecPosition - pViewer->GetAbsOrigin(); + VectorNormalize( dir ); + + // Larger dot product corresponds to a smaller angle + float flDot = dir.Dot( forward ); + + // Return the result + if ( pflDot ) + { + *pflDot = flDot; + } + + // Within the goal tolerance + if ( flDot >= flDotTolerance ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Determines whether an entity is within a certain angular tolerance to viewer +// Input : *pEntity - entity which is the "viewer" +// *pTarget - entity to test against +// flTolerance - tolerance (as dot-product) +// *pflDot - if not NULL, holds the +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool UTIL_IsFacingWithinTolerance( CBaseEntity *pViewer, CBaseEntity *pTarget, float flDotTolerance, float *pflDot /*= NULL*/ ) +{ + if ( pViewer == NULL || pTarget == NULL ) + return false; + + return UTIL_IsFacingWithinTolerance( pViewer, pTarget->GetAbsOrigin(), flDotTolerance, pflDot ); +} + +//----------------------------------------------------------------------------- +// Purpose: Fills in color for debug purposes based on a relationship +// Input : nRelationship - relationship to test +// *pR, *pG, *pB - colors to fill +//----------------------------------------------------------------------------- +void UTIL_GetDebugColorForRelationship( int nRelationship, int &r, int &g, int &b ) +{ + switch ( nRelationship ) + { + case D_LI: + r = 0; + g = 255; + b = 0; + break; + case D_NU: + r = 0; + g = 0; + b = 255; + break; + case D_HT: + r = 255; + g = 0; + b = 0; + break; + case D_FR: + r = 255; + g = 255; + b = 0; + break; + default: + r = 255; + g = 255; + b = 255; + break; + } +} + +void LoadAndSpawnEntities_ParseEntKVBlockHelper( CBaseEntity *pNode, KeyValues *pkvNode ) +{ + KeyValues *pkvNodeData = pkvNode->GetFirstSubKey(); + while ( pkvNodeData ) + { + // Handle the connections block + if ( !Q_strcmp(pkvNodeData->GetName(), "connections") ) + { + LoadAndSpawnEntities_ParseEntKVBlockHelper( pNode, pkvNodeData ); + } + else + { + pNode->KeyValue( pkvNodeData->GetName(), pkvNodeData->GetString() ); + } + + pkvNodeData = pkvNodeData->GetNextKey(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Loads and parses a file and spawns entities defined in it. +//----------------------------------------------------------------------------- +bool UTIL_LoadAndSpawnEntitiesFromScript( CUtlVector &entities, const char *pScriptFile, const char *pBlock, bool bActivate ) +{ + KeyValues *pkvFile = new KeyValues( pBlock ); + + if ( pkvFile->LoadFromFile( filesystem, pScriptFile, "MOD" ) ) + { + // Load each block, and spawn the entities + KeyValues *pkvNode = pkvFile->GetFirstSubKey(); + while ( pkvNode ) + { + // Get name + const char *pNodeName = pkvNode->GetName(); + + if ( stricmp( pNodeName, "entity" ) ) + { + pkvNode = pkvNode->GetNextKey(); + continue; + } + + KeyValues *pClassname = pkvNode->FindKey( "classname" ); + + if ( pClassname ) + { + // Use the classname instead + pNodeName = pClassname->GetString(); + } + + // Spawn the entity + CBaseEntity *pNode = CreateEntityByName( pNodeName ); + + if ( pNode ) + { + LoadAndSpawnEntities_ParseEntKVBlockHelper( pNode, pkvNode ); + DispatchSpawn( pNode ); + entities.AddToTail( pNode ); + } + else + { + Warning( "UTIL_LoadAndSpawnEntitiesFromScript: Failed to spawn entity, type: '%s'\n", pNodeName ); + } + + // Move to next entity + pkvNode = pkvNode->GetNextKey(); + } + + if ( bActivate == true ) + { + bool bAsyncAnims = mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, false ); + // Then activate all the entities + for ( int i = 0; i < entities.Count(); i++ ) + { + entities[i]->Activate(); + } + mdlcache->SetAsyncLoad( MDLCACHE_ANIMBLOCK, bAsyncAnims ); + } + } + else + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Convert a vector an angle from worldspace to the entity's parent's local space +// Input : *pEntity - Entity whose parent we're concerned with +//----------------------------------------------------------------------------- +void UTIL_ParentToWorldSpace( CBaseEntity *pEntity, Vector &vecPosition, QAngle &vecAngles ) +{ + if ( pEntity == NULL ) + return; + + // Construct the entity-to-world matrix + // Start with making an entity-to-parent matrix + matrix3x4_t matEntityToParent; + AngleMatrix( vecAngles, matEntityToParent ); + MatrixSetColumn( vecPosition, 3, matEntityToParent ); + + // concatenate with our parent's transform + matrix3x4_t matScratch, matResult; + matrix3x4_t matParentToWorld; + + if ( pEntity->GetParent() != NULL ) + { + matParentToWorld = pEntity->GetParentToWorldTransform( matScratch ); + } + else + { + matParentToWorld = pEntity->EntityToWorldTransform(); + } + + ConcatTransforms( matParentToWorld, matEntityToParent, matResult ); + + // pull our absolute position out of the matrix + MatrixGetColumn( matResult, 3, vecPosition ); + MatrixAngles( matResult, vecAngles ); +} + +//----------------------------------------------------------------------------- +// Purpose: Convert a vector and quaternion from worldspace to the entity's parent's local space +// Input : *pEntity - Entity whose parent we're concerned with +//----------------------------------------------------------------------------- +void UTIL_ParentToWorldSpace( CBaseEntity *pEntity, Vector &vecPosition, Quaternion &quat ) +{ + if ( pEntity == NULL ) + return; + + QAngle vecAngles; + QuaternionAngles( quat, vecAngles ); + UTIL_ParentToWorldSpace( pEntity, vecPosition, vecAngles ); + AngleQuaternion( vecAngles, quat ); +} + +//----------------------------------------------------------------------------- +// Purpose: Convert a vector an angle from worldspace to the entity's parent's local space +// Input : *pEntity - Entity whose parent we're concerned with +//----------------------------------------------------------------------------- +void UTIL_WorldToParentSpace( CBaseEntity *pEntity, Vector &vecPosition, QAngle &vecAngles ) +{ + if ( pEntity == NULL ) + return; + + // Construct the entity-to-world matrix + // Start with making an entity-to-parent matrix + matrix3x4_t matEntityToParent; + AngleMatrix( vecAngles, matEntityToParent ); + MatrixSetColumn( vecPosition, 3, matEntityToParent ); + + // concatenate with our parent's transform + matrix3x4_t matScratch, matResult; + matrix3x4_t matWorldToParent; + + if ( pEntity->GetParent() != NULL ) + { + matScratch = pEntity->GetParentToWorldTransform( matScratch ); + } + else + { + matScratch = pEntity->EntityToWorldTransform(); + } + + MatrixInvert( matScratch, matWorldToParent ); + ConcatTransforms( matWorldToParent, matEntityToParent, matResult ); + + // pull our absolute position out of the matrix + MatrixGetColumn( matResult, 3, vecPosition ); + MatrixAngles( matResult, vecAngles ); +} + +//----------------------------------------------------------------------------- +// Purpose: Convert a vector and quaternion from worldspace to the entity's parent's local space +// Input : *pEntity - Entity whose parent we're concerned with +//----------------------------------------------------------------------------- +void UTIL_WorldToParentSpace( CBaseEntity *pEntity, Vector &vecPosition, Quaternion &quat ) +{ + if ( pEntity == NULL ) + return; + + QAngle vecAngles; + QuaternionAngles( quat, vecAngles ); + UTIL_WorldToParentSpace( pEntity, vecPosition, vecAngles ); + AngleQuaternion( vecAngles, quat ); +} + +//----------------------------------------------------------------------------- +// Purpose: Given a vector, clamps the scalar axes to MAX_COORD_FLOAT ranges from worldsize.h +// Input : *pVecPos - +//----------------------------------------------------------------------------- +void UTIL_BoundToWorldSize( Vector *pVecPos ) +{ + Assert( pVecPos ); + for ( int i = 0; i < 3; ++i ) + { + (*pVecPos)[ i ] = clamp( (*pVecPos)[ i ], MIN_COORD_FLOAT, MAX_COORD_FLOAT ); + } +} + +//============================================================================= +// +// Tests! +// + +#define NUM_KDTREE_TESTS 2500 +#define NUM_KDTREE_ENTITY_SIZE 256 + +void CC_KDTreeTest( const CCommand &args ) +{ + Msg( "Testing kd-tree entity queries." ); + + // Get the testing spot. +// CBaseEntity *pSpot = gEntList.FindEntityByClassname( NULL, "info_player_start" ); +// Vector vecStart = pSpot->GetAbsOrigin(); + + CBasePlayer *pPlayer = static_cast( UTIL_GetLocalPlayer() ); + Vector vecStart = pPlayer->GetAbsOrigin(); + + static Vector *vecTargets = NULL; + static bool bFirst = true; + + // Generate the targets - rays (1K long). + if ( bFirst ) + { + vecTargets = new Vector [NUM_KDTREE_TESTS]; + double flRadius = 0; + double flTheta = 0; + double flPhi = 0; + for ( int i = 0; i < NUM_KDTREE_TESTS; ++i ) + { + flRadius += NUM_KDTREE_TESTS * 123.123; + flRadius = fmod( flRadius, 128.0 ); + flRadius = fabs( flRadius ); + + flTheta += NUM_KDTREE_TESTS * 76.76; + flTheta = fmod( flTheta, (double) DEG2RAD( 360 ) ); + flTheta = fabs( flTheta ); + + flPhi += NUM_KDTREE_TESTS * 1997.99; + flPhi = fmod( flPhi, (double) DEG2RAD( 180 ) ); + flPhi = fabs( flPhi ); + + float st, ct, sp, cp; + SinCos( flTheta, &st, &ct ); + SinCos( flPhi, &sp, &cp ); + + vecTargets[i].x = flRadius * ct * sp; + vecTargets[i].y = flRadius * st * sp; + vecTargets[i].z = flRadius * cp; + + // Make the trace 1024 units long. + Vector vecDir = vecTargets[i] - vecStart; + VectorNormalize( vecDir ); + vecTargets[i] = vecStart + vecDir * 1024; + } + + bFirst = false; + } + + int nTestType = 0; + if ( args.ArgC() >= 2 ) + { + nTestType = atoi( args[ 1 ] ); + } + + vtune( true ); + +#ifdef VPROF_ENABLED + g_VProfCurrentProfile.Resume(); + g_VProfCurrentProfile.Start(); + g_VProfCurrentProfile.Reset(); + g_VProfCurrentProfile.MarkFrame(); +#endif + + switch ( nTestType ) + { + case 0: + { + VPROF( "TraceTotal" ); + + trace_t trace; + for ( int iTest = 0; iTest < NUM_KDTREE_TESTS; ++iTest ) + { + UTIL_TraceLine( vecStart, vecTargets[iTest], MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &trace ); + } + break; + } + case 1: + { + VPROF( "TraceTotal" ); + + trace_t trace; + for ( int iTest = 0; iTest < NUM_KDTREE_TESTS; ++iTest ) + { + UTIL_TraceHull( vecStart, vecTargets[iTest], VEC_HULL_MIN_SCALED( pPlayer ), VEC_HULL_MAX_SCALED( pPlayer ), MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &trace ); + } + break; + } + case 2: + { + Vector vecMins[NUM_KDTREE_TESTS]; + Vector vecMaxs[NUM_KDTREE_TESTS]; + int iTest; + for ( iTest = 0; iTest < NUM_KDTREE_TESTS; ++iTest ) + { + vecMins[iTest] = vecStart; + vecMaxs[iTest] = vecStart; + for ( int iAxis = 0; iAxis < 3; ++iAxis ) + { + if ( vecTargets[iTest].x < vecMins[iTest].x ) { vecMins[iTest].x = vecTargets[iTest].x; } + if ( vecTargets[iTest].y < vecMins[iTest].y ) { vecMins[iTest].y = vecTargets[iTest].y; } + if ( vecTargets[iTest].z < vecMins[iTest].z ) { vecMins[iTest].z = vecTargets[iTest].z; } + + if ( vecTargets[iTest].x > vecMaxs[iTest].x ) { vecMaxs[iTest].x = vecTargets[iTest].x; } + if ( vecTargets[iTest].y > vecMaxs[iTest].y ) { vecMaxs[iTest].y = vecTargets[iTest].y; } + if ( vecTargets[iTest].z > vecMaxs[iTest].z ) { vecMaxs[iTest].z = vecTargets[iTest].z; } + } + } + + + VPROF( "TraceTotal" ); + + int nCount = 0; + + Vector vecDelta; + trace_t trace; + CBaseEntity *pList[1024]; + for ( iTest = 0; iTest < NUM_KDTREE_TESTS; ++iTest ) + { + nCount += UTIL_EntitiesInBox( pList, 1024, vecMins[iTest], vecMaxs[iTest], 0 ); + } + + Msg( "Count = %d\n", nCount ); + break; + } + case 3: + { + Vector vecDelta; + float flRadius[NUM_KDTREE_TESTS]; + int iTest; + for ( iTest = 0; iTest < NUM_KDTREE_TESTS; ++iTest ) + { + VectorSubtract( vecTargets[iTest], vecStart, vecDelta ); + flRadius[iTest] = vecDelta.Length() * 0.5f; + } + + VPROF( "TraceTotal" ); + + int nCount = 0; + + trace_t trace; + CBaseEntity *pList[1024]; + for ( iTest = 0; iTest < NUM_KDTREE_TESTS; ++iTest ) + { + nCount += UTIL_EntitiesInSphere( pList, 1024, vecStart, flRadius[iTest], 0 ); + } + + Msg( "Count = %d\n", nCount ); + break; + } + default: + { + break; + } + } + +#ifdef VPROF_ENABLED + g_VProfCurrentProfile.MarkFrame(); + g_VProfCurrentProfile.Pause(); + g_VProfCurrentProfile.OutputReport( VPRT_FULL ); +#endif + + vtune( false ); +} + +static ConCommand kdtree_test( "kdtree_test", CC_KDTreeTest, "Tests spatial partition for entities queries.", FCVAR_CHEAT ); + +void CC_VoxelTreeView( void ) +{ + Msg( "VoxelTreeView\n" ); + partition->RenderAllObjectsInTree( 10.0f ); +} + +static ConCommand voxeltree_view( "voxeltree_view", CC_VoxelTreeView, "View entities in the voxel-tree.", FCVAR_CHEAT ); + +void CC_VoxelTreePlayerView( void ) +{ + Msg( "VoxelTreePlayerView\n" ); + + CBasePlayer *pPlayer = static_cast( UTIL_GetLocalPlayer() ); + Vector vecStart = pPlayer->GetAbsOrigin(); + partition->RenderObjectsInPlayerLeafs( vecStart - VEC_HULL_MIN_SCALED( pPlayer ), vecStart + VEC_HULL_MAX_SCALED( pPlayer ), 3.0f ); +} + +static ConCommand voxeltree_playerview( "voxeltree_playerview", CC_VoxelTreePlayerView, "View entities in the voxel-tree at the player position.", FCVAR_CHEAT ); + +void CC_VoxelTreeBox( const CCommand &args ) +{ + Vector vecMin, vecMax; + if ( args.ArgC() >= 6 ) + { + vecMin.x = atof( args[ 1 ] ); + vecMin.y = atof( args[ 2 ] ); + vecMin.z = atof( args[ 3 ] ); + + vecMax.x = atof( args[ 4 ] ); + vecMax.y = atof( args[ 5 ] ); + vecMax.z = atof( args[ 6 ] ); + } + else + { + return; + } + + float flTime = 10.0f; + + Vector vecPoints[8]; + vecPoints[0].Init( vecMin.x, vecMin.y, vecMin.z ); + vecPoints[1].Init( vecMin.x, vecMax.y, vecMin.z ); + vecPoints[2].Init( vecMax.x, vecMax.y, vecMin.z ); + vecPoints[3].Init( vecMax.x, vecMin.y, vecMin.z ); + vecPoints[4].Init( vecMin.x, vecMin.y, vecMax.z ); + vecPoints[5].Init( vecMin.x, vecMax.y, vecMax.z ); + vecPoints[6].Init( vecMax.x, vecMax.y, vecMax.z ); + vecPoints[7].Init( vecMax.x, vecMin.y, vecMax.z ); + + debugoverlay->AddLineOverlay( vecPoints[0], vecPoints[1], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[1], vecPoints[2], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[2], vecPoints[3], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[3], vecPoints[0], 255, 0, 0, true, flTime ); + + debugoverlay->AddLineOverlay( vecPoints[4], vecPoints[5], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[5], vecPoints[6], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[6], vecPoints[7], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[7], vecPoints[4], 255, 0, 0, true, flTime ); + + debugoverlay->AddLineOverlay( vecPoints[0], vecPoints[4], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[3], vecPoints[7], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[1], vecPoints[5], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[2], vecPoints[6], 255, 0, 0, true, flTime ); + + Msg( "VoxelTreeBox - (%f %f %f) to (%f %f %f)\n", vecMin.x, vecMin.y, vecMin.z, vecMax.x, vecMax.y, vecMax.z ); + partition->RenderObjectsInBox( vecMin, vecMax, flTime ); +} + +static ConCommand voxeltree_box( "voxeltree_box", CC_VoxelTreeBox, "View entities in the voxel-tree inside box .", FCVAR_CHEAT ); + +void CC_VoxelTreeSphere( const CCommand &args ) +{ + Vector vecCenter; + float flRadius; + if ( args.ArgC() >= 4 ) + { + vecCenter.x = atof( args[ 1 ] ); + vecCenter.y = atof( args[ 2 ] ); + vecCenter.z = atof( args[ 3 ] ); + + flRadius = atof( args[ 3 ] ); + } + else + { + return; + } + + float flTime = 3.0f; + + Vector vecMin, vecMax; + vecMin.Init( vecCenter.x - flRadius, vecCenter.y - flRadius, vecCenter.z - flRadius ); + vecMax.Init( vecCenter.x + flRadius, vecCenter.y + flRadius, vecCenter.z + flRadius ); + + Vector vecPoints[8]; + vecPoints[0].Init( vecMin.x, vecMin.y, vecMin.z ); + vecPoints[1].Init( vecMin.x, vecMax.y, vecMin.z ); + vecPoints[2].Init( vecMax.x, vecMax.y, vecMin.z ); + vecPoints[3].Init( vecMax.x, vecMin.y, vecMin.z ); + vecPoints[4].Init( vecMin.x, vecMin.y, vecMax.z ); + vecPoints[5].Init( vecMin.x, vecMax.y, vecMax.z ); + vecPoints[6].Init( vecMax.x, vecMax.y, vecMax.z ); + vecPoints[7].Init( vecMax.x, vecMin.y, vecMax.z ); + + debugoverlay->AddLineOverlay( vecPoints[0], vecPoints[1], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[1], vecPoints[2], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[2], vecPoints[3], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[3], vecPoints[0], 255, 0, 0, true, flTime ); + + debugoverlay->AddLineOverlay( vecPoints[4], vecPoints[5], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[5], vecPoints[6], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[6], vecPoints[7], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[7], vecPoints[4], 255, 0, 0, true, flTime ); + + debugoverlay->AddLineOverlay( vecPoints[0], vecPoints[4], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[3], vecPoints[7], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[1], vecPoints[5], 255, 0, 0, true, flTime ); + debugoverlay->AddLineOverlay( vecPoints[2], vecPoints[6], 255, 0, 0, true, flTime ); + + Msg( "VoxelTreeSphere - (%f %f %f), %f\n", vecCenter.x, vecCenter.y, vecCenter.z, flRadius ); + partition->RenderObjectsInSphere( vecCenter, flRadius, flTime ); +} + +static ConCommand voxeltree_sphere( "voxeltree_sphere", CC_VoxelTreeSphere, "View entities in the voxel-tree inside sphere .", FCVAR_CHEAT ); + + + +#define NUM_COLLISION_TESTS 2500 +void CC_CollisionTest( const CCommand &args ) +{ + if ( !physenv ) + return; + + Msg( "Testing collision system\n" ); + partition->ReportStats( "" ); + int i; + CBaseEntity *pSpot = gEntList.FindEntityByClassname( NULL, "info_player_start"); + Vector start = pSpot->GetAbsOrigin(); + static Vector *targets = NULL; + static bool first = true; + static float test[2] = {1,1}; + if ( first ) + { + targets = new Vector[NUM_COLLISION_TESTS]; + float radius = 0; + float theta = 0; + float phi = 0; + for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) + { + radius += NUM_COLLISION_TESTS * 123.123; + radius = fabs(fmod(radius, 128)); + theta += NUM_COLLISION_TESTS * 76.76; + theta = fabs(fmod(theta, DEG2RAD(360))); + phi += NUM_COLLISION_TESTS * 1997.99; + phi = fabs(fmod(phi, DEG2RAD(180))); + + float st, ct, sp, cp; + SinCos( theta, &st, &ct ); + SinCos( phi, &sp, &cp ); + + targets[i].x = radius * ct * sp; + targets[i].y = radius * st * sp; + targets[i].z = radius * cp; + + // make the trace 1024 units long + Vector dir = targets[i] - start; + VectorNormalize(dir); + targets[i] = start + dir * 1024; + } + first = false; + } + + //Vector results[NUM_COLLISION_TESTS]; + + int testType = 0; + if ( args.ArgC() >= 2 ) + { + testType = atoi(args[1]); + } + float duration = 0; + Vector size[2]; + size[0].Init(0,0,0); + size[1].Init(16,16,16); + unsigned int dots = 0; + int nMask = MASK_ALL & ~(CONTENTS_MONSTER | CONTENTS_HITBOX ); + for ( int j = 0; j < 2; j++ ) + { + float startTime = engine->Time(); + if ( testType == 1 ) + { + trace_t tr; + for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) + { + UTIL_TraceHull( start, targets[i], -size[1], size[1], nMask, NULL, COLLISION_GROUP_NONE, &tr ); + } + } + else + { + testType = 0; + trace_t tr; + + for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) + { + if ( i == 0 ) + { + partition->RenderLeafsForRayTraceStart( 10.0f ); + } + + UTIL_TraceLine( start, targets[i], nMask, NULL, COLLISION_GROUP_NONE, &tr ); + + if ( i == 0 ) + { + partition->RenderLeafsForRayTraceEnd( ); + } + } + } + + duration += engine->Time() - startTime; + } + test[testType] = duration; + Msg("%d collisions in %.2f ms (%u dots)\n", NUM_COLLISION_TESTS, duration*1000, dots ); + partition->ReportStats( "" ); +#if 1 + int red = 255, green = 0, blue = 0; + for ( i = 0; i < 1 /*NUM_COLLISION_TESTS*/; i++ ) + { + NDebugOverlay::Line( start, targets[i], red, green, blue, false, 2 ); + } +#endif +} +static ConCommand collision_test("collision_test", CC_CollisionTest, "Tests collision system", FCVAR_CHEAT ); + + + + diff --git a/sp/src/game/server/util.h b/sp/src/game/server/util.h new file mode 100644 index 00000000..82f485ba --- /dev/null +++ b/sp/src/game/server/util.h @@ -0,0 +1,684 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Misc utility code. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef UTIL_H +#define UTIL_H +#ifdef _WIN32 +#pragma once +#endif + +#include "ai_activity.h" +#include "steam/steam_gameserver.h" +#include "enginecallback.h" +#include "basetypes.h" +#include "tempentity.h" +#include "string_t.h" +#include "gamestringpool.h" +#include "engine/IEngineTrace.h" +#include "worldsize.h" +#include "dt_send.h" +#include "server_class.h" +#include "shake.h" + +#include "vstdlib/random.h" +#include + +#include "utlvector.h" +#include "util_shared.h" +#include "shareddefs.h" +#include "networkvar.h" + +struct levellist_t; +class IServerNetworkable; +class IEntityFactory; + +#ifdef _WIN32 + #define SETUP_EXTERNC(mapClassName)\ + extern "C" _declspec( dllexport ) IServerNetworkable* mapClassName( void ); +#else + #define SETUP_EXTERNC(mapClassName) +#endif + +// +// How did I ever live without ASSERT? +// +#ifdef DEBUG +void DBG_AssertFunction(bool fExpr, const char* szExpr, const char* szFile, int szLine, const char* szMessage); +#define ASSERT(f) DBG_AssertFunction((bool)((f)!=0), #f, __FILE__, __LINE__, NULL) +#define ASSERTSZ(f, sz) DBG_AssertFunction((bool)((f)!=0), #f, __FILE__, __LINE__, sz) +#else // !DEBUG +#define ASSERT(f) +#define ASSERTSZ(f, sz) +#endif // !DEBUG + +#include "tier0/memdbgon.h" + +// entity creation +// creates an entity that has not been linked to a classname +template< class T > +T *_CreateEntityTemplate( T *newEnt, const char *className ) +{ + newEnt = new T; // this is the only place 'new' should be used! + newEnt->PostConstructor( className ); + return newEnt; +} + +#include "tier0/memdbgoff.h" + +CBaseEntity *CreateEntityByName( const char *className, int iForceEdictIndex ); + +// creates an entity by name, and ensure it's correctness +// does not spawn the entity +// use the CREATE_ENTITY() macro which wraps this, instead of using it directly +template< class T > +T *_CreateEntity( T *newClass, const char *className ) +{ + T *newEnt = dynamic_cast( CreateEntityByName(className, -1) ); + if ( !newEnt ) + { + Warning( "classname %s used to create wrong class type\n", className ); + Assert(0); + } + + return newEnt; +} + +#define CREATE_ENTITY( newClass, className ) _CreateEntity( (newClass*)NULL, className ) +#define CREATE_UNSAVED_ENTITY( newClass, className ) _CreateEntityTemplate( (newClass*)NULL, className ) + + +// This is the glue that hooks .MAP entity class names to our CPP classes +abstract_class IEntityFactoryDictionary +{ +public: + virtual void InstallFactory( IEntityFactory *pFactory, const char *pClassName ) = 0; + virtual IServerNetworkable *Create( const char *pClassName ) = 0; + virtual void Destroy( const char *pClassName, IServerNetworkable *pNetworkable ) = 0; + virtual IEntityFactory *FindFactory( const char *pClassName ) = 0; + virtual const char *GetCannonicalName( const char *pClassName ) = 0; +}; + +IEntityFactoryDictionary *EntityFactoryDictionary(); + +inline bool CanCreateEntityClass( const char *pszClassname ) +{ + return ( EntityFactoryDictionary() != NULL && EntityFactoryDictionary()->FindFactory( pszClassname ) != NULL ); +} + +abstract_class IEntityFactory +{ +public: + virtual IServerNetworkable *Create( const char *pClassName ) = 0; + virtual void Destroy( IServerNetworkable *pNetworkable ) = 0; + virtual size_t GetEntitySize() = 0; +}; + +template +class CEntityFactory : public IEntityFactory +{ +public: + CEntityFactory( const char *pClassName ) + { + EntityFactoryDictionary()->InstallFactory( this, pClassName ); + } + + IServerNetworkable *Create( const char *pClassName ) + { + T* pEnt = _CreateEntityTemplate((T*)NULL, pClassName); + return pEnt->NetworkProp(); + } + + void Destroy( IServerNetworkable *pNetworkable ) + { + if ( pNetworkable ) + { + pNetworkable->Release(); + } + } + + virtual size_t GetEntitySize() + { + return sizeof(T); + } +}; + +#define LINK_ENTITY_TO_CLASS(mapClassName,DLLClassName) \ + static CEntityFactory mapClassName( #mapClassName ); + + +// +// Conversion among the three types of "entity", including identity-conversions. +// +inline int ENTINDEX( edict_t *pEdict) +{ + int nResult = pEdict ? pEdict->m_EdictIndex : 0; + Assert( nResult == engine->IndexOfEdict(pEdict) ); + return nResult; +} + +int ENTINDEX( CBaseEntity *pEnt ); + +inline edict_t* INDEXENT( int iEdictNum ) +{ + return engine->PEntityOfEntIndex(iEdictNum); +} + +// Testing the three types of "entity" for nullity +inline bool FNullEnt(const edict_t* pent) +{ + return pent == NULL || ENTINDEX((edict_t*)pent) == 0; +} + +// Dot products for view cone checking +#define VIEW_FIELD_FULL (float)-1.0 // +-180 degrees +#define VIEW_FIELD_WIDE (float)-0.7 // +-135 degrees 0.1 // +-85 degrees, used for full FOV checks +#define VIEW_FIELD_NARROW (float)0.7 // +-45 degrees, more narrow check used to set up ranged attacks +#define VIEW_FIELD_ULTRA_NARROW (float)0.9 // +-25 degrees, more narrow check used to set up ranged attacks + +class CBaseEntity; +class CBasePlayer; + +extern CGlobalVars *gpGlobals; + +// Misc useful +inline bool FStrEq(const char *sz1, const char *sz2) +{ +#ifdef MAPBASE + // V_stricmp() already checks if the pointers are equal, so having a pointer comparison here is pointless. + // I'm not sure if this was already automatically phased out by the compiler, but if it wasn't, then this is a very good change. + return ( V_stricmp(sz1, sz2) == 0 ); +#else + return ( sz1 == sz2 || V_stricmp(sz1, sz2) == 0 ); +#endif +} + +#if 0 +// UNDONE: Remove/alter MAKE_STRING so we can do this? +inline bool FStrEq( string_t str1, string_t str2 ) +{ + // now that these are pooled, we can compare them with + // integer equality + return str1 == str2; +} +#endif + +const char *nexttoken(char *token, const char *str, char sep, size_t tokenLen); + +// Misc. Prototypes +void UTIL_SetSize (CBaseEntity *pEnt, const Vector &vecMin, const Vector &vecMax); +void UTIL_ClearTrace ( trace_t &trace ); +void UTIL_SetTrace (trace_t& tr, const Ray_t &ray, edict_t* edict, float fraction, int hitgroup, unsigned int contents, const Vector& normal, float intercept ); + +int UTIL_PrecacheDecal ( const char *name, bool preload = false ); + +//----------------------------------------------------------------------------- + +float UTIL_GetSimulationInterval(); + +//----------------------------------------------------------------------------- +// Purpose: Gets a player pointer by 1-based index +// If player is not yet spawned or connected, returns NULL +// Input : playerIndex - index of the player - first player is index 1 +//----------------------------------------------------------------------------- + +// NOTENOTE: Use UTIL_GetLocalPlayer instead of UTIL_PlayerByIndex IF you're in single player +// and you want the player. +CBasePlayer *UTIL_PlayerByIndex( int playerIndex ); + +// NOTENOTE: Use this instead of UTIL_PlayerByIndex IF you're in single player +// and you want the player. +// not useable in multiplayer - see UTIL_GetListenServerHost() +CBasePlayer* UTIL_GetLocalPlayer( void ); + +// get the local player on a listen server +CBasePlayer *UTIL_GetListenServerHost( void ); + +//----------------------------------------------------------------------------- +// Purpose: Convenience function so we don't have to make this check all over +//----------------------------------------------------------------------------- +static CBasePlayer * UTIL_GetLocalPlayerOrListenServerHost( void ) +{ + if ( gpGlobals->maxClients > 1 ) + { + if ( engine->IsDedicatedServer() ) + { + return NULL; + } + + return UTIL_GetListenServerHost(); + } + + return UTIL_GetLocalPlayer(); +} + +CBasePlayer* UTIL_PlayerByUserId( int userID ); +CBasePlayer* UTIL_PlayerByName( const char *name ); // not case sensitive + +// Returns true if the command was issued by the listenserver host, or by the dedicated server, via rcon or the server console. +// This is valid during ConCommand execution. +bool UTIL_IsCommandIssuedByServerAdmin( void ); + +CBaseEntity* UTIL_EntityByIndex( int entityIndex ); + +void UTIL_GetPlayerConnectionInfo( int playerIndex, int& ping, int &packetloss ); + +void UTIL_SetClientVisibilityPVS( edict_t *pClient, const unsigned char *pvs, int pvssize ); +bool UTIL_ClientPVSIsExpanded(); + +edict_t *UTIL_FindClientInPVS( edict_t *pEdict ); +edict_t *UTIL_FindClientInVisibilityPVS( edict_t *pEdict ); + +// This is a version which finds any clients whose PVS intersects the box +CBaseEntity *UTIL_FindClientInPVS( const Vector &vecBoxMins, const Vector &vecBoxMaxs ); + +CBaseEntity *UTIL_EntitiesInPVS( CBaseEntity *pPVSEntity, CBaseEntity *pStartingEntity ); + +//----------------------------------------------------------------------------- +// class CFlaggedEntitiesEnum +//----------------------------------------------------------------------------- +// enumerate entities that match a set of edict flags into a static array +class CFlaggedEntitiesEnum : public IPartitionEnumerator +{ +public: + CFlaggedEntitiesEnum( CBaseEntity **pList, int listMax, int flagMask ); + + // This gets called by the enumeration methods with each element + // that passes the test. + virtual IterationRetval_t EnumElement( IHandleEntity *pHandleEntity ); + + int GetCount() { return m_count; } + bool AddToList( CBaseEntity *pEntity ); + +private: + CBaseEntity **m_pList; + int m_listMax; + int m_flagMask; + int m_count; +}; + +// Pass in an array of pointers and an array size, it fills the array and returns the number inserted +int UTIL_EntitiesInBox( const Vector &mins, const Vector &maxs, CFlaggedEntitiesEnum *pEnum ); +int UTIL_EntitiesAlongRay( const Ray_t &ray, CFlaggedEntitiesEnum *pEnum ); +int UTIL_EntitiesInSphere( const Vector ¢er, float radius, CFlaggedEntitiesEnum *pEnum ); +#ifdef MAPBASE +int UTIL_EntitiesAtPoint( const Vector &point, CFlaggedEntitiesEnum *pEnum ); +#endif + +inline int UTIL_EntitiesInBox( CBaseEntity **pList, int listMax, const Vector &mins, const Vector &maxs, int flagMask ) +{ + CFlaggedEntitiesEnum boxEnum( pList, listMax, flagMask ); + return UTIL_EntitiesInBox( mins, maxs, &boxEnum ); +} + +inline int UTIL_EntitiesAlongRay( CBaseEntity **pList, int listMax, const Ray_t &ray, int flagMask ) +{ + CFlaggedEntitiesEnum rayEnum( pList, listMax, flagMask ); + return UTIL_EntitiesAlongRay( ray, &rayEnum ); +} + +inline int UTIL_EntitiesInSphere( CBaseEntity **pList, int listMax, const Vector ¢er, float radius, int flagMask ) +{ + CFlaggedEntitiesEnum sphereEnum( pList, listMax, flagMask ); + return UTIL_EntitiesInSphere( center, radius, &sphereEnum ); +} + +#ifdef MAPBASE +inline int UTIL_EntitiesAtPoint( CBaseEntity **pList, int listMax, const Vector &point, int flagMask ) +{ + CFlaggedEntitiesEnum pointEnum( pList, listMax, flagMask ); + return UTIL_EntitiesAtPoint( point, &pointEnum ); +} +#endif + +// marks the entity for deletion so it will get removed next frame +void UTIL_Remove( IServerNetworkable *oldObj ); +void UTIL_Remove( CBaseEntity *oldObj ); + +// deletes an entity, without any delay. Only use this when sure no pointers rely on this entity. +void UTIL_DisableRemoveImmediate(); +void UTIL_EnableRemoveImmediate(); +void UTIL_RemoveImmediate( CBaseEntity *oldObj ); + +// make this a fixed size so it just sits on the stack +#define MAX_SPHERE_QUERY 512 +class CEntitySphereQuery +{ +public: + // currently this builds the list in the constructor + // UNDONE: make an iterative query of ISpatialPartition so we could + // make queries like this optimal + CEntitySphereQuery( const Vector ¢er, float radius, int flagMask=0 ); + CBaseEntity *GetCurrentEntity(); + inline void NextEntity() { m_listIndex++; } + +private: + int m_listIndex; + int m_listCount; + CBaseEntity *m_pList[MAX_SPHERE_QUERY]; +}; + +enum soundlevel_t; + +// Drops an entity onto the floor +int UTIL_DropToFloor( CBaseEntity *pEntity, unsigned int mask, CBaseEntity *pIgnore = NULL ); + +// Returns false if any part of the bottom of the entity is off an edge that is not a staircase. +bool UTIL_CheckBottom( CBaseEntity *pEntity, ITraceFilter *pTraceFilter, float flStepSize ); + +void UTIL_SetOrigin ( CBaseEntity *entity, const Vector &vecOrigin, bool bFireTriggers = false ); +void UTIL_EmitAmbientSound ( int entindex, const Vector &vecOrigin, const char *samp, float vol, soundlevel_t soundlevel, int fFlags, int pitch, float soundtime = 0.0f, float *duration = NULL ); +void UTIL_ParticleEffect ( const Vector &vecOrigin, const Vector &vecDirection, ULONG ulColor, ULONG ulCount ); +void UTIL_ScreenShake ( const Vector ¢er, float amplitude, float frequency, float duration, float radius, ShakeCommand_t eCommand, bool bAirShake=false ); +void UTIL_ScreenShakeObject ( CBaseEntity *pEnt, const Vector ¢er, float amplitude, float frequency, float duration, float radius, ShakeCommand_t eCommand, bool bAirShake=false ); +void UTIL_ViewPunch ( const Vector ¢er, QAngle angPunch, float radius, bool bInAir ); +void UTIL_ShowMessage ( const char *pString, CBasePlayer *pPlayer ); +void UTIL_ShowMessageAll ( const char *pString ); +void UTIL_ScreenFadeAll ( const color32 &color, float fadeTime, float holdTime, int flags ); +void UTIL_ScreenFade ( CBaseEntity *pEntity, const color32 &color, float fadeTime, float fadeHold, int flags ); +void UTIL_MuzzleFlash ( const Vector &origin, const QAngle &angles, int scale, int type ); +Vector UTIL_PointOnLineNearestPoint(const Vector& vStartPos, const Vector& vEndPos, const Vector& vPoint, bool clampEnds = false ); + +int UTIL_EntityInSolid( CBaseEntity *ent ); + +bool UTIL_IsMasterTriggered (string_t sMaster, CBaseEntity *pActivator); +void UTIL_BloodStream( const Vector &origin, const Vector &direction, int color, int amount ); +void UTIL_BloodSpray( const Vector &pos, const Vector &dir, int color, int amount, int flags ); +Vector UTIL_RandomBloodVector( void ); +void UTIL_ImpactTrace( trace_t *pTrace, int iDamageType, const char *pCustomImpactName = NULL ); +void UTIL_PlayerDecalTrace( trace_t *pTrace, int playernum ); +void UTIL_Smoke( const Vector &origin, const float scale, const float framerate ); +void UTIL_AxisStringToPointDir( Vector &start, Vector &dir, const char *pString ); +void UTIL_AxisStringToPointPoint( Vector &start, Vector &end, const char *pString ); +void UTIL_AxisStringToUnitDir( Vector &dir, const char *pString ); +void UTIL_ClipPunchAngleOffset( QAngle &in, const QAngle &punch, const QAngle &clip ); +void UTIL_PredictedPosition( CBaseEntity *pTarget, float flTimeDelta, Vector *vecPredictedPosition ); +#ifdef MAPBASE +void UTIL_PredictedPosition( CBaseEntity *pTarget, const Vector &vecActualPosition, float flTimeDelta, Vector *vecPredictedPosition ); +void UTIL_PredictedAngles( CBaseEntity *pTarget, const QAngle &angActualAngles, float flTimeDelta, QAngle *angPredictedAngles ); +#endif +void UTIL_Beam( Vector &Start, Vector &End, int nModelIndex, int nHaloIndex, unsigned char FrameStart, unsigned char FrameRate, + float Life, unsigned char Width, unsigned char EndWidth, unsigned char FadeLength, unsigned char Noise, unsigned char Red, unsigned char Green, + unsigned char Blue, unsigned char Brightness, unsigned char Speed); + +char *UTIL_VarArgs( PRINTF_FORMAT_STRING const char *format, ... ); +bool UTIL_IsValidEntity( CBaseEntity *pEnt ); +bool UTIL_TeamsMatch( const char *pTeamName1, const char *pTeamName2 ); + +// snaps a vector to the nearest axis vector (if within epsilon) +void UTIL_SnapDirectionToAxis( Vector &direction, float epsilon = 0.002f ); + +//Set the entity to point at the target specified +bool UTIL_PointAtEntity( CBaseEntity *pEnt, CBaseEntity *pTarget ); +void UTIL_PointAtNamedEntity( CBaseEntity *pEnt, string_t strTarget ); + +// Copy the pose parameter values from one entity to the other +bool UTIL_TransferPoseParameters( CBaseEntity *pSourceEntity, CBaseEntity *pDestEntity ); + +// Search for water transition along a vertical line +float UTIL_WaterLevel( const Vector &position, float minz, float maxz ); + +// Like UTIL_WaterLevel, but *way* less expensive. +// I didn't replace UTIL_WaterLevel everywhere to avoid breaking anything. +float UTIL_FindWaterSurface( const Vector &position, float minz, float maxz ); + +void UTIL_Bubbles( const Vector& mins, const Vector& maxs, int count ); +void UTIL_BubbleTrail( const Vector& from, const Vector& to, int count ); + +// allows precacheing of other entities +void UTIL_PrecacheOther( const char *szClassname, const char *modelName = NULL ); + +#ifdef MAPBASE +// Tests whether this entity exists in the dictionary and if it does, precaches it. (as opposed to complaining when it's missing) +bool UTIL_TestPrecacheOther( const char *szClassname, const char *modelName = NULL ); +#endif + +// prints a message to each client +void UTIL_ClientPrintAll( int msg_dest, const char *msg_name, const char *param1 = NULL, const char *param2 = NULL, const char *param3 = NULL, const char *param4 = NULL ); +inline void UTIL_CenterPrintAll( const char *msg_name, const char *param1 = NULL, const char *param2 = NULL, const char *param3 = NULL, const char *param4 = NULL ) +{ + UTIL_ClientPrintAll( HUD_PRINTCENTER, msg_name, param1, param2, param3, param4 ); +} + +void UTIL_ValidateSoundName( string_t &name, const char *defaultStr ); + +void UTIL_ClientPrintFilter( IRecipientFilter& filter, int msg_dest, const char *msg_name, const char *param1 = NULL, const char *param2 = NULL, const char *param3 = NULL, const char *param4 = NULL ); + +// prints messages through the HUD +void ClientPrint( CBasePlayer *player, int msg_dest, const char *msg_name, const char *param1 = NULL, const char *param2 = NULL, const char *param3 = NULL, const char *param4 = NULL ); + +// prints a message to the HUD say (chat) +void UTIL_SayText( const char *pText, CBasePlayer *pEntity ); +void UTIL_SayTextAll( const char *pText, CBasePlayer *pEntity = NULL, bool bChat = false ); +void UTIL_SayTextFilter( IRecipientFilter& filter, const char *pText, CBasePlayer *pEntity, bool bChat ); +void UTIL_SayText2Filter( IRecipientFilter& filter, CBasePlayer *pEntity, bool bChat, const char *msg_name, const char *param1 = NULL, const char *param2 = NULL, const char *param3 = NULL, const char *param4 = NULL ); + +byte *UTIL_LoadFileForMe( const char *filename, int *pLength ); +void UTIL_FreeFile( byte *buffer ); + +class CGameTrace; +typedef CGameTrace trace_t; + +//----------------------------------------------------------------------------- +// These are inlined for backwards compatibility +//----------------------------------------------------------------------------- +inline float UTIL_Approach( float target, float value, float speed ) +{ + return Approach( target, value, speed ); +} + +inline float UTIL_ApproachAngle( float target, float value, float speed ) +{ + return ApproachAngle( target, value, speed ); +} + +inline float UTIL_AngleDistance( float next, float cur ) +{ + return AngleDistance( next, cur ); +} + +inline float UTIL_AngleMod(float a) +{ + return anglemod(a); +} + +inline float UTIL_AngleDiff( float destAngle, float srcAngle ) +{ + return AngleDiff( destAngle, srcAngle ); +} + +typedef struct hudtextparms_s +{ + float x; + float y; + int effect; + byte r1, g1, b1, a1; + byte r2, g2, b2, a2; + float fadeinTime; + float fadeoutTime; + float holdTime; + float fxTime; + int channel; +} hudtextparms_t; + + +//----------------------------------------------------------------------------- +// Sets the model to be associated with an entity +//----------------------------------------------------------------------------- +void UTIL_SetModel( CBaseEntity *pEntity, const char *pModelName ); + + +// prints as transparent 'title' to the HUD +void UTIL_HudMessageAll( const hudtextparms_t &textparms, const char *pMessage, const char *pszFont = NULL, bool bAutobreak = false ); +void UTIL_HudMessage( CBasePlayer *pToPlayer, const hudtextparms_t &textparms, const char *pMessage, const char *pszFont = NULL, bool bAutobreak = false ); + +// brings up hud keyboard hints display +void UTIL_HudHintText( CBaseEntity *pEntity, const char *pMessage ); + +// Writes message to console with timestamp and FragLog header. +void UTIL_LogPrintf( PRINTF_FORMAT_STRING const char *fmt, ... ); + +// Sorta like FInViewCone, but for nonNPCs. +float UTIL_DotPoints ( const Vector &vecSrc, const Vector &vecCheck, const Vector &vecDir ); + +void UTIL_StripToken( const char *pKey, char *pDest );// for redundant keynames + +// Misc functions +int BuildChangeList( levellist_t *pLevelList, int maxList ); + +// computes gravity scale for an absolute gravity. Pass the result into CBaseEntity::SetGravity() +float UTIL_ScaleForGravity( float desiredGravity ); + + + +// +// Constants that were used only by QC (maybe not used at all now) +// +// Un-comment only as needed +// + +#include "globals.h" + +#define LFO_SQUARE 1 +#define LFO_TRIANGLE 2 +#define LFO_RANDOM 3 + +// func_rotating +#define SF_BRUSH_ROTATE_Y_AXIS 0 +#define SF_BRUSH_ROTATE_START_ON 1 +#define SF_BRUSH_ROTATE_BACKWARDS 2 +#define SF_BRUSH_ROTATE_Z_AXIS 4 +#define SF_BRUSH_ROTATE_X_AXIS 8 + +// brought over from bmodels.cpp +#define SF_BRUSH_ACCDCC 16 // brush should accelerate and decelerate when toggled +#define SF_BRUSH_HURT 32 // rotating brush that inflicts pain based on rotation speed +#define SF_ROTATING_NOT_SOLID 64 // some special rotating objects are not solid. + +#define SF_BRUSH_ROTATE_SMALLRADIUS 128 +#define SF_BRUSH_ROTATE_MEDIUMRADIUS 256 +#define SF_BRUSH_ROTATE_LARGERADIUS 512 +// changed bit to not conflict with much older flag SF_BRUSH_ACCDCC +#define SF_BRUSH_ROTATE_CLIENTSIDE 1024 + +#define PUSH_BLOCK_ONLY_X 1 +#define PUSH_BLOCK_ONLY_Y 2 + +#define SF_LIGHT_START_OFF 1 + +#define SPAWNFLAG_NOMESSAGE 1 +#define SPAWNFLAG_NOTOUCH 1 +#define SPAWNFLAG_DROIDONLY 4 + +#define SPAWNFLAG_USEONLY 1 // can't be touched, must be used (buttons) + +#define TELE_PLAYER_ONLY 1 +#define TELE_SILENT 2 + +// Sound Utilities + +enum soundlevel_t; + +void SENTENCEG_Init(); +void SENTENCEG_Stop(edict_t *entity, int isentenceg, int ipick); +int SENTENCEG_PlayRndI(edict_t *entity, int isentenceg, float volume, soundlevel_t soundlevel, int flags, int pitch); +int SENTENCEG_PlayRndSz(edict_t *entity, const char *szrootname, float volume, soundlevel_t soundlevel, int flags, int pitch); +int SENTENCEG_PlaySequentialSz(edict_t *entity, const char *szrootname, float volume, soundlevel_t soundlevel, int flags, int pitch, int ipick, int freset); +void SENTENCEG_PlaySentenceIndex( edict_t *entity, int iSentenceIndex, float volume, soundlevel_t soundlevel, int flags, int pitch ); +int SENTENCEG_PickRndSz(const char *szrootname); +int SENTENCEG_GetIndex(const char *szrootname); +int SENTENCEG_Lookup(const char *sample); + +char TEXTURETYPE_Find( trace_t *ptr ); + +void UTIL_EmitSoundSuit(edict_t *entity, const char *sample); +int UTIL_EmitGroupIDSuit(edict_t *entity, int isentenceg); +int UTIL_EmitGroupnameSuit(edict_t *entity, const char *groupname); +void UTIL_RestartAmbientSounds( void ); + +class EntityMatrix : public VMatrix +{ +public: + void InitFromEntity( CBaseEntity *pEntity, int iAttachment=0 ); + void InitFromEntityLocal( CBaseEntity *entity ); + + inline Vector LocalToWorld( const Vector &vVec ) const + { + return VMul4x3( vVec ); + } + + inline Vector WorldToLocal( const Vector &vVec ) const + { + return VMul4x3Transpose( vVec ); + } + + inline Vector LocalToWorldRotation( const Vector &vVec ) const + { + return VMul3x3( vVec ); + } + + inline Vector WorldToLocalRotation( const Vector &vVec ) const + { + return VMul3x3Transpose( vVec ); + } +}; + +inline float UTIL_DistApprox( const Vector &vec1, const Vector &vec2 ); +inline float UTIL_DistApprox2D( const Vector &vec1, const Vector &vec2 ); + +//--------------------------------------------------------- +//--------------------------------------------------------- +inline float UTIL_DistApprox( const Vector &vec1, const Vector &vec2 ) +{ + float dx; + float dy; + float dz; + + dx = vec1.x - vec2.x; + dy = vec1.y - vec2.y; + dz = vec1.z - vec2.z; + + return fabs(dx) + fabs(dy) + fabs(dz); +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +inline float UTIL_DistApprox2D( const Vector &vec1, const Vector &vec2 ) +{ + float dx; + float dy; + + dx = vec1.x - vec2.x; + dy = vec1.y - vec2.y; + + return fabs(dx) + fabs(dy); +} + +// Find out if an entity is facing another entity or position within a given tolerance range +bool UTIL_IsFacingWithinTolerance( CBaseEntity *pViewer, const Vector &vecPosition, float flDotTolerance, float *pflDot = NULL ); +bool UTIL_IsFacingWithinTolerance( CBaseEntity *pViewer, CBaseEntity *pTarget, float flDotTolerance, float *pflDot = NULL ); + +void UTIL_GetDebugColorForRelationship( int nRelationship, int &r, int &g, int &b ); + +struct datamap_t; +extern const char *UTIL_FunctionToName( datamap_t *pMap, inputfunc_t *function ); + +int UTIL_GetCommandClientIndex( void ); +CBasePlayer *UTIL_GetCommandClient( void ); +bool UTIL_GetModDir( char *lpszTextOut, unsigned int nSize ); + +AngularImpulse WorldToLocalRotation( const VMatrix &localToWorld, const Vector &worldAxis, float rotation ); +void UTIL_WorldToParentSpace( CBaseEntity *pEntity, Vector &vecPosition, QAngle &vecAngles ); +void UTIL_WorldToParentSpace( CBaseEntity *pEntity, Vector &vecPosition, Quaternion &quat ); +void UTIL_ParentToWorldSpace( CBaseEntity *pEntity, Vector &vecPosition, QAngle &vecAngles ); +void UTIL_ParentToWorldSpace( CBaseEntity *pEntity, Vector &vecPosition, Quaternion &quat ); + +bool UTIL_LoadAndSpawnEntitiesFromScript( CUtlVector &entities, const char *pScriptFile, const char *pBlock, bool bActivate = true ); + +// Given a vector, clamps the scalar axes to MAX_COORD_FLOAT ranges from worldsize.h +void UTIL_BoundToWorldSize( Vector *pVecPos ); + +#endif // UTIL_H diff --git a/sp/src/game/server/variant_t.cpp b/sp/src/game/server/variant_t.cpp new file mode 100644 index 00000000..3b45275b --- /dev/null +++ b/sp/src/game/server/variant_t.cpp @@ -0,0 +1,284 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "variant_t.h" +#ifdef MAPBASE +#include "mapbase/variant_tools.h" +#include "mapbase/matchers.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +void variant_t::SetEntity( CBaseEntity *val ) +{ + eVal = val; + fieldType = FIELD_EHANDLE; +} + +#ifdef MAPBASE +const char *variant_t::GetDebug() +{ + /* + case FIELD_BOOLEAN: *((bool *)data) = bVal != 0; break; + case FIELD_CHARACTER: *((char *)data) = iVal; break; + case FIELD_SHORT: *((short *)data) = iVal; break; + case FIELD_INTEGER: *((int *)data) = iVal; break; + case FIELD_STRING: *((string_t *)data) = iszVal; break; + case FIELD_FLOAT: *((float *)data) = flVal; break; + case FIELD_COLOR32: *((color32 *)data) = rgbaVal; break; + + case FIELD_VECTOR: + case FIELD_POSITION_VECTOR: + { + ((float *)data)[0] = vecVal[0]; + ((float *)data)[1] = vecVal[1]; + ((float *)data)[2] = vecVal[2]; + break; + } + + case FIELD_EHANDLE: *((EHANDLE *)data) = eVal; break; + case FIELD_CLASSPTR: *((CBaseEntity **)data) = eVal; break; + */ + + const char *fieldtype = "unknown"; + switch (FieldType()) + { + case FIELD_VOID: fieldtype = "Void"; break; + case FIELD_FLOAT: fieldtype = "Float"; break; + case FIELD_STRING: fieldtype = "String"; break; + case FIELD_INTEGER: fieldtype = "Integer"; break; + case FIELD_BOOLEAN: fieldtype = "Boolean"; break; + case FIELD_EHANDLE: fieldtype = "Entity"; break; + case FIELD_CLASSPTR: fieldtype = "EntityPtr"; break; + case FIELD_POSITION_VECTOR: + case FIELD_VECTOR: fieldtype = "Vector"; break; + case FIELD_CHARACTER: fieldtype = "Character"; break; + case FIELD_SHORT: fieldtype = "Short"; break; + case FIELD_COLOR32: fieldtype = "Color32"; break; + default: fieldtype = UTIL_VarArgs("unknown: %i", FieldType()); + } + return UTIL_VarArgs("%s (%s)", String(), fieldtype); +} + +#ifdef MAPBASE_VSCRIPT +void variant_t::SetScriptVariant( ScriptVariant_t &var ) +{ + switch (FieldType()) + { + case FIELD_VOID: var = NULL; break; + case FIELD_INTEGER: var = iVal; break; + case FIELD_FLOAT: var = flVal; break; + case FIELD_STRING: var = STRING(iszVal); break; + case FIELD_POSITION_VECTOR: + case FIELD_VECTOR: var = reinterpret_cast(&flVal); break; // HACKHACK + case FIELD_BOOLEAN: var = bVal; break; + case FIELD_EHANDLE: var = ToHScript( eVal ); break; + case FIELD_CLASSPTR: var = ToHScript( eVal ); break; + case FIELD_SHORT: var = (short)iVal; break; + case FIELD_CHARACTER: var = (char)iVal; break; + case FIELD_COLOR32: + { + Color *clr = new Color( rgbaVal.r, rgbaVal.g, rgbaVal.b, rgbaVal.a ); + var = g_pScriptVM->RegisterInstance( clr, true ); + break; + } + default: var = ToString(); break; + } +} +#endif + +// cmp1 = val1 float +// cmp2 = val2 float +#define VariantToFloat(val1, val2, lenallowed) \ + float cmp1 = val1.Float() ? val1.Float() : val1.Int(); \ + float cmp2 = val2.Float() ? val2.Float() : val2.Int(); \ + if (lenallowed && val2.FieldType() == FIELD_STRING) \ + cmp2 = strlen(val2.String()); + +// Integer parsing has been deactivated for consistency's sake. They now become floats only. +#define INTEGER_PARSING_DEACTIVATED 1 + +// "intchar" is the result of me not knowing where to find a version of isdigit that applies to negative numbers and floats. +#define intchar(c) (c >= '-' && c <= '9') + +// Attempts to determine the field type from whatever is in the string and creates a variant_t with the converted value and resulting field type. +// Right now, Int/Float, String, and Vector are the only fields likely to be used by entities in variant_t parsing, so they're the only ones supported. +// Expand to other fields when necessary. +variant_t Variant_Parse(const char *szValue) +{ +#ifdef INTEGER_PARSING_DEACTIVATED + bool isint = true; + bool isvector = false; + for (size_t i = 0; i < strlen(szValue); i++) + { + if (!intchar(szValue[i])) + { + isint = false; + + if (szValue[i] == ' ') + isvector = true; + else + isvector = false; + } + } + + variant_t var; + + if (isint) + var.SetFloat(atof(szValue)); + else if (isvector) + { + var.SetString(MAKE_STRING(szValue)); + var.Convert(FIELD_VECTOR); + } + else + var.SetString(MAKE_STRING(szValue)); +#else + bool isint = true; + bool isfloat = false; + for (size_t i = 0; i < strlen(szValue); i++) + { + if (szValue[i] == '.') + isfloat = true; + else if (!intchar(szValue[i])) + isint = false; + } + + variant_t var = variant_t(); + + if (isint) + { + if (isfloat) + var.SetFloat(atof(szValue)); + else + var.SetInt(atoi(szValue)); + } + else + var.SetString(MAKE_STRING(szValue)); +#endif + + return var; +} + +// Passes strings to Variant_Parse, uses the other input data for finding procedural entities. +variant_t Variant_ParseInput(inputdata_t inputdata) +{ + if (inputdata.value.FieldType() == FIELD_STRING) + { + if (inputdata.value.String()[0] == '!') + { + variant_t var = variant_t(); + var.SetEntity(gEntList.FindEntityProcedural(inputdata.value.String(), inputdata.pCaller, inputdata.pActivator, inputdata.pCaller)); + if (var.Entity()) + return var; + } + } + + return Variant_Parse(inputdata.value.String()); +} + +// Passes string variants to Variant_Parse +variant_t Variant_ParseString(variant_t value) +{ + if (value.FieldType() != FIELD_STRING) + return value; + + return Variant_Parse(value.String()); +} + +bool Variant_Equal(variant_t val1, variant_t val2, bool bLenAllowed) +{ + //if (!val2.Convert(val1.FieldType())) + // return false; + + // Add more fields if they become necessary + switch (val1.FieldType()) + { + case FIELD_INTEGER: + case FIELD_FLOAT: + { + VariantToFloat(val1, val2, bLenAllowed); + return cmp1 == cmp2; + } + case FIELD_BOOLEAN: return val1.Bool() == val2.Bool(); + case FIELD_EHANDLE: return val1.Entity() == val2.Entity(); + case FIELD_VECTOR: + { + Vector vec1; val1.Vector3D(vec1); + Vector vec2; val2.Vector3D(vec2); + return vec1 == vec2; + } +#ifdef MAPBASE_MATCHERS + // logic_compare allows wildcards on either string + default: return Matcher_NamesMatch_MutualWildcard(val1.String(), val2.String()); +#else + default: return FStrEq(val1.String(), val2.String()); +#endif + } + + return false; +} + +// val1 > val2 +bool Variant_Greater(variant_t val1, variant_t val2, bool bLenAllowed) +{ + //if (!val2.Convert(val1.FieldType())) + // return false; + + // Add more fields if they become necessary + switch (val1.FieldType()) + { + case FIELD_INTEGER: + case FIELD_FLOAT: + { + VariantToFloat(val1, val2, bLenAllowed); + return cmp1 > cmp2; + } + case FIELD_BOOLEAN: return val1.Bool() && !val2.Bool(); + case FIELD_VECTOR: + { + Vector vec1; val1.Vector3D(vec1); + Vector vec2; val2.Vector3D(vec2); + return (vec1.x > vec2.x) && (vec1.y > vec2.y) && (vec1.z > vec2.z); + } + default: return strlen(val1.String()) > strlen(val2.String()); + } + + return false; +} + +// val1 >= val2 +bool Variant_GreaterOrEqual(variant_t val1, variant_t val2, bool bLenAllowed) +{ + //if (!val2.Convert(val1.FieldType())) + // return false; + + // Add more fields if they become necessary + switch (val1.FieldType()) + { + case FIELD_INTEGER: + case FIELD_FLOAT: + { + VariantToFloat(val1, val2, bLenAllowed); + return cmp1 >= cmp2; + } + case FIELD_BOOLEAN: return val1.Bool() >= val2.Bool(); + case FIELD_VECTOR: + { + Vector vec1; val1.Vector3D(vec1); + Vector vec2; val2.Vector3D(vec2); + return (vec1.x >= vec2.x) && (vec1.y >= vec2.y) && (vec1.z >= vec2.z); + } + default: return strlen(val1.String()) >= strlen(val2.String()); + } + + return false; +} +#endif + + diff --git a/sp/src/game/server/variant_t.h b/sp/src/game/server/variant_t.h new file mode 100644 index 00000000..fc460bfe --- /dev/null +++ b/sp/src/game/server/variant_t.h @@ -0,0 +1,164 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VARIANT_T_H +#define VARIANT_T_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "ehandle.h" +#include "mathlib/vmatrix.h" + +class CBaseEntity; + +#ifdef MAPBASE_VSCRIPT +struct ScriptVariant_t; +#endif + + +// +// A variant class for passing data in entity input/output connections. +// +class variant_t +{ + union + { + bool bVal; + string_t iszVal; + int iVal; + float flVal; + float vecVal[3]; + color32 rgbaVal; + }; + CHandle eVal; // this can't be in the union because it has a constructor. + + fieldtype_t fieldType; + +public: + + // constructor + variant_t() : fieldType(FIELD_VOID), iVal(0) {} + + inline bool Bool( void ) const { return( fieldType == FIELD_BOOLEAN ) ? bVal : false; } + inline const char *String( void ) const { return( fieldType == FIELD_STRING ) ? STRING(iszVal) : ToString(); } + inline string_t StringID( void ) const { return( fieldType == FIELD_STRING ) ? iszVal : NULL_STRING; } + inline int Int( void ) const { return( fieldType == FIELD_INTEGER ) ? iVal : 0; } + inline float Float( void ) const { return( fieldType == FIELD_FLOAT ) ? flVal : 0; } + inline const CHandle &Entity(void) const; + inline color32 Color32(void) const { return rgbaVal; } + inline void Vector3D(Vector &vec) const; +#ifdef MAPBASE + // Gets angles from a vector + inline void Angle3D(QAngle &ang) const; +#endif + + fieldtype_t FieldType( void ) { return fieldType; } + + void SetBool( bool b ) { bVal = b; fieldType = FIELD_BOOLEAN; } + void SetString( string_t str ) { iszVal = str, fieldType = FIELD_STRING; } + void SetInt( int val ) { iVal = val, fieldType = FIELD_INTEGER; } + void SetFloat( float val ) { flVal = val, fieldType = FIELD_FLOAT; } + void SetEntity( CBaseEntity *val ); + void SetVector3D( const Vector &val ) { vecVal[0] = val[0]; vecVal[1] = val[1]; vecVal[2] = val[2]; fieldType = FIELD_VECTOR; } + void SetPositionVector3D( const Vector &val ) { vecVal[0] = val[0]; vecVal[1] = val[1]; vecVal[2] = val[2]; fieldType = FIELD_POSITION_VECTOR; } +#ifdef MAPBASE + // Passes in angles as a vector + void SetAngle3D( const QAngle &val ) { vecVal[0] = val[0]; vecVal[1] = val[1]; vecVal[2] = val[2]; fieldType = FIELD_VECTOR; } +#endif + void SetColor32( color32 val ) { rgbaVal = val; fieldType = FIELD_COLOR32; } + void SetColor32( int r, int g, int b, int a ) { rgbaVal.r = r; rgbaVal.g = g; rgbaVal.b = b; rgbaVal.a = a; fieldType = FIELD_COLOR32; } + void Set( fieldtype_t ftype, void *data ); + void SetOther( void *data ); + bool Convert( fieldtype_t newType ); +#ifdef MAPBASE + // Special conversion specifically for FIELD_EHANDLE with !activator, etc. + bool Convert( fieldtype_t newType, CBaseEntity *pSelf, CBaseEntity *pActivator, CBaseEntity *pCaller ); + // Hands over the value + the field type. + // ex: "Otis (String)", "3 (Integer)", or "npc_combine_s (Entity)" + const char *GetDebug(); +#endif + +#ifdef MAPBASE_VSCRIPT + void SetScriptVariant( ScriptVariant_t &var ); +#endif + + static typedescription_t m_SaveBool[]; + static typedescription_t m_SaveInt[]; + static typedescription_t m_SaveFloat[]; + static typedescription_t m_SaveEHandle[]; + static typedescription_t m_SaveString[]; + static typedescription_t m_SaveColor[]; + static typedescription_t m_SaveVector[]; + static typedescription_t m_SavePositionVector[]; + static typedescription_t m_SaveVMatrix[]; + static typedescription_t m_SaveVMatrixWorldspace[]; + static typedescription_t m_SaveMatrix3x4Worldspace[]; + +protected: + + // + // Returns a string representation of the value without modifying the variant. + // + const char *ToString( void ) const; + + friend class CVariantSaveDataOps; +}; + + +//----------------------------------------------------------------------------- +// Purpose: Returns this variant as a vector. +//----------------------------------------------------------------------------- +inline void variant_t::Vector3D(Vector &vec) const +{ + if (( fieldType == FIELD_VECTOR ) || ( fieldType == FIELD_POSITION_VECTOR )) + { + vec[0] = vecVal[0]; + vec[1] = vecVal[1]; + vec[2] = vecVal[2]; + } + else + { + vec = vec3_origin; + } +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Returns this variant as angles. +//----------------------------------------------------------------------------- +inline void variant_t::Angle3D(QAngle &ang) const +{ + if (( fieldType == FIELD_VECTOR ) || ( fieldType == FIELD_POSITION_VECTOR )) + { + ang[0] = vecVal[0]; + ang[1] = vecVal[1]; + ang[2] = vecVal[2]; + } + else + { + ang = vec3_angle; + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Returns this variant as an EHANDLE. +//----------------------------------------------------------------------------- +inline const CHandle &variant_t::Entity(void) const +{ + if ( fieldType == FIELD_EHANDLE ) + return eVal; + + static CHandle hNull; + hNull.Set(NULL); + return(hNull); +} + + +#endif // VARIANT_T_H diff --git a/sp/src/game/server/vehicle_base.cpp b/sp/src/game/server/vehicle_base.cpp new file mode 100644 index 00000000..5e348ee9 --- /dev/null +++ b/sp/src/game/server/vehicle_base.cpp @@ -0,0 +1,1488 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: UNDONE: Rename this to prop_vehicle.cpp !!! +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "vcollide_parse.h" +#include "vehicle_base.h" +#include "ndebugoverlay.h" +#include "igamemovement.h" +#include "soundenvelope.h" +#include "in_buttons.h" +#include "npc_vehicledriver.h" +#include "physics_saverestore.h" +#include "saverestore_utlvector.h" +#include "func_break.h" +#include "physics_impact_damage.h" +#include "entityblocker.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SF_PROP_VEHICLE_ALWAYSTHINK 0x00000001 + +ConVar g_debug_vehiclebase( "g_debug_vehiclebase", "0", FCVAR_CHEAT ); +extern ConVar g_debug_vehicledriver; + +// CFourWheelServerVehicle +BEGIN_SIMPLE_DATADESC_( CFourWheelServerVehicle, CBaseServerVehicle ) + + DEFINE_EMBEDDED( m_ViewSmoothing ), + +END_DATADESC() + +// CPropVehicle +BEGIN_DATADESC( CPropVehicle ) + + DEFINE_EMBEDDED( m_VehiclePhysics ), + + // These are necessary to save here because the 'owner' of these fields must be the prop_vehicle + DEFINE_PHYSPTR( m_VehiclePhysics.m_pVehicle ), + DEFINE_PHYSPTR_ARRAY( m_VehiclePhysics.m_pWheels ), + + DEFINE_FIELD( m_nVehicleType, FIELD_INTEGER ), + + // Physics Influence + DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), + +#ifdef HL2_EPISODIC + DEFINE_UTLVECTOR( m_hPhysicsChildren, FIELD_EHANDLE ), +#endif // HL2_EPISODIC + + // Keys + DEFINE_KEYFIELD( m_vehicleScript, FIELD_STRING, "VehicleScript" ), + DEFINE_FIELD( m_vecSmoothedVelocity, FIELD_VECTOR ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_FLOAT, "Throttle", InputThrottle ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "Steer", InputSteering ), + DEFINE_INPUTFUNC( FIELD_FLOAT, "Action", InputAction ), + DEFINE_INPUTFUNC( FIELD_VOID, "HandBrakeOn", InputHandBrakeOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "HandBrakeOff", InputHandBrakeOff ), + +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +BEGIN_ENT_SCRIPTDESC( CPropVehicle, CBaseAnimating, "The base class for four-wheel physics vehicles." ) + + DEFINE_SCRIPTFUNC_NAMED( ScriptGetVehicleType, "GetVehicleType", "Get a vehicle's type." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetPhysics, "GetPhysics", "Get a vehicle's physics." ) + +END_SCRIPTDESC(); +#endif + +LINK_ENTITY_TO_CLASS( prop_vehicle, CPropVehicle ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +#pragma warning (disable:4355) +CPropVehicle::CPropVehicle() : m_VehiclePhysics( this ) +{ + SetVehicleType( VEHICLE_TYPE_CAR_WHEELS ); +} +#pragma warning (default:4355) + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPropVehicle::~CPropVehicle () +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicle::Spawn( ) +{ + CFourWheelServerVehicle *pServerVehicle = dynamic_cast(GetServerVehicle()); + m_VehiclePhysics.SetOuter( this, pServerVehicle ); + + // NOTE: The model has to be set before we can spawn vehicle physics + BaseClass::Spawn(); + SetCollisionGroup( COLLISION_GROUP_VEHICLE ); + + m_VehiclePhysics.Spawn(); + if (!m_VehiclePhysics.Initialize( STRING(m_vehicleScript), m_nVehicleType )) + return; + SetNextThink( gpGlobals->curtime ); + + m_vecSmoothedVelocity.Init(); +} + +// this allows reloading the script variables from disk over an existing vehicle state +// This is useful for tuning vehicles or updating old saved game formats +CON_COMMAND(vehicle_flushscript, "Flush and reload all vehicle scripts") +{ + PhysFlushVehicleScripts(); + for ( CBaseEntity *pEnt = gEntList.FirstEnt(); pEnt != NULL; pEnt = gEntList.NextEnt(pEnt) ) + { + IServerVehicle *pServerVehicle = pEnt->GetServerVehicle(); + if ( pServerVehicle ) + { + pServerVehicle->ReloadScript(); + } + } +} +//----------------------------------------------------------------------------- +// Purpose: Restore +//----------------------------------------------------------------------------- +int CPropVehicle::Restore( IRestore &restore ) +{ + CFourWheelServerVehicle *pServerVehicle = dynamic_cast(GetServerVehicle()); + m_VehiclePhysics.SetOuter( this, pServerVehicle ); + return BaseClass::Restore( restore ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Tell the vehicle physics system whenever we teleport, so it can fixup the wheels. +//----------------------------------------------------------------------------- +void CPropVehicle::Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ) +{ + matrix3x4_t startMatrixInv; + + MatrixInvert( EntityToWorldTransform(), startMatrixInv ); + BaseClass::Teleport( newPosition, newAngles, newVelocity ); + + // Calculate the relative transform of the teleport + matrix3x4_t xform; + ConcatTransforms( EntityToWorldTransform(), startMatrixInv, xform ); + m_VehiclePhysics.Teleport( xform ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicle::DrawDebugGeometryOverlays() +{ + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + m_VehiclePhysics.DrawDebugGeometryOverlays(); + } + BaseClass::DrawDebugGeometryOverlays(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPropVehicle::DrawDebugTextOverlays() +{ + int nOffset = BaseClass::DrawDebugTextOverlays(); + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + nOffset = m_VehiclePhysics.DrawDebugTextOverlays( nOffset ); + } + return nOffset; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBasePlayer *CPropVehicle::HasPhysicsAttacker( float dt ) +{ + if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) + { + return m_hPhysicsAttacker; + } + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Keep track of physgun influence +//----------------------------------------------------------------------------- +void CPropVehicle::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + m_hPhysicsAttacker = pPhysGunUser; + m_flLastPhysicsInfluenceTime = gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicle::InputThrottle( inputdata_t &inputdata ) +{ + m_VehiclePhysics.SetThrottle( inputdata.value.Float() ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicle::InputSteering( inputdata_t &inputdata ) +{ + m_VehiclePhysics.SetSteering( inputdata.value.Float(), 2*gpGlobals->frametime ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicle::InputAction( inputdata_t &inputdata ) +{ + m_VehiclePhysics.SetAction( inputdata.value.Float() ); +} + +void CPropVehicle::InputHandBrakeOn( inputdata_t &inputdata ) +{ + m_VehiclePhysics.SetHandbrake( true ); +} + +void CPropVehicle::InputHandBrakeOff( inputdata_t &inputdata ) +{ + m_VehiclePhysics.ReleaseHandbrake(); +} + +#ifdef MAPBASE_VSCRIPT +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +HSCRIPT CPropVehicle::ScriptGetPhysics() +{ + HSCRIPT hScript = NULL; + CFourWheelVehiclePhysics *pPhysics = GetPhysics(); + if (pPhysics) + { + hScript = g_pScriptVM->RegisterInstance( pPhysics ); + } + + return hScript; +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicle::Think() +{ + m_VehiclePhysics.Think(); + + // Derived classes of CPropVehicle have their own code to determine how frequently to think. + // But the prop_vehicle entity native to this class will only think one time, so this flag + // was added to allow prop_vehicle to always think without affecting the derived classes. + if( HasSpawnFlags(SF_PROP_VEHICLE_ALWAYSTHINK) ) + { + SetNextThink(gpGlobals->curtime); + } +} + +#define SMOOTHING_FACTOR 0.9 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicle::VPhysicsUpdate( IPhysicsObject *pPhysics ) +{ + if ( IsMarkedForDeletion() ) + return; + + Vector velocity; + VPhysicsGetObject()->GetVelocity( &velocity, NULL ); + + //Update our smoothed velocity + m_vecSmoothedVelocity = m_vecSmoothedVelocity * SMOOTHING_FACTOR + velocity * ( 1 - SMOOTHING_FACTOR ); + + // must be a wheel + if (!m_VehiclePhysics.VPhysicsUpdate( pPhysics )) + return; + + BaseClass::VPhysicsUpdate( pPhysics ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : const Vector +//----------------------------------------------------------------------------- +Vector CPropVehicle::GetSmoothedVelocity( void ) +{ + return m_vecSmoothedVelocity; +} + +//============================================================================= +#ifdef HL2_EPISODIC + +//----------------------------------------------------------------------------- +// Purpose: Add an entity to a list which receives physics callbacks from the vehicle +//----------------------------------------------------------------------------- +void CPropVehicle::AddPhysicsChild( CBaseEntity *pChild ) +{ + // Don't add something we already have + if ( m_hPhysicsChildren.Find( pChild ) != m_hPhysicsChildren.InvalidIndex() ) + return ; + + m_hPhysicsChildren.AddToTail( pChild ); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes entity from physics callback list +//----------------------------------------------------------------------------- +void CPropVehicle::RemovePhysicsChild( CBaseEntity *pChild ) +{ + int elemID = m_hPhysicsChildren.Find( pChild ); + + if ( m_hPhysicsChildren.IsValidIndex( elemID ) ) + { + m_hPhysicsChildren.Remove( elemID ); + } +} + +#endif //HL2_EPISODIC +//============================================================================= + +//----------------------------------------------------------------------------- +// Purpose: Player driveable vehicle class +//----------------------------------------------------------------------------- + +IMPLEMENT_SERVERCLASS_ST(CPropVehicleDriveable, DT_PropVehicleDriveable) + + SendPropEHandle(SENDINFO(m_hPlayer)), +// SendPropFloat(SENDINFO_DT_NAME(m_controls.throttle, m_throttle), 8, SPROP_ROUNDUP, 0.0f, 1.0f), + SendPropInt(SENDINFO(m_nSpeed), 8), + SendPropInt(SENDINFO(m_nRPM), 13), + SendPropFloat(SENDINFO(m_flThrottle), 0, SPROP_NOSCALE ), + SendPropInt(SENDINFO(m_nBoostTimeLeft), 8), + SendPropInt(SENDINFO(m_nHasBoost), 1, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_nScannerDisabledWeapons), 1, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_nScannerDisabledVehicle), 1, SPROP_UNSIGNED), + SendPropInt(SENDINFO(m_bEnterAnimOn), 1, SPROP_UNSIGNED ), + SendPropInt(SENDINFO(m_bExitAnimOn), 1, SPROP_UNSIGNED ), + SendPropInt(SENDINFO(m_bUnableToFire), 1, SPROP_UNSIGNED ), + SendPropVector(SENDINFO(m_vecEyeExitEndpoint), -1, SPROP_COORD), + SendPropBool(SENDINFO(m_bHasGun)), + SendPropVector(SENDINFO(m_vecGunCrosshair), -1, SPROP_COORD), +END_SEND_TABLE(); + +BEGIN_DATADESC( CPropVehicleDriveable ) + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Lock", InputLock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Unlock", InputUnlock ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), + DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ), +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_VOID, "EnterVehicle", InputEnterVehicle ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnterVehicleImmediate", InputEnterVehicleImmediate ), + DEFINE_INPUTFUNC( FIELD_VOID, "ExitVehicle", InputExitVehicle ), + DEFINE_INPUTFUNC( FIELD_VOID, "ExitVehicleImmediate", InputExitVehicleImmediate ), +#endif + DEFINE_INPUT( m_bHasGun, FIELD_BOOLEAN, "EnableGun" ), + + // Outputs + DEFINE_OUTPUT( m_playerOn, "PlayerOn" ), + DEFINE_OUTPUT( m_playerOff, "PlayerOff" ), + DEFINE_OUTPUT( m_pressedAttack, "PressedAttack" ), + DEFINE_OUTPUT( m_pressedAttack2, "PressedAttack2" ), + DEFINE_OUTPUT( m_attackaxis, "AttackAxis" ), + DEFINE_OUTPUT( m_attack2axis, "Attack2Axis" ), +#ifdef MAPBASE + DEFINE_OUTPUT( m_OnPlayerUse, "OnPlayerUse" ), +#endif + DEFINE_FIELD( m_hPlayer, FIELD_EHANDLE ), + + DEFINE_EMBEDDEDBYREF( m_pServerVehicle ), + DEFINE_FIELD( m_nSpeed, FIELD_INTEGER ), + DEFINE_FIELD( m_nRPM, FIELD_INTEGER ), + DEFINE_FIELD( m_flThrottle, FIELD_FLOAT ), + DEFINE_FIELD( m_nBoostTimeLeft, FIELD_INTEGER ), + DEFINE_FIELD( m_nHasBoost, FIELD_INTEGER ), + DEFINE_FIELD( m_nScannerDisabledWeapons, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nScannerDisabledVehicle, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bUnableToFire, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vecEyeExitEndpoint, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecGunCrosshair, FIELD_VECTOR ), + + DEFINE_FIELD( m_bEngineLocked, FIELD_BOOLEAN ), + DEFINE_KEYFIELD( m_bLocked, FIELD_BOOLEAN, "VehicleLocked" ), + DEFINE_FIELD( m_flMinimumSpeedToEnterExit, FIELD_FLOAT ), + DEFINE_FIELD( m_bEnterAnimOn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bExitAnimOn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flTurnOffKeepUpright, FIELD_TIME ), + //DEFINE_FIELD( m_flNoImpactDamageTime, FIELD_TIME ), + + DEFINE_FIELD( m_hNPCDriver, FIELD_EHANDLE ), + DEFINE_FIELD( m_hKeepUpright, FIELD_EHANDLE ), + +END_DATADESC() + +#ifdef MAPBASE_VSCRIPT +BEGIN_ENT_SCRIPTDESC( CPropVehicleDriveable, CPropVehicle, "The base class for driveable vehicles." ) + + DEFINE_SCRIPTFUNC( IsOverturned, "Check if the vehicle is overturned." ) + DEFINE_SCRIPTFUNC( IsVehicleBodyInWater, "Check if the vehicle's body is submerged in water." ) + DEFINE_SCRIPTFUNC( StartEngine, "Start the engine." ) + DEFINE_SCRIPTFUNC( StopEngine, "Stop the engine." ) + DEFINE_SCRIPTFUNC( IsEngineOn, "Check if the engine is on." ) + DEFINE_SCRIPTFUNC_NAMED( ScriptGetDriver, "GetDriver", "Get a vehicle's driver, which could be either a player or a npc_vehicledriver." ) + +END_SCRIPTDESC(); +#endif + + +LINK_ENTITY_TO_CLASS( prop_vehicle_driveable, CPropVehicleDriveable ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPropVehicleDriveable::CPropVehicleDriveable( void ) : + m_pServerVehicle( NULL ), + m_hKeepUpright( NULL ), + m_flTurnOffKeepUpright( 0 ), + m_flNoImpactDamageTime( 0 ) +{ + m_vecEyeExitEndpoint.Init(); + m_vecGunCrosshair.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPropVehicleDriveable::~CPropVehicleDriveable( void ) +{ + DestroyServerVehicle(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::CreateServerVehicle( void ) +{ + // Create our server vehicle + m_pServerVehicle = new CFourWheelServerVehicle(); + m_pServerVehicle->SetVehicle( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::DestroyServerVehicle() +{ + if ( m_pServerVehicle ) + { + delete m_pServerVehicle; + m_pServerVehicle = NULL; + } +} + +//------------------------------------------------ +// Precache +//------------------------------------------------ +void CPropVehicleDriveable::Precache( void ) +{ + BaseClass::Precache(); + + // This step is needed because if we're precaching from a templated instance, we'll miss our vehicle + // script sounds unless we do the parse below. This instance of the vehicle will be nuked when we're actually created. + if ( m_pServerVehicle == NULL ) + { + CreateServerVehicle(); + } + + // Load the script file and precache our assets + if ( m_pServerVehicle ) + { + m_pServerVehicle->Initialize( STRING( m_vehicleScript ) ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::Spawn( void ) +{ + // Has to be created before Spawn is called (since that causes Precache to be called) + DestroyServerVehicle(); + CreateServerVehicle(); + + // Initialize our vehicle via script + if ( m_pServerVehicle->Initialize( STRING(m_vehicleScript) ) == false ) + { + Warning( "Vehicle (%s) unable to properly initialize due to script error in (%s)!\n", GetEntityName().ToCStr(), STRING( m_vehicleScript ) ); + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + return; + } + + BaseClass::Spawn(); + + m_flMinimumSpeedToEnterExit = 0; + m_takedamage = DAMAGE_EVENTS_ONLY; + m_bEngineLocked = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPropVehicleDriveable::Restore( IRestore &restore ) +{ + // Has to be created before we can restore + // and we can't create it in the constructor because it could be + // overridden by a derived class. + DestroyServerVehicle(); + CreateServerVehicle(); + + int nRetVal = BaseClass::Restore( restore ); + + return nRetVal; +} + +//----------------------------------------------------------------------------- +// Purpose: Do extra fix-up after restore +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::OnRestore( void ) +{ + BaseClass::OnRestore(); + + // NOTE: This is necessary to prevent overflow of datatables on level transition + // since the last exit eyepoint in the last level will have been fixed up + // based on the level landmarks, resulting in a position that lies outside + // typical map coordinates. If we're not in the middle of an exit anim, the + // eye exit endpoint field isn't being used at all. + if ( !m_bExitAnimOn ) + { + m_vecEyeExitEndpoint = GetAbsOrigin(); + } + + m_flNoImpactDamageTime = gpGlobals->curtime + 5.0f; + + IServerVehicle *pServerVehicle = GetServerVehicle(); + if ( pServerVehicle != NULL ) + { + // Restore the passenger information we're holding on to + pServerVehicle->RestorePassengerInfo(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Vehicles are permanently oriented off angle for vphysics. +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::GetVectors(Vector* pForward, Vector* pRight, Vector* pUp) const +{ + // This call is necessary to cause m_rgflCoordinateFrame to be recomputed + const matrix3x4_t &entityToWorld = EntityToWorldTransform(); + + if (pForward != NULL) + { + MatrixGetColumn( entityToWorld, 1, *pForward ); + } + + if (pRight != NULL) + { + MatrixGetColumn( entityToWorld, 0, *pRight ); + } + + if (pUp != NULL) + { + MatrixGetColumn( entityToWorld, 2, *pUp ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: AngleVectors equivalent that accounts for the hacked 90 degree rotation of vehicles +// BUGBUG: VPhysics is hardcoded so that vehicles must face down Y instead of X like everything else +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::VehicleAngleVectors( const QAngle &angles, Vector *pForward, Vector *pRight, Vector *pUp ) +{ + AngleVectors( angles, pRight, pForward, pUp ); + if ( pForward ) + { + *pForward *= -1; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( !pPlayer ) + return; + + ResetUseKey( pPlayer ); + +#ifdef MAPBASE + m_OnPlayerUse.FireOutput(pActivator, this); +#endif + + m_pServerVehicle->HandlePassengerEntry( pPlayer, (value>0) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CPropVehicleDriveable::GetDriver( void ) +{ + if ( m_hNPCDriver ) + return m_hNPCDriver; + + return m_hPlayer; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::EnterVehicle( CBaseCombatCharacter *pPassenger ) +{ + if ( pPassenger == NULL ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pPassenger ); + if ( pPlayer != NULL ) + { + // Remove any player who may be in the vehicle at the moment + if ( m_hPlayer ) + { + ExitVehicle( VEHICLE_ROLE_DRIVER ); + } + + m_hPlayer = pPlayer; + m_playerOn.FireOutput( pPlayer, this, 0 ); + + // Don't start the engine if the player's using an entry animation, + // because we want to start the engine once the animation is done. + if ( !m_bEnterAnimOn ) + { + StartEngine(); + } + + // Start Thinking + SetNextThink( gpGlobals->curtime ); + + Vector vecViewOffset = m_pServerVehicle->GetSavedViewOffset(); + + // Clear our state + m_pServerVehicle->InitViewSmoothing( pPlayer->GetAbsOrigin() + vecViewOffset, pPlayer->EyeAngles() ); + + m_VehiclePhysics.GetVehicle()->OnVehicleEnter(); + } + else + { + // NPCs are not yet supported - jdw + Assert( 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::ExitVehicle( int nRole ) +{ + CBasePlayer *pPlayer = m_hPlayer; + if ( !pPlayer ) + return; + + m_hPlayer = NULL; + ResetUseKey( pPlayer ); + + m_playerOff.FireOutput( pPlayer, this, 0 ); + + // clear out the fire buttons + m_attackaxis.Set( 0, pPlayer, this ); + m_attack2axis.Set( 0, pPlayer, this ); + + m_nSpeed = 0; + m_flThrottle = 0.0f; + + StopEngine(); + + m_VehiclePhysics.GetVehicle()->OnVehicleExit(); + + // Clear our state + m_pServerVehicle->InitViewSmoothing( vec3_origin, vec3_angle ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::ResetUseKey( CBasePlayer *pPlayer ) +{ + pPlayer->m_afButtonPressed &= ~IN_USE; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::DriveVehicle( CBasePlayer *pPlayer, CUserCmd *ucmd ) +{ + //Lose control when the player dies + if ( pPlayer->IsAlive() == false ) + return; + + DriveVehicle( TICK_INTERVAL, ucmd, pPlayer->m_afButtonPressed, pPlayer->m_afButtonReleased ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::DriveVehicle( float flFrameTime, CUserCmd *ucmd, int iButtonsDown, int iButtonsReleased ) +{ + int iButtons = ucmd->buttons; + + m_VehiclePhysics.UpdateDriverControls( ucmd, flFrameTime ); + + m_nSpeed = m_VehiclePhysics.GetSpeed(); //send speed to client + m_nRPM = clamp( m_VehiclePhysics.GetRPM(), 0, 4095 ); + m_nBoostTimeLeft = m_VehiclePhysics.BoostTimeLeft(); + m_nHasBoost = m_VehiclePhysics.HasBoost(); + m_flThrottle = m_VehiclePhysics.GetThrottle(); + + m_nScannerDisabledWeapons = false; // off for now, change once we have scanners + m_nScannerDisabledVehicle = false; // off for now, change once we have scanners + + // + // Fire the appropriate outputs based on button pressed events. + // + // BUGBUG: m_afButtonPressed is broken - check the player.cpp code!!! + float attack = 0, attack2 = 0; + + if ( iButtonsDown & IN_ATTACK ) + { + m_pressedAttack.FireOutput( this, this, 0 ); + } + if ( iButtonsDown & IN_ATTACK2 ) + { + m_pressedAttack2.FireOutput( this, this, 0 ); + } + + if ( iButtons & IN_ATTACK ) + { + attack = 1; + } + if ( iButtons & IN_ATTACK2 ) + { + attack2 = 1; + } + + m_attackaxis.Set( attack, this, this ); + m_attack2axis.Set( attack2, this, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Tells whether or not the car has been overturned +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::IsOverturned( void ) +{ + Vector vUp; + VehicleAngleVectors( GetAbsAngles(), NULL, NULL, &vUp ); + + float upDot = DotProduct( Vector(0,0,1), vUp ); + + // Tweak this number to adjust what's considered "overturned" + if ( upDot < 0.0f ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::Think() +{ + BaseClass::Think(); + + if ( ShouldThink() ) + { + SetNextThink( gpGlobals->curtime ); + } + + // If we have an NPC Driver, tell him to drive + if ( m_hNPCDriver ) + { + GetServerVehicle()->NPC_DriveVehicle(); + } + + // Keep thinking while we're waiting to turn off the keep upright + if ( m_flTurnOffKeepUpright ) + { + SetNextThink( gpGlobals->curtime ); + + // Time up? + if ( m_hKeepUpright != NULL && m_flTurnOffKeepUpright < gpGlobals->curtime ) + { + variant_t emptyVariant; + m_hKeepUpright->AcceptInput( "TurnOff", this, this, emptyVariant, USE_TOGGLE ); + m_flTurnOffKeepUpright = 0; + + UTIL_Remove( m_hKeepUpright ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) +{ + // If the engine's not active, prevent driving + if ( !IsEngineOn() || m_bEngineLocked ) + return; + + // If the player's entering/exiting the vehicle, prevent movement + if ( m_bEnterAnimOn || m_bExitAnimOn ) + return; + + DriveVehicle( player, ucmd ); +} + +//----------------------------------------------------------------------------- +// Purpose: Prevent the player from entering / exiting the vehicle +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputLock( inputdata_t &inputdata ) +{ + m_bLocked = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Allow the player to enter / exit the vehicle +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputUnlock( inputdata_t &inputdata ) +{ + m_bLocked = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Return true of the player's allowed to enter the vehicle +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::CanEnterVehicle( CBaseEntity *pEntity ) +{ + // Only drivers are supported + Assert( pEntity && pEntity->IsPlayer() ); + + // Prevent entering if the vehicle's being driven by an NPC + if ( GetDriver() && GetDriver() != pEntity ) + return false; + + // Can't enter if we're upside-down + if ( IsOverturned() ) + return false; + + // Prevent entering if the vehicle's locked, or if it's moving too fast. + return ( !m_bLocked && (m_nSpeed <= m_flMinimumSpeedToEnterExit) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Return true of the player's allowed to exit the vehicle +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::CanExitVehicle( CBaseEntity *pEntity ) +{ + // Prevent exiting if the vehicle's locked, or if it's moving too fast. + return ( !m_bEnterAnimOn && !m_bExitAnimOn && !m_bLocked && (m_nSpeed <= m_flMinimumSpeedToEnterExit) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputTurnOn( inputdata_t &inputdata ) +{ + m_bEngineLocked = false; + + StartEngine(); + m_VehiclePhysics.SetDisableEngine( false ); + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputTurnOff( inputdata_t &inputdata ) +{ + m_bEngineLocked = true; + + StopEngine(); + m_VehiclePhysics.SetDisableEngine( true ); +} + +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputEnterVehicle( inputdata_t &inputdata ) +{ + if ( m_bEnterAnimOn ) + return; + + // Try the activator first & use them if they are a player. + CBaseCombatCharacter *pPassenger = ToBaseCombatCharacter( inputdata.pActivator ); + if ( pPassenger == NULL ) + { + // Activator was not a player, just grab the singleplayer player. + pPassenger = UTIL_PlayerByIndex( 1 ); + if ( pPassenger == NULL ) + return; + } + + // FIXME: I hate code like this. I should really add a parameter to HandlePassengerEntry + // to allow entry into locked vehicles + bool bWasLocked = m_bLocked; + m_bLocked = false; + GetServerVehicle()->HandlePassengerEntry( pPassenger, true ); + m_bLocked = bWasLocked; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputEnterVehicleImmediate( inputdata_t &inputdata ) +{ + if ( m_bEnterAnimOn ) + return; + + // Try the activator first & use them if they are a player. + CBaseCombatCharacter *pPassenger = ToBaseCombatCharacter( inputdata.pActivator ); + if ( pPassenger == NULL ) + { + // Activator was not a player, just grab the singleplayer player. + pPassenger = UTIL_PlayerByIndex( 1 ); + if ( pPassenger == NULL ) + return; + } + + CBasePlayer *pPlayer = ToBasePlayer( pPassenger ); + if ( pPlayer != NULL ) + { + if ( pPlayer->IsInAVehicle() ) + { + // Force the player out of whatever vehicle they are in. + pPlayer->LeaveVehicle(); + } + + pPlayer->GetInVehicle( GetServerVehicle(), VEHICLE_ROLE_DRIVER ); + } + else + { + // NPCs are not currently supported - jdw + Assert( 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputExitVehicle( inputdata_t &inputdata ) +{ + if (!GetDriver()) + return; + + if ( CanExitVehicle(GetDriver()) ) + { + GetServerVehicle()->HandlePassengerExit(GetDriver()->MyCombatCharacterPointer()); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::InputExitVehicleImmediate( inputdata_t &inputdata ) +{ + if (!GetDriver()) + return; + + if (GetDriver()->IsPlayer()) + { + static_cast(GetDriver())->LeaveVehicle(); + } +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: Check to see if the engine is on. +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::IsEngineOn( void ) +{ + return m_VehiclePhysics.IsOn(); +} + +//----------------------------------------------------------------------------- +// Purpose: Turn on the engine, but only if we're allowed to +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::StartEngine( void ) +{ + if ( m_bEngineLocked ) + { + m_VehiclePhysics.SetHandbrake( true ); + return; + } + + m_VehiclePhysics.TurnOn(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::StopEngine( void ) +{ + m_VehiclePhysics.TurnOff(); +} + +//----------------------------------------------------------------------------- +// Purpose: // The player takes damage if he hits something going fast enough +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + +//============================================================================= +#ifdef HL2_EPISODIC + + // Notify all children + for ( int i = 0; i < m_hPhysicsChildren.Count(); i++ ) + { + if ( m_hPhysicsChildren[i] == NULL ) + continue; + + m_hPhysicsChildren[i]->VPhysicsCollision( index, pEvent ); + } + +#endif // HL2_EPISODIC +//============================================================================= + + // Don't care if we don't have a driver + CBaseCombatCharacter *pDriver = GetDriver() ? GetDriver()->MyCombatCharacterPointer() : NULL; + if ( !pDriver ) + return; + + // Make sure we don't keep hitting the same entity + int otherIndex = !index; + CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex]; + if ( pEvent->deltaCollisionTime < 0.5 && (pHitEntity == this) ) + return; + + BaseClass::VPhysicsCollision( index, pEvent ); + + // if this is a bone follower, promote to the owner entity + if ( pHitEntity->GetOwnerEntity() && (pHitEntity->GetEffects() & EF_NODRAW) ) + { + CBaseEntity *pOwner = pHitEntity->GetOwnerEntity(); + // no friendly bone follower damage + // this allows strider legs to damage the player on impact but not d0g for example + if ( pDriver->IRelationType( pOwner ) == D_LI ) + return; + } + + // If we hit hard enough, damage the player + // Don't take damage from ramming bad guys + if ( pHitEntity->MyNPCPointer() ) + { + return; + } + + // Don't take damage from ramming ragdolls + if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PART_OF_RAGDOLL ) + return; + + // Ignore func_breakables + CBreakable *pBreakable = dynamic_cast(pHitEntity); + if ( pBreakable ) + { + // ROBIN: Do we want to only do this on func_breakables that are about to die? + //if ( pBreakable->HasSpawnFlags( SF_PHYSICS_BREAK_IMMEDIATELY ) ) + return; + } + + // Over our skill's minimum crash level? + int damageType = 0; + float flDamage = CalculatePhysicsImpactDamage( index, pEvent, gDefaultPlayerVehicleImpactDamageTable, 1.0, true, damageType ); + if ( flDamage > 0 && m_flNoImpactDamageTime < gpGlobals->curtime ) + { + Vector damagePos; + pEvent->pInternalData->GetContactPoint( damagePos ); + Vector damageForce = pEvent->postVelocity[index] * pEvent->pObjects[index]->GetMass(); + CTakeDamageInfo info( this, GetDriver(), damageForce, damagePos, flDamage, (damageType|DMG_VEHICLE) ); + GetDriver()->TakeDamage( info ); + } +} + +int CPropVehicleDriveable::VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ) +{ + return GetPhysics()->VPhysicsGetObjectList( pList, listMax ); +} + +//----------------------------------------------------------------------------- +// Purpose: Handle trace attacks from the physcannon +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + // If we've just been zapped by the physcannon, try and right ourselves + if ( info.GetDamageType() & DMG_PHYSGUN ) + { + float flUprightStrength = GetUprightStrength(); + if ( flUprightStrength ) + { + // Update our strength value if we already have an upright controller + if ( m_hKeepUpright ) + { + variant_t limitVariant; + limitVariant.SetFloat( flUprightStrength ); + m_hKeepUpright->AcceptInput( "SetAngularLimit", this, this, limitVariant, USE_TOGGLE ); + } + else + { + // If we don't have one, create an upright controller for us + m_hKeepUpright = CreateKeepUpright( GetAbsOrigin(), vec3_angle, this, GetUprightStrength(), false ); + } + + Assert( m_hKeepUpright ); + variant_t emptyVariant; + m_hKeepUpright->AcceptInput( "TurnOn", this, this, emptyVariant, USE_TOGGLE ); + + // Turn off the keepupright after a short time + m_flTurnOffKeepUpright = gpGlobals->curtime + GetUprightTime(); + SetNextThink( gpGlobals->curtime ); + } + +#ifdef HL2_EPISODIC + // Notify all children + for ( int i = 0; i < m_hPhysicsChildren.Count(); i++ ) + { + if ( m_hPhysicsChildren[i] == NULL ) + continue; + + variant_t emptyVariant; + m_hPhysicsChildren[i]->AcceptInput( "VehiclePunted", info.GetAttacker(), this, emptyVariant, USE_TOGGLE ); + } +#endif // HL2_EPISODIC + + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + +//============================================================================= +// Passenger carrier + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPassenger - +// bCompanion - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::NPC_CanEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) +{ + // Always allowed unless a leaf class says otherwise + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPassenger - +// bCompanion - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::NPC_CanExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) +{ + // Always allowed unless a leaf class says otherwise + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPassenger - +// bCompanion - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::NPC_AddPassenger( CAI_BaseNPC *pPassenger, string_t strRoleName, int nSeatID ) +{ + // Must be allowed to enter + if ( NPC_CanEnterVehicle( pPassenger, true /*FIXME*/ ) == false ) + return false; + + IServerVehicle *pVehicleServer = GetServerVehicle(); + if ( pVehicleServer != NULL ) + return pVehicleServer->NPC_AddPassenger( pPassenger, strRoleName, nSeatID ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPassenger - +// bCompanion - +//----------------------------------------------------------------------------- +bool CPropVehicleDriveable::NPC_RemovePassenger( CAI_BaseNPC *pPassenger ) +{ + // Must be allowed to exit + if ( NPC_CanExitVehicle( pPassenger, true /*FIXME*/ ) == false ) + return false; + + IServerVehicle *pVehicleServer = GetServerVehicle(); + if ( pVehicleServer != NULL ) + return pVehicleServer->NPC_RemovePassenger( pPassenger ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pVictim - +// &info - +//----------------------------------------------------------------------------- +void CPropVehicleDriveable::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + CBaseEntity *pDriver = GetDriver(); + if ( pDriver != NULL ) + { + pDriver->Event_KilledOther( pVictim, info ); + } + + BaseClass::Event_KilledOther( pVictim, info ); +} + +//======================================================================================================================================== +// FOUR WHEEL PHYSICS VEHICLE SERVER VEHICLE +//======================================================================================================================================== +CFourWheelServerVehicle::CFourWheelServerVehicle( void ) +{ + // Setup our smoothing data + memset( &m_ViewSmoothing, 0, sizeof( m_ViewSmoothing ) ); + + m_ViewSmoothing.bClampEyeAngles = true; + m_ViewSmoothing.bDampenEyePosition = true; + m_ViewSmoothing.flPitchCurveZero = PITCH_CURVE_ZERO; + m_ViewSmoothing.flPitchCurveLinear = PITCH_CURVE_LINEAR; + m_ViewSmoothing.flRollCurveZero = ROLL_CURVE_ZERO; + m_ViewSmoothing.flRollCurveLinear = ROLL_CURVE_LINEAR; +} + +#ifdef HL2_EPISODIC +ConVar r_JeepFOV( "r_JeepFOV", "82", FCVAR_CHEAT | FCVAR_REPLICATED ); +#else +ConVar r_JeepFOV( "r_JeepFOV", "90", FCVAR_CHEAT | FCVAR_REPLICATED ); +#endif // HL2_EPISODIC + +//----------------------------------------------------------------------------- +// Purpose: Setup our view smoothing information +//----------------------------------------------------------------------------- +void CFourWheelServerVehicle::InitViewSmoothing( const Vector &vecOrigin, const QAngle &vecAngles ) +{ + m_ViewSmoothing.bWasRunningAnim = false; + m_ViewSmoothing.vecOriginSaved = vecOrigin; + m_ViewSmoothing.vecAnglesSaved = vecAngles; + m_ViewSmoothing.flFOV = r_JeepFOV.GetFloat(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelServerVehicle::SetVehicle( CBaseEntity *pVehicle ) +{ + ASSERT( dynamic_cast(pVehicle) ); + BaseClass::SetVehicle( pVehicle ); + + // Save this for view smoothing + if ( pVehicle != NULL ) + { + m_ViewSmoothing.pVehicle = pVehicle->GetBaseAnimating(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Modify the player view/camera while in a vehicle +//----------------------------------------------------------------------------- +void CFourWheelServerVehicle::GetVehicleViewPosition( int nRole, Vector *pAbsOrigin, QAngle *pAbsAngles, float *pFOV /*= NULL*/ ) +{ + CBaseEntity *pDriver = GetPassenger( nRole ); + if ( pDriver && pDriver->IsPlayer()) + { + CBasePlayer *pPlayerDriver = ToBasePlayer( pDriver ); + CPropVehicleDriveable *pVehicle = GetFourWheelVehicle(); + SharedVehicleViewSmoothing( pPlayerDriver, + pAbsOrigin, pAbsAngles, + pVehicle->IsEnterAnimOn(), pVehicle->IsExitAnimOn(), + pVehicle->GetEyeExitEndpoint(), + &m_ViewSmoothing, + pFOV ); + } + else + { + // NPCs are not supported + Assert( 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const vehicleparams_t *CFourWheelServerVehicle::GetVehicleParams( void ) +{ + return &GetFourWheelVehiclePhysics()->GetVehicleParams(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const vehicle_operatingparams_t *CFourWheelServerVehicle::GetVehicleOperatingParams( void ) +{ + return &GetFourWheelVehiclePhysics()->GetVehicleOperatingParams(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const vehicle_controlparams_t *CFourWheelServerVehicle::GetVehicleControlParams( void ) +{ + return &GetFourWheelVehiclePhysics()->GetVehicleControls(); +} + +IPhysicsVehicleController *CFourWheelServerVehicle::GetVehicleController() +{ + return GetFourWheelVehiclePhysics()->GetVehicleController(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPropVehicleDriveable *CFourWheelServerVehicle::GetFourWheelVehicle( void ) +{ + return (CPropVehicleDriveable *)m_pVehicle; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CFourWheelVehiclePhysics *CFourWheelServerVehicle::GetFourWheelVehiclePhysics( void ) +{ + CPropVehicleDriveable *pVehicle = GetFourWheelVehicle(); + return pVehicle->GetPhysics(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFourWheelServerVehicle::IsVehicleUpright( void ) +{ + return (GetFourWheelVehicle()->IsOverturned() == false); +} + +bool CFourWheelServerVehicle::IsVehicleBodyInWater() +{ + return GetFourWheelVehicle()->IsVehicleBodyInWater(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFourWheelServerVehicle::IsPassengerEntering( void ) +{ + return GetFourWheelVehicle()->IsEnterAnimOn(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CFourWheelServerVehicle::IsPassengerExiting( void ) +{ + return GetFourWheelVehicle()->IsExitAnimOn(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelServerVehicle::NPC_SetDriver( CNPC_VehicleDriver *pDriver ) +{ + if ( pDriver ) + { + m_nNPCButtons = 0; + GetFourWheelVehicle()->m_hNPCDriver = pDriver; + GetFourWheelVehicle()->StartEngine(); + SetVehicleVolume( 1.0 ); // Vehicles driven by NPCs are louder + + // Set our owner entity to be the NPC, so it can path check without hitting us + GetFourWheelVehicle()->SetOwnerEntity( pDriver ); + + // Start Thinking + GetFourWheelVehicle()->SetNextThink( gpGlobals->curtime ); + } + else + { + GetFourWheelVehicle()->m_hNPCDriver = NULL; + GetFourWheelVehicle()->StopEngine(); + GetFourWheelVehicle()->SetOwnerEntity( NULL ); + SetVehicleVolume( 0.5 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CFourWheelServerVehicle::NPC_DriveVehicle( void ) +{ + +#ifdef HL2_DLL + if ( g_debug_vehicledriver.GetInt() ) + { + if ( m_nNPCButtons ) + { + Vector vecForward, vecRight; + GetFourWheelVehicle()->GetVectors( &vecForward, &vecRight, NULL ); + if ( m_nNPCButtons & IN_FORWARD ) + { + NDebugOverlay::Line( GetFourWheelVehicle()->GetAbsOrigin(), GetFourWheelVehicle()->GetAbsOrigin() + vecForward * 200, 0,255,0, true, 0.1 ); + } + if ( m_nNPCButtons & IN_BACK ) + { + NDebugOverlay::Line( GetFourWheelVehicle()->GetAbsOrigin(), GetFourWheelVehicle()->GetAbsOrigin() - vecForward * 200, 0,255,0, true, 0.1 ); + } + if ( m_nNPCButtons & IN_MOVELEFT ) + { + NDebugOverlay::Line( GetFourWheelVehicle()->GetAbsOrigin(), GetFourWheelVehicle()->GetAbsOrigin() - vecRight * 200 * -m_flTurnDegrees, 0,255,0, true, 0.1 ); + } + if ( m_nNPCButtons & IN_MOVERIGHT ) + { + NDebugOverlay::Line( GetFourWheelVehicle()->GetAbsOrigin(), GetFourWheelVehicle()->GetAbsOrigin() + vecRight * 200 * m_flTurnDegrees, 0,255,0, true, 0.1 ); + } + if ( m_nNPCButtons & IN_JUMP ) + { + NDebugOverlay::Box( GetFourWheelVehicle()->GetAbsOrigin(), -Vector(20,20,20), Vector(20,20,20), 0,255,0, true, 0.1 ); + } + } + } +#endif + + int buttonsChanged = m_nPrevNPCButtons ^ m_nNPCButtons; + int afButtonPressed = buttonsChanged & m_nNPCButtons; // The changed ones still down are "pressed" + int afButtonReleased = buttonsChanged & (~m_nNPCButtons); // The ones not down are "released" + CUserCmd fakeCmd; + fakeCmd.Reset(); + fakeCmd.buttons = m_nNPCButtons; + fakeCmd.forwardmove += 200.0f * ( m_nNPCButtons & IN_FORWARD ); + fakeCmd.forwardmove -= 200.0f * ( m_nNPCButtons & IN_BACK ); + fakeCmd.sidemove -= 200.0f * ( m_nNPCButtons & IN_MOVELEFT ); + fakeCmd.sidemove += 200.0f * ( m_nNPCButtons & IN_MOVERIGHT ); + + GetFourWheelVehicle()->DriveVehicle( gpGlobals->frametime, &fakeCmd, afButtonPressed, afButtonReleased ); + m_nPrevNPCButtons = m_nNPCButtons; + + // NPC's cheat by using analog steering. + GetFourWheelVehiclePhysics()->SetSteering( m_flTurnDegrees, 0 ); + + // Clear out attack buttons each frame + m_nNPCButtons &= ~IN_ATTACK; + m_nNPCButtons &= ~IN_ATTACK2; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : nWheelIndex - +// &vecPos - +//----------------------------------------------------------------------------- +bool CFourWheelServerVehicle::GetWheelContactPoint( int nWheelIndex, Vector &vecPos ) +{ + // Dig through a couple layers to get to our data + CFourWheelVehiclePhysics *pVehiclePhysics = GetFourWheelVehiclePhysics(); + if ( pVehiclePhysics ) + { + IPhysicsVehicleController *pVehicleController = pVehiclePhysics->GetVehicle(); + if ( pVehicleController ) + { + return pVehicleController->GetWheelContactPoint( nWheelIndex, &vecPos, NULL ); + } + } + return false; +} diff --git a/sp/src/game/server/vehicle_base.h b/sp/src/game/server/vehicle_base.h new file mode 100644 index 00000000..831ea6b4 --- /dev/null +++ b/sp/src/game/server/vehicle_base.h @@ -0,0 +1,336 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VEHICLE_BASE_H +#define VEHICLE_BASE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "vphysics/vehicles.h" +#include "iservervehicle.h" +#include "fourwheelvehiclephysics.h" +#include "props.h" +#include "vehicle_sounds.h" +#include "phys_controller.h" +#include "entityblocker.h" +#include "vehicle_baseserver.h" +#include "vehicle_viewblend_shared.h" + +class CNPC_VehicleDriver; +class CFourWheelVehiclePhysics; +class CPropVehicleDriveable; +class CSoundPatch; + +// the tires are considered to be skidding if they have sliding velocity of 10 in/s or more +const float DEFAULT_SKID_THRESHOLD = 10.0f; + +//----------------------------------------------------------------------------- +// Purpose: Four wheel physics vehicle server vehicle +//----------------------------------------------------------------------------- +class CFourWheelServerVehicle : public CBaseServerVehicle +{ + DECLARE_CLASS( CFourWheelServerVehicle, CBaseServerVehicle ); + +// IServerVehicle +public: + virtual ~CFourWheelServerVehicle( void ) + { + } + + CFourWheelServerVehicle( void ); + virtual bool IsVehicleUpright( void ); + virtual bool IsVehicleBodyInWater( void ); + virtual void GetVehicleViewPosition( int nRole, Vector *pOrigin, QAngle *pAngles, float *pFOV = NULL ); + IPhysicsVehicleController *GetVehicleController(); + const vehicleparams_t *GetVehicleParams( void ); + const vehicle_controlparams_t *GetVehicleControlParams( void ); + const vehicle_operatingparams_t *GetVehicleOperatingParams( void ); + + // NPC Driving + void NPC_SetDriver( CNPC_VehicleDriver *pDriver ); + void NPC_DriveVehicle( void ); + + CPropVehicleDriveable *GetFourWheelVehicle( void ); + bool GetWheelContactPoint( int nWheelIndex, Vector &vecPos ); + +public: + virtual void SetVehicle( CBaseEntity *pVehicle ); + void InitViewSmoothing( const Vector &vecStartOrigin, const QAngle &vecStartAngles ); + bool IsPassengerEntering( void ); + bool IsPassengerExiting( void ); + + DECLARE_SIMPLE_DATADESC(); + +private: + CFourWheelVehiclePhysics *GetFourWheelVehiclePhysics( void ); + + ViewSmoothingData_t m_ViewSmoothing; +}; + +//----------------------------------------------------------------------------- +// Purpose: Base class for four wheel physics vehicles +//----------------------------------------------------------------------------- +class CPropVehicle : public CBaseProp, public CDefaultPlayerPickupVPhysics +{ + DECLARE_CLASS( CPropVehicle, CBaseProp ); +public: + CPropVehicle(); + virtual ~CPropVehicle(); + + void SetVehicleType( unsigned int nVehicleType ) { m_nVehicleType = nVehicleType; } + unsigned int GetVehicleType( void ) { return m_nVehicleType; } + + // CBaseEntity + void Spawn( void ); + virtual int Restore( IRestore &restore ); + void VPhysicsUpdate( IPhysicsObject *pPhysics ); + void DrawDebugGeometryOverlays(); + int DrawDebugTextOverlays(); + void Teleport( const Vector *newPosition, const QAngle *newAngles, const Vector *newVelocity ); + virtual void Think( void ); + CFourWheelVehiclePhysics *GetPhysics( void ) { return &m_VehiclePhysics; } + CBasePlayer *HasPhysicsAttacker( float dt ); + void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + + Vector GetSmoothedVelocity( void ); //Save and update our smoothed velocity for prediction + + virtual void DampenEyePosition( Vector &vecVehicleEyePos, QAngle &vecVehicleEyeAngles ) {} + + // Inputs + void InputThrottle( inputdata_t &inputdata ); + void InputSteering( inputdata_t &inputdata ); + void InputAction( inputdata_t &inputdata ); + void InputHandBrakeOn( inputdata_t &inputdata ); + void InputHandBrakeOff( inputdata_t &inputdata ); + + DECLARE_DATADESC(); +#ifdef MAPBASE_VSCRIPT + DECLARE_ENT_SCRIPTDESC(); + + HSCRIPT ScriptGetPhysics(); + int ScriptGetVehicleType() { return GetVehicleType(); } +#endif + +#ifdef HL2_EPISODIC + void AddPhysicsChild( CBaseEntity *pChild ); + void RemovePhysicsChild( CBaseEntity *pChild ); +#endif //HL2_EPISODIC + +protected: + // engine sounds + void SoundInit(); + void SoundShutdown(); + void SoundUpdate( const vehicle_operatingparams_t ¶ms, const vehicleparams_t &vehicle ); + void CalcWheelData( vehicleparams_t &vehicle ); + void ResetControls(); + + // Upright strength of the controller (angular limit) + virtual float GetUprightStrength( void ) { return 8.0f; } + virtual float GetUprightTime( void ) { return 5.0f; } + +protected: + CFourWheelVehiclePhysics m_VehiclePhysics; + unsigned int m_nVehicleType; + string_t m_vehicleScript; + +#ifdef HL2_EPISODIC + CUtlVector m_hPhysicsChildren; // List of entities who wish to get physics callbacks from the vehicle +#endif //HL2_EPISODIC + +private: + Vector m_vecSmoothedVelocity; + + CHandle m_hPhysicsAttacker; + + float m_flLastPhysicsInfluenceTime; +}; + +//============================================================================= +// NPC Passenger Carrier interface + +class INPCPassengerCarrier +{ +public: + virtual bool NPC_CanEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) = 0; + virtual bool NPC_CanExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) = 0; + virtual bool NPC_AddPassenger( CAI_BaseNPC *pPassenger, string_t strRoleName, int nSeatID ) = 0; + virtual bool NPC_RemovePassenger( CAI_BaseNPC *pPassenger ) = 0; + virtual void NPC_FinishedEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) = 0; + virtual void NPC_FinishedExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) = 0; +}; + +//----------------------------------------------------------------------------- +// Purpose: Drivable four wheel physics vehicles +//----------------------------------------------------------------------------- +class CPropVehicleDriveable : public CPropVehicle, public IDrivableVehicle, public INPCPassengerCarrier +{ + DECLARE_CLASS( CPropVehicleDriveable, CPropVehicle ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); +#ifdef MAPBASE_VSCRIPT + DECLARE_ENT_SCRIPTDESC(); +#endif +public: + CPropVehicleDriveable( void ); + ~CPropVehicleDriveable( void ); + + virtual void Precache( void ); + virtual void Spawn( void ); + virtual int Restore( IRestore &restore ); + virtual void OnRestore(); + virtual void CreateServerVehicle( void ); + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_IMPULSE_USE; }; + virtual void GetVectors(Vector* pForward, Vector* pRight, Vector* pUp) const; + virtual void VehicleAngleVectors( const QAngle &angles, Vector *pForward, Vector *pRight, Vector *pUp ); + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual void Think( void ); + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + virtual void Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ); + + // Vehicle handling + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + virtual int VPhysicsGetObjectList( IPhysicsObject **pList, int listMax ); + + // Inputs + void InputLock( inputdata_t &inputdata ); + void InputUnlock( inputdata_t &inputdata ); + void InputTurnOn( inputdata_t &inputdata ); + void InputTurnOff( inputdata_t &inputdata ); +#ifdef MAPBASE + virtual void InputEnterVehicle( inputdata_t &inputdata ); + virtual void InputEnterVehicleImmediate( inputdata_t &inputdata ); + virtual void InputExitVehicle( inputdata_t &inputdata ); + virtual void InputExitVehicleImmediate( inputdata_t &inputdata ); +#endif + + // Locals + void ResetUseKey( CBasePlayer *pPlayer ); + + // Driving + void DriveVehicle( CBasePlayer *pPlayer, CUserCmd *ucmd ); // Player driving entrypoint + virtual void DriveVehicle( float flFrameTime, CUserCmd *ucmd, int iButtonsDown, int iButtonsReleased ); // Driving Button handling + + virtual bool IsOverturned( void ); + virtual bool IsVehicleBodyInWater( void ) { return false; } + + // Engine handling + void StartEngine( void ); + void StopEngine( void ); + bool IsEngineOn( void ); + +// IDrivableVehicle +public: + virtual CBaseEntity *GetDriver( void ); + virtual void ItemPostFrame( CBasePlayer *pPlayer ) { return; } + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ); + virtual void ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ) { return; } + virtual void FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ) { return; } + virtual bool CanEnterVehicle( CBaseEntity *pEntity ); + virtual bool CanExitVehicle( CBaseEntity *pEntity ); + virtual void SetVehicleEntryAnim( bool bOn ) { m_bEnterAnimOn = bOn; } + virtual void SetVehicleExitAnim( bool bOn, Vector vecEyeExitEndpoint ) { m_bExitAnimOn = bOn; if ( bOn ) m_vecEyeExitEndpoint = vecEyeExitEndpoint; } + virtual void EnterVehicle( CBaseCombatCharacter *pPassenger ); + + virtual bool AllowBlockedExit( CBaseCombatCharacter *pPassenger, int nRole ) { return true; } + virtual bool AllowMidairExit( CBaseCombatCharacter *pPassenger, int nRole ) { return false; } + virtual void PreExitVehicle( CBaseCombatCharacter *pPassenger, int nRole ) {} + virtual void ExitVehicle( int nRole ); + virtual string_t GetVehicleScriptName() { return m_vehicleScript; } + + virtual bool PassengerShouldReceiveDamage( CTakeDamageInfo &info ) { return true; } + + // If this is a vehicle, returns the vehicle interface + virtual IServerVehicle *GetServerVehicle() { return m_pServerVehicle; } + +#ifdef MAPBASE_VSCRIPT + HSCRIPT ScriptGetDriver() { return ToHScript( GetDriver() ); } +#endif + +protected: + + virtual bool ShouldThink() { return ( GetDriver() != NULL ); } + + inline bool HasGun(); + void DestroyServerVehicle(); + + // Contained IServerVehicle + CFourWheelServerVehicle *m_pServerVehicle; + + COutputEvent m_playerOn; + COutputEvent m_playerOff; + + COutputEvent m_pressedAttack; + COutputEvent m_pressedAttack2; + + COutputFloat m_attackaxis; + COutputFloat m_attack2axis; + +#ifdef MAPBASE + COutputEvent m_OnPlayerUse; +#endif + + CNetworkHandle( CBasePlayer, m_hPlayer ); +public: + + CNetworkVar( int, m_nSpeed ); + CNetworkVar( int, m_nRPM ); + CNetworkVar( float, m_flThrottle ); + CNetworkVar( int, m_nBoostTimeLeft ); + CNetworkVar( int, m_nHasBoost ); + + CNetworkVector( m_vecEyeExitEndpoint ); + CNetworkVector( m_vecGunCrosshair ); + CNetworkVar( bool, m_bUnableToFire ); + CNetworkVar( bool, m_bHasGun ); + + CNetworkVar( bool, m_nScannerDisabledWeapons ); + CNetworkVar( bool, m_nScannerDisabledVehicle ); + + // NPC Driver + CHandle m_hNPCDriver; + EHANDLE m_hKeepUpright; + + // -------------------------------- + // NPC Passengers +public: + + virtual bool NPC_CanEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ); + virtual bool NPC_CanExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ); + virtual bool NPC_AddPassenger( CAI_BaseNPC *pPassenger, string_t strRoleName, int nSeatID ); + virtual bool NPC_RemovePassenger( CAI_BaseNPC *pPassenger ); + virtual void NPC_FinishedEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) {} + virtual void NPC_FinishedExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) {} + + // NPC Passengers + // -------------------------------- + + bool IsEnterAnimOn( void ) { return m_bEnterAnimOn; } + bool IsExitAnimOn( void ) { return m_bExitAnimOn; } + const Vector &GetEyeExitEndpoint( void ) { return m_vecEyeExitEndpoint; } + +protected: + // Entering / Exiting + bool m_bEngineLocked; // Mapmaker override on whether the vehicle's allowed to be turned on/off + bool m_bLocked; + float m_flMinimumSpeedToEnterExit; + CNetworkVar( bool, m_bEnterAnimOn ); + CNetworkVar( bool, m_bExitAnimOn ); + + // Used to turn the keepupright off after a short time + float m_flTurnOffKeepUpright; + float m_flNoImpactDamageTime; +}; + + +inline bool CPropVehicleDriveable::HasGun() +{ + return m_bHasGun; +} + + +#endif // VEHICLE_BASE_H diff --git a/sp/src/game/server/vehicle_baseserver.cpp b/sp/src/game/server/vehicle_baseserver.cpp new file mode 100644 index 00000000..a2fe513e --- /dev/null +++ b/sp/src/game/server/vehicle_baseserver.cpp @@ -0,0 +1,2747 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "vcollide_parse.h" +#include "vehicle_base.h" +#include "npc_vehicledriver.h" +#include "in_buttons.h" +#include "engine/IEngineSound.h" +#include "soundenvelope.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "saverestore_utlvector.h" +#include "KeyValues.h" +#include "studio.h" +#include "bone_setup.h" +#include "collisionutils.h" +#include "animation.h" +#include "env_player_surface_trigger.h" +#include "rumble_shared.h" + +#ifdef HL2_DLL + #include "hl2_player.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar g_debug_vehiclesound( "g_debug_vehiclesound", "0", FCVAR_CHEAT ); +ConVar g_debug_vehicleexit( "g_debug_vehicleexit", "0", FCVAR_CHEAT ); + +ConVar sv_vehicle_autoaim_scale("sv_vehicle_autoaim_scale", "8"); + +bool ShouldVehicleIgnoreEntity( CBaseEntity *pVehicle, CBaseEntity *pCollide ); + +#define HITBOX_SET 2 + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC_NO_BASE( vehicle_gear_t ) + + DEFINE_FIELD( flMinSpeed, FIELD_FLOAT ), + DEFINE_FIELD( flMaxSpeed, FIELD_FLOAT ), + DEFINE_FIELD( flSpeedApproachFactor,FIELD_FLOAT ), + +END_DATADESC() + +BEGIN_DATADESC_NO_BASE( vehicle_crashsound_t ) + DEFINE_FIELD( flMinSpeed, FIELD_FLOAT ), + DEFINE_FIELD( flMinDeltaSpeed, FIELD_FLOAT ), + DEFINE_FIELD( iszCrashSound, FIELD_STRING ), + DEFINE_FIELD( gearLimit, FIELD_INTEGER ), +END_DATADESC() + +BEGIN_DATADESC_NO_BASE( vehiclesounds_t ) + + DEFINE_AUTO_ARRAY( iszSound, FIELD_STRING ), + DEFINE_UTLVECTOR( pGears, FIELD_EMBEDDED ), + DEFINE_UTLVECTOR( crashSounds, FIELD_EMBEDDED ), + DEFINE_AUTO_ARRAY( iszStateSounds, FIELD_STRING ), + DEFINE_AUTO_ARRAY( minStateTime, FIELD_FLOAT ), + +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( CPassengerInfo ) + DEFINE_FIELD( m_hPassenger, FIELD_EHANDLE ), + DEFINE_FIELD( m_strRoleName, FIELD_STRING ), + DEFINE_FIELD( m_strSeatName, FIELD_STRING ), + // NOT SAVED + // DEFINE_FIELD( m_nRole, FIELD_INTEGER ), + // DEFINE_FIELD( m_nSeat, FIELD_INTEGER ), +END_DATADESC() + +BEGIN_SIMPLE_DATADESC( CBaseServerVehicle ) + +// These are reset every time by the constructor of the owning class +// DEFINE_FIELD( m_pVehicle, FIELD_CLASSPTR ), +// DEFINE_FIELD( m_pDrivableVehicle; ??? ), + + // Controls + DEFINE_FIELD( m_nNPCButtons, FIELD_INTEGER ), + DEFINE_FIELD( m_nPrevNPCButtons, FIELD_INTEGER ), + DEFINE_FIELD( m_flTurnDegrees, FIELD_FLOAT ), + DEFINE_FIELD( m_flVehicleVolume, FIELD_FLOAT ), + + // We're going to reparse this data from file in Precache + DEFINE_EMBEDDED( m_vehicleSounds ), + + DEFINE_FIELD( m_iSoundGear, FIELD_INTEGER ), + DEFINE_FIELD( m_flSpeedPercentage, FIELD_FLOAT ), + DEFINE_SOUNDPATCH( m_pStateSound ), + DEFINE_SOUNDPATCH( m_pStateSoundFade ), + DEFINE_FIELD( m_soundState, FIELD_INTEGER ), + DEFINE_FIELD( m_soundStateStartTime, FIELD_TIME ), + DEFINE_FIELD( m_lastSpeed, FIELD_FLOAT ), + +// NOT SAVED +// DEFINE_FIELD( m_EntryAnimations, CUtlVector ), +// DEFINE_FIELD( m_ExitAnimations, CUtlVector ), +// DEFINE_FIELD( m_bParsedAnimations, FIELD_BOOLEAN ), +// DEFINE_UTLVECTOR( m_PassengerRoles, FIELD_EMBEDDED ), + + DEFINE_FIELD( m_iCurrentExitAnim, FIELD_INTEGER ), + DEFINE_FIELD( m_vecCurrentExitEndPoint, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_chPreviousTextureType, FIELD_CHARACTER ), + + DEFINE_FIELD( m_savedViewOffset, FIELD_VECTOR ), + DEFINE_FIELD( m_hExitBlocker, FIELD_EHANDLE ), + + DEFINE_UTLVECTOR( m_PassengerInfo, FIELD_EMBEDDED ), + +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Base class for drivable vehicle handling. Contain it in your +// drivable vehicle. +//----------------------------------------------------------------------------- +CBaseServerVehicle::CBaseServerVehicle( void ) +{ + m_pVehicle = NULL; + m_pDrivableVehicle = NULL; + m_nNPCButtons = 0; + m_nPrevNPCButtons = 0; + m_flTurnDegrees = 0; + + m_bParsedAnimations = false; + m_iCurrentExitAnim = 0; + m_vecCurrentExitEndPoint = vec3_origin; + + m_flVehicleVolume = 0.5; + m_iSoundGear = 0; + m_pStateSound = NULL; + m_pStateSoundFade = NULL; + m_soundState = SS_NONE; + m_flSpeedPercentage = 0; + m_bUseLegacyExitChecks = false; + + m_vehicleSounds.Init(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseServerVehicle::~CBaseServerVehicle( void ) +{ + SoundShutdown(0); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::Precache( void ) +{ + int i; + // Precache our other sounds + for ( i = 0; i < VS_NUM_SOUNDS; i++ ) + { + if ( m_vehicleSounds.iszSound[i] != NULL_STRING ) + { + CBaseEntity::PrecacheScriptSound( STRING(m_vehicleSounds.iszSound[i]) ); + } + } + for ( i = 0; i < m_vehicleSounds.crashSounds.Count(); i++ ) + { + if ( m_vehicleSounds.crashSounds[i].iszCrashSound != NULL_STRING ) + { + CBaseEntity::PrecacheScriptSound( STRING(m_vehicleSounds.crashSounds[i].iszCrashSound) ); + } + } + + for ( i = 0; i < SS_NUM_STATES; i++ ) + { + if ( m_vehicleSounds.iszStateSounds[i] != NULL_STRING ) + { + CBaseEntity::PrecacheScriptSound( STRING(m_vehicleSounds.iszStateSounds[i]) ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Parses the vehicle's script for the vehicle sounds +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::Initialize( const char *pScriptName ) +{ + // Attempt to parse our vehicle script + if ( PhysFindOrAddVehicleScript( pScriptName, NULL, &m_vehicleSounds ) == false ) + return false; + + Precache(); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::SetVehicle( CBaseEntity *pVehicle ) +{ + m_pVehicle = pVehicle; + m_pDrivableVehicle = dynamic_cast(m_pVehicle); + Assert( m_pDrivableVehicle ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +IDrivableVehicle *CBaseServerVehicle::GetDrivableVehicle( void ) +{ + Assert( m_pDrivableVehicle ); + return m_pDrivableVehicle; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the driver. Unlike GetPassenger(VEHICLE_ROLE_DRIVER), it will return +// the NPC driver if it has one. +//----------------------------------------------------------------------------- +CBaseEntity *CBaseServerVehicle::GetDriver( void ) +{ + return GetPassenger( VEHICLE_ROLE_DRIVER ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseCombatCharacter *CBaseServerVehicle::GetPassenger( int nRole ) +{ + Assert( nRole == VEHICLE_ROLE_DRIVER ); + CBaseEntity *pDriver = GetDrivableVehicle()->GetDriver(); + + if ( pDriver == NULL ) + return NULL; + + return pDriver->MyCombatCharacterPointer(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseServerVehicle::GetPassengerRole( CBaseCombatCharacter *pPassenger ) +{ + if ( pPassenger == GetDrivableVehicle()->GetDriver() ) + return VEHICLE_ROLE_DRIVER; + + return VEHICLE_ROLE_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a passenger to the vehicle +// Input : nSeat - seat to sit in +// *pPassenger - character to enter +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::NPC_AddPassenger( CBaseCombatCharacter *pPassenger, string_t strRoleName, int nSeat ) +{ + // Players cannot yet use this code! - jdw + Assert( pPassenger != NULL && pPassenger->IsPlayer() == false ); + if ( pPassenger == NULL || pPassenger->IsPlayer() ) + return false; + + // Find our role + int nRole = FindRoleIndexByName( strRoleName ); + if ( nRole == -1 ) + return false; + + // Cannot evict a passenger already in this position + CBaseCombatCharacter *pCurrentPassenger = NPC_GetPassengerInSeat( nRole, nSeat ); + if ( pCurrentPassenger == pPassenger ) + return true; + + // If we weren't the same passenger, we need to be empty + if ( pCurrentPassenger != NULL ) + return false; + + // Find the seat + for ( int i = 0; i < m_PassengerInfo.Count(); i++ ) + { + if ( m_PassengerInfo[i].GetSeat() == nSeat && m_PassengerInfo[i].GetRole() == nRole ) + { + m_PassengerInfo[i].m_hPassenger = pPassenger; + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Removes a passenger from the vehicle +// Input : *pPassenger - Passenger to remove +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::NPC_RemovePassenger( CBaseCombatCharacter *pPassenger ) +{ + // Players cannot yet use this code! - jdw + Assert( pPassenger != NULL && pPassenger->IsPlayer() == false ); + if ( pPassenger == NULL || pPassenger->IsPlayer() ) + return false; + + // Find the seat + for ( int i = 0; i < m_PassengerInfo.Count(); i++ ) + { + if ( m_PassengerInfo[i].m_hPassenger == pPassenger ) + { + m_PassengerInfo[i].m_hPassenger = NULL; + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the attachment point index for the passenger's seat +// Input : *pPassenger - Passenger in the seat +// Output : int - Attachment point index for the vehicle +//----------------------------------------------------------------------------- +int CBaseServerVehicle::NPC_GetPassengerSeatAttachment( CBaseCombatCharacter *pPassenger ) +{ + // Get the role and seat the the supplied passenger + for ( int i = 0; i < m_PassengerInfo.Count(); i++ ) + { + // If this is the passenger, get the attachment it'll be at + if ( m_PassengerInfo[i].m_hPassenger == pPassenger ) + { + // The seat is the attachment point + int nSeat = m_PassengerInfo[i].GetSeat(); + int nRole = m_PassengerInfo[i].GetRole(); + + return m_PassengerRoles[nRole].m_PassengerSeats[nSeat].GetAttachmentID(); + } + } + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the worldspace position and angles of the specified seat +// Input : *pPassenger - Passenger's seat to use +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::NPC_GetPassengerSeatPosition( CBaseCombatCharacter *pPassenger, Vector *vecResultPos, QAngle *vecResultAngles ) +{ + // Get our attachment point + int nSeatAttachment = NPC_GetPassengerSeatAttachment( pPassenger ); + if ( nSeatAttachment == -1 ) + return false; + + // Figure out which entrypoint hitbox the player is in + CBaseAnimating *pAnimating = dynamic_cast< CBaseAnimating * >( m_pVehicle ); + if ( pAnimating == NULL ) + return false; + + Vector vecPos; + QAngle vecAngles; + pAnimating->GetAttachment( nSeatAttachment, vecPos, vecAngles ); + + if ( vecResultPos != NULL ) + { + *vecResultPos = vecPos; + } + + if ( vecResultAngles != NULL ) + { + *vecResultAngles = vecAngles; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the localspace position and angles of the specified seat +// Input : *pPassenger - Passenger's seat to use +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::NPC_GetPassengerSeatPositionLocal( CBaseCombatCharacter *pPassenger, Vector *vecResultPos, QAngle *vecResultAngles ) +{ + // Get our attachment point + int nSeatAttachment = NPC_GetPassengerSeatAttachment( pPassenger ); + if ( nSeatAttachment == -1 ) + return false; + + // Figure out which entrypoint hitbox the player is in + CBaseAnimating *pAnimating = m_pVehicle->GetBaseAnimating(); + if ( pAnimating == NULL ) + return false; + + Vector vecPos; + QAngle vecAngles; + pAnimating->InvalidateBoneCache(); // NOTE: We're moving with velocity, so we're almost always out of date + pAnimating->GetAttachmentLocal( nSeatAttachment, vecPos, vecAngles ); + + if ( vecResultPos != NULL ) + { + *vecResultPos = vecPos; + } + + if ( vecResultAngles != NULL ) + { + *vecResultAngles = vecAngles; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Retrieves a list of animations used to enter/exit the seat occupied by the passenger +// Input : *pPassenger - Passenger who's seat anims to retrieve +// nType - which set of animations to retrieve +//----------------------------------------------------------------------------- +const PassengerSeatAnims_t *CBaseServerVehicle::NPC_GetPassengerSeatAnims( CBaseCombatCharacter *pPassenger, PassengerSeatAnimType_t nType ) +{ + // Get the role and seat the the supplied passenger + for ( int i = 0; i < m_PassengerInfo.Count(); i++ ) + { + if ( m_PassengerInfo[i].m_hPassenger == pPassenger ) + { + int nSeat = m_PassengerInfo[i].GetSeat(); + int nRole = m_PassengerInfo[i].GetRole(); + switch( nType ) + { + case PASSENGER_SEAT_ENTRY: + return &m_PassengerRoles[nRole].m_PassengerSeats[nSeat].m_EntryTransitions; + break; + + case PASSENGER_SEAT_EXIT: + return &m_PassengerRoles[nRole].m_PassengerSeats[nSeat].m_ExitTransitions; + break; + + default: + return NULL; + break; + } + } + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Get and set the current driver. Use PassengerRole_t enum in shareddefs.h for adding passengers +//----------------------------------------------------------------------------- +void CBaseServerVehicle::SetPassenger( int nRole, CBaseCombatCharacter *pPassenger ) +{ + // Baseclass only handles vehicles with a single passenger + Assert( nRole == VEHICLE_ROLE_DRIVER ); + + if ( pPassenger != NULL && pPassenger->IsPlayer() == false ) + { + // Use NPC_AddPassenger() for NPCs at the moment, these will all be collapsed into one system -- jdw + Assert( 0 ); + return; + } + + // Getting in? or out? + if ( pPassenger != NULL ) + { + CBasePlayer *pPlayer = ToBasePlayer( pPassenger ); + if ( pPlayer != NULL ) + { + m_savedViewOffset = pPlayer->GetViewOffset(); + pPlayer->SetViewOffset( vec3_origin ); + pPlayer->ShowCrosshair( false ); + + GetDrivableVehicle()->EnterVehicle( pPassenger ); + +#ifdef HL2_DLL + // Stop the player sprint and flashlight. + CHL2_Player *pHL2Player = dynamic_cast( pPlayer ); + if ( pHL2Player ) + { + if ( pHL2Player->IsSprinting() ) + { + pHL2Player->StopSprinting(); + } + + if ( pHL2Player->FlashlightIsOn() ) + { + pHL2Player->FlashlightTurnOff(); + } + } +#endif + } + } + else + { + CBasePlayer *pPlayer = ToBasePlayer( GetDriver() ); + if ( pPlayer ) + { + // Restore the exiting player's view offset + pPlayer->SetViewOffset( m_savedViewOffset ); + pPlayer->ShowCrosshair( true ); + } + + GetDrivableVehicle()->ExitVehicle( nRole ); + GetDrivableVehicle()->SetVehicleEntryAnim( false ); + UTIL_Remove( m_hExitBlocker ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Get a position in *world space* inside the vehicle for the player to start at +//----------------------------------------------------------------------------- +void CBaseServerVehicle::GetPassengerSeatPoint( int nRole, Vector *pPoint, QAngle *pAngles ) +{ + Assert( nRole == VEHICLE_ROLE_DRIVER ); + + CBaseAnimating *pAnimating = dynamic_cast(m_pVehicle); + if ( pAnimating ) + { + char pAttachmentName[32]; + Q_snprintf( pAttachmentName, sizeof( pAttachmentName ), "vehicle_feet_passenger%d", nRole ); + int nFeetAttachmentIndex = pAnimating->LookupAttachment(pAttachmentName); + int nIdleSequence = pAnimating->SelectWeightedSequence( ACT_IDLE ); + if ( nFeetAttachmentIndex > 0 && nIdleSequence != -1 ) + { + // FIXME: This really wants to be a faster query than this implementation! + Vector vecOrigin; + QAngle vecAngles; + if ( GetLocalAttachmentAtTime( nIdleSequence, nFeetAttachmentIndex, 0.0f, &vecOrigin, &vecAngles ) ) + { + UTIL_ParentToWorldSpace( pAnimating, vecOrigin, vecAngles ); + if ( pPoint ) + { + *pPoint = vecOrigin; + } + + if ( pAngles ) + { + *pAngles = vecAngles; + } + + return; + } + } + } + + // Couldn't find the attachment point, so just use the origin + if ( pPoint ) + { + *pPoint = m_pVehicle->GetAbsOrigin(); + } + + if ( pAngles ) + { + *pAngles = m_pVehicle->GetAbsAngles(); + } +} + +//--------------------------------------------------------------------------------- +// Check Exit Point for leaving vehicle. +// +// Input: yaw/roll from vehicle angle to check for exit +// distance from origin to drop player (allows for different shaped vehicles +// Output: returns true if valid location, pEndPoint +// updated with actual exit point +//--------------------------------------------------------------------------------- +bool CBaseServerVehicle::CheckExitPoint( float yaw, int distance, Vector *pEndPoint ) +{ + QAngle vehicleAngles = m_pVehicle->GetLocalAngles(); + Vector vecStart = m_pVehicle->GetAbsOrigin(); + Vector vecDir; + + vecStart.z += 12; // always 12" from ground + vehicleAngles[YAW] += yaw; + AngleVectors( vehicleAngles, NULL, &vecDir, NULL ); + // Vehicles are oriented along the Y axis + vecDir *= -1; + *pEndPoint = vecStart + vecDir * distance; + + trace_t tr; + UTIL_TraceHull( vecStart, *pEndPoint, VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, m_pVehicle, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction < 1.0 ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Where does this passenger exit the vehicle? +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::GetPassengerExitPoint( int nRole, Vector *pExitPoint, QAngle *pAngles ) +{ + Assert( nRole == VEHICLE_ROLE_DRIVER ); + + // First, see if we've got an attachment point + CBaseAnimating *pAnimating = dynamic_cast(m_pVehicle); + if ( pAnimating ) + { + Vector vehicleExitOrigin; + QAngle vehicleExitAngles; + if ( pAnimating->GetAttachment( "vehicle_driver_exit", vehicleExitOrigin, vehicleExitAngles ) ) + { + // Make sure it's clear + trace_t tr; + UTIL_TraceHull( vehicleExitOrigin + Vector(0, 0, 12), vehicleExitOrigin, VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, m_pVehicle, COLLISION_GROUP_NONE, &tr ); + if ( !tr.startsolid ) + { + *pAngles = vehicleExitAngles; + *pExitPoint = tr.endpos; + return true; + } + } + } + + // left side + if( CheckExitPoint( 90, 90, pExitPoint ) ) // angle from car, distance from origin, actual exit point + return true; + + // right side + if( CheckExitPoint( -90, 90, pExitPoint ) ) + return true; + + // front + if( CheckExitPoint( 0, 100, pExitPoint ) ) + return true; + + // back + if( CheckExitPoint( 180, 170, pExitPoint ) ) + return true; + + // All else failed, try popping them out the top. + Vector vecWorldMins, vecWorldMaxs; + m_pVehicle->CollisionProp()->WorldSpaceAABB( &vecWorldMins, &vecWorldMaxs ); + pExitPoint->x = (vecWorldMins.x + vecWorldMaxs.x) * 0.5f; + pExitPoint->y = (vecWorldMins.y + vecWorldMaxs.y) * 0.5f; + pExitPoint->z = vecWorldMaxs.z + 50.0f; + + // Make sure it's clear + trace_t tr; + UTIL_TraceHull( m_pVehicle->CollisionProp()->WorldSpaceCenter(), *pExitPoint, VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, m_pVehicle, COLLISION_GROUP_NONE, &tr ); + if ( !tr.startsolid ) + { + return true; + } + + // No clear exit point available! + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::ParseExitAnim( KeyValues *pkvExitList, bool bEscapeExit ) +{ + // Look through the entry animations list + KeyValues *pkvExitAnim = pkvExitList->GetFirstSubKey(); + while ( pkvExitAnim ) + { + // Add 'em to our list + int iIndex = m_ExitAnimations.AddToTail(); + Q_strncpy( m_ExitAnimations[iIndex].szAnimName, pkvExitAnim->GetName(), sizeof(m_ExitAnimations[iIndex].szAnimName) ); + m_ExitAnimations[iIndex].bEscapeExit = bEscapeExit; + if ( !Q_strncmp( pkvExitAnim->GetString(), "upsidedown", 10 ) ) + { + m_ExitAnimations[iIndex].bUpright = false; + } + else + { + m_ExitAnimations[iIndex].bUpright = true; + } + + pkvExitAnim = pkvExitAnim->GetNextKey(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Parse the transition information +// Input : *pTransitionKeyValues - key values to parse +//----------------------------------------------------------------------------- +void CBaseServerVehicle::ParseNPCSeatTransition( KeyValues *pTransitionKeyValues, CPassengerSeatTransition *pTransition ) +{ + // Store it + const char *lpszAnimName = pTransitionKeyValues->GetString( "animation" ); + pTransition->m_strAnimationName = AllocPooledString( lpszAnimName ); + pTransition->m_nPriority = pTransitionKeyValues->GetInt( "priority" ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sorting function for vehicle seat animation priorities +//----------------------------------------------------------------------------- +typedef CPassengerSeatTransition SortSeatPriorityType; +int __cdecl SeatPrioritySort( const SortSeatPriorityType *s1, const SortSeatPriorityType *s2 ) +{ + return ( s1->GetPriority() > s2->GetPriority() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Parse one set of entry/exit data +// Input : *pSetKeyValues - Key values for this set +//----------------------------------------------------------------------------- +void CBaseServerVehicle::ParseNPCPassengerSeat( KeyValues *pSetKeyValues, CPassengerSeat *pSeat ) +{ + CBaseAnimating *pAnimating = (CBaseAnimating *) m_pVehicle; + + // Get our attachment name + const char *lpszAttachmentName = pSetKeyValues->GetString( "target_attachment" ); + int nAttachmentID = pAnimating->LookupAttachment( lpszAttachmentName ); + pSeat->m_nAttachmentID = nAttachmentID; + pSeat->m_strSeatName = AllocPooledString( lpszAttachmentName ); + + KeyValues *pKey = pSetKeyValues->GetFirstSubKey(); + while ( pKey != NULL ) + { + const char *lpszName = pKey->GetName(); + + if ( Q_stricmp( lpszName, "entry" ) == 0 ) + { + int nIndex = pSeat->m_EntryTransitions.AddToTail(); + Assert( pSeat->m_EntryTransitions.IsValidIndex( nIndex ) ); + + ParseNPCSeatTransition( pKey, &pSeat->m_EntryTransitions[nIndex] ); + } + else if ( Q_stricmp( lpszName, "exit" ) == 0 ) + { + int nIndex = pSeat->m_ExitTransitions.AddToTail(); + Assert( pSeat->m_ExitTransitions.IsValidIndex( nIndex ) ); + + ParseNPCSeatTransition( pKey, &pSeat->m_ExitTransitions[nIndex] ); + } + + // Advance + pKey = pKey->GetNextKey(); + } + + // Sort the seats based on their priority + pSeat->m_EntryTransitions.Sort( SeatPrioritySort ); + pSeat->m_ExitTransitions.Sort( SeatPrioritySort ); +} + +//----------------------------------------------------------------------------- +// Purpose: Find a passenger role (by name), or create a new one of that names +// Input : strName - name of the role +// : *nIndex - the index into the CUtlBuffer where this role resides +// Output : CPassengerRole * - Role found or created +//----------------------------------------------------------------------------- +CPassengerRole *CBaseServerVehicle::FindOrCreatePassengerRole( string_t strName, int *nIndex ) +{ + // Try to find an already created container of the same name + for ( int i = 0; i < m_PassengerRoles.Count(); i++ ) + { + // If we match, return it + if ( FStrEq( STRING( m_PassengerRoles[i].m_strName ), STRING( strName ) ) ) + { + // Supply the index, if requested + if ( nIndex != NULL ) + { + *nIndex = i; + } + + return &m_PassengerRoles[i]; + } + } + + // Create a new container + int nNewIndex = m_PassengerRoles.AddToTail(); + Assert( m_PassengerRoles.IsValidIndex( nNewIndex ) ); + + m_PassengerRoles[nNewIndex].m_strName = strName; + + // Supply the index, if requested + if ( nIndex != NULL ) + { + *nIndex = nNewIndex; + } + + return &m_PassengerRoles[nNewIndex]; +} + +ConVar g_debug_npc_vehicle_roles( "g_debug_npc_vehicle_roles", "0" ); + +//----------------------------------------------------------------------------- +// Purpose: Parse NPC entry and exit anim data +// Input : *pModelKeyValues - Key values from the vehicle model +//----------------------------------------------------------------------------- +void CBaseServerVehicle::ParseNPCRoles( KeyValues *pkvPassengerList ) +{ + // Get the definition section + if ( pkvPassengerList == NULL ) + return; + + // Get our animating class + CBaseAnimating *pAnimating = dynamic_cast(m_pVehicle); + Assert( pAnimating != NULL ); + if ( pAnimating == NULL ) + return; + + // For attachment polling + CStudioHdr *pStudioHdr = pAnimating->GetModelPtr(); + Assert( pStudioHdr != NULL ); + if ( pStudioHdr == NULL ) + return; + + // Parse all subkeys + int nRoleIndex; + KeyValues *pkvPassengerKey = pkvPassengerList->GetFirstSubKey(); + while ( pkvPassengerKey != NULL ) + { + string_t strRoleName = AllocPooledString( pkvPassengerKey->GetName() ); + + // Find or create the container + CPassengerRole *pRole = FindOrCreatePassengerRole( strRoleName, &nRoleIndex ); + if ( pRole == NULL ) + continue; + + // Add a new role + int nSeatIndex = pRole->m_PassengerSeats.AddToTail(); + Assert( pRole->m_PassengerSeats.IsValidIndex( nSeatIndex ) ); + + // Parse the information + ParseNPCPassengerSeat( pkvPassengerKey, &pRole->m_PassengerSeats[nSeatIndex] ); + + // Add a matching entry into our passenger manifest + int nPassengerIndex = m_PassengerInfo.AddToTail(); + m_PassengerInfo[nPassengerIndex].m_hPassenger = NULL; + m_PassengerInfo[nPassengerIndex].m_nSeat = nSeatIndex; + m_PassengerInfo[nPassengerIndex].m_nRole = nRoleIndex; + + // The following are used for index fix-ups after save game restoration + m_PassengerInfo[nPassengerIndex].m_strRoleName = strRoleName; + m_PassengerInfo[nPassengerIndex].m_strSeatName = pRole->m_PassengerSeats[nSeatIndex].m_strSeatName; + + // Advance to the next key + pkvPassengerKey = pkvPassengerKey->GetNextKey(); + } + + // ====================================================================================================== + // Debug print + + if ( g_debug_npc_vehicle_roles.GetBool() ) + { + Msg("Passenger Roles Parsed:\t%d\n\n", m_PassengerRoles.Count() ); + for ( int i = 0; i < m_PassengerRoles.Count(); i++ ) + { + Msg("\tPassenger Role:\t%s (%d seats)\n", STRING(m_PassengerRoles[i].m_strName), m_PassengerRoles[i].m_PassengerSeats.Count() ); + + // Iterate through all information sets under this name + for ( int j = 0; j < m_PassengerRoles[i].m_PassengerSeats.Count(); j++ ) + { + Msg("\t\tAttachment: %d\n", m_PassengerRoles[i].m_PassengerSeats[j].m_nAttachmentID ); + + // Entries + Msg("\t\tEntries:\t%d\n", m_PassengerRoles[i].m_PassengerSeats[j].m_EntryTransitions.Count() ); + Msg("\t\t=====================\n" ); + + for ( int nEntry = 0; nEntry < m_PassengerRoles[i].m_PassengerSeats[j].m_EntryTransitions.Count(); nEntry++ ) + { + Msg("\t\t\tAnimation:\t%s\t(Priority %d)\n", STRING(m_PassengerRoles[i].m_PassengerSeats[j].m_EntryTransitions[nEntry].m_strAnimationName), + m_PassengerRoles[i].m_PassengerSeats[j].m_EntryTransitions[nEntry].m_nPriority ); + } + + Msg("\n"); + + // Exits + Msg("\t\tExits:\t%d\n", m_PassengerRoles[i].m_PassengerSeats[j].m_ExitTransitions.Count() ); + Msg("\t\t=====================\n" ); + + for ( int nExits = 0; nExits < m_PassengerRoles[i].m_PassengerSeats[j].m_ExitTransitions.Count(); nExits++ ) + { + Msg("\t\t\tAnimation:\t%s\t(Priority %d)\n", STRING(m_PassengerRoles[i].m_PassengerSeats[j].m_ExitTransitions[nExits].m_strAnimationName), + m_PassengerRoles[i].m_PassengerSeats[j].m_ExitTransitions[nExits].m_nPriority ); + } + } + + Msg("\n"); + } + } + + // ====================================================================================================== +} +//----------------------------------------------------------------------------- +// Purpose: Get an attachment point at a specified time in its cycle (note: not exactly a speedy query, use sparingly!) +// Input : nSequence - sequence to test +// nAttachmentIndex - attachment to test +// flCyclePoint - 0.0 - 1.0 +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::GetLocalAttachmentAtTime( int nQuerySequence, int nAttachmentIndex, float flCyclePoint, Vector *vecOriginOut, QAngle *vecAnglesOut ) +{ + CBaseAnimating *pAnimating = m_pVehicle->GetBaseAnimating(); + if ( pAnimating == NULL ) + return false; + + // TODO: It's annoying to stomp and restore this off each time when we're just going to stomp it again later, but the function + // should really leave the car in an acceptable state to run this query -- jdw + + // Store this off for restoration later + int nOldSequence = pAnimating->GetSequence(); + float flOldCycle = pAnimating->GetCycle(); + + // Setup the model for the query + pAnimating->SetSequence( nQuerySequence ); + pAnimating->SetCycle( flCyclePoint ); + pAnimating->InvalidateBoneCache(); + + // Query for the point + Vector vecOrigin; + QAngle vecAngles; + pAnimating->GetAttachmentLocal( nAttachmentIndex, vecOrigin, vecAngles ); + + if ( vecOriginOut != NULL ) + { + *vecOriginOut = vecOrigin; + } + + if ( vecAnglesOut != NULL ) + { + *vecAnglesOut = vecAngles; + } + + // Restore the model after the query + pAnimating->SetSequence( nOldSequence ); + pAnimating->SetCycle( flOldCycle ); + pAnimating->InvalidateBoneCache(); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Get an attachment point at a specified time in its cycle (note: not exactly a speedy query, use sparingly!) +// Input : lpszAnimName - name of the sequence to test +// nAttachmentIndex - attachment to test +// flCyclePoint - 0.0 - 1.0 +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::GetLocalAttachmentAtTime( const char *lpszAnimName, int nAttachmentIndex, float flCyclePoint, Vector *vecOriginOut, QAngle *vecAnglesOut ) +{ + CBaseAnimating *pAnimating = m_pVehicle->GetBaseAnimating(); + if ( pAnimating == NULL ) + return false; + + int nQuerySequence = pAnimating->LookupSequence( lpszAnimName ); + if ( nQuerySequence < 0 ) + return false; + + return GetLocalAttachmentAtTime( nQuerySequence, nAttachmentIndex, flCyclePoint, vecOriginOut, vecAnglesOut ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::CacheEntryExitPoints( void ) +{ + CBaseAnimating *pAnimating = m_pVehicle->GetBaseAnimating(); + if ( pAnimating == NULL ) + return; + + int nAttachment = pAnimating->LookupAttachment( "vehicle_driver_eyes" ); + + // For each exit animation, determine where the end point is and cache it + for ( int i = 0; i < m_ExitAnimations.Count(); i++ ) + { + if ( GetLocalAttachmentAtTime( m_ExitAnimations[i].szAnimName, nAttachment, 1.0f, &m_ExitAnimations[i].vecExitPointLocal, &m_ExitAnimations[i].vecExitAnglesLocal ) == false ) + { + Warning("Exit animation %s failed to cache target points properly!\n", m_ExitAnimations[i].szAnimName ); + } + + if ( g_debug_vehicleexit.GetBool() ) + { + Vector vecExitPoint = m_ExitAnimations[i].vecExitPointLocal; + QAngle vecExitAngles = m_ExitAnimations[i].vecExitAnglesLocal; + UTIL_ParentToWorldSpace( pAnimating, vecExitPoint, vecExitAngles ); + + NDebugOverlay::Box( vecExitPoint, -Vector(8,8,8), Vector(8,8,8), 0, 255, 0, 0, 20.0f ); + NDebugOverlay::Axis( vecExitPoint, vecExitAngles, 8.0f, true, 20.0f ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::ParseEntryExitAnims( void ) +{ + // Try and find the right animation to play in the model's keyvalues + KeyValues *modelKeyValues = new KeyValues(""); + if ( modelKeyValues->LoadFromBuffer( modelinfo->GetModelName( m_pVehicle->GetModel() ), modelinfo->GetModelKeyValueText( m_pVehicle->GetModel() ) ) ) + { + // Do we have an entry section? + KeyValues *pkvEntryList = modelKeyValues->FindKey("vehicle_entry"); + if ( pkvEntryList ) + { + // Look through the entry animations list + KeyValues *pkvEntryAnim = pkvEntryList->GetFirstSubKey(); + while ( pkvEntryAnim ) + { + // Add 'em to our list + int iIndex = m_EntryAnimations.AddToTail(); + Q_strncpy( m_EntryAnimations[iIndex].szAnimName, pkvEntryAnim->GetName(), sizeof(m_EntryAnimations[iIndex].szAnimName) ); + m_EntryAnimations[iIndex].iHitboxGroup = pkvEntryAnim->GetInt(); + + pkvEntryAnim = pkvEntryAnim->GetNextKey(); + } + } + + // Do we have an exit section? + KeyValues *pkvExitList = modelKeyValues->FindKey("vehicle_exit"); + if ( pkvExitList ) + { + ParseExitAnim( pkvExitList, false ); + } + + // Do we have an exit section? + pkvExitList = modelKeyValues->FindKey("vehicle_escape_exit"); + if ( pkvExitList ) + { + ParseExitAnim( pkvExitList, true ); + } + + // Parse the NPC vehicle roles as well + KeyValues *pkvPassengerList = modelKeyValues->FindKey( "vehicle_npc_passengers" ); + if ( pkvPassengerList ) + { + ParseNPCRoles( pkvPassengerList ); + } + } + + modelKeyValues->deleteThis(); + + // Determine the entry and exit points for the + CacheEntryExitPoints(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::HandlePassengerEntry( CBaseCombatCharacter *pPassenger, bool bAllowEntryOutsideZone ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pPassenger ); + if ( pPlayer != NULL ) + { + // Find out which hitbox the player's eyepoint is within + int iEntryAnim = GetEntryAnimForPoint( pPlayer->EyePosition() ); + + // Get this interface for animation queries + CBaseAnimating *pAnimating = dynamic_cast(m_pVehicle); + if ( !pAnimating ) + return; + + // Are we in an entrypoint zone? + if ( iEntryAnim == ACTIVITY_NOT_AVAILABLE ) + { + // Normal get in refuses to allow entry + if ( !bAllowEntryOutsideZone ) + return; + + // We failed to find a valid entry anim, but we've got to get back in because the player's + // got stuck exiting the vehicle. For now, just use the first get in anim + // UNDONE: We need a better solution for this. + iEntryAnim = pAnimating->LookupSequence( m_EntryAnimations[0].szAnimName ); + } + + // Check to see if this vehicle can be controlled or if it's locked + if ( GetDrivableVehicle()->CanEnterVehicle( pPlayer ) ) + { + // Make sure the passenger can get in as well + if ( pPlayer->CanEnterVehicle( this, VEHICLE_ROLE_DRIVER ) ) + { + // Setup the "enter" vehicle sequence and skip the animation if it isn't present. + pAnimating->SetCycle( 0 ); + pAnimating->m_flAnimTime = gpGlobals->curtime; + pAnimating->ResetSequence( iEntryAnim ); + pAnimating->ResetClientsideFrame(); + pAnimating->InvalidateBoneCache(); // This is necessary because we need to query attachment points this frame for blending! + GetDrivableVehicle()->SetVehicleEntryAnim( true ); + + pPlayer->GetInVehicle( this, VEHICLE_ROLE_DRIVER ); + } + } + } + else + { + // NPCs handle transitioning themselves, they should NOT call this function + Assert( 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::HandlePassengerExit( CBaseCombatCharacter *pPassenger ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pPassenger ); + if ( pPlayer != NULL ) + { + // Clear hud hints + UTIL_HudHintText( pPlayer, "" ); + + vbs_sound_update_t params; + InitSoundParams(params); + params.bExitVehicle = true; + SoundState_Update( params ); + + // Find the right exit anim to use based on available exit points. + Vector vecExitPoint; + bool bAllPointsBlocked; + int iSequence = GetExitAnimToUse( vecExitPoint, bAllPointsBlocked ); + + // If all exit points were blocked and this vehicle doesn't allow exiting in + // these cases, bail. + Vector vecNewPos = pPlayer->GetAbsOrigin(); + QAngle angNewAngles = pPlayer->GetAbsAngles(); + + int nRole = GetPassengerRole( pPlayer ); + if ( ( bAllPointsBlocked ) || ( iSequence == ACTIVITY_NOT_AVAILABLE ) ) + { + // Animation-driven exit points are all blocked, or we have none. Fall back to the more simple static exit points. + if ( !GetPassengerExitPoint( nRole, &vecNewPos, &angNewAngles ) && !GetDrivableVehicle()->AllowBlockedExit( pPlayer, nRole ) ) + return false; + + // At this point, the player has exited the vehicle but did so without playing an animation. We need to give the vehicle a + // chance to do any post-animation clean-up it may need to perform. + HandleEntryExitFinish( false, true ); + } + + // Now we either have an exit sequence to play, a valid static exit position, or we don't care + // whether we're blocked or not. We're getting out, one way or another. + GetDrivableVehicle()->PreExitVehicle( pPlayer, nRole ); + + if ( iSequence > ACTIVITY_NOT_AVAILABLE ) + { + CBaseAnimating *pAnimating = dynamic_cast(m_pVehicle); + if ( pAnimating ) + { + pAnimating->SetCycle( 0 ); + pAnimating->m_flAnimTime = gpGlobals->curtime; + pAnimating->ResetSequence( iSequence ); + pAnimating->ResetClientsideFrame(); + GetDrivableVehicle()->SetVehicleExitAnim( true, vecExitPoint ); + + // Re-deploy our weapon + if ( pPlayer && pPlayer->IsAlive() ) + { + if ( pPlayer->GetActiveWeapon() ) + { + pPlayer->GetActiveWeapon()->Deploy(); + pPlayer->ShowCrosshair( true ); + } + } + + // To prevent anything moving into the volume the player's going to occupy at the end of the exit + // NOTE: Set the player as the blocker's owner so the player is allowed to intersect it + Vector vecExitFeetPoint = vecExitPoint - VEC_VIEW; + m_hExitBlocker = CEntityBlocker::Create( vecExitFeetPoint, VEC_HULL_MIN, VEC_HULL_MAX, pPlayer, true ); + + // We may as well stand where we're going to get out at and stop being parented + pPlayer->SetAbsOrigin( vecExitFeetPoint ); + pPlayer->SetParent( NULL ); + + return true; + } + } + + // Couldn't find an animation, so exit immediately + pPlayer->LeaveVehicle( vecNewPos, angNewAngles ); + return true; + } + else + { + // NPCs handle transitioning themselves, they should NOT call this function + Assert( 0 ); + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseServerVehicle::GetEntryAnimForPoint( const Vector &vecEyePoint ) +{ + // Parse the vehicle animations the first time they get in the vehicle + if ( !m_bParsedAnimations ) + { + // Load the entry/exit animations from the vehicle + ParseEntryExitAnims(); + m_bParsedAnimations = true; + } + + // No entry anims? Vehicles with no entry anims are always enterable. + if ( !m_EntryAnimations.Count() ) + return 0; + + // Figure out which entrypoint hitbox the player is in + CBaseAnimating *pAnimating = dynamic_cast(m_pVehicle); + if ( !pAnimating ) + return 0; + + CStudioHdr *pStudioHdr = pAnimating->GetModelPtr(); + if (!pStudioHdr) + return 0; + int iHitboxSet = FindHitboxSetByName( pStudioHdr, "entryboxes" ); + mstudiohitboxset_t *set = pStudioHdr->pHitboxSet( iHitboxSet ); + if ( !set || !set->numhitboxes ) + return 0; + + // Loop through the hitboxes and find out which one we're in + for ( int i = 0; i < set->numhitboxes; i++ ) + { + mstudiobbox_t *pbox = set->pHitbox( i ); + + Vector vecPosition; + QAngle vecAngles; + pAnimating->GetBonePosition( pbox->bone, vecPosition, vecAngles ); + + // Build a rotation matrix from orientation + matrix3x4_t fRotateMatrix; + AngleMatrix( vecAngles, vecPosition, fRotateMatrix); + + Vector localEyePoint; + VectorITransform( vecEyePoint, fRotateMatrix, localEyePoint ); + if ( IsPointInBox( localEyePoint, pbox->bbmin, pbox->bbmax ) ) + { + // Find the entry animation for this hitbox + int iCount = m_EntryAnimations.Count(); + for ( int entry = 0; entry < iCount; entry++ ) + { + if ( m_EntryAnimations[entry].iHitboxGroup == pbox->group ) + { + // Get the sequence for the animation + return pAnimating->LookupSequence( m_EntryAnimations[entry].szAnimName ); + } + } + } + } + + // Fail + return ACTIVITY_NOT_AVAILABLE; +} + +//----------------------------------------------------------------------------- +// Purpose: Find an exit animation that'll get the player to a valid position +// Input : vecEyeExitEndpoint - Returns with the final eye position after exiting. +// bAllPointsBlocked - Returns whether all exit points were found to be blocked. +// Output : +//----------------------------------------------------------------------------- +int CBaseServerVehicle::GetExitAnimToUse( Vector &vecEyeExitEndpoint, bool &bAllPointsBlocked ) +{ + bAllPointsBlocked = false; + + // Parse the vehicle animations the first time they get in the vehicle + if ( !m_bParsedAnimations ) + { + // Load the entry/exit animations from the vehicle + ParseEntryExitAnims(); + m_bParsedAnimations = true; + } + + // No exit anims? + if ( !m_ExitAnimations.Count() ) + return ACTIVITY_NOT_AVAILABLE; + + // Figure out which entrypoint hitbox the player is in + CBaseAnimating *pAnimating = dynamic_cast(m_pVehicle); + if ( !pAnimating ) + return ACTIVITY_NOT_AVAILABLE; + + CStudioHdr *pStudioHdr = pAnimating->GetModelPtr(); + if (!pStudioHdr) + return ACTIVITY_NOT_AVAILABLE; + + bool bUpright = IsVehicleUpright(); + + // Loop through the exit animations and find one that ends in a clear position + // Also attempt to choose the animation which brings you closest to your view direction. + CBasePlayer *pPlayer = ToBasePlayer( GetDriver() ); + if ( pPlayer == NULL ) + return ACTIVITY_NOT_AVAILABLE; + + int nRole = GetPassengerRole( pPlayer ); + + int nBestExitAnim = -1; + bool bBestExitIsEscapePoint = true; + Vector vecViewDirection, vecViewOrigin, vecBestExitPoint( 0, 0, 0 ); + vecViewOrigin = pPlayer->EyePosition(); + pPlayer->EyeVectors( &vecViewDirection ); + vecViewDirection.z = 0.0f; + VectorNormalize( vecViewDirection ); + + float flMaxCosAngleDelta = -2.0f; + + int iCount = m_ExitAnimations.Count(); + for ( int i = 0; i < iCount; i++ ) + { + if ( m_ExitAnimations[i].bUpright != bUpright ) + continue; + + // Don't use an escape point if we found a non-escape point already + if ( !bBestExitIsEscapePoint && m_ExitAnimations[i].bEscapeExit ) + continue; + + Vector vehicleExitOrigin; + QAngle vehicleExitAngles; + + // NOTE: HL2 and Ep1 used a method that relied on the animators to place attachment points in the model which marked where + // the player would exit to. This was rendered unnecessary in Ep2, but the choreo vehicles of these older products + // did not have proper exit animations and relied on the exact queries that were happening before. For choreo vehicles, + // we now just allow them to perform those older queries to keep those products happy. - jdw + + // Get the position we think we're going to end up at + if ( m_bUseLegacyExitChecks ) + { + pAnimating->GetAttachment( m_ExitAnimations[i].szAnimName, vehicleExitOrigin, vehicleExitAngles ); + } + else + { + vehicleExitOrigin = m_ExitAnimations[i].vecExitPointLocal; + vehicleExitAngles = m_ExitAnimations[i].vecExitAnglesLocal; + UTIL_ParentToWorldSpace( pAnimating, vehicleExitOrigin, vehicleExitAngles ); + } + + // Don't bother checking points which are farther from our view direction. + Vector vecDelta; + VectorSubtract( vehicleExitOrigin, vecViewOrigin, vecDelta ); + vecDelta.z = 0.0f; + VectorNormalize( vecDelta ); + float flCosAngleDelta = DotProduct( vecDelta, vecViewDirection ); + + // But always check non-escape exits if our current best exit is an escape exit. + if ( !bBestExitIsEscapePoint || m_ExitAnimations[i].bEscapeExit ) + { + if ( flCosAngleDelta < flMaxCosAngleDelta ) + continue; + } + + // The attachment points are where the driver's eyes will end up, so we subtract the view offset + // to get the actual exit position. + vehicleExitOrigin -= VEC_VIEW; + + Vector vecMove(0,0,64); + Vector vecStart = vehicleExitOrigin + vecMove; + Vector vecEnd = vehicleExitOrigin - vecMove; + + // Starting at the exit point, trace a flat plane down until we hit ground + // NOTE: The hull has no vertical span because we want to test the lateral constraints against the ground, not height (yet) + trace_t tr; + UTIL_TraceHull( vecStart, vecEnd, VEC_HULL_MIN, Vector( VEC_HULL_MAX.x, VEC_HULL_MAX.y, VEC_HULL_MIN.z ), MASK_PLAYERSOLID, NULL, COLLISION_GROUP_NONE, &tr ); + + if ( g_debug_vehicleexit.GetBool() ) + { + NDebugOverlay::SweptBox( vecStart, vecEnd, VEC_HULL_MIN, Vector( VEC_HULL_MAX.x, VEC_HULL_MAX.y, VEC_HULL_MIN.y ), vec3_angle, 255, 255, 255, 8.0f, 20.0f ); + } + + if ( tr.fraction < 1.0f ) + { + // If we hit the ground, try to now "stand up" at that point to see if we'll fit + UTIL_TraceHull( tr.endpos, tr.endpos, VEC_HULL_MIN, VEC_HULL_MAX, MASK_PLAYERSOLID, NULL, COLLISION_GROUP_NONE, &tr ); + + // See if we're unable to stand at this space + if ( tr.startsolid ) + { + if ( g_debug_vehicleexit.GetBool() ) + { + NDebugOverlay::Box( tr.endpos, VEC_HULL_MIN, VEC_HULL_MAX, 255, 0, 0, 8.0f, 20.0f ); + } + continue; + } + + if ( g_debug_vehicleexit.GetBool() ) + { + NDebugOverlay::Box( tr.endpos, VEC_HULL_MIN, VEC_HULL_MAX, 0, 255, 0, 8.0f, 20.0f ); + } + } + else if ( tr.allsolid || ( ( tr.fraction == 1.0 ) && !GetDrivableVehicle()->AllowMidairExit( pPlayer, nRole ) ) ) + { + if ( g_debug_vehicleexit.GetBool() ) + { + NDebugOverlay::Box( tr.endpos, VEC_HULL_MIN, VEC_HULL_MAX, 255,0,0, 64, 10 ); + } + continue; + } + + // Calculate the exit endpoint & viewpoint + Vector vecExitEndPoint = tr.endpos; + + // Make sure we can trace to the center of the exit point + UTIL_TraceLine( vecViewOrigin, vecExitEndPoint, MASK_PLAYERSOLID, pAnimating, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction != 1.0 ) + { +#ifdef HL2_EPISODIC + if ( ShouldVehicleIgnoreEntity( GetVehicleEnt(), tr.m_pEnt ) == false ) +#endif //HL2_EPISODIC + { + if ( g_debug_vehicleexit.GetBool() ) + { + NDebugOverlay::Line( vecViewOrigin, vecExitEndPoint, 255,0,0, true, 10 ); + } + continue; + } + } + + bBestExitIsEscapePoint = m_ExitAnimations[i].bEscapeExit; + vecBestExitPoint = vecExitEndPoint; + nBestExitAnim = i; + flMaxCosAngleDelta = flCosAngleDelta; + } + + if ( nBestExitAnim >= 0 ) + { + m_vecCurrentExitEndPoint = vecBestExitPoint; + + if ( g_debug_vehicleexit.GetBool() ) + { + NDebugOverlay::Cross3D( m_vecCurrentExitEndPoint, 16, 0, 255, 0, true, 10 ); + NDebugOverlay::Box( m_vecCurrentExitEndPoint, VEC_HULL_MIN, VEC_HULL_MAX, 255,255,255, 8, 10 ); + } + + vecEyeExitEndpoint = vecBestExitPoint + VEC_VIEW; + m_iCurrentExitAnim = nBestExitAnim; + return pAnimating->LookupSequence( m_ExitAnimations[m_iCurrentExitAnim].szAnimName ); + } + + // Fail, all exit points were blocked. + bAllPointsBlocked = true; + return ACTIVITY_NOT_AVAILABLE; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::HandleEntryExitFinish( bool bExitAnimOn, bool bResetAnim ) +{ + // Parse the vehicle animations. This is needed because they may have + // saved, and loaded during exit anim, which would clear the exit anim. + if ( !m_bParsedAnimations ) + { + // Load the entry/exit animations from the vehicle + ParseEntryExitAnims(); + m_bParsedAnimations = true; + } + + // Figure out which entrypoint hitbox the player is in + CBaseAnimating *pAnimating = m_pVehicle->GetBaseAnimating(); + if ( !pAnimating ) + return; + + // Did the entry anim just finish? + if ( bExitAnimOn ) + { + // The exit animation just finished + CBasePlayer *pPlayer = ToBasePlayer( GetDriver() ); + if ( pPlayer != NULL ) + { + Vector vecEyes; + QAngle vecEyeAng; + if ( m_iCurrentExitAnim >= 0 && m_iCurrentExitAnim < m_ExitAnimations.Count() ) + { + // Convert our offset points to worldspace ones + vecEyes = m_ExitAnimations[m_iCurrentExitAnim].vecExitPointLocal; + vecEyeAng = m_ExitAnimations[m_iCurrentExitAnim].vecExitAnglesLocal; + UTIL_ParentToWorldSpace( pAnimating, vecEyes, vecEyeAng ); + + // Use the endpoint we figured out when we exited + vecEyes = m_vecCurrentExitEndPoint; + } + else + { + pAnimating->GetAttachment( "vehicle_driver_eyes", vecEyes, vecEyeAng ); + } + + if ( g_debug_vehicleexit.GetBool() ) + { + NDebugOverlay::Box( vecEyes, -Vector(2,2,2), Vector(2,2,2), 255,0,0, 64, 10.0 ); + } + + // If the end point isn't clear, get back in the vehicle + /* + trace_t tr; + UTIL_TraceHull( vecEyes, vecEyes, VEC_HULL_MIN, VEC_HULL_MAX, MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr ); + if ( tr.startsolid && tr.fraction < 1.0 ) + { + pPlayer->LeaveVehicle( vecEyes, vecEyeAng ); + m_pVehicle->Use( pPlayer, pPlayer, USE_TOGGLE, 1 ); + return; + } + */ + + pPlayer->LeaveVehicle( vecEyes, vecEyeAng ); + } + } + + // Only reset the animation if we're told to + if ( bResetAnim ) + { + // Start the vehicle idling again + int iSequence = pAnimating->SelectWeightedSequence( ACT_IDLE ); + if ( iSequence > ACTIVITY_NOT_AVAILABLE ) + { + pAnimating->SetCycle( 0 ); + pAnimating->m_flAnimTime = gpGlobals->curtime; + pAnimating->ResetSequence( iSequence ); + pAnimating->ResetClientsideFrame(); + } + } + + GetDrivableVehicle()->SetVehicleEntryAnim( false ); + GetDrivableVehicle()->SetVehicleExitAnim( false, vec3_origin ); +} + +//----------------------------------------------------------------------------- +// Purpose: Where does the passenger see from? +//----------------------------------------------------------------------------- +void CBaseServerVehicle::GetVehicleViewPosition( int nRole, Vector *pAbsOrigin, QAngle *pAbsAngles, float *pFOV /*= NULL*/ ) +{ + Assert( nRole == VEHICLE_ROLE_DRIVER ); + CBaseCombatCharacter *pPassenger = GetPassenger( VEHICLE_ROLE_DRIVER ); + Assert( pPassenger ); + + CBasePlayer *pPlayer = ToBasePlayer( pPassenger ); + if ( pPlayer != NULL ) + { + // Call through the player to resolve the actual position (if available) + if ( pAbsOrigin != NULL ) + { + *pAbsOrigin = pPlayer->EyePosition(); + } + + if ( pAbsAngles != NULL ) + { + *pAbsAngles = pPlayer->EyeAngles(); + } + + if ( pFOV ) + { + *pFOV = pPlayer->GetFOV(); + } + } + else + { + // NPCs are not supported + Assert( 0 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) +{ + GetDrivableVehicle()->SetupMove( player, ucmd, pHelper, move ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ) +{ + GetDrivableVehicle()->ProcessMovement( pPlayer, pMoveData ); + + trace_t tr; + UTIL_TraceLine( pPlayer->GetAbsOrigin(), pPlayer->GetAbsOrigin() - Vector( 0, 0, 256 ), MASK_PLAYERSOLID, GetVehicleEnt(), COLLISION_GROUP_NONE, &tr ); + + // If our gamematerial has changed, tell any player surface triggers that are watching + IPhysicsSurfaceProps *physprops = MoveHelper()->GetSurfaceProps(); + const surfacedata_t *pSurfaceProp = physprops->GetSurfaceData( tr.surface.surfaceProps ); + char cCurrGameMaterial = pSurfaceProp->game.material; + + // Changed? + if ( m_chPreviousTextureType != cCurrGameMaterial ) + { + CEnvPlayerSurfaceTrigger::SetPlayerSurface( pPlayer, cCurrGameMaterial ); + } + + m_chPreviousTextureType = cCurrGameMaterial; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ) +{ + GetDrivableVehicle()->FinishMove( player, ucmd, move ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::ItemPostFrame( CBasePlayer *player ) +{ + Assert( player == GetDriver() ); + + GetDrivableVehicle()->ItemPostFrame( player ); + + if ( player->m_afButtonPressed & IN_USE ) + { + if ( GetDrivableVehicle()->CanExitVehicle(player) ) + { + if ( !HandlePassengerExit( player ) && ( player != NULL ) ) + { + player->PlayUseDenySound(); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_ThrottleForward( void ) +{ + m_nNPCButtons |= IN_FORWARD; + m_nNPCButtons &= ~IN_BACK; + m_nNPCButtons &= ~IN_JUMP; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_ThrottleReverse( void ) +{ + m_nNPCButtons |= IN_BACK; + m_nNPCButtons &= ~IN_FORWARD; + m_nNPCButtons &= ~IN_JUMP; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_ThrottleCenter( void ) +{ + m_nNPCButtons &= ~IN_FORWARD; + m_nNPCButtons &= ~IN_BACK; + m_nNPCButtons &= ~IN_JUMP; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_Brake( void ) +{ + m_nNPCButtons &= ~IN_FORWARD; + m_nNPCButtons &= ~IN_BACK; + m_nNPCButtons |= IN_JUMP; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_TurnLeft( float flDegrees ) +{ + m_nNPCButtons |= IN_MOVELEFT; + m_nNPCButtons &= ~IN_MOVERIGHT; + m_flTurnDegrees = -flDegrees; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_TurnRight( float flDegrees ) +{ + m_nNPCButtons |= IN_MOVERIGHT; + m_nNPCButtons &= ~IN_MOVELEFT; + m_flTurnDegrees = flDegrees; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_TurnCenter( void ) +{ + m_nNPCButtons &= ~IN_MOVERIGHT; + m_nNPCButtons &= ~IN_MOVELEFT; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_PrimaryFire( void ) +{ + m_nNPCButtons |= IN_ATTACK; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::NPC_SecondaryFire( void ) +{ + m_nNPCButtons |= IN_ATTACK2; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::Weapon_PrimaryRanges( float *flMinRange, float *flMaxRange ) +{ + *flMinRange = 64; + *flMaxRange = 1024; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::Weapon_SecondaryRanges( float *flMinRange, float *flMaxRange ) +{ + *flMinRange = 64; + *flMaxRange = 1024; +} + +//----------------------------------------------------------------------------- +// Purpose: Return the time at which this vehicle's primary weapon can fire again +//----------------------------------------------------------------------------- +float CBaseServerVehicle::Weapon_PrimaryCanFireAt( void ) +{ + return gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: Return the time at which this vehicle's secondary weapon can fire again +//----------------------------------------------------------------------------- +float CBaseServerVehicle::Weapon_SecondaryCanFireAt( void ) +{ + return gpGlobals->curtime; +} + +const char *pSoundStateNames[] = +{ + "SS_NONE", + "SS_SHUTDOWN", + "SS_SHUTDOWN_WATER", + "SS_START_WATER", + "SS_START_IDLE", + "SS_IDLE", + "SS_GEAR_0", + "SS_GEAR_1", + "SS_GEAR_2", + "SS_GEAR_3", + "SS_GEAR_4", + "SS_SLOWDOWN", + "SS_SLOWDOWN_HIGHSPEED", + "SS_GEAR_0_RESUME", + "SS_GEAR_1_RESUME", + "SS_GEAR_2_RESUME", + "SS_GEAR_3_RESUME", + "SS_GEAR_4_RESUME", + "SS_TURBO", + "SS_REVERSE", +}; + + +static int SoundStateIndexFromName( const char *pName ) +{ + for ( int i = 0; i < SS_NUM_STATES; i++ ) + { + Assert( i < ARRAYSIZE(pSoundStateNames) ); + if ( !strcmpi( pSoundStateNames[i], pName ) ) + return i; + } + return -1; +} + +static const char *SoundStateNameFromIndex( int index ) +{ + index = clamp(index, 0, SS_NUM_STATES-1 ); + return pSoundStateNames[index]; +} + +void CBaseServerVehicle::PlaySound( const char *pSound ) +{ + if ( !pSound || !pSound[0] ) + return; + + if ( g_debug_vehiclesound.GetInt() ) + { + Msg("Playing non-looping vehicle sound: %s\n", pSound ); + } + m_pVehicle->EmitSound( pSound ); +} + +void CBaseServerVehicle::StopLoopingSound( float fadeTime ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + if ( m_pStateSoundFade ) + { + controller.SoundDestroy( m_pStateSoundFade ); + m_pStateSoundFade = NULL; + } + if ( m_pStateSound ) + { + m_pStateSoundFade = m_pStateSound; + m_pStateSound = NULL; + controller.SoundFadeOut( m_pStateSoundFade, fadeTime, false ); + } +} + +void CBaseServerVehicle::PlayLoopingSound( const char *pSoundName ) +{ + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + + CPASAttenuationFilter filter( m_pVehicle ); + CSoundPatch *pNewSound = NULL; + if ( pSoundName && pSoundName[0] ) + { + pNewSound = controller.SoundCreate( filter, m_pVehicle->entindex(), CHAN_STATIC, pSoundName, ATTN_NORM ); + } + + if ( m_pStateSound && pNewSound && controller.SoundGetName( pNewSound ) == controller.SoundGetName( m_pStateSound ) ) + { + // if the sound is the same, don't play this, just re-use the old one + controller.SoundDestroy( pNewSound ); + pNewSound = m_pStateSound; + controller.SoundChangeVolume( pNewSound, 1.0f, 0.0f ); + m_pStateSound = NULL; + } + else if ( g_debug_vehiclesound.GetInt() ) + { + const char *pStopSound = m_pStateSound ? controller.SoundGetName( m_pStateSound ).ToCStr() : "NULL"; + const char *pStartSound = pNewSound ? controller.SoundGetName( pNewSound ).ToCStr() : "NULL"; + Msg("Stop %s, start %s\n", pStopSound, pStartSound ); + } + + StopLoopingSound(); + m_pStateSound = pNewSound; + if ( m_pStateSound ) + { + controller.Play( m_pStateSound, 1.0f, 100 ); + } +} + +static sound_states MapGearToState( vbs_sound_update_t ¶ms, int gear ) +{ + switch( gear ) + { + case 0: return params.bReverse ? SS_REVERSE : SS_GEAR_0; + case 1: return SS_GEAR_1; + case 2: return SS_GEAR_2; + case 3: return SS_GEAR_3; + default:case 4: return SS_GEAR_4; + } +} + +static sound_states MapGearToMidState( vbs_sound_update_t ¶ms, int gear ) +{ + switch( gear ) + { + case 0: return params.bReverse ? SS_REVERSE : SS_GEAR_0_RESUME; + case 1: return SS_GEAR_1_RESUME; + case 2: return SS_GEAR_2_RESUME; + case 3: return SS_GEAR_3_RESUME; + default:case 4: return SS_GEAR_4_RESUME; + } +} + +bool CBaseServerVehicle::PlayCrashSound( float speed ) +{ + int i; + float delta = 0; + float absSpeed = fabs(speed); + float absLastSpeed = fabs(m_lastSpeed); + if ( absLastSpeed > absSpeed ) + { + delta = fabs(m_lastSpeed - speed); + } + + float rumble = delta / 8.0f; + + if( rumble > 60.0f ) + rumble = 60.0f; + + if( rumble > 5.0f ) + { + if ( GetDriver() ) + { + UTIL_ScreenShake( GetDriver()->GetAbsOrigin(), rumble, 150.0f, 1.0f, 240.0f, SHAKE_START_RUMBLEONLY, true ); + } + } + + for ( i = 0; i < m_vehicleSounds.crashSounds.Count(); i++ ) + { + const vehicle_crashsound_t &crash = m_vehicleSounds.crashSounds[i]; + if ( !crash.gearLimit ) + continue; + + if ( m_iSoundGear <= crash.gearLimit ) + { + if ( delta > crash.flMinDeltaSpeed && absLastSpeed > crash.flMinSpeed ) + { + PlaySound( crash.iszCrashSound.ToCStr() ); + return true; + } + } + } + + for ( i = m_vehicleSounds.crashSounds.Count()-1; i >= 0; --i ) + { + const vehicle_crashsound_t &crash = m_vehicleSounds.crashSounds[i]; + if ( delta > crash.flMinDeltaSpeed && absLastSpeed > crash.flMinSpeed ) + { + PlaySound( crash.iszCrashSound.ToCStr() ); + return true; + } + } + return false; +} + + +bool CBaseServerVehicle::CheckCrash( vbs_sound_update_t ¶ms ) +{ + if ( params.bVehicleInWater ) + return false; + + bool bCrashed = PlayCrashSound( params.flWorldSpaceSpeed ); + if ( bCrashed ) + { + if ( g_debug_vehiclesound.GetInt() ) + { + Msg("Crashed!: speed %.2f, lastSpeed %.2f\n", params.flWorldSpaceSpeed, m_lastSpeed ); + } + } + m_lastSpeed = params.flWorldSpaceSpeed; + return bCrashed; +} + + +sound_states CBaseServerVehicle::SoundState_ChooseState( vbs_sound_update_t ¶ms ) +{ + float timeInState = gpGlobals->curtime - m_soundStateStartTime; + bool bInStateForMinTime = timeInState > m_vehicleSounds.minStateTime[m_soundState] ? true : false; + + sound_states stateOut = m_soundState; + + // exit overrides everything else + if ( params.bExitVehicle ) + { + switch ( m_soundState ) + { + case SS_NONE: + case SS_SHUTDOWN: + case SS_SHUTDOWN_WATER: + return m_soundState; + } + return SS_SHUTDOWN; + } + + // check global state in states that don't mask them + switch( m_soundState ) + { + // global states masked for these states. + case SS_NONE: + case SS_START_IDLE: + case SS_SHUTDOWN: + break; + case SS_START_WATER: + case SS_SHUTDOWN_WATER: + if ( !params.bVehicleInWater ) + return SS_START_IDLE; + break; + + case SS_TURBO: + if ( params.bVehicleInWater ) + return SS_SHUTDOWN_WATER; + if ( CheckCrash(params) ) + return SS_IDLE; + break; + + case SS_IDLE: + if ( params.bVehicleInWater ) + return SS_SHUTDOWN_WATER; + break; + + case SS_REVERSE: + case SS_GEAR_0: + case SS_GEAR_1: + case SS_GEAR_2: + case SS_GEAR_3: + case SS_GEAR_4: + case SS_SLOWDOWN: + case SS_SLOWDOWN_HIGHSPEED: + case SS_GEAR_0_RESUME: + case SS_GEAR_1_RESUME: + case SS_GEAR_2_RESUME: + case SS_GEAR_3_RESUME: + case SS_GEAR_4_RESUME: + if ( params.bVehicleInWater ) + { + return SS_SHUTDOWN_WATER; + } + if ( params.bTurbo ) + { + return SS_TURBO; + } + if ( CheckCrash(params) ) + return SS_IDLE; + break; + } + + switch( m_soundState ) + { + case SS_START_IDLE: + if ( bInStateForMinTime || params.bThrottleDown ) + return SS_IDLE; + break; + case SS_IDLE: + if ( bInStateForMinTime && params.bThrottleDown ) + { + if ( params.bTurbo ) + return SS_TURBO; + return params.bReverse ? SS_REVERSE : SS_GEAR_0; + } + break; + + case SS_GEAR_0_RESUME: + case SS_GEAR_0: + if ( (bInStateForMinTime && !params.bThrottleDown) || params.bReverse ) + { + return SS_IDLE; + } + if ( m_iSoundGear > 0 ) + { + return SS_GEAR_1; + } + break; + case SS_GEAR_1_RESUME: + case SS_GEAR_1: + if ( bInStateForMinTime ) + { + if ( !params.bThrottleDown ) + return SS_SLOWDOWN; + } + if ( m_iSoundGear != 1 ) + return MapGearToState( params, m_iSoundGear); + break; + + case SS_GEAR_2_RESUME: + case SS_GEAR_2: + if ( bInStateForMinTime ) + { + if ( !params.bThrottleDown ) + return SS_SLOWDOWN; + else if ( m_iSoundGear != 2 ) + return MapGearToState(params, m_iSoundGear); + } + break; + + case SS_GEAR_3_RESUME: + case SS_GEAR_3: + if ( bInStateForMinTime ) + { + if ( !params.bThrottleDown ) + return SS_SLOWDOWN; + else if ( m_iSoundGear != 3 ) + return MapGearToState(params, m_iSoundGear); + } + break; + + case SS_GEAR_4_RESUME: + case SS_GEAR_4: + if ( bInStateForMinTime && !params.bThrottleDown ) + { + return SS_SLOWDOWN; + } + if ( m_iSoundGear != 4 ) + { + return MapGearToMidState(params, m_iSoundGear); + } + break; + case SS_REVERSE: + if ( bInStateForMinTime && !params.bReverse ) + { + return SS_SLOWDOWN; + } + break; + + case SS_SLOWDOWN_HIGHSPEED: + case SS_SLOWDOWN: + if ( params.bThrottleDown ) + { + // map gears + return MapGearToMidState(params, m_iSoundGear); + } + if ( m_iSoundGear == 0 ) + { + return SS_IDLE; + } + break; + + case SS_NONE: + stateOut = params.bVehicleInWater ? SS_START_WATER : SS_START_IDLE; + break; + case SS_TURBO: + if ( bInStateForMinTime && !params.bTurbo ) + { + return MapGearToMidState(params, m_iSoundGear); + } + break; + default: + break; + } + + return stateOut; +} + +const char *CBaseServerVehicle::StateSoundName( sound_states state ) +{ + return m_vehicleSounds.iszStateSounds[state].ToCStr(); +} + +void CBaseServerVehicle::SoundState_OnNewState( sound_states lastState ) +{ + if ( g_debug_vehiclesound.GetInt() ) + { + int index = m_soundState; + Msg("Switched to state: %d (%s)\n", m_soundState, SoundStateNameFromIndex(index) ); + } + + switch ( m_soundState ) + { + case SS_SHUTDOWN: + case SS_SHUTDOWN_WATER: + case SS_START_WATER: + StopLoopingSound(); + PlaySound( StateSoundName(m_soundState) ); + break; + case SS_IDLE: + m_lastSpeed = -1; + PlayLoopingSound( StateSoundName(m_soundState) ); + break; + case SS_START_IDLE: + case SS_REVERSE: + case SS_GEAR_0: + case SS_GEAR_0_RESUME: + case SS_GEAR_1: + case SS_GEAR_1_RESUME: + case SS_GEAR_2: + case SS_GEAR_2_RESUME: + case SS_GEAR_3: + case SS_GEAR_3_RESUME: + case SS_GEAR_4: + case SS_GEAR_4_RESUME: + case SS_TURBO: + PlayLoopingSound( StateSoundName(m_soundState) ); + break; + + case SS_SLOWDOWN_HIGHSPEED: + case SS_SLOWDOWN: + if ( m_iSoundGear < 2 ) + { + PlayLoopingSound( StateSoundName( SS_SLOWDOWN ) ); + } + else + { + PlayLoopingSound( StateSoundName( SS_SLOWDOWN_HIGHSPEED ) ); + } + break; + + default:break; + } + + m_soundStateStartTime = gpGlobals->curtime; +} + + +void CBaseServerVehicle::SoundState_Update( vbs_sound_update_t ¶ms ) +{ + sound_states newState = SoundState_ChooseState( params ); + if ( newState != m_soundState ) + { + sound_states lastState = m_soundState; + m_soundState = newState; + SoundState_OnNewState( lastState ); + } + + switch( m_soundState ) + { + case SS_SHUTDOWN: + case SS_SHUTDOWN_WATER: + case SS_START_WATER: + case SS_START_IDLE: + case SS_IDLE: + case SS_REVERSE: + case SS_GEAR_0: + case SS_GEAR_4: + case SS_SLOWDOWN_HIGHSPEED: + case SS_SLOWDOWN: + case SS_GEAR_0_RESUME: + case SS_GEAR_4_RESUME: + break; + default:break; + } +} + +void CBaseServerVehicle::InitSoundParams( vbs_sound_update_t ¶ms ) +{ + params.Defaults(); + params.bVehicleInWater = IsVehicleBodyInWater(); +} + +//----------------------------------------------------------------------------- +// Purpose: Vehicle Sound Start +//----------------------------------------------------------------------------- +void CBaseServerVehicle::SoundStart() +{ + StartEngineRumble(); + + m_soundState = SS_NONE; + vbs_sound_update_t params; + InitSoundParams(params); + + SoundState_Update( params ); +} + +// vehicle is starting up disabled, but in some cases you still want to play a sound +// HACK: handle those here. +void CBaseServerVehicle::SoundStartDisabled() +{ + m_soundState = SS_NONE; + vbs_sound_update_t params; + InitSoundParams(params); + sound_states newState = SoundState_ChooseState( params ); + + switch( newState ) + { + case SS_START_WATER: + PlaySound( StateSoundName(newState) ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::SoundShutdown( float flFadeTime ) +{ + StopEngineRumble(); + + // Stop any looping sounds that may be running, as the following stop sound may not exist + // and thus leave a looping sound playing after the user gets out. + for ( int i = 0; i < NUM_SOUNDS_TO_STOP_ON_EXIT; i++ ) + { + StopSound( g_iSoundsToStopOnExit[i] ); + } + + CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + if ( m_pStateSoundFade ) + { + controller.SoundFadeOut( m_pStateSoundFade, flFadeTime, true ); + m_pStateSoundFade = NULL; + } + if ( m_pStateSound ) + { + controller.SoundFadeOut( m_pStateSound, flFadeTime, true ); + m_pStateSound = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseServerVehicle::SoundUpdate( vbs_sound_update_t ¶ms ) +{ + if ( g_debug_vehiclesound.GetInt() > 1 ) + { + Msg("Throttle: %s, Reverse: %s\n", params.bThrottleDown?"on":"off", params.bReverse?"on":"off" ); + } + + float flCurrentSpeed = params.flCurrentSpeedFraction; + if ( g_debug_vehiclesound.GetInt() > 1 ) + { + Msg("CurrentSpeed: %.3f ", flCurrentSpeed ); + } + + // Figure out our speed for the purposes of sound playing. + // We slow the transition down a little to make the gear changes slower. + if ( m_vehicleSounds.pGears.Count() > 0 ) + { + if ( flCurrentSpeed > m_flSpeedPercentage ) + { + // don't accelerate when the throttle isn't down + if ( !params.bThrottleDown ) + { + flCurrentSpeed = m_flSpeedPercentage; + } + flCurrentSpeed = Approach( flCurrentSpeed, m_flSpeedPercentage, params.flFrameTime * m_vehicleSounds.pGears[m_iSoundGear].flSpeedApproachFactor ); + } + } + m_flSpeedPercentage = clamp( flCurrentSpeed, 0.0f, 1.0f ); + + if ( g_debug_vehiclesound.GetInt() > 1 ) + { + Msg("Sound Speed: %.3f\n", m_flSpeedPercentage ); + } + + // Only do gear changes when the throttle's down + RecalculateSoundGear( params ); + + SoundState_Update( params ); +} + +//----------------------------------------------------------------------------- +// Purpose: Play a non-gear based vehicle sound +//----------------------------------------------------------------------------- +void CBaseServerVehicle::PlaySound( vehiclesound iSound ) +{ + if ( m_vehicleSounds.iszSound[iSound] != NULL_STRING ) + { + CPASAttenuationFilter filter( m_pVehicle ); + + EmitSound_t ep; + ep.m_nChannel = CHAN_VOICE; + ep.m_pSoundName = STRING(m_vehicleSounds.iszSound[iSound]); + ep.m_flVolume = m_flVehicleVolume; + ep.m_SoundLevel = SNDLVL_NORM; + + CBaseEntity::EmitSound( filter, m_pVehicle->entindex(), ep ); + if ( g_debug_vehiclesound.GetInt() ) + { + Msg("Playing vehicle sound: %s\n", ep.m_pSoundName ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Stop a non-gear based vehicle sound +//----------------------------------------------------------------------------- +void CBaseServerVehicle::StopSound( vehiclesound iSound ) +{ + if ( m_vehicleSounds.iszSound[iSound] != NULL_STRING ) + { + CBaseEntity::StopSound( m_pVehicle->entindex(), CHAN_VOICE, STRING(m_vehicleSounds.iszSound[iSound]) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Calculate the gear we should be in based upon the vehicle's current speed +//----------------------------------------------------------------------------- +void CBaseServerVehicle::RecalculateSoundGear( vbs_sound_update_t ¶ms ) +{ + int iNumGears = m_vehicleSounds.pGears.Count(); + for ( int i = (iNumGears-1); i >= 0; i-- ) + { + if ( m_flSpeedPercentage > m_vehicleSounds.pGears[i].flMinSpeed ) + { + m_iSoundGear = i; + break; + } + } + + // If we're going in reverse, we want to stay in first gear + if ( params.bReverse ) + { + m_iSoundGear = 0; + } +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CBaseServerVehicle::StartEngineRumble() +{ + return; +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CBaseServerVehicle::StopEngineRumble() +{ + return; +} + +//----------------------------------------------------------------------------- +// Purpose: Find the passenger in the given seat of the vehicle +// Input : nSeatID - seat ID to check +// Output : CBaseCombatCharacter - character in the seat +//----------------------------------------------------------------------------- +CBaseCombatCharacter *CBaseServerVehicle::NPC_GetPassengerInSeat( int nRoleID, int nSeatID ) +{ + // Search all passengers in the vehicle + for ( int i = 0; i < m_PassengerInfo.Count(); i++ ) + { + // If the seat ID matches, return the entity in that seat + if ( m_PassengerInfo[i].GetSeat() == nSeatID && m_PassengerInfo[i].GetRole() == nRoleID ) + return m_PassengerInfo[i].m_hPassenger; + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Find the first available seat (ranked by priority) +// Input : nRoleID - Role index +// Output : int - Seat by index +//----------------------------------------------------------------------------- +int CBaseServerVehicle::NPC_GetAvailableSeat_Any( CBaseCombatCharacter *pPassenger, int nRoleID ) +{ + // Look through all available seats + for ( int i = 0; i < m_PassengerRoles[nRoleID].m_PassengerSeats.Count(); i++ ) + { + // See if anyone is already in this seat + CBaseCombatCharacter *pCurrentPassenger = NPC_GetPassengerInSeat( nRoleID, i ); + if ( pCurrentPassenger != NULL && pCurrentPassenger != pPassenger ) + continue; + + // This seat is open + return i; + } + + // Nothing found + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Find the seat with the nearest entry point to the querier +// Input : *pPassenger - Terget to be nearest to +// nRoleID - Role index +// Output : int - Seat by index +//----------------------------------------------------------------------------- +int CBaseServerVehicle::NPC_GetAvailableSeat_Nearest( CBaseCombatCharacter *pPassenger, int nRoleID ) +{ + // Not yet implemented + Assert( 0 ); + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Get a seat in the vehicle based on our role and criteria +// Input : *pPassenger - Entity attempting to find a seat +// strRoleName - Role the seat must serve +// nQueryType - Method for choosing the best seat (if multiple) +// Output : int - Seat by unique ID +//----------------------------------------------------------------------------- +int CBaseServerVehicle::NPC_GetAvailableSeat( CBaseCombatCharacter *pPassenger, string_t strRoleName, VehicleSeatQuery_e nQueryType ) +{ + // Parse the vehicle animations the first time they get in the vehicle + if ( m_bParsedAnimations == false ) + { + // Load the entry/exit animations from the vehicle + ParseEntryExitAnims(); + m_bParsedAnimations = true; + } + + // Get the role index + int nRole = FindRoleIndexByName( strRoleName ); + if ( m_PassengerRoles.IsValidIndex( nRole ) == false ) + return -1; + + switch( nQueryType ) + { + case VEHICLE_SEAT_ANY: + return NPC_GetAvailableSeat_Any( pPassenger, nRole ); + break; + + case VEHICLE_SEAT_NEAREST: + return NPC_GetAvailableSeat_Nearest( pPassenger, nRole ); + break; + + default: + Assert( 0 ); + break; + }; + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Determine if there's an available seat of a given role name +// Input : strRoleName - name of the role +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::NPC_HasAvailableSeat( string_t strRoleName ) +{ + return ( NPC_GetAvailableSeat( NULL, strRoleName, VEHICLE_SEAT_ANY ) != -1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Find a role index by name +// Input : strRoleName - name of the role +//----------------------------------------------------------------------------- +int CBaseServerVehicle::FindRoleIndexByName( string_t strRoleName ) +{ + // Search through all our known roles + for ( int i = 0; i < m_PassengerRoles.Count(); i++ ) + { + // Return the index if the name matches + if ( FStrEq( STRING( m_PassengerRoles[i].GetName() ), STRING( strRoleName ) ) ) + return i; + } + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Find a seat index by its name +// Input : strSeatName - name of the seat +//----------------------------------------------------------------------------- +int CBaseServerVehicle::FindSeatIndexByName( int nRoleIndex, string_t strSeatName ) +{ + // Role must be valid + if ( m_PassengerRoles.IsValidIndex( nRoleIndex ) == false ) + return -1; + + // Used for attachment polling + CBaseAnimating *pAnimating = dynamic_cast(GetVehicleEnt()); + if ( pAnimating == NULL ) + return -1; + + // Get the index of the named attachment in the model + int nAttachmentID = pAnimating->LookupAttachment( STRING( strSeatName ) ); + + // Look through the roles for this seat attachment ID + for ( int i = 0; i < m_PassengerRoles[nRoleIndex].m_PassengerSeats.Count(); i++ ) + { + // Return that index if found + if ( m_PassengerRoles[nRoleIndex].m_PassengerSeats[i].GetAttachmentID() == nAttachmentID ) + return i; + } + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Called after loading a saved game +//----------------------------------------------------------------------------- +void CBaseServerVehicle::RestorePassengerInfo( void ) +{ + // If there is passenger information, then we have passengers in the vehicle + if ( m_PassengerInfo.Count() != 0 && m_bParsedAnimations == false ) + { + // Load the entry/exit animations from the vehicle + ParseEntryExitAnims(); + m_bParsedAnimations = true; + } + + // FIXME: If a passenger cannot fix-up its indices, it must be removed from the vehicle! + + // Fix-up every passenger with updated indices + for ( int i = 0; i < m_PassengerInfo.Count(); i++ ) + { + // Fix up the role first + int nRoleIndex = FindRoleIndexByName( m_PassengerInfo[i].m_strRoleName ); + if ( m_PassengerRoles.IsValidIndex( nRoleIndex ) ) + { + // New role index + m_PassengerInfo[i].m_nRole = nRoleIndex; + + // Then fix up the seat via attachment name + int nSeatIndex = FindSeatIndexByName( nRoleIndex, m_PassengerInfo[i].m_strSeatName ); + if ( m_PassengerRoles[nRoleIndex].m_PassengerSeats.IsValidIndex( nSeatIndex ) ) + { + // New seat index + m_PassengerInfo[i].m_nSeat = nSeatIndex; + } + else + { + // The seat attachment was not found. This most likely means that the seat attachment name has changed + // in the target vehicle and the NPC passenger is now stranded! + Assert( 0 ); + } + } + else + { + // The role was not found. This most likely means that the role names have changed + // in the target vehicle and the NPC passenger is now stranded! + Assert( 0 ); + } + } +} + +void CBaseServerVehicle::ReloadScript() +{ + if ( m_pDrivableVehicle ) + { + string_t script = m_pDrivableVehicle->GetVehicleScriptName(); + IPhysicsVehicleController *pController = GetVehicleController(); + vehicleparams_t *pVehicleParams = pController ? &(pController->GetVehicleParamsForChange()) : NULL; + PhysFindOrAddVehicleScript( script.ToCStr(), pVehicleParams, &m_vehicleSounds ); + if ( pController ) + { + pController->VehicleDataReload(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Passes this call down into the server vehicle where the tests are done +//----------------------------------------------------------------------------- +bool CBaseServerVehicle::PassengerShouldReceiveDamage( CTakeDamageInfo &info ) +{ + if ( GetDrivableVehicle() ) + return GetDrivableVehicle()->PassengerShouldReceiveDamage( info ); + + return true; +} + +//=========================================================================================================== +// Vehicle Sounds +//=========================================================================================================== + +// These are sounds that are to be automatically stopped whenever the vehicle's driver leaves it +vehiclesound g_iSoundsToStopOnExit[] = +{ + VS_ENGINE2_START, + VS_ENGINE2_STOP, +}; + +const char *vehiclesound_parsenames[VS_NUM_SOUNDS] = +{ + "skid_lowfriction", + "skid_normalfriction", + "skid_highfriction", + "engine2_start", + "engine2_stop", + "misc1", + "misc2", + "misc3", + "misc4", +}; + +CVehicleSoundsParser::CVehicleSoundsParser( void ) +{ + // UNDONE: Revisit this pattern - move sub-block processing ideas into the parser architecture + m_iCurrentGear = -1; + m_iCurrentState = -1; + m_iCurrentCrashSound = -1; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVehicleSoundsParser::ParseKeyValue( void *pData, const char *pKey, const char *pValue ) +{ + vehiclesounds_t *pSounds = (vehiclesounds_t *)pData; + // New gear? + if ( !strcmpi( pKey, "gear" ) ) + { + // Create, initialize, and add a new gear to our list + int iNewGear = pSounds->pGears.AddToTail(); + pSounds->pGears[iNewGear].flMaxSpeed = 0; + pSounds->pGears[iNewGear].flSpeedApproachFactor = 1.0; + + // Set our min speed to the previous gear's max + if ( iNewGear == 0 ) + { + // First gear, so our minspeed is 0 + pSounds->pGears[iNewGear].flMinSpeed = 0; + } + else + { + pSounds->pGears[iNewGear].flMinSpeed = pSounds->pGears[iNewGear-1].flMaxSpeed; + } + + // Remember which gear we're reading data from + m_iCurrentGear = iNewGear; + } + else if ( !strcmpi( pKey, "state" ) ) + { + m_iCurrentState = 0; + } + else if ( !strcmpi( pKey, "crashsound" ) ) + { + m_iCurrentCrashSound = pSounds->crashSounds.AddToTail(); + pSounds->crashSounds[m_iCurrentCrashSound].flMinSpeed = 0; + pSounds->crashSounds[m_iCurrentCrashSound].flMinDeltaSpeed = 0; + pSounds->crashSounds[m_iCurrentCrashSound].iszCrashSound = NULL_STRING; + } + else + { + int i; + + // Are we currently in a gear block? + if ( m_iCurrentGear >= 0 ) + { + Assert( m_iCurrentGear < pSounds->pGears.Count() ); + + // Check gear keys + if ( !strcmpi( pKey, "max_speed" ) ) + { + pSounds->pGears[m_iCurrentGear].flMaxSpeed = atof(pValue); + return; + } + if ( !strcmpi( pKey, "speed_approach_factor" ) ) + { + pSounds->pGears[m_iCurrentGear].flSpeedApproachFactor = atof(pValue); + return; + } + } + // We're done reading a gear, so stop checking them. + m_iCurrentGear = -1; + + if ( m_iCurrentState >= 0 ) + { + if ( !strcmpi( pKey, "name" ) ) + { + m_iCurrentState = SoundStateIndexFromName( pValue ); + pSounds->iszStateSounds[m_iCurrentState] = NULL_STRING; + pSounds->minStateTime[m_iCurrentState] = 0.0f; + return; + } + else if ( !strcmpi( pKey, "sound" ) ) + { + pSounds->iszStateSounds[m_iCurrentState] = AllocPooledString(pValue); + return; + } + else if ( !strcmpi( pKey, "min_time" ) ) + { + pSounds->minStateTime[m_iCurrentState] = atof(pValue); + return; + } + } + // + m_iCurrentState = -1; + + if ( m_iCurrentCrashSound >= 0 ) + { + if ( !strcmpi( pKey, "min_speed" ) ) + { + pSounds->crashSounds[m_iCurrentCrashSound].flMinSpeed = atof(pValue); + return; + } + else if ( !strcmpi( pKey, "sound" ) ) + { + pSounds->crashSounds[m_iCurrentCrashSound].iszCrashSound = AllocPooledString(pValue); + return; + } + else if ( !strcmpi( pKey, "min_speed_change" ) ) + { + pSounds->crashSounds[m_iCurrentCrashSound].flMinDeltaSpeed = atof(pValue); + return; + } + else if ( !strcmpi( pKey, "gear_limit" ) ) + { + pSounds->crashSounds[m_iCurrentCrashSound].gearLimit = atoi(pValue); + return; + } + } + m_iCurrentCrashSound = -1; + + for ( i = 0; i < VS_NUM_SOUNDS; i++ ) + { + if ( !strcmpi( pKey, vehiclesound_parsenames[i] ) ) + { + pSounds->iszSound[i] = AllocPooledString(pValue); + return; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVehicleSoundsParser::SetDefaults( void *pData ) +{ + vehiclesounds_t *pSounds = (vehiclesounds_t *)pData; + pSounds->Init(); +} + diff --git a/sp/src/game/server/vehicle_baseserver.h b/sp/src/game/server/vehicle_baseserver.h new file mode 100644 index 00000000..cdf1f75a --- /dev/null +++ b/sp/src/game/server/vehicle_baseserver.h @@ -0,0 +1,328 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef VEHICLE_BASESERVER_H +#define VEHICLE_BASESERVER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "vehicle_sounds.h" +#include "entityblocker.h" + +class CSoundPatch; + +struct vbs_sound_update_t +{ + float flFrameTime; + float flCurrentSpeedFraction; + float flWorldSpaceSpeed; + bool bThrottleDown; + bool bReverse; + bool bTurbo; + bool bVehicleInWater; + bool bExitVehicle; + + void Defaults() + { + flFrameTime = gpGlobals->frametime; + flCurrentSpeedFraction = 0; + flWorldSpaceSpeed = 0; + bThrottleDown = false; + bReverse = false; + bTurbo = false; + bVehicleInWater = false; + bExitVehicle = false; + } +}; + +// ----------------------------------------- +// Information about the passenger in the car +// ----------------------------------------- +class CPassengerInfo +{ +public: + CPassengerInfo( void ) : m_nRole( -1 ), m_nSeat( -1 ), m_strRoleName( NULL_STRING ), m_strSeatName( NULL_STRING ) {} + + DECLARE_SIMPLE_DATADESC(); + + int GetSeat( void ) const { return m_nSeat; } + int GetRole( void ) const { return m_nRole; } + CBaseCombatCharacter *GetPassenger( void ) const { return m_hPassenger; } + +private: + int m_nRole; // Role (by index) + int m_nSeat; // Seat (by index) + string_t m_strRoleName; // Used in restoration for fix-up + string_t m_strSeatName; // Used in restoration for fix-up + CHandle m_hPassenger; // Actual passenger + + friend class CBaseServerVehicle; +}; + +// ----------------------------------------- +// Seat transition information (animation and priority) +// ----------------------------------------- + +class CPassengerSeatTransition +{ +public: + CPassengerSeatTransition( void ) : m_strAnimationName( NULL_STRING ), m_nPriority( -1 ) {}; + + string_t GetAnimationName( void ) const { return m_strAnimationName; } + int GetPriority( void ) const { return m_nPriority; } + +private: + string_t m_strAnimationName; // Name of animation to play + int m_nPriority; // Priority of the transition + + friend class CBaseServerVehicle; +}; + +// ----------------------------------------- +// Seat in a vehicle (attachment and a collection of animations to reach it) +// ----------------------------------------- +class CPassengerSeat +{ +public: + CPassengerSeat( void ) : m_nAttachmentID( -1 ) {}; + int GetAttachmentID( void ) const { return m_nAttachmentID; } + +private: + string_t m_strSeatName; // Used for save/load fixup + int m_nAttachmentID; // Goal attachment + CUtlVector m_EntryTransitions; // Entry information + CUtlVector m_ExitTransitions; // Exit information + + friend class CBaseServerVehicle; +}; + +// ----------------------------------------- +// Passenger role information +// ----------------------------------------- +class CPassengerRole +{ +public: + CPassengerRole( void ) : m_strName( NULL_STRING ) {}; + string_t GetName( void ) const { return m_strName; } + +private: + string_t m_strName; // Name of the set + CUtlVector m_PassengerSeats; // Passenger info + + friend class CBaseServerVehicle; +}; + +//----------------------------------------------------------------------------- +// Purpose: Base class for drivable vehicle handling. Contain it in your +// drivable vehicle. +//----------------------------------------------------------------------------- +class CBaseServerVehicle : public IServerVehicle +{ +public: + DECLARE_SIMPLE_DATADESC(); + DECLARE_CLASS_NOBASE( CBaseServerVehicle ); + + CBaseServerVehicle( void ); + ~CBaseServerVehicle( void ); + + virtual void Precache( void ); + +// IVehicle +public: + virtual CBaseCombatCharacter *GetPassenger( int nRole = VEHICLE_ROLE_DRIVER ); + + virtual int GetPassengerRole( CBaseCombatCharacter *pPassenger ); + virtual void GetVehicleViewPosition( int nRole, Vector *pOrigin, QAngle *pAngles, float *pFOV = NULL ); + virtual bool IsPassengerUsingStandardWeapons( int nRole = VEHICLE_ROLE_DRIVER ) { return false; } + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ); + virtual void ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ); + virtual void FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ); + virtual void ItemPostFrame( CBasePlayer *pPlayer ); + +// IServerVehicle +public: + virtual CBaseEntity *GetVehicleEnt( void ) { return m_pVehicle; } + virtual void SetPassenger( int nRole, CBaseCombatCharacter *pPassenger ); + virtual bool IsPassengerVisible( int nRole = VEHICLE_ROLE_DRIVER ) { return false; } + virtual bool IsPassengerDamagable( int nRole = VEHICLE_ROLE_DRIVER ) { return true; } + virtual bool PassengerShouldReceiveDamage( CTakeDamageInfo &info ); + + virtual bool IsVehicleUpright( void ) { return true; } + virtual bool IsPassengerEntering( void ) { Assert( 0 ); return false; } + virtual bool IsPassengerExiting( void ) { Assert( 0 ); return false; } + + virtual void HandlePassengerEntry( CBaseCombatCharacter *pPassenger, bool bAllowEntryOutsideZone = false ); + virtual bool HandlePassengerExit( CBaseCombatCharacter *pPassenger ); + + virtual void GetPassengerSeatPoint( int nRole, Vector *pPoint, QAngle *pAngles ); + virtual bool GetPassengerExitPoint( int nRole, Vector *pPoint, QAngle *pAngles ); + virtual Class_T ClassifyPassenger( CBaseCombatCharacter *pPassenger, Class_T defaultClassification ) { return defaultClassification; } + virtual float PassengerDamageModifier( const CTakeDamageInfo &info ) { return 1.0; } + virtual const vehicleparams_t *GetVehicleParams( void ) { return NULL; } + virtual bool IsVehicleBodyInWater( void ) { return false; } + virtual IPhysicsVehicleController *GetVehicleController() { return NULL; } + + // NPC Driving + virtual bool NPC_CanDrive( void ) { return true; } + virtual void NPC_SetDriver( CNPC_VehicleDriver *pDriver ) { return; } + virtual void NPC_DriveVehicle( void ) { return; } + virtual void NPC_ThrottleCenter( void ); + virtual void NPC_ThrottleReverse( void ); + virtual void NPC_ThrottleForward( void ); + virtual void NPC_Brake( void ); + virtual void NPC_TurnLeft( float flDegrees ); + virtual void NPC_TurnRight( float flDegrees ); + virtual void NPC_TurnCenter( void ); + virtual void NPC_PrimaryFire( void ); + virtual void NPC_SecondaryFire( void ); + virtual bool NPC_HasPrimaryWeapon( void ) { return false; } + virtual bool NPC_HasSecondaryWeapon( void ) { return false; } + virtual void NPC_AimPrimaryWeapon( Vector vecTarget ) { return; } + virtual void NPC_AimSecondaryWeapon( Vector vecTarget ) { return; } + + // Weapon handling + virtual void Weapon_PrimaryRanges( float *flMinRange, float *flMaxRange ); + virtual void Weapon_SecondaryRanges( float *flMinRange, float *flMaxRange ); + virtual float Weapon_PrimaryCanFireAt( void ); // Return the time at which this vehicle's primary weapon can fire again + virtual float Weapon_SecondaryCanFireAt( void ); // Return the time at which this vehicle's secondary weapon can fire again + + // ---------------------------------------------------------------------------- + // NPC passenger data + +public: + + bool NPC_AddPassenger( CBaseCombatCharacter *pPassenger, string_t strRoleName, int nSeat ); + bool NPC_RemovePassenger( CBaseCombatCharacter *pPassenger ); + virtual bool NPC_GetPassengerSeatPosition( CBaseCombatCharacter *pPassenger, Vector *vecResultPos, QAngle *vecResultAngle ); + virtual bool NPC_GetPassengerSeatPositionLocal( CBaseCombatCharacter *pPassenger, Vector *vecResultPos, QAngle *vecResultAngles ); + virtual int NPC_GetPassengerSeatAttachment( CBaseCombatCharacter *pPassenger ); + virtual int NPC_GetAvailableSeat( CBaseCombatCharacter *pPassenger, string_t strRoleName, VehicleSeatQuery_e nQueryType ); + bool NPC_HasAvailableSeat( string_t strRoleName ); + + + virtual const PassengerSeatAnims_t *NPC_GetPassengerSeatAnims( CBaseCombatCharacter *pPassenger, PassengerSeatAnimType_t nType ); + virtual CBaseCombatCharacter *NPC_GetPassengerInSeat( int nRoleID, int nSeatID ); + + Vector GetSavedViewOffset( void ) { return m_savedViewOffset; } + +private: + + // Vehicle entering/exiting + void ParseNPCRoles( KeyValues *pModelKeyValues ); + void ParseNPCPassengerSeat( KeyValues *pSetKeyValues, CPassengerSeat *pSeat ); + void ParseNPCSeatTransition( KeyValues *pTransitionKeyValues, CPassengerSeatTransition *pTransition ); + +protected: + + int FindRoleIndexByName( string_t strRoleName ); + int FindSeatIndexByName( int nRoleIndex, string_t strSeatName ); + int NPC_GetAvailableSeat_Any( CBaseCombatCharacter *pPassenger, int nRoleID ); + int NPC_GetAvailableSeat_Nearest( CBaseCombatCharacter *pPassenger, int nRoleID ); + + CPassengerRole *FindOrCreatePassengerRole( string_t strName, int *nIndex = NULL ); + + CUtlVector< CPassengerInfo > m_PassengerInfo; + CUtlVector< CPassengerRole > m_PassengerRoles; // Not save/restored + + // ---------------------------------------------------------------------------- + void ReloadScript(); // debug/tuning +public: + + void UseLegacyExitChecks( bool bState ) { m_bUseLegacyExitChecks = bState; } + void RestorePassengerInfo( void ); + + virtual CBaseEntity *GetDriver( void ); // Player Driving + virtual void ParseEntryExitAnims( void ); + void ParseExitAnim( KeyValues *pkvExitList, bool bEscapeExit ); + virtual bool CheckExitPoint( float yaw, int distance, Vector *pEndPoint ); + virtual int GetEntryAnimForPoint( const Vector &vecPoint ); + virtual int GetExitAnimToUse( Vector &vecEyeExitEndpoint, bool &bAllPointsBlocked ); + virtual void HandleEntryExitFinish( bool bExitAnimOn, bool bResetAnim ); + + virtual void SetVehicle( CBaseEntity *pVehicle ); + IDrivableVehicle *GetDrivableVehicle( void ); + + // Sound handling + bool Initialize( const char *pScriptName ); + virtual void SoundStart(); + virtual void SoundStartDisabled(); + virtual void SoundShutdown( float flFadeTime = 0.0 ); + virtual void SoundUpdate( vbs_sound_update_t ¶ms ); + virtual void PlaySound( vehiclesound iSound ); + virtual void StopSound( vehiclesound iSound ); + virtual void RecalculateSoundGear( vbs_sound_update_t ¶ms ); + void SetVehicleVolume( float flVolume ) { m_flVehicleVolume = clamp( flVolume, 0.0f, 1.0f ); } + + // Rumble + virtual void StartEngineRumble(); + virtual void StopEngineRumble(); + +public: + CBaseEntity *m_pVehicle; + IDrivableVehicle *m_pDrivableVehicle; + + // NPC Driving + int m_nNPCButtons; + int m_nPrevNPCButtons; + float m_flTurnDegrees; + + // Entry / Exit anims + struct entryanim_t + { + int iHitboxGroup; + char szAnimName[128]; + }; + CUtlVector< entryanim_t > m_EntryAnimations; + + struct exitanim_t + { + bool bUpright; + bool bEscapeExit; + char szAnimName[128]; + Vector vecExitPointLocal; // Point the animation leaves the player at when finished + QAngle vecExitAnglesLocal; + }; + + CUtlVector< exitanim_t > m_ExitAnimations; + bool m_bParsedAnimations; + bool m_bUseLegacyExitChecks; // HACK: Choreo vehicles use non-sensical setups to move the player, we need to poll their attachment point positions + int m_iCurrentExitAnim; + Vector m_vecCurrentExitEndPoint; + Vector m_savedViewOffset; + CHandle m_hExitBlocker; // Entity to prevent other entities blocking the player's exit point during the exit animation + + char m_chPreviousTextureType; + +// sound state + vehiclesounds_t m_vehicleSounds; +private: + float m_flVehicleVolume; + int m_iSoundGear; // The sound "gear" that we're currently in + float m_flSpeedPercentage; + + CSoundPatch *m_pStateSound; + CSoundPatch *m_pStateSoundFade; + sound_states m_soundState; + float m_soundStateStartTime; + float m_lastSpeed; + + void SoundState_OnNewState( sound_states lastState ); + void SoundState_Update( vbs_sound_update_t ¶ms ); + sound_states SoundState_ChooseState( vbs_sound_update_t ¶ms ); + void PlaySound( const char *pSound ); + void StopLoopingSound( float fadeTime = 0.25f ); + void PlayLoopingSound( const char *pSoundName ); + bool PlayCrashSound( float speed ); + bool CheckCrash( vbs_sound_update_t ¶ms ); + const char *StateSoundName( sound_states state ); + void InitSoundParams( vbs_sound_update_t ¶ms ); + void CacheEntryExitPoints( void ); + bool GetLocalAttachmentAtTime( int nQuerySequence, int nAttachmentIndex, float flCyclePoint, Vector *vecOriginOut, QAngle *vecAnglesOut ); + bool GetLocalAttachmentAtTime( const char *lpszAnimName, int nAttachmentIndex, float flCyclePoint, Vector *vecOriginOut, QAngle *vecAnglesOut ); +}; + +#endif // VEHICLE_BASESERVER_H diff --git a/sp/src/game/server/vehicle_choreo_generic.cpp b/sp/src/game/server/vehicle_choreo_generic.cpp new file mode 100644 index 00000000..3b3a53f9 --- /dev/null +++ b/sp/src/game/server/vehicle_choreo_generic.cpp @@ -0,0 +1,995 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "npcevent.h" +#include "vehicle_base.h" +#include "engine/IEngineSound.h" +#include "in_buttons.h" +#include "soundenvelope.h" +#include "soundent.h" +#include "physics_saverestore.h" +#include "vphysics/constraints.h" +#include "vcollide_parse.h" +#include "ndebugoverlay.h" +#include "hl2_player.h" +#include "props.h" +#include "vehicle_choreo_generic_shared.h" +#include "ai_utils.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define VEHICLE_HITBOX_DRIVER 1 + +#define CHOREO_VEHICLE_VIEW_FOV 90 +#define CHOREO_VEHICLE_VIEW_YAW_MIN -60 +#define CHOREO_VEHICLE_VIEW_YAW_MAX 60 +#define CHOREO_VEHICLE_VIEW_PITCH_MIN -90 +#define CHOREO_VEHICLE_VIEW_PITCH_MAX 38 + +BEGIN_DATADESC_NO_BASE( vehicleview_t ) + DEFINE_FIELD( bClampEyeAngles, FIELD_BOOLEAN ), + DEFINE_FIELD( flPitchCurveZero, FIELD_FLOAT ), + DEFINE_FIELD( flPitchCurveLinear, FIELD_FLOAT ), + DEFINE_FIELD( flRollCurveZero, FIELD_FLOAT ), + DEFINE_FIELD( flRollCurveLinear, FIELD_FLOAT ), + DEFINE_FIELD( flFOV, FIELD_FLOAT ), + DEFINE_FIELD( flYawMin, FIELD_FLOAT ), + DEFINE_FIELD( flYawMax, FIELD_FLOAT ), + DEFINE_FIELD( flPitchMin, FIELD_FLOAT ), + DEFINE_FIELD( flPitchMax, FIELD_FLOAT ), +END_DATADESC() + +// +// Anim events. +// +enum +{ + AE_CHOREO_VEHICLE_OPEN = 1, + AE_CHOREO_VEHICLE_CLOSE = 2, +}; + + +extern ConVar g_debug_vehicledriver; + + +class CPropVehicleChoreoGeneric; + +static const char *pChoreoGenericFollowerBoneNames[] = +{ + "base", +}; + + +//----------------------------------------------------------------------------- +// Purpose: A KeyValues parse for vehicle sound blocks +//----------------------------------------------------------------------------- +class CVehicleChoreoViewParser : public IVPhysicsKeyHandler +{ +public: + CVehicleChoreoViewParser( void ); + +private: + virtual void ParseKeyValue( void *pData, const char *pKey, const char *pValue ); + virtual void SetDefaults( void *pData ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CChoreoGenericServerVehicle : public CBaseServerVehicle +{ + typedef CBaseServerVehicle BaseClass; + +// IServerVehicle +public: + void GetVehicleViewPosition( int nRole, Vector *pAbsOrigin, QAngle *pAbsAngles, float *pFOV = NULL ); + virtual void ItemPostFrame( CBasePlayer *pPlayer ); + +protected: + + CPropVehicleChoreoGeneric *GetVehicle( void ); +}; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPropVehicleChoreoGeneric : public CDynamicProp, public IDrivableVehicle +{ + DECLARE_CLASS( CPropVehicleChoreoGeneric, CDynamicProp ); + +public: + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CPropVehicleChoreoGeneric( void ) + { + m_ServerVehicle.SetVehicle( this ); + m_bIgnoreMoveParent = false; + m_bForcePlayerEyePoint = false; + } + + ~CPropVehicleChoreoGeneric( void ) + { + } + + // CBaseEntity + virtual void Precache( void ); + void Spawn( void ); + void Think(void); + virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_IMPULSE_USE; }; + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual void DrawDebugGeometryOverlays( void ); + + virtual Vector BodyTarget( const Vector &posSrc, bool bNoisy = true ); + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr ); + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + void PlayerControlInit( CBasePlayer *pPlayer ); + void PlayerControlShutdown( void ); + void ResetUseKey( CBasePlayer *pPlayer ); + + virtual bool OverridePropdata() { return true; } + + bool ParseViewParams( const char *pScriptName ); + + void GetVectors(Vector* pForward, Vector* pRight, Vector* pUp) const; + + bool CreateVPhysics() + { + SetSolid(SOLID_VPHYSICS); + SetMoveType(MOVETYPE_NONE); + return true; + } + bool ShouldForceExit() { return m_bForcedExit; } + void ClearForcedExit() { m_bForcedExit = false; } + + // CBaseAnimating + void HandleAnimEvent( animevent_t *pEvent ); + + // Inputs + void InputEnterVehicleImmediate( inputdata_t &inputdata ); + void InputEnterVehicle( inputdata_t &inputdata ); + void InputExitVehicle( inputdata_t &inputdata ); + void InputLock( inputdata_t &inputdata ); + void InputUnlock( inputdata_t &inputdata ); + void InputOpen( inputdata_t &inputdata ); + void InputClose( inputdata_t &inputdata ); + void InputViewlock( inputdata_t &inputdata ); + + bool ShouldIgnoreParent( void ) { return m_bIgnoreMoveParent; } + + // Tuned to match HL2s definition, but this should probably return false in all cases + virtual bool PassengerShouldReceiveDamage( CTakeDamageInfo &info ) { return (info.GetDamageType() & (DMG_BLAST|DMG_RADIATION)) == 0; } + + CNetworkHandle( CBasePlayer, m_hPlayer ); + + CNetworkVarEmbedded( vehicleview_t, m_vehicleView ); +private: + vehicleview_t m_savedVehicleView; // gets saved out for viewlock/unlock input + +// IDrivableVehicle +public: + + virtual CBaseEntity *GetDriver( void ); + virtual void ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ) { return; } + virtual void FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ) { return; } + virtual bool CanEnterVehicle( CBaseEntity *pEntity ); + virtual bool CanExitVehicle( CBaseEntity *pEntity ); + virtual void SetVehicleEntryAnim( bool bOn ); + virtual void SetVehicleExitAnim( bool bOn, Vector vecEyeExitEndpoint ) { m_bExitAnimOn = bOn; if ( bOn ) m_vecEyeExitEndpoint = vecEyeExitEndpoint; } + virtual void EnterVehicle( CBaseCombatCharacter *pPassenger ); + + virtual bool AllowBlockedExit( CBaseCombatCharacter *pPassenger, int nRole ) { return true; } + virtual bool AllowMidairExit( CBaseCombatCharacter *pPassenger, int nRole ) { return true; } + virtual void PreExitVehicle( CBaseCombatCharacter *pPassenger, int nRole ) {} + virtual void ExitVehicle( int nRole ); + + virtual void ItemPostFrame( CBasePlayer *pPlayer ) {} + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) {} + virtual string_t GetVehicleScriptName() { return m_vehicleScript; } + + // If this is a vehicle, returns the vehicle interface + virtual IServerVehicle *GetServerVehicle() { return &m_ServerVehicle; } + +#ifdef MAPBASE + virtual bool IsPassengerUsingStandardWeapons( int nRole = VEHICLE_ROLE_DRIVER ) { return m_bAllowStandardWeapons; } + CNetworkVar( bool, m_bAllowStandardWeapons ); +#endif + + bool ShouldCollide( int collisionGroup, int contentsMask ) const; + + bool m_bForcePlayerEyePoint; // Uses player's eyepoint instead of 'vehicle_driver_eyes' attachment + +protected: + + // Contained IServerVehicle + CChoreoGenericServerVehicle m_ServerVehicle; + +private: + + // Entering / Exiting + bool m_bLocked; + CNetworkVar( bool, m_bEnterAnimOn ); + CNetworkVar( bool, m_bExitAnimOn ); + CNetworkVector( m_vecEyeExitEndpoint ); + bool m_bForcedExit; + bool m_bIgnoreMoveParent; + bool m_bIgnorePlayerCollisions; + + // Vehicle script filename + string_t m_vehicleScript; + + COutputEvent m_playerOn; + COutputEvent m_playerOff; + COutputEvent m_OnOpen; + COutputEvent m_OnClose; +}; + +LINK_ENTITY_TO_CLASS( prop_vehicle_choreo_generic, CPropVehicleChoreoGeneric ); + +BEGIN_DATADESC( CPropVehicleChoreoGeneric ) + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Lock", InputLock ), + DEFINE_INPUTFUNC( FIELD_VOID, "Unlock", InputUnlock ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnterVehicle", InputEnterVehicle ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnterVehicleImmediate", InputEnterVehicleImmediate ), + DEFINE_INPUTFUNC( FIELD_VOID, "ExitVehicle", InputExitVehicle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Open", InputOpen ), + DEFINE_INPUTFUNC( FIELD_VOID, "Close", InputClose ), + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "Viewlock", InputViewlock ), + + // Keys + DEFINE_EMBEDDED( m_ServerVehicle ), + + DEFINE_FIELD( m_hPlayer, FIELD_EHANDLE ), + DEFINE_FIELD( m_bEnterAnimOn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bExitAnimOn, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bForcedExit, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vecEyeExitEndpoint, FIELD_POSITION_VECTOR ), + + DEFINE_KEYFIELD( m_vehicleScript, FIELD_STRING, "vehiclescript" ), + DEFINE_KEYFIELD( m_bLocked, FIELD_BOOLEAN, "vehiclelocked" ), + + DEFINE_KEYFIELD( m_bIgnoreMoveParent, FIELD_BOOLEAN, "ignoremoveparent" ), + DEFINE_KEYFIELD( m_bIgnorePlayerCollisions, FIELD_BOOLEAN, "ignoreplayer" ), + DEFINE_KEYFIELD( m_bForcePlayerEyePoint, FIELD_BOOLEAN, "useplayereyes" ), + +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bAllowStandardWeapons, FIELD_BOOLEAN, "AllowStandardWeapons" ), +#endif + + DEFINE_OUTPUT( m_playerOn, "PlayerOn" ), + DEFINE_OUTPUT( m_playerOff, "PlayerOff" ), + DEFINE_OUTPUT( m_OnOpen, "OnOpen" ), + DEFINE_OUTPUT( m_OnClose, "OnClose" ), + + DEFINE_EMBEDDED( m_vehicleView ), + DEFINE_EMBEDDED( m_savedVehicleView ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST(CPropVehicleChoreoGeneric, DT_PropVehicleChoreoGeneric) + SendPropEHandle(SENDINFO(m_hPlayer)), + SendPropBool(SENDINFO(m_bEnterAnimOn)), + SendPropBool(SENDINFO(m_bExitAnimOn)), + SendPropVector(SENDINFO(m_vecEyeExitEndpoint), -1, SPROP_COORD), + SendPropBool( SENDINFO_STRUCTELEM( m_vehicleView.bClampEyeAngles ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flPitchCurveZero ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flPitchCurveLinear ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flRollCurveZero ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flRollCurveLinear ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flFOV ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flYawMin ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flYawMax ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flPitchMin ) ), + SendPropFloat( SENDINFO_STRUCTELEM( m_vehicleView.flPitchMax ) ), +#ifdef MAPBASE + SendPropBool( SENDINFO( m_bAllowStandardWeapons ) ), +#endif +END_SEND_TABLE(); + + +bool ShouldVehicleIgnoreEntity( CBaseEntity *pVehicle, CBaseEntity *pCollide ) +{ + if ( pCollide->GetParent() == pVehicle ) + return true; + + CPropVehicleChoreoGeneric *pChoreoVehicle = dynamic_cast ( pVehicle ); + + if ( pChoreoVehicle == NULL ) + return false; + + if ( pCollide == NULL ) + return false; + + if ( pChoreoVehicle->ShouldIgnoreParent() == false ) + return false; + + if ( pChoreoVehicle->GetMoveParent() == pCollide ) + return true; + + return false; +} + + +//------------------------------------------------ +// Precache +//------------------------------------------------ +void CPropVehicleChoreoGeneric::Precache( void ) +{ + BaseClass::Precache(); + + m_ServerVehicle.Initialize( STRING(m_vehicleScript) ); + m_ServerVehicle.UseLegacyExitChecks( true ); +} + + +//------------------------------------------------ +// Spawn +//------------------------------------------------ +void CPropVehicleChoreoGeneric::Spawn( void ) +{ + Precache(); + SetModel( STRING( GetModelName() ) ); + SetCollisionGroup( COLLISION_GROUP_VEHICLE ); + + if ( GetSolid() != SOLID_NONE ) + { + BaseClass::Spawn(); + } + + m_takedamage = DAMAGE_EVENTS_ONLY; + + SetNextThink( gpGlobals->curtime ); + + ParseViewParams( STRING(m_vehicleScript) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr ) +{ + if ( ptr->hitbox == VEHICLE_HITBOX_DRIVER ) + { + if ( m_hPlayer != NULL ) + { + m_hPlayer->TakeDamage( info ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPropVehicleChoreoGeneric::OnTakeDamage( const CTakeDamageInfo &inputInfo ) +{ + CTakeDamageInfo info = inputInfo; + info.ScaleDamage( 25 ); + + // reset the damage + info.SetDamage( inputInfo.GetDamage() ); + + // Check to do damage to prisoner + if ( m_hPlayer != NULL ) + { + // Take no damage from physics damages + if ( info.GetDamageType() & DMG_CRUSH ) + return 0; + + // Take the damage + m_hPlayer->TakeDamage( info ); + } + + return 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Vector CPropVehicleChoreoGeneric::BodyTarget( const Vector &posSrc, bool bNoisy ) +{ + Vector shotPos; + + int eyeAttachmentIndex = LookupAttachment("vehicle_driver_eyes"); + GetAttachment( eyeAttachmentIndex, shotPos ); + + if ( bNoisy ) + { + shotPos[0] += random->RandomFloat( -8.0f, 8.0f ); + shotPos[1] += random->RandomFloat( -8.0f, 8.0f ); + shotPos[2] += random->RandomFloat( -8.0f, 8.0f ); + } + + return shotPos; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::Think(void) +{ + SetNextThink( gpGlobals->curtime + 0.1 ); + + if ( GetDriver() ) + { + BaseClass::Think(); + + // If the enter or exit animation has finished, tell the server vehicle + if ( IsSequenceFinished() && (m_bExitAnimOn || m_bEnterAnimOn) ) + { + GetServerVehicle()->HandleEntryExitFinish( m_bExitAnimOn, true ); + } + } + + StudioFrameAdvance(); + DispatchAnimEvents( this ); +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPropVehicleChoreoGeneric::InputOpen( inputdata_t &inputdata ) +{ + int nSequence = LookupSequence( "open" ); + + // Set to the desired anim, or default anim if the desired is not present + if ( nSequence > ACTIVITY_NOT_AVAILABLE ) + { + SetCycle( 0 ); + m_flAnimTime = gpGlobals->curtime; + ResetSequence( nSequence ); + ResetClientsideFrame(); + } + else + { + // Not available try to get default anim + Msg( "Choreo Generic Vehicle %s: missing open sequence\n", GetDebugName() ); + SetSequence( 0 ); + } +} + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPropVehicleChoreoGeneric::InputClose( inputdata_t &inputdata ) +{ + if ( m_bLocked || m_bEnterAnimOn ) + return; + + int nSequence = LookupSequence( "close" ); + + // Set to the desired anim, or default anim if the desired is not present + if ( nSequence > ACTIVITY_NOT_AVAILABLE ) + { + SetCycle( 0 ); + m_flAnimTime = gpGlobals->curtime; + ResetSequence( nSequence ); + ResetClientsideFrame(); + } + else + { + // Not available try to get default anim + Msg( "Choreo Generic Vehicle %s: missing close sequence\n", GetDebugName() ); + SetSequence( 0 ); + } +} + + + +//------------------------------------------------------------------------------ +// Purpose: +//------------------------------------------------------------------------------ +void CPropVehicleChoreoGeneric::InputViewlock( inputdata_t &inputdata ) +{ + if (inputdata.value.Bool()) // lock + { + if (m_savedVehicleView.flFOV == 0) // not already locked + { + m_savedVehicleView = m_vehicleView; + m_vehicleView.flYawMax = m_vehicleView.flYawMin = m_vehicleView.flPitchMin = m_vehicleView.flPitchMax = 0.0f; + } + } + else + { //unlock + Assert(m_savedVehicleView.flFOV); // is nonzero if something is saved, is zero if nothing was saved. + if (m_savedVehicleView.flFOV) + { + // m_vehicleView = m_savedVehicleView; + m_savedVehicleView.flFOV = 0; + + + m_vehicleView.flYawMax.Set( m_savedVehicleView.flYawMax); + m_vehicleView.flYawMin.Set( m_savedVehicleView.flYawMin); + m_vehicleView.flPitchMin.Set(m_savedVehicleView.flPitchMin); + m_vehicleView.flPitchMax.Set(m_savedVehicleView.flPitchMax); + + /* // note: the straight assignments, as in the lower two lines below, do not call the = overload and thus are never transmitted! + m_vehicleView.flYawMax = 50; // m_savedVehicleView.flYawMax; + m_vehicleView.flYawMin = -50; // m_savedVehicleView.flYawMin; + m_vehicleView.flPitchMin = m_savedVehicleView.flPitchMin; + m_vehicleView.flPitchMax = m_savedVehicleView.flPitchMax; + */ + } + } +} + + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::HandleAnimEvent( animevent_t *pEvent ) +{ + if ( pEvent->event == AE_CHOREO_VEHICLE_OPEN ) + { + m_OnOpen.FireOutput( this, this ); + m_bLocked = false; + } + else if ( pEvent->event == AE_CHOREO_VEHICLE_CLOSE ) + { + m_OnClose.FireOutput( this, this ); + m_bLocked = true; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + if ( !pPlayer ) + return; + + ResetUseKey( pPlayer ); + + GetServerVehicle()->HandlePassengerEntry( pPlayer, (value > 0) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Return true of the player's allowed to enter / exit the vehicle +//----------------------------------------------------------------------------- +bool CPropVehicleChoreoGeneric::CanEnterVehicle( CBaseEntity *pEntity ) +{ + // Prevent entering if the vehicle's being driven by an NPC + if ( GetDriver() && GetDriver() != pEntity ) + return false; + + // Prevent entering if the vehicle's locked + return !m_bLocked; +} + + +//----------------------------------------------------------------------------- +// Purpose: Return true of the player is allowed to exit the vehicle. +//----------------------------------------------------------------------------- +bool CPropVehicleChoreoGeneric::CanExitVehicle( CBaseEntity *pEntity ) +{ + // Prevent exiting if the vehicle's locked, rotating, or playing an entry/exit anim. + return ( !m_bLocked && (GetLocalAngularVelocity() == vec3_angle) && !m_bEnterAnimOn && !m_bExitAnimOn ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Override base class to add display +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::DrawDebugGeometryOverlays(void) +{ + // Draw if BBOX is on + if ( m_debugOverlays & OVERLAY_BBOX_BIT ) + { + } + + BaseClass::DrawDebugGeometryOverlays(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::EnterVehicle( CBaseCombatCharacter *pPassenger ) +{ + if ( pPassenger == NULL ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pPassenger ); + if ( pPlayer != NULL ) + { + // Remove any player who may be in the vehicle at the moment + if ( m_hPlayer ) + { + ExitVehicle( VEHICLE_ROLE_DRIVER ); + } + + m_hPlayer = pPlayer; + m_playerOn.FireOutput( pPlayer, this, 0 ); + + m_ServerVehicle.SoundStart(); + } + else + { + // NPCs not supported yet - jdw + Assert( 0 ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::SetVehicleEntryAnim( bool bOn ) +{ + m_bEnterAnimOn = bOn; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::ExitVehicle( int nRole ) +{ + CBasePlayer *pPlayer = m_hPlayer; + if ( !pPlayer ) + return; + + m_hPlayer = NULL; + ResetUseKey( pPlayer ); + + m_playerOff.FireOutput( pPlayer, this, 0 ); + m_bEnterAnimOn = false; + + m_ServerVehicle.SoundShutdown( 1.0 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::ResetUseKey( CBasePlayer *pPlayer ) +{ + pPlayer->m_afButtonPressed &= ~IN_USE; +} + + +//----------------------------------------------------------------------------- +// Purpose: Vehicles are permanently oriented off angle for vphysics. +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::GetVectors(Vector* pForward, Vector* pRight, Vector* pUp) const +{ + // This call is necessary to cause m_rgflCoordinateFrame to be recomputed + const matrix3x4_t &entityToWorld = EntityToWorldTransform(); + + if (pForward != NULL) + { + MatrixGetColumn( entityToWorld, 1, *pForward ); + } + + if (pRight != NULL) + { + MatrixGetColumn( entityToWorld, 0, *pRight ); + } + + if (pUp != NULL) + { + MatrixGetColumn( entityToWorld, 2, *pUp ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CPropVehicleChoreoGeneric::GetDriver( void ) +{ + return m_hPlayer; +} + +//----------------------------------------------------------------------------- +// Purpose: Prevent the player from entering / exiting the vehicle +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::InputLock( inputdata_t &inputdata ) +{ + m_bLocked = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Allow the player to enter / exit the vehicle +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::InputUnlock( inputdata_t &inputdata ) +{ + m_bLocked = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Force the player to enter the vehicle. +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::InputEnterVehicle( inputdata_t &inputdata ) +{ + if ( m_bEnterAnimOn ) + return; + + // Try the activator first & use them if they are a player. + CBasePlayer *pPlayer = ToBasePlayer( inputdata.pActivator ); + if ( pPlayer == NULL ) + { + // Activator was not a player, just grab the single-player player. + pPlayer = AI_GetSinglePlayer(); + if ( pPlayer == NULL ) + return; + } + + // Force us to drop anything we're holding + pPlayer->ForceDropOfCarriedPhysObjects(); + + // FIXME: I hate code like this. I should really add a parameter to HandlePassengerEntry + // to allow entry into locked vehicles + bool bWasLocked = m_bLocked; + m_bLocked = false; + GetServerVehicle()->HandlePassengerEntry( pPlayer, true ); + m_bLocked = bWasLocked; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::InputEnterVehicleImmediate( inputdata_t &inputdata ) +{ + if ( m_bEnterAnimOn ) + return; + + // Try the activator first & use them if they are a player. + CBasePlayer *pPlayer = ToBasePlayer( inputdata.pActivator ); + if ( pPlayer == NULL ) + { + // Activator was not a player, just grab the singleplayer player. + pPlayer = AI_GetSinglePlayer(); + if ( pPlayer == NULL ) + return; + } + + if ( pPlayer->IsInAVehicle() ) + { + // Force the player out of whatever vehicle they are in. + pPlayer->LeaveVehicle(); + } + + // Force us to drop anything we're holding + pPlayer->ForceDropOfCarriedPhysObjects(); + + pPlayer->GetInVehicle( GetServerVehicle(), VEHICLE_ROLE_DRIVER ); +} + +//----------------------------------------------------------------------------- +// Purpose: Force the player to exit the vehicle. +//----------------------------------------------------------------------------- +void CPropVehicleChoreoGeneric::InputExitVehicle( inputdata_t &inputdata ) +{ + m_bForcedExit = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Parses the vehicle's script for the vehicle view parameters +//----------------------------------------------------------------------------- +bool CPropVehicleChoreoGeneric::ParseViewParams( const char *pScriptName ) +{ + byte *pFile = UTIL_LoadFileForMe( pScriptName, NULL ); + if ( !pFile ) + return false; + + IVPhysicsKeyParser *pParse = physcollision->VPhysicsKeyParserCreate( (char *)pFile ); + CVehicleChoreoViewParser viewParser; + while ( !pParse->Finished() ) + { + const char *pBlock = pParse->GetCurrentBlockName(); + if ( !strcmpi( pBlock, "vehicle_view" ) ) + { + pParse->ParseCustom( &m_vehicleView, &viewParser ); + } + else + { + pParse->SkipBlock(); + } + } + physcollision->VPhysicsKeyParserDestroy( pParse ); + UTIL_FreeFile( pFile ); + + Precache(); + + return true; +} + +//======================================================================================================================================== +// CRANE VEHICLE SERVER VEHICLE +//======================================================================================================================================== +CPropVehicleChoreoGeneric *CChoreoGenericServerVehicle::GetVehicle( void ) +{ + return (CPropVehicleChoreoGeneric *)GetDrivableVehicle(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : pPlayer - +//----------------------------------------------------------------------------- +void CChoreoGenericServerVehicle::ItemPostFrame( CBasePlayer *player ) +{ + Assert( player == GetDriver() ); + + GetDrivableVehicle()->ItemPostFrame( player ); + + if (( player->m_afButtonPressed & IN_USE ) || GetVehicle()->ShouldForceExit() ) + { + GetVehicle()->ClearForcedExit(); + if ( GetDrivableVehicle()->CanExitVehicle(player) ) + { + // Let the vehicle try to play the exit animation + if ( !HandlePassengerExit( player ) && ( player != NULL ) ) + { + player->PlayUseDenySound(); + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CChoreoGenericServerVehicle::GetVehicleViewPosition( int nRole, Vector *pAbsOrigin, QAngle *pAbsAngles, float *pFOV /*= NULL*/ ) +{ + // FIXME: This needs to be reconciled with the other versions of this function! + Assert( nRole == VEHICLE_ROLE_DRIVER ); + CBasePlayer *pPlayer = ToBasePlayer( GetDrivableVehicle()->GetDriver() ); + Assert( pPlayer ); + + // Use the player's eyes instead of the attachment point + if ( GetVehicle()->m_bForcePlayerEyePoint ) + { + // Call to BaseClass because CBasePlayer::EyePosition calls this function. + *pAbsOrigin = pPlayer->CBaseCombatCharacter::EyePosition(); + *pAbsAngles = pPlayer->CBaseCombatCharacter::EyeAngles(); + return; + } + + *pAbsAngles = pPlayer->EyeAngles(); // yuck. this is an in/out parameter. + + float flPitchFactor = 1.0; + matrix3x4_t vehicleEyePosToWorld; + Vector vehicleEyeOrigin; + QAngle vehicleEyeAngles; + GetVehicle()->GetAttachment( "vehicle_driver_eyes", vehicleEyeOrigin, vehicleEyeAngles ); + AngleMatrix( vehicleEyeAngles, vehicleEyePosToWorld ); + + // Compute the relative rotation between the unperterbed eye attachment + the eye angles + matrix3x4_t cameraToWorld; + AngleMatrix( *pAbsAngles, cameraToWorld ); + + matrix3x4_t worldToEyePos; + MatrixInvert( vehicleEyePosToWorld, worldToEyePos ); + + matrix3x4_t vehicleCameraToEyePos; + ConcatTransforms( worldToEyePos, cameraToWorld, vehicleCameraToEyePos ); + + // Now perterb the attachment point + vehicleEyeAngles.x = RemapAngleRange( PITCH_CURVE_ZERO * flPitchFactor, PITCH_CURVE_LINEAR, vehicleEyeAngles.x ); + vehicleEyeAngles.z = RemapAngleRange( ROLL_CURVE_ZERO * flPitchFactor, ROLL_CURVE_LINEAR, vehicleEyeAngles.z ); + + AngleMatrix( vehicleEyeAngles, vehicleEyeOrigin, vehicleEyePosToWorld ); + + // Now treat the relative eye angles as being relative to this new, perterbed view position... + matrix3x4_t newCameraToWorld; + ConcatTransforms( vehicleEyePosToWorld, vehicleCameraToEyePos, newCameraToWorld ); + + // output new view abs angles + MatrixAngles( newCameraToWorld, *pAbsAngles ); + + // UNDONE: *pOrigin would already be correct in single player if the HandleView() on the server ran after vphysics + MatrixGetColumn( newCameraToWorld, 3, *pAbsOrigin ); +} + +bool CPropVehicleChoreoGeneric::ShouldCollide( int collisionGroup, int contentsMask ) const +{ + if ( m_bIgnorePlayerCollisions == true ) + { + if ( collisionGroup == COLLISION_GROUP_PLAYER || collisionGroup == COLLISION_GROUP_PLAYER_MOVEMENT ) + return false; + } + + return BaseClass::ShouldCollide( collisionGroup, contentsMask ); +} + + +CVehicleChoreoViewParser::CVehicleChoreoViewParser( void ) +{ + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVehicleChoreoViewParser::ParseKeyValue( void *pData, const char *pKey, const char *pValue ) +{ + vehicleview_t *pView = (vehicleview_t *)pData; + // New gear? + if ( !strcmpi( pKey, "clamp" ) ) + { + pView->bClampEyeAngles = !!atoi( pValue ); + } + else if ( !strcmpi( pKey, "pitchcurvezero" ) ) + { + pView->flPitchCurveZero = atof( pValue ); + } + else if ( !strcmpi( pKey, "pitchcurvelinear" ) ) + { + pView->flPitchCurveLinear = atof( pValue ); + } + else if ( !strcmpi( pKey, "rollcurvezero" ) ) + { + pView->flRollCurveZero = atof( pValue ); + } + else if ( !strcmpi( pKey, "rollcurvelinear" ) ) + { + pView->flRollCurveLinear = atof( pValue ); + } + else if ( !strcmpi( pKey, "yawmin" ) ) + { + pView->flYawMin = atof( pValue ); + } + else if ( !strcmpi( pKey, "yawmax" ) ) + { + pView->flYawMax = atof( pValue ); + } + else if ( !strcmpi( pKey, "pitchmin" ) ) + { + pView->flPitchMin = atof( pValue ); + } + else if ( !strcmpi( pKey, "pitchmax" ) ) + { + pView->flPitchMax = atof( pValue ); + } + else if ( !strcmpi( pKey, "fov" ) ) + { + pView->flFOV = atof( pValue ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVehicleChoreoViewParser::SetDefaults( void *pData ) +{ + vehicleview_t *pView = (vehicleview_t *)pData; + + pView->bClampEyeAngles = true; + + pView->flPitchCurveZero = PITCH_CURVE_ZERO; + pView->flPitchCurveLinear = PITCH_CURVE_LINEAR; + pView->flRollCurveZero = ROLL_CURVE_ZERO; + pView->flRollCurveLinear = ROLL_CURVE_LINEAR; + pView->flFOV = CHOREO_VEHICLE_VIEW_FOV; + pView->flYawMin = CHOREO_VEHICLE_VIEW_YAW_MIN; + pView->flYawMax = CHOREO_VEHICLE_VIEW_YAW_MAX; + pView->flPitchMin = CHOREO_VEHICLE_VIEW_PITCH_MIN; + pView->flPitchMax = CHOREO_VEHICLE_VIEW_PITCH_MAX; + +} diff --git a/sp/src/game/server/vehicle_sounds.h b/sp/src/game/server/vehicle_sounds.h new file mode 100644 index 00000000..82557b91 --- /dev/null +++ b/sp/src/game/server/vehicle_sounds.h @@ -0,0 +1,137 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef VEHICLE_SOUNDS_H +#define VEHICLE_SOUNDS_H +#ifdef _WIN32 +#pragma once +#endif + +#include "vcollide_parse.h" + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +enum vehiclesound +{ + VS_SKID_FRICTION_LOW, + VS_SKID_FRICTION_NORMAL, + VS_SKID_FRICTION_HIGH, + VS_ENGINE2_START, + VS_ENGINE2_STOP, + VS_MISC1, + VS_MISC2, + VS_MISC3, + VS_MISC4, + + VS_NUM_SOUNDS, +}; + +extern const char *vehiclesound_parsenames[VS_NUM_SOUNDS]; + +// This is a list of vehiclesounds to automatically stop when the vehicle's driver exits the vehicle +#define NUM_SOUNDS_TO_STOP_ON_EXIT 4 +extern vehiclesound g_iSoundsToStopOnExit[NUM_SOUNDS_TO_STOP_ON_EXIT]; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +struct vehicle_gear_t +{ + DECLARE_DATADESC(); + + float flMinSpeed; + float flMaxSpeed; + float flSpeedApproachFactor; +}; + +struct vehicle_crashsound_t +{ + DECLARE_DATADESC(); + + float flMinSpeed; + float flMinDeltaSpeed; + int gearLimit; + string_t iszCrashSound; +}; + +enum sound_states +{ + SS_NONE = 0, + SS_SHUTDOWN, + SS_SHUTDOWN_WATER, + SS_START_WATER, + SS_START_IDLE, + SS_IDLE, + SS_GEAR_0, + SS_GEAR_1, + SS_GEAR_2, + SS_GEAR_3, + SS_GEAR_4, + SS_SLOWDOWN, + SS_SLOWDOWN_HIGHSPEED, // not a real state, just a slot for state sounds + SS_GEAR_0_RESUME, + SS_GEAR_1_RESUME, + SS_GEAR_2_RESUME, + SS_GEAR_3_RESUME, + SS_GEAR_4_RESUME, + SS_TURBO, + SS_REVERSE, + + SS_NUM_STATES, +}; + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +struct vehiclesounds_t +{ + void Init( void ) + { + pGears.Purge(); + crashSounds.Purge(); + + for ( int i = 0; i < VS_NUM_SOUNDS; i++ ) + { + iszSound[i] = NULL_STRING; + } + + for ( int i = 0; i < SS_NUM_STATES; i++ ) + { + iszStateSounds[i] = NULL_STRING; + minStateTime[i] = 0.0f; + } + } + + DECLARE_DATADESC(); + + CUtlVector pGears; + CUtlVector crashSounds; + string_t iszSound[ VS_NUM_SOUNDS ]; + string_t iszStateSounds[SS_NUM_STATES]; + float minStateTime[SS_NUM_STATES]; +}; + +//----------------------------------------------------------------------------- +// Purpose: A KeyValues parse for vehicle sound blocks +//----------------------------------------------------------------------------- +class CVehicleSoundsParser : public IVPhysicsKeyHandler +{ +public: + CVehicleSoundsParser( void ); + + virtual void ParseKeyValue( void *pData, const char *pKey, const char *pValue ); + virtual void SetDefaults( void *pData ); + +private: + // Index of the gear we're currently reading data into + int m_iCurrentGear; + int m_iCurrentState; + int m_iCurrentCrashSound; +}; + +#endif // VEHICLE_SOUNDS_H diff --git a/sp/src/game/server/vguiscreen.cpp b/sp/src/game/server/vguiscreen.cpp new file mode 100644 index 00000000..e049da22 --- /dev/null +++ b/sp/src/game/server/vguiscreen.cpp @@ -0,0 +1,419 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: This is an entity that represents a vgui screen +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "vguiscreen.h" +#include "networkstringtable_gamedll.h" +#include "saverestore_stringtable.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// This is an entity that represents a vgui screen +//----------------------------------------------------------------------------- + +IMPLEMENT_SERVERCLASS_ST(CVGuiScreen, DT_VGuiScreen) + SendPropFloat(SENDINFO(m_flWidth), 0, SPROP_NOSCALE ), + SendPropFloat(SENDINFO(m_flHeight), 0, SPROP_NOSCALE ), + SendPropInt(SENDINFO(m_nAttachmentIndex), 5, SPROP_UNSIGNED ), + SendPropInt(SENDINFO(m_nPanelName), MAX_VGUI_SCREEN_STRING_BITS, SPROP_UNSIGNED ), + SendPropInt(SENDINFO(m_fScreenFlags), VGUI_SCREEN_MAX_BITS, SPROP_UNSIGNED ), + SendPropInt(SENDINFO(m_nOverlayMaterial), MAX_MATERIAL_STRING_BITS, SPROP_UNSIGNED ), + SendPropEHandle(SENDINFO(m_hPlayerOwner)), +END_SEND_TABLE(); + +LINK_ENTITY_TO_CLASS( vgui_screen, CVGuiScreen ); +LINK_ENTITY_TO_CLASS( vgui_screen_team, CVGuiScreen ); +PRECACHE_REGISTER( vgui_screen ); + + +//----------------------------------------------------------------------------- +// Save/load +//----------------------------------------------------------------------------- +BEGIN_DATADESC( CVGuiScreen ) + + DEFINE_CUSTOM_FIELD( m_nPanelName, &g_VguiScreenStringOps ), + DEFINE_FIELD( m_nAttachmentIndex, FIELD_INTEGER ), +// DEFINE_FIELD( m_nOverlayMaterial, FIELD_INTEGER ), + DEFINE_FIELD( m_fScreenFlags, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_flWidth, FIELD_FLOAT, "width" ), + DEFINE_KEYFIELD( m_flHeight, FIELD_FLOAT, "height" ), + DEFINE_KEYFIELD( m_strOverlayMaterial, FIELD_STRING, "overlaymaterial" ), + DEFINE_FIELD( m_hPlayerOwner, FIELD_EHANDLE ), + + DEFINE_INPUTFUNC( FIELD_VOID, "SetActive", InputSetActive ), + DEFINE_INPUTFUNC( FIELD_VOID, "SetInactive", InputSetInactive ), + +END_DATADESC() + + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CVGuiScreen::CVGuiScreen() +{ + m_nOverlayMaterial = OVERLAY_MATERIAL_INVALID_STRING; + m_hPlayerOwner = NULL; +} + + +//----------------------------------------------------------------------------- +// Read in worldcraft data... +//----------------------------------------------------------------------------- +bool CVGuiScreen::KeyValue( const char *szKeyName, const char *szValue ) +{ + //!! temp hack, until worldcraft is fixed + // strip the # tokens from (duplicate) key names + char *s = (char *)strchr( szKeyName, '#' ); + if ( s ) + { + *s = '\0'; + } + + if ( FStrEq( szKeyName, "panelname" )) + { + SetPanelName( szValue ); + return true; + } + + // NOTE: Have to do these separate because they set two values instead of one + if( FStrEq( szKeyName, "angles" ) ) + { + Assert( GetMoveParent() == NULL ); + QAngle angles; + UTIL_StringToVector( angles.Base(), szValue ); + + // Because the vgui screen basis is strange (z is front, y is up, x is right) + // we need to rotate the typical basis before applying it + VMatrix mat, rotation, tmp; + MatrixFromAngles( angles, mat ); + MatrixBuildRotationAboutAxis( rotation, Vector( 0, 1, 0 ), 90 ); + MatrixMultiply( mat, rotation, tmp ); + MatrixBuildRotateZ( rotation, 90 ); + MatrixMultiply( tmp, rotation, mat ); + MatrixToAngles( mat, angles ); + SetAbsAngles( angles ); + + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + + +//----------------------------------------------------------------------------- +// Precache... +//----------------------------------------------------------------------------- +void CVGuiScreen::Precache() +{ + BaseClass::Precache(); + if ( m_strOverlayMaterial != NULL_STRING ) + { + PrecacheMaterial( STRING(m_strOverlayMaterial) ); + } +} + + +//----------------------------------------------------------------------------- +// Spawn... +//----------------------------------------------------------------------------- +void CVGuiScreen::Spawn() +{ + Precache(); + + // This has no model, but we want it to transmit if it's in the PVS anyways + AddEFlags( EFL_FORCE_CHECK_TRANSMIT ); + m_nAttachmentIndex = 0; + SetSolid( SOLID_OBB ); + AddSolidFlags( FSOLID_NOT_SOLID ); + SetActualSize( m_flWidth, m_flHeight ); + m_fScreenFlags.Set( VGUI_SCREEN_ACTIVE ); + + m_takedamage = DAMAGE_NO; + AddFlag( FL_NOTARGET ); +} + +//----------------------------------------------------------------------------- +// Spawn... +//----------------------------------------------------------------------------- +void CVGuiScreen::Activate() +{ + BaseClass::Activate(); + + if ( m_nOverlayMaterial == OVERLAY_MATERIAL_INVALID_STRING && m_strOverlayMaterial != NULL_STRING ) + { + SetOverlayMaterial( STRING(m_strOverlayMaterial) ); + } +} + +void CVGuiScreen::OnRestore() +{ + UpdateTransmitState(); + + BaseClass::OnRestore(); +} + +void CVGuiScreen::SetAttachmentIndex( int nIndex ) +{ + m_nAttachmentIndex = nIndex; +} + +void CVGuiScreen::SetOverlayMaterial( const char *pMaterial ) +{ + int iMaterial = GetMaterialIndex( pMaterial ); + + if ( iMaterial == 0 ) + { + m_nOverlayMaterial = OVERLAY_MATERIAL_INVALID_STRING; + } + else + { + m_nOverlayMaterial = iMaterial; + } +} + +bool CVGuiScreen::IsActive() const +{ + return (m_fScreenFlags & VGUI_SCREEN_ACTIVE) != 0; +} + +void CVGuiScreen::SetActive( bool bActive ) +{ + if (bActive != IsActive()) + { + if (!bActive) + { + m_fScreenFlags &= ~VGUI_SCREEN_ACTIVE; + } + else + { + m_fScreenFlags.Set( m_fScreenFlags | VGUI_SCREEN_ACTIVE ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CVGuiScreen::IsAttachedToViewModel() const +{ + return (m_fScreenFlags & VGUI_SCREEN_ATTACHED_TO_VIEWMODEL) != 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : bAttached - +//----------------------------------------------------------------------------- +void CVGuiScreen::SetAttachedToViewModel( bool bAttached ) +{ + if (bAttached != IsActive()) + { + if (!bAttached) + { + m_fScreenFlags &= ~VGUI_SCREEN_ATTACHED_TO_VIEWMODEL; + } + else + { + m_fScreenFlags.Set( m_fScreenFlags | VGUI_SCREEN_ATTACHED_TO_VIEWMODEL ); + + // attached screens have different transmit rules + DispatchUpdateTransmitState(); + } + + // attached screens have different transmit rules + DispatchUpdateTransmitState(); + } +} + +void CVGuiScreen::SetTransparency( bool bTransparent ) +{ + if (!bTransparent) + { + m_fScreenFlags &= ~VGUI_SCREEN_TRANSPARENT; + } + else + { + m_fScreenFlags.Set( m_fScreenFlags | VGUI_SCREEN_TRANSPARENT ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVGuiScreen::InputSetActive( inputdata_t &inputdata ) +{ + SetActive( true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVGuiScreen::InputSetInactive( inputdata_t &inputdata ) +{ + SetActive( false ); +} + +bool CVGuiScreen::IsVisibleOnlyToTeammates() const +{ + return (m_fScreenFlags & VGUI_SCREEN_VISIBLE_TO_TEAMMATES) != 0; +} + +void CVGuiScreen::MakeVisibleOnlyToTeammates( bool bActive ) +{ + if (bActive != IsVisibleOnlyToTeammates()) + { + if (!bActive) + { + m_fScreenFlags &= ~VGUI_SCREEN_VISIBLE_TO_TEAMMATES; + } + else + { + m_fScreenFlags.Set( m_fScreenFlags | VGUI_SCREEN_VISIBLE_TO_TEAMMATES ); + } + } +} + +bool CVGuiScreen::IsVisibleToTeam( int nTeam ) +{ + // FIXME: Should this maybe go into a derived class of some sort? + // Don't bother with screens on the wrong team + if ( IsVisibleOnlyToTeammates() && (nTeam > 0) ) + { + // Hmmm... sort of a hack... + CBaseEntity *pOwner = GetOwnerEntity(); + if ( pOwner && (nTeam != pOwner->GetTeamNumber()) ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Screens attached to view models only go to client if viewmodel is being sent, too. +// Input : *recipient - +// *pvs - +// clientArea - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +int CVGuiScreen::UpdateTransmitState() +{ + if ( IsAttachedToViewModel() ) + { + // only send to the owner, or someone spectating the owner. + return SetTransmitState( FL_EDICT_FULLCHECK ); + } + else if ( GetMoveParent() ) + { + // Let the parent object trigger the send. This is more efficient than having it call CBaseEntity::ShouldTransmit + // for all the vgui screens in the map. + return SetTransmitState( FL_EDICT_PVSCHECK ); + } + else + { + return BaseClass::UpdateTransmitState(); + } +} + +int CVGuiScreen::ShouldTransmit( const CCheckTransmitInfo *pInfo ) +{ + Assert( IsAttachedToViewModel() ); + + CBaseEntity *pViewModel = GetOwnerEntity(); + + if ( pViewModel ) + { + return pViewModel->ShouldTransmit( pInfo ); + } + + return BaseClass::ShouldTransmit( pInfo ); +} + +//----------------------------------------------------------------------------- +// Convert the panel name into an integer +//----------------------------------------------------------------------------- +void CVGuiScreen::SetPanelName( const char *pPanelName ) +{ + m_nPanelName = g_pStringTableVguiScreen->AddString( CBaseEntity::IsServer(), pPanelName ); +} + +const char *CVGuiScreen::GetPanelName() const +{ + return g_pStringTableVguiScreen->GetString( m_nPanelName ); +} + + +//----------------------------------------------------------------------------- +// Sets the screen size + resolution +//----------------------------------------------------------------------------- +void CVGuiScreen::SetActualSize( float flWidth, float flHeight ) +{ + m_flWidth = flWidth; + m_flHeight = flHeight; + + Vector mins, maxs; + mins.Init( 0.0f, 0.0f, -0.1f ); + maxs.Init( 0.0f, 0.0f, 0.1f ); + if (flWidth > 0) + maxs.x = flWidth; + else + mins.x = flWidth; + if (flHeight > 0) + maxs.y = flHeight; + else + mins.y = flHeight; + + UTIL_SetSize( this, mins, maxs ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CVGuiScreen::SetPlayerOwner( CBasePlayer *pPlayer, bool bOwnerOnlyInput /* = false */ ) +{ + m_hPlayerOwner = pPlayer; + + if ( bOwnerOnlyInput ) + { + m_fScreenFlags.Set( VGUI_SCREEN_ONLY_USABLE_BY_OWNER ); + } +} + + +//----------------------------------------------------------------------------- +// Precaches a vgui screen +//----------------------------------------------------------------------------- +void PrecacheVGuiScreen( const char *pScreenType ) +{ + g_pStringTableVguiScreen->AddString( CBaseEntity::IsServer(), pScreenType ); +} + + +//----------------------------------------------------------------------------- +// Creates a vgui screen, attaches it to another player +//----------------------------------------------------------------------------- +CVGuiScreen *CreateVGuiScreen( const char *pScreenClassname, const char *pScreenType, CBaseEntity *pAttachedTo, CBaseEntity *pOwner, int nAttachmentIndex ) +{ + Assert( pAttachedTo ); + CVGuiScreen *pScreen = (CVGuiScreen *)CBaseEntity::Create( pScreenClassname, vec3_origin, vec3_angle, pAttachedTo ); + + pScreen->SetPanelName( pScreenType ); + pScreen->FollowEntity( pAttachedTo ); + pScreen->SetOwnerEntity( pOwner ); + pScreen->SetAttachmentIndex( nAttachmentIndex ); + + return pScreen; +} + +void DestroyVGuiScreen( CVGuiScreen *pVGuiScreen ) +{ + if (pVGuiScreen) + { + UTIL_Remove( pVGuiScreen ); + } +} diff --git a/sp/src/game/server/vguiscreen.h b/sp/src/game/server/vguiscreen.h new file mode 100644 index 00000000..cf720916 --- /dev/null +++ b/sp/src/game/server/vguiscreen.h @@ -0,0 +1,88 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: This is an entity that represents a vgui screen +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VGUISCREEN_H +#define VGUISCREEN_H + +#ifdef _WIN32 +#pragma once +#endif + + +//----------------------------------------------------------------------------- +// This is an entity that represents a vgui screen +//----------------------------------------------------------------------------- +class CVGuiScreen : public CBaseEntity +{ +public: + DECLARE_CLASS( CVGuiScreen, CBaseEntity ); + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CVGuiScreen(); + + virtual void Precache(); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual void Spawn(); + virtual void Activate(); + virtual void OnRestore(); + + const char *GetPanelName() const; + + // Sets the screen size + resolution + void SetActualSize( float flWidth, float flHeight ); + + // Activates/deactivates the screen + bool IsActive() const; + void SetActive( bool bActive ); + + // Is this screen only visible to teammates? + bool IsVisibleOnlyToTeammates() const; + void MakeVisibleOnlyToTeammates( bool bActive ); + bool IsVisibleToTeam( int nTeam ); + + // Sets the overlay material + void SetOverlayMaterial( const char *pMaterial ); + + void SetAttachedToViewModel( bool bAttached ); + bool IsAttachedToViewModel() const; + + void SetTransparency( bool bTransparent ); + + virtual int UpdateTransmitState( void ); + virtual int ShouldTransmit( const CCheckTransmitInfo *pInfo ); + + void SetPlayerOwner( CBasePlayer *pPlayer, bool bOwnerOnlyInput = false ); + +private: + void SetAttachmentIndex( int nIndex ); + void SetPanelName( const char *pPanelName ); + void InputSetActive( inputdata_t &inputdata ); + void InputSetInactive( inputdata_t &inputdata ); + + string_t m_strOverlayMaterial; + + CNetworkVar( float, m_flWidth ); + CNetworkVar( float, m_flHeight ); + CNetworkVar( int, m_nPanelName ); // The name of the panel + CNetworkVar( int, m_nAttachmentIndex ); + CNetworkVar( int, m_nOverlayMaterial ); + CNetworkVar( int, m_fScreenFlags ); + CNetworkVar( EHANDLE, m_hPlayerOwner ); + + friend CVGuiScreen *CreateVGuiScreen( const char *pScreenClassname, const char *pScreenType, CBaseEntity *pAttachedTo, CBaseEntity *pOwner, int nAttachmentIndex ); +}; + + +void PrecacheVGuiScreen( const char *pScreenType ); +void PrecacheVGuiScreenOverlayMaterial( const char *pMaterialName ); +CVGuiScreen *CreateVGuiScreen( const char *pScreenClassname, const char *pScreenType, CBaseEntity *pAttachedTo, CBaseEntity *pOwner, int nAttachmentIndex ); +void DestroyVGuiScreen( CVGuiScreen *pVGuiScreen ); + + +#endif // VGUISCREEN_H diff --git a/sp/src/game/server/vote_controller.cpp b/sp/src/game/server/vote_controller.cpp new file mode 100644 index 00000000..316974ad --- /dev/null +++ b/sp/src/game/server/vote_controller.cpp @@ -0,0 +1,1100 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Base VoteController. Handles holding and voting on issues. +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "vote_controller.h" +#include "shareddefs.h" +#include "eiface.h" +#include "team.h" +#include "gameinterface.h" + +#ifdef TF_DLL +#include "tf/tf_gamerules.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define MAX_VOTER_HISTORY 64 + +// Datatable +IMPLEMENT_SERVERCLASS_ST( CVoteController, DT_VoteController ) + SendPropInt( SENDINFO( m_iActiveIssueIndex ) ), + SendPropInt( SENDINFO( m_iOnlyTeamToVote ) ), + SendPropArray3( SENDINFO_ARRAY3( m_nVoteOptionCount ), SendPropInt( SENDINFO_ARRAY( m_nVoteOptionCount ), 8, SPROP_UNSIGNED ) ), + SendPropInt( SENDINFO( m_nPotentialVotes ) ), + SendPropBool( SENDINFO( m_bIsYesNoVote ) ) +END_SEND_TABLE() + +BEGIN_DATADESC( CVoteController ) + DEFINE_THINKFUNC( VoteControllerThink ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( vote_controller, CVoteController ); + +CVoteController *g_voteController = NULL; + +ConVar sv_vote_timer_duration("sv_vote_timer_duration", "15", FCVAR_DEVELOPMENTONLY, "How long to allow voting on an issue"); +ConVar sv_vote_command_delay("sv_vote_command_delay", "2", FCVAR_DEVELOPMENTONLY, "How long after a vote passes until the action happens", false, 0, true, 4.5); +ConVar sv_allow_votes("sv_allow_votes", "1", 0, "Allow voting?"); +ConVar sv_vote_failure_timer("sv_vote_failure_timer", "300", 0, "A vote that fails cannot be re-submitted for this long"); +#ifdef TF_DLL +ConVar sv_vote_failure_timer_mvm( "sv_vote_failure_timer_mvm", "120", 0, "A vote that fails in MvM cannot be re-submitted for this long" ); +#endif // TF_DLL +ConVar sv_vote_creation_timer("sv_vote_creation_timer", "120", FCVAR_DEVELOPMENTONLY, "How often someone can individually call a vote."); +ConVar sv_vote_quorum_ratio( "sv_vote_quorum_ratio", "0.6", 1, "The minimum ratio of players needed to vote on an issue to resolve it.", true, 0.1, true, 1.0 ); +ConVar sv_vote_allow_spectators( "sv_vote_allow_spectators", "0", 0, "Allow spectators to vote?" ); +ConVar sv_vote_ui_hide_disabled_issues( "sv_vote_ui_hide_disabled_issues", "1", 0, "Suppress listing of disabled issues in the vote setup screen." ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CommandListIssues( void ) +{ + CBasePlayer *commandIssuer = UTIL_GetCommandClient(); + + if ( g_voteController && commandIssuer ) + { + g_voteController->ListIssues(commandIssuer); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +ConCommand ListIssues("listissues", CommandListIssues, "List all the issues that can be voted on.", 0); + +//----------------------------------------------------------------------------- +// Purpose: This should eventually ask the player what team they are voting on +// to take into account different idle / spectator rules. +//----------------------------------------------------------------------------- + +int GetVoterTeam( CBaseEntity *pEntity ) +{ + if ( !pEntity ) + return TEAM_UNASSIGNED; + + int iTeam = pEntity->GetTeamNumber(); + + return iTeam; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CON_COMMAND( callvote, "Start a vote on an issue." ) +{ + if ( !g_voteController ) + { + DevMsg( "Vote Controller Not Found!\n" ); + return; + } + + CBasePlayer *pVoteCaller = UTIL_GetCommandClient(); + if( !pVoteCaller ) + return; + + if ( !sv_vote_allow_spectators.GetBool() ) + { + if ( pVoteCaller->GetTeamNumber() == TEAM_SPECTATOR ) + { + g_voteController->SendVoteFailedMessage( VOTE_FAILED_SPECTATOR, pVoteCaller ); + return; + } + } + + // Prevent spamming commands +#ifndef _DEBUG + int nCooldown = 0; + if ( !g_voteController->CanEntityCallVote( pVoteCaller, nCooldown ) ) + { + g_voteController->SendVoteFailedMessage( VOTE_FAILED_RATE_EXCEEDED, pVoteCaller, nCooldown ); + return; + } +#endif + + // Parameters + char szEmptyDetails[MAX_VOTE_DETAILS_LENGTH]; + szEmptyDetails[0] = '\0'; + const char *arg2 = args[1]; + const char *arg3 = args.ArgC() >= 3 ? args[2] : szEmptyDetails; + + // If we don't have any arguments, invoke VoteSetup UI + if( args.ArgC() < 2 ) + { + g_voteController->SetupVote( pVoteCaller->entindex() ); + return; + } + + g_voteController->CreateVote( pVoteCaller->entindex(), arg2, arg3 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CVoteController::~CVoteController() +{ + g_voteController = NULL; + + for( int issueIndex = 0; issueIndex < m_potentialIssues.Count(); ++issueIndex ) + { + delete m_potentialIssues[issueIndex]; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVoteController::ResetData( void ) +{ + m_iActiveIssueIndex = INVALID_ISSUE; + + for ( int index = 0; index < m_nVoteOptionCount.Count(); index++ ) + { + m_nVoteOptionCount.Set( index, 0 ); + } + + m_nPotentialVotes = 0; + m_acceptingVotesTimer.Invalidate(); + m_executeCommandTimer.Invalidate(); + m_iEntityHoldingVote = -1; + m_iOnlyTeamToVote = TEAM_INVALID; + m_bIsYesNoVote = true; + + for( int voteIndex = 0; voteIndex < ARRAYSIZE( m_nVotesCast ); ++voteIndex ) + { + m_nVotesCast[voteIndex] = VOTE_UNCAST; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVoteController::Spawn( void ) +{ + ResetData(); + + BaseClass::Spawn(); + + SetThink( &CVoteController::VoteControllerThink ); + SetNextThink( gpGlobals->curtime ); + + SetDefLessFunc( m_VoteCallers ); + + g_voteController = this; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CVoteController::UpdateTransmitState( void ) +{ + // ALWAYS transmit to all clients. + return SetTransmitState( FL_EDICT_ALWAYS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CVoteController::CanTeamCastVote( int iTeam ) const +{ + if ( m_iOnlyTeamToVote == TEAM_INVALID ) + return true; + + return iTeam == m_iOnlyTeamToVote; +} + +//----------------------------------------------------------------------------- +// Purpose: Handles menu-driven setup of Voting +//----------------------------------------------------------------------------- +bool CVoteController::SetupVote( int iEntIndex ) +{ + CBasePlayer *pVoteCaller = UTIL_PlayerByIndex( iEntIndex ); + if( !pVoteCaller ) + return false; + + int nIssueCount = 0; + + // Passing an nIssueCount of 0 triggers a "Voting disabled on server" message in the setup UI + if ( sv_allow_votes.GetBool() ) + { + for( int iIndex = 0; iIndex < m_potentialIssues.Count(); ++iIndex ) + { + // Hide disabled issues? + CBaseIssue *pCurrentIssue = m_potentialIssues[iIndex]; + if ( pCurrentIssue ) + { + if ( !pCurrentIssue->IsEnabled() && sv_vote_ui_hide_disabled_issues.GetBool() ) + continue; + + nIssueCount++; + } + } + } + + CSingleUserRecipientFilter filter( pVoteCaller ); + filter.MakeReliable(); + UserMessageBegin( filter, "VoteSetup" ); + WRITE_BYTE( nIssueCount ); + + for( int iIndex = 0; iIndex < m_potentialIssues.Count(); ++iIndex ) + { + CBaseIssue *pCurrentIssue = m_potentialIssues[iIndex]; + if ( pCurrentIssue ) + { + if ( pCurrentIssue->IsEnabled() ) + { + WRITE_STRING( pCurrentIssue->GetTypeString() ); + } + else + { + // Don't send/display disabled issues when set + if ( sv_vote_ui_hide_disabled_issues.GetBool() ) + continue; + + char szDisabledIssueStr[MAX_COMMAND_LENGTH + 12]; + V_strcpy( szDisabledIssueStr, pCurrentIssue->GetTypeString() ); + V_strcat( szDisabledIssueStr, " (Disabled on Server)", sizeof(szDisabledIssueStr) ); + + WRITE_STRING( szDisabledIssueStr ); + } + } + } + + MessageEnd(); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Handles console-driven setup of Voting +//----------------------------------------------------------------------------- +bool CVoteController::CreateVote( int iEntIndex, const char *pszTypeString, const char *pszDetailString ) +{ + // Terrible Hack: Dedicated servers pass 99 as the EntIndex + bool bDedicatedServer = ( iEntIndex == DEDICATED_SERVER ) ? true : false; + + if( !sv_allow_votes.GetBool() ) + return false; + + // Already running a vote? + if( IsVoteActive() ) + return false; + + CBasePlayer *pVoteCaller = UTIL_PlayerByIndex( iEntIndex ); + if( !pVoteCaller && !bDedicatedServer ) + return false; + + // Find the issue the user is asking for + for( int issueIndex = 0; issueIndex < m_potentialIssues.Count(); ++issueIndex ) + { + CBaseIssue *pCurrentIssue = m_potentialIssues[issueIndex]; + if ( !pCurrentIssue ) + return false; + + if( FStrEq( pszTypeString, pCurrentIssue->GetTypeString() ) ) + { + vote_create_failed_t nErrorCode = VOTE_FAILED_GENERIC; + int nTime = 0; + if( pCurrentIssue->CanCallVote( iEntIndex, pszDetailString, nErrorCode, nTime ) ) + { + // Establish a bunch of data on this particular issue + pCurrentIssue->SetIssueDetails( pszDetailString ); + m_bIsYesNoVote = pCurrentIssue->IsYesNoVote(); + m_iActiveIssueIndex = issueIndex; + m_iEntityHoldingVote = iEntIndex; + if ( !bDedicatedServer ) + { + if( pCurrentIssue->IsAllyRestrictedVote() ) + { + m_iOnlyTeamToVote = GetVoterTeam( pVoteCaller ); + } + else + { + m_iOnlyTeamToVote = TEAM_INVALID; + } + } + + // Now get our choices + m_VoteOptions.RemoveAll(); + pCurrentIssue->GetVoteOptions( m_VoteOptions ); + int nNumVoteOptions = m_VoteOptions.Count(); + if ( nNumVoteOptions >= 2 ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "vote_options" ); + if ( event ) + { + event->SetInt( "count", nNumVoteOptions ); + for ( int iIndex = 0; iIndex < nNumVoteOptions; iIndex++ ) + { + char szNumber[2]; + Q_snprintf( szNumber, sizeof( szNumber ), "%i", iIndex + 1 ); + + char szOptionName[8] = "option"; + Q_strncat( szOptionName, szNumber, sizeof( szOptionName ), COPY_ALL_CHARACTERS ); + + event->SetString( szOptionName, m_VoteOptions[iIndex] ); + } + gameeventmanager->FireEvent( event ); + } + } + else + { + Assert( nNumVoteOptions >= 2 ); + } + + // Have the issue start working on it + pCurrentIssue->OnVoteStarted(); + + // Now the vote handling and UI + m_nPotentialVotes = pCurrentIssue->CountPotentialVoters(); + m_acceptingVotesTimer.Start( sv_vote_timer_duration.GetFloat() ); + + // Force the vote holder to agree with a Yes/No vote + if ( m_bIsYesNoVote && !bDedicatedServer ) + { + TryCastVote( iEntIndex, "Option1" ); + } + + // Get the data out to the client + CBroadcastRecipientFilter filter; + filter.MakeReliable(); + UserMessageBegin( filter, "VoteStart" ); + WRITE_BYTE( m_iOnlyTeamToVote ); // move into the filter + WRITE_BYTE( m_iEntityHoldingVote ); + WRITE_STRING( pCurrentIssue->GetDisplayString() ); + WRITE_STRING( pCurrentIssue->GetDetailsString() ); + WRITE_BOOL( m_bIsYesNoVote ); + MessageEnd(); + + if ( !bDedicatedServer ) + { + TrackVoteCaller( pVoteCaller ); + } + + return true; + } + else + { + if ( !bDedicatedServer ) + { + SendVoteFailedMessage( nErrorCode, pVoteCaller, nTime ); + } + } + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Sent to everyone, unless we pass a player pointer +//----------------------------------------------------------------------------- +void CVoteController::SendVoteFailedMessage( vote_create_failed_t nReason, CBasePlayer *pVoteCaller, int nTime ) +{ + // driller: need to merge all failure case stuff into a single path + if ( pVoteCaller ) + { + CSingleUserRecipientFilter user( pVoteCaller ); + user.MakeReliable(); + + UserMessageBegin( user, "CallVoteFailed" ); + WRITE_BYTE( nReason ); + WRITE_SHORT( nTime ); + MessageEnd(); + } + else + { + UTIL_LogPrintf("Vote failed \"%s %s\" \n", + m_potentialIssues[m_iActiveIssueIndex]->GetTypeString(), + m_potentialIssues[m_iActiveIssueIndex]->GetDetailsString() ); + + CBroadcastRecipientFilter filter; + filter.MakeReliable(); + + UserMessageBegin( filter, "VoteFailed" ); + WRITE_BYTE( m_iOnlyTeamToVote ); + WRITE_BYTE( nReason ); + MessageEnd(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Player generated a vote command. i.e. /vote option1 +//----------------------------------------------------------------------------- +CVoteController::TryCastVoteResult CVoteController::TryCastVote( int iEntIndex, const char *pszVoteString ) +{ + if( !sv_allow_votes.GetBool() ) + return CAST_FAIL_SERVER_DISABLE; + + if( iEntIndex >= ARRAYSIZE( m_nVotesCast ) ) + return CAST_FAIL_SYSTEM_ERROR; + + if( !IsVoteActive() ) + return CAST_FAIL_NO_ACTIVE_ISSUE; + + if( m_executeCommandTimer.HasStarted() ) + return CAST_FAIL_VOTE_CLOSED; + + if( m_potentialIssues[m_iActiveIssueIndex] && m_potentialIssues[m_iActiveIssueIndex]->IsAllyRestrictedVote() ) + { + CBaseEntity *pVoteHolder = UTIL_EntityByIndex( m_iEntityHoldingVote ); + CBaseEntity *pVoter = UTIL_EntityByIndex( iEntIndex ); + + if( ( pVoteHolder == NULL ) || ( pVoter == NULL ) || ( GetVoterTeam( pVoteHolder ) != GetVoterTeam( pVoter ) ) ) + { + return CAST_FAIL_TEAM_RESTRICTED; + } + } + + // Look for a previous vote + int nOldVote = m_nVotesCast[iEntIndex]; +#ifndef DEBUG + if( nOldVote != VOTE_UNCAST ) + { + return CAST_FAIL_NO_CHANGES; + } +#endif // !DEBUG + + // Which option are they voting for? + int nCurrentVote = VOTE_UNCAST; + if ( Q_strnicmp( pszVoteString, "Option", 6 ) != 0 ) + return CAST_FAIL_SYSTEM_ERROR; + + nCurrentVote = (CastVote)( atoi( pszVoteString + 6 ) - 1 ); + + if ( nCurrentVote < VOTE_OPTION1 || nCurrentVote > VOTE_OPTION5 ) + return CAST_FAIL_SYSTEM_ERROR; + + // They're changing their vote +#ifdef DEBUG + if ( nOldVote != VOTE_UNCAST ) + { + if( nOldVote == nCurrentVote ) + { + return CAST_FAIL_DUPLICATE; + } + VoteChoice_Decrement( nOldVote ); + } +#endif // DEBUG + + // With a Yes/No vote, slam anything past "No" to No + if ( m_potentialIssues[m_iActiveIssueIndex]->IsYesNoVote() ) + { + if ( nCurrentVote > VOTE_OPTION2 ) + nCurrentVote = VOTE_OPTION2; + } + + // Register and track this vote + VoteChoice_Increment( nCurrentVote ); + m_nVotesCast[iEntIndex] = nCurrentVote; + + // Tell the client-side UI + IGameEvent *event = gameeventmanager->CreateEvent( "vote_cast" ); + if ( event ) + { + event->SetInt( "vote_option", nCurrentVote ); + event->SetInt( "team", m_iOnlyTeamToVote ); + event->SetInt( "entityid", iEntIndex ); + gameeventmanager->FireEvent( event ); + } + + CheckForEarlyVoteClose(); + + return CAST_OK; +} + +//----------------------------------------------------------------------------- +// Purpose: Increments the vote count for a particular vote option +// i.e. nVoteChoice = 0 might mean a Yes vote +//----------------------------------------------------------------------------- +void CVoteController::VoteChoice_Increment( int nVoteChoice ) +{ + if ( nVoteChoice < VOTE_OPTION1 || nVoteChoice > VOTE_OPTION5 ) + return; + + int nValue = m_nVoteOptionCount.Get( nVoteChoice ); + m_nVoteOptionCount.Set( nVoteChoice, ++nValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVoteController::VoteChoice_Decrement( int nVoteChoice ) +{ + if ( nVoteChoice < VOTE_OPTION1 || nVoteChoice > VOTE_OPTION5 ) + return; + + int nValue = m_nVoteOptionCount.Get( nVoteChoice ); + m_nVoteOptionCount.Set( nVoteChoice, --nValue ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVoteController::VoteControllerThink( void ) +{ + if ( !m_potentialIssues.IsValidIndex( m_iActiveIssueIndex ) ) + { + SetNextThink( gpGlobals->curtime + 0.5f ); + + return; + } + + // Vote time is up - process the result + if( m_acceptingVotesTimer.HasStarted() && m_acceptingVotesTimer.IsElapsed() ) + { + m_acceptingVotesTimer.Invalidate(); + + int nVoteTally = 0; + for ( int index = 0; index < MAX_VOTE_OPTIONS; index++ ) + { + nVoteTally += m_nVoteOptionCount.Get( index ); + } + + bool bVotePassed = true; + + // for record-keeping + if ( m_potentialIssues[m_iActiveIssueIndex]->IsYesNoVote() ) + { + m_potentialIssues[m_iActiveIssueIndex]->SetYesNoVoteCount( m_nVoteOptionCount[VOTE_OPTION1], m_nVoteOptionCount[VOTE_OPTION2], m_nPotentialVotes ); + } + + // Have we exceeded the required ratio of Voted-vs-Abstained? + if ( nVoteTally >= m_nPotentialVotes * sv_vote_quorum_ratio.GetFloat() ) + { + int nWinningVoteOption = GetWinningVoteOption(); + Assert( nWinningVoteOption >= 0 && nWinningVoteOption < m_VoteOptions.Count() ); + + if ( nWinningVoteOption >= 0 && nWinningVoteOption < MAX_VOTE_OPTIONS ) + { + // YES/NO VOTES + if ( m_potentialIssues[m_iActiveIssueIndex]->IsYesNoVote() ) + { + // Option1 is Yes + if ( nWinningVoteOption != VOTE_OPTION1 ) + { + SendVoteFailedMessage( VOTE_FAILED_YES_MUST_EXCEED_NO ); + bVotePassed = false; + } + } + // GENERAL VOTES: + // We set the details string after the vote, since that's when + // we finally have a parameter to pass along and execute + else if ( nWinningVoteOption < m_VoteOptions.Count() ) + { + m_potentialIssues[m_iActiveIssueIndex]->SetIssueDetails( m_VoteOptions[nWinningVoteOption] ); + } + } + } + else + { + SendVoteFailedMessage( VOTE_FAILED_QUORUM_FAILURE ); + bVotePassed = false; + } + + if ( bVotePassed ) + { + m_executeCommandTimer.Start( sv_vote_command_delay.GetFloat() ); + m_resetVoteTimer.Start( 5.0 ); + + UTIL_LogPrintf("Vote succeeded \"%s %s\"\n", + m_potentialIssues[m_iActiveIssueIndex]->GetTypeString(), + m_potentialIssues[m_iActiveIssueIndex]->GetDetailsString() ); + + CBroadcastRecipientFilter filter; + filter.MakeReliable(); + + UserMessageBegin( filter, "VotePass" ); + WRITE_BYTE( m_iOnlyTeamToVote ); + WRITE_STRING( m_potentialIssues[m_iActiveIssueIndex]->GetVotePassedString() ); + WRITE_STRING( m_potentialIssues[m_iActiveIssueIndex]->GetDetailsString() ); + MessageEnd(); + } + else + { + m_potentialIssues[m_iActiveIssueIndex]->OnVoteFailed( m_iEntityHoldingVote ); + m_resetVoteTimer.Start( 5.0 ); + } + } + + // Vote passed check moved down to FrameUpdatePostEntityThink at bottom of this file... + + if ( m_resetVoteTimer.HasStarted() && m_resetVoteTimer.IsElapsed() ) + { + ResetData(); + m_resetVoteTimer.Invalidate(); + } + + // Size maintenance on m_VoteCallers + if ( m_VoteCallers.Count() >= MAX_VOTER_HISTORY ) + { + // Remove older entries + for ( int iIdx = m_VoteCallers.FirstInorder(); iIdx != m_VoteCallers.InvalidIndex(); iIdx = m_VoteCallers.NextInorder( iIdx ) ) + { + if ( m_VoteCallers[ iIdx ] - gpGlobals->curtime <= 0 ) + { + m_VoteCallers.Remove( iIdx ); + } + } + } + + SetNextThink( gpGlobals->curtime + 0.5f ); +} + +//----------------------------------------------------------------------------- +// Purpose: End the vote early if everyone's voted +//----------------------------------------------------------------------------- +void CVoteController::CheckForEarlyVoteClose( void ) +{ + int nVoteTally = 0; + for ( int index = 0; index < MAX_VOTE_OPTIONS; index++ ) + { + nVoteTally += m_nVoteOptionCount.Get( index ); + } + + if( nVoteTally >= m_nPotentialVotes ) + { + m_acceptingVotesTimer.Start( 0 ); // Run the timer out right now + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CVoteController::IsValidVoter( CBasePlayer *pWhom ) +{ + if ( pWhom == NULL ) + return false; + + if ( !pWhom->IsConnected() ) + return false; + + if ( !sv_vote_allow_spectators.GetBool() ) + { + if ( pWhom->GetTeamNumber() == TEAM_SPECTATOR ) + return false; + } + +#ifndef DEBUG // Don't want to do this check for debug builds (so we can test with bots) + if ( pWhom->IsBot() ) + return false; + + if ( pWhom->IsFakeClient() ) + return false; +#endif // DEBUG + + if ( pWhom->IsHLTV() ) + return false; + + if ( pWhom->IsReplay() ) + return false; + +#ifdef TF_DLL + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( pWhom->GetTeamNumber() != TF_TEAM_PVE_DEFENDERS ) + return false; + } +#endif // TF_DLL + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVoteController::RegisterIssue( CBaseIssue *pszNewIssue ) +{ + m_potentialIssues.AddToTail( pszNewIssue ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CVoteController::ListIssues( CBasePlayer *pForWhom ) +{ + if( !sv_allow_votes.GetBool() ) + return; + + ClientPrint( pForWhom, HUD_PRINTCONSOLE, "---Vote commands---\n" ); + + for( int issueIndex = 0; issueIndex < m_potentialIssues.Count(); ++issueIndex ) + { + CBaseIssue *pCurrentIssue = m_potentialIssues[issueIndex]; + pCurrentIssue->ListIssueDetails( pForWhom ); + } + ClientPrint( pForWhom, HUD_PRINTCONSOLE, "--- End Vote commands---\n" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CVoteController::GetWinningVoteOption( void ) +{ + if ( m_potentialIssues[m_iActiveIssueIndex]->IsYesNoVote() ) + { + return ( m_nVoteOptionCount[VOTE_OPTION1] > m_nVoteOptionCount[VOTE_OPTION2] ) ? VOTE_OPTION1 : VOTE_OPTION2; + } + else + { + CUtlVector pVoteCounts; + + // Which option had the most votes? + // driller: Need to handle ties + int nHighest = m_nVoteOptionCount[0]; + for ( int iIndex = 0; iIndex < m_nVoteOptionCount.Count(); iIndex ++ ) + { + nHighest = ( ( nHighest < m_nVoteOptionCount[iIndex] ) ? m_nVoteOptionCount[iIndex] : nHighest ); + pVoteCounts.AddToTail( m_nVoteOptionCount[iIndex] ); + } + + m_nHighestCountIndex = -1; + for ( int iIndex = 0; iIndex < m_nVoteOptionCount.Count(); iIndex++ ) + { + if ( m_nVoteOptionCount[iIndex] == nHighest ) + { + m_nHighestCountIndex = iIndex; + // henryg: break on first match, not last. this avoids a crash + // if we are all tied at zero and we pick something beyond the + // last vote option. this code really ought to ignore attempts + // to tally votes for options beyond the last valid one! + break; + } + } + + return m_nHighestCountIndex; + } + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Store steamIDs for every player that calls a vote +//----------------------------------------------------------------------------- +void CVoteController::TrackVoteCaller( CBasePlayer *pPlayer ) +{ + if ( !pPlayer ) + return; + + CSteamID steamID; + pPlayer->GetSteamID( &steamID ); + + int iIdx = m_VoteCallers.Find( steamID.ConvertToUint64() ); + if ( iIdx != m_VoteCallers.InvalidIndex() ) + { + // Already being tracked - update timer + m_VoteCallers[ iIdx ] = gpGlobals->curtime + sv_vote_creation_timer.GetInt(); + return; + } + + m_VoteCallers.Insert( steamID.ConvertToUint64(), gpGlobals->curtime + sv_vote_creation_timer.GetInt() ); +}; + +//----------------------------------------------------------------------------- +// Purpose: Check the history of steamIDs that called votes and test against a timer +//----------------------------------------------------------------------------- +bool CVoteController::CanEntityCallVote( CBasePlayer *pPlayer, int &nCooldown ) +{ + if ( !pPlayer ) + return false; + + CSteamID steamID; + pPlayer->GetSteamID( &steamID ); + + // Has this SteamID tried to call a vote recently? + int iIdx = m_VoteCallers.Find( steamID.ConvertToUint64() ); + if ( iIdx != m_VoteCallers.InvalidIndex() ) + { + // Timer elapsed? + nCooldown = (int)( m_VoteCallers[ iIdx ] - gpGlobals->curtime ); + if ( nCooldown > 0 ) + return false; + + // Expired + m_VoteCallers.Remove( iIdx ); + } + + return true; +}; + +//----------------------------------------------------------------------------- +// Purpose: BaseIssue +//----------------------------------------------------------------------------- +CBaseIssue::CBaseIssue( const char *pszTypeString ) +{ + Q_strcpy( m_szTypeString, pszTypeString ); + + m_iNumYesVotes = 0; + m_iNumNoVotes = 0; + m_iNumPotentialVotes = 0; + + ASSERT( g_voteController ); + g_voteController->RegisterIssue( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseIssue::~CBaseIssue() +{ + for ( int index = 0; index < m_FailedVotes.Count(); index++ ) + { + FailedVote *pFailedVote = m_FailedVotes[index]; + delete pFailedVote; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char *CBaseIssue::GetTypeString( void ) +{ + return m_szTypeString; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char *CBaseIssue::GetDetailsString( void ) +{ + return m_szDetailsString; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseIssue::SetIssueDetails( const char *pszDetails ) +{ + Q_strcpy( m_szDetailsString, pszDetails ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseIssue::IsAllyRestrictedVote( void ) +{ + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char *CBaseIssue::GetVotePassedString( void ) +{ + return "Unknown vote passed."; +} + +//----------------------------------------------------------------------------- +// Purpose: Store failures to prevent vote spam +//----------------------------------------------------------------------------- +void CBaseIssue::OnVoteFailed( int iEntityHoldingVote ) +{ + // Don't track failed dedicated server votes + if ( BRecordVoteFailureEventForEntity( iEntityHoldingVote ) ) + { + // Check for an existing match + for ( int index = 0; index < m_FailedVotes.Count(); index++ ) + { + FailedVote *pFailedVote = m_FailedVotes[index]; + if ( Q_strcmp( pFailedVote->szFailedVoteParameter, GetDetailsString() ) == 0 ) + { + int nTime = sv_vote_failure_timer.GetInt(); + +#ifdef TF_DLL + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + nTime = sv_vote_failure_timer_mvm.GetInt(); + } +#endif // TF_DLL + + pFailedVote->flLockoutTime = gpGlobals->curtime + nTime; + + return; + } + } + + // Need to create a new one + FailedVote *pNewFailedVote = new FailedVote; + int iIndex = m_FailedVotes.AddToTail( pNewFailedVote ); + Q_strcpy( m_FailedVotes[iIndex]->szFailedVoteParameter, GetDetailsString() ); + m_FailedVotes[iIndex]->flLockoutTime = gpGlobals->curtime + sv_vote_failure_timer.GetFloat(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseIssue::CanTeamCallVote( int iTeam ) const +{ + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseIssue::CanCallVote( int iEntIndex, const char *pszDetails, vote_create_failed_t &nFailCode, int &nTime ) +{ + // Automated server vote - don't bother testing against it + if ( !BRecordVoteFailureEventForEntity( iEntIndex ) ) + return true; + + // Bogus player + if( iEntIndex == -1 ) + return false; + +#ifdef TF_DLL + if ( TFGameRules() && TFGameRules()->IsInWaitingForPlayers() && !TFGameRules()->IsInTournamentMode() ) + { + nFailCode = VOTE_FAILED_WAITINGFORPLAYERS; + return false; + } +#endif // TF_DLL + + CBaseEntity *pVoteCaller = UTIL_EntityByIndex( iEntIndex ); + if( pVoteCaller && !CanTeamCallVote( GetVoterTeam( pVoteCaller ) ) ) + { + nFailCode = VOTE_FAILED_TEAM_CANT_CALL; + return false; + } + + // Did this fail recently? + for( int iIndex = 0; iIndex < m_FailedVotes.Count(); iIndex++ ) + { + FailedVote *pCurrentFailure = m_FailedVotes[iIndex]; + int nTimeRemaining = pCurrentFailure->flLockoutTime - gpGlobals->curtime; + bool bFailed = false; + + // If this issue requires a parameter, see if we're voting for the same one again (i.e. changelevel ctf_2fort) + if ( Q_strlen( pCurrentFailure->szFailedVoteParameter ) > 0 ) + { + if( nTimeRemaining > 1 && FStrEq( pCurrentFailure->szFailedVoteParameter, pszDetails ) ) + { + bFailed = true; + } + } + // Otherwise we have a parameter-less vote, so just check the lockout timer (i.e. restartgame) + else + { + if( nTimeRemaining > 1 ) + { + bFailed = true; + + } + } + + if ( bFailed ) + { + nFailCode = VOTE_FAILED_FAILED_RECENTLY; + nTime = nTimeRemaining; + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseIssue::CountPotentialVoters( void ) +{ + int nTotalPlayers = 0; + + for( int playerIndex = 1; playerIndex <= MAX_PLAYERS; ++playerIndex ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( playerIndex ); + if( g_voteController->IsValidVoter( pPlayer ) ) + { + if ( g_voteController->CanTeamCastVote( GetVoterTeam( pPlayer ) ) ) + { + nTotalPlayers++; + } + } + } + + return nTotalPlayers; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CBaseIssue::GetNumberVoteOptions( void ) +{ + return 2; // The default issue is Yes/No (so 2), but it can be anywhere between 1 and MAX_VOTE_COUNT +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseIssue::IsYesNoVote( void ) +{ + return true; // Default +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseIssue::SetYesNoVoteCount( int iNumYesVotes, int iNumNoVotes, int iNumPotentialVotes ) +{ + m_iNumYesVotes = iNumYesVotes; + m_iNumNoVotes = iNumNoVotes; + m_iNumPotentialVotes = iNumPotentialVotes; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CBaseIssue::ListStandardNoArgCommand( CBasePlayer *forWhom, const char *issueString ) +{ + ClientPrint( forWhom, HUD_PRINTCONSOLE, "callvote %s1\n", issueString ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CBaseIssue::GetVoteOptions( CUtlVector &vecNames ) +{ + // The default vote issue is a Yes/No vote + vecNames.AddToHead( "Yes" ); + vecNames.AddToTail( "No" ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Game system to detect maps without cameras in them, and move on +//----------------------------------------------------------------------------- +class CVoteControllerSystem : public CAutoGameSystemPerFrame +{ +public: + CVoteControllerSystem( char const *name ) : CAutoGameSystemPerFrame( name ) + { + } + + virtual void LevelInitPreEntity() + { + } + + virtual void FrameUpdatePostEntityThink( void ) + { + // Executing the vote controller command needs to happen in the PostEntityThink as it can restart levels and + // blast entities, etc. If you're doing this during a regular think, this can cause entities thinking after + // you in Physics_RunThinkFunctions() to get grumpy and crash. + if( g_voteController ) + { + // Vote passed - execute the command + if( g_voteController->m_executeCommandTimer.HasStarted() && g_voteController->m_executeCommandTimer.IsElapsed() ) + { + g_voteController->m_executeCommandTimer.Invalidate(); + g_voteController->m_potentialIssues[ g_voteController->m_iActiveIssueIndex ]->ExecuteCommand(); + } + } + } +}; + +CVoteControllerSystem VoteControllerSystem( "CVoteControllerSystem" ); + diff --git a/sp/src/game/server/vote_controller.h b/sp/src/game/server/vote_controller.h new file mode 100644 index 00000000..0376d6a6 --- /dev/null +++ b/sp/src/game/server/vote_controller.h @@ -0,0 +1,130 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Player-driven Voting System for Multiplayer Source games (currently implemented for TF2) +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VOTE_CONTROLLER_H +#define VOTE_CONTROLLER_H + +#ifdef _WIN32 +#pragma once +#endif + +#include "shareddefs.h" + +#define MAX_COMMAND_LENGTH 64 +#define MAX_CREATE_ERROR_STRING 96 + +class CBaseIssue // Abstract base class for all things-that-can-be-voted-on. +{ +public: + CBaseIssue(const char *typeString); + virtual ~CBaseIssue(); + const char *GetTypeString( void ); // Connection between console command and specific type of issue + virtual const char *GetDetailsString(); + virtual void SetIssueDetails( const char *pszDetails ); // We need to know the details part of the con command for later + virtual void OnVoteFailed( int iEntityHoldingVote ); // The moment the vote fails, also has some time for feedback before the window goes away + virtual void OnVoteStarted( void ) {} // Called as soon as the vote starts + virtual bool IsEnabled( void ) { return false; } // Query the issue to see if it's enabled + virtual bool CanTeamCallVote( int iTeam ) const; // Can someone on the given team call this vote? + virtual bool CanCallVote( int nEntIndex, const char *pszDetails, vote_create_failed_t &nFailCode, int &nTime ); // Can this guy hold a vote on this issue? + virtual bool IsAllyRestrictedVote( void ); // Can only members of the same team vote on this? + virtual const char *GetDisplayString( void ) = 0; // The string that will be passed to the client for display + virtual void ExecuteCommand( void ) = 0; // Where the magic happens. Do your thing. + virtual void ListIssueDetails( CBasePlayer *pForWhom ) = 0; // Someone would like to know all your valid details + virtual const char *GetVotePassedString( void ); // Get the string an issue would like to display when it passes. + virtual int CountPotentialVoters( void ); + virtual int GetNumberVoteOptions( void ); // How many choices this vote will have. i.e. Returns 2 on a Yes/No issue (the default). + virtual bool IsYesNoVote( void ); + virtual void SetYesNoVoteCount( int iNumYesVotes, int iNumNoVotes, int iNumPotentialVotes ); + virtual bool GetVoteOptions( CUtlVector &vecNames ); // We use this to generate options for voting + virtual bool BRecordVoteFailureEventForEntity( int iVoteCallingEntityIndex ) const { return iVoteCallingEntityIndex != DEDICATED_SERVER; } + +protected: + static void ListStandardNoArgCommand( CBasePlayer *forWhom, const char *issueString ); // List a Yes vote command + + struct FailedVote + { + char szFailedVoteParameter[MAX_VOTE_DETAILS_LENGTH]; + float flLockoutTime; + }; + + CUtlVector m_FailedVotes; + + char m_szTypeString[MAX_COMMAND_LENGTH]; + char m_szDetailsString[MAX_VOTE_DETAILS_LENGTH]; + + int m_iNumYesVotes; + int m_iNumNoVotes; + int m_iNumPotentialVotes; +}; + +class CVoteController : public CBaseEntity +{ + DECLARE_CLASS( CVoteController, CBaseEntity ); + +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + virtual ~CVoteController(); + + enum TryCastVoteResult + { + CAST_OK, + CAST_FAIL_SERVER_DISABLE, + CAST_FAIL_NO_ACTIVE_ISSUE, + CAST_FAIL_TEAM_RESTRICTED, + CAST_FAIL_NO_CHANGES, + CAST_FAIL_DUPLICATE, + CAST_FAIL_VOTE_CLOSED, + CAST_FAIL_SYSTEM_ERROR + }; + + virtual void Spawn( void ); + virtual int UpdateTransmitState( void ); + + bool SetupVote( int iEntIndex ); // This creates a list of issues for the UI + bool CreateVote( int iEntIndex, const char *pszTypeString, const char *pszDetailString ); // This is what the UI passes in + TryCastVoteResult TryCastVote( int iEntIndex, const char *pszVoteString ); + void RegisterIssue( CBaseIssue *pNewIssue ); + void ListIssues( CBasePlayer *pForWhom ); + bool IsValidVoter( CBasePlayer *pWhom ); + bool CanTeamCastVote( int iTeam ) const; + void SendVoteFailedMessage( vote_create_failed_t nReason = VOTE_FAILED_GENERIC, CBasePlayer *pVoteCaller = NULL, int nTime = -1 ); + void VoteChoice_Increment( int nVoteChoice ); + void VoteChoice_Decrement( int nVoteChoice ); + int GetWinningVoteOption( void ); + void TrackVoteCaller( CBasePlayer *pPlayer ); + bool CanEntityCallVote( CBasePlayer *pPlayer, int &nCooldown ); + bool IsVoteActive( void ) { return m_iActiveIssueIndex != INVALID_ISSUE; } + +protected: + void ResetData( void ); + void VoteControllerThink( void ); + void CheckForEarlyVoteClose( void ); // If everyone has voted (and changing votes is not allowed) then end early + + CNetworkVar( int, m_iActiveIssueIndex ); // Type of thing being voted on + CNetworkVar( int, m_iOnlyTeamToVote ); // If an Ally restricted vote, the team number that is allowed to vote + CNetworkArray( int, m_nVoteOptionCount, MAX_VOTE_OPTIONS ); // Vote options counter + CNetworkVar( int, m_nPotentialVotes ); // How many votes could come in, so we can close ballot early + CNetworkVar( bool, m_bIsYesNoVote ); // Is the current issue Yes/No? + CountdownTimer m_acceptingVotesTimer; // How long from vote start until we count the ballots + CountdownTimer m_executeCommandTimer; // How long after end of vote time until we execute a passed vote + CountdownTimer m_resetVoteTimer; // when the current vote will end + int m_nVotesCast[MAX_PLAYERS + 1]; // arrays are zero-based and player indices are one-based + int m_iEntityHoldingVote; + int m_nHighestCountIndex; + + CUtlVector m_potentialIssues; + CUtlVector m_VoteOptions; + CUtlMap m_VoteCallers; // History of SteamIDs that have tried to call votes. + + friend class CVoteControllerSystem; +}; + +extern CVoteController *g_voteController; + +#endif // VOTE_CONTROLLER_H diff --git a/sp/src/game/server/vscript_server.cpp b/sp/src/game/server/vscript_server.cpp new file mode 100644 index 00000000..6927b0f1 --- /dev/null +++ b/sp/src/game/server/vscript_server.cpp @@ -0,0 +1,781 @@ +//========== Copyright © 2008, Valve Corporation, All rights reserved. ======== +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "vscript_server.h" +#include "icommandline.h" +#include "tier1/utlbuffer.h" +#include "tier1/fmtstr.h" +#include "filesystem.h" +#include "eventqueue.h" +#include "characterset.h" +#include "sceneentity.h" // for exposing scene precache function +#include "gamerules.h" +#include "vscript_server.nut" +#ifdef MAPBASE_VSCRIPT +#include "world.h" +#include "mapbase/vscript_singletons.h" +#endif + +extern ScriptClassDesc_t * GetScriptDesc( CBaseEntity * ); + +// #define VMPROFILE 1 + +#ifdef VMPROFILE + +#define VMPROF_START float debugStartTime = Plat_FloatTime(); +#define VMPROF_SHOW( funcname, funcdesc ) DevMsg("***VSCRIPT PROFILE***: %s %s: %6.4f milliseconds\n", (##funcname), (##funcdesc), (Plat_FloatTime() - debugStartTime)*1000.0 ); + +#else // !VMPROFILE + +#define VMPROF_START +#define VMPROF_SHOW + +#endif // VMPROFILE + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +class CScriptEntityIterator +{ +public: +#ifdef MAPBASE_VSCRIPT + HSCRIPT GetLocalPlayer() + { + return ToHScript( UTIL_GetLocalPlayerOrListenServerHost() ); + } +#endif + HSCRIPT First() { return Next(NULL); } + + HSCRIPT Next( HSCRIPT hStartEntity ) + { + return ToHScript( gEntList.NextEnt( ToEnt( hStartEntity ) ) ); + } + + HSCRIPT CreateByClassname( const char *className ) + { + return ToHScript( CreateEntityByName( className ) ); + } + + HSCRIPT FindByClassname( HSCRIPT hStartEntity, const char *szName ) + { + return ToHScript( gEntList.FindEntityByClassname( ToEnt( hStartEntity ), szName ) ); + } + + HSCRIPT FindByName( HSCRIPT hStartEntity, const char *szName ) + { + return ToHScript( gEntList.FindEntityByName( ToEnt( hStartEntity ), szName ) ); + } + + HSCRIPT FindInSphere( HSCRIPT hStartEntity, const Vector &vecCenter, float flRadius ) + { + return ToHScript( gEntList.FindEntityInSphere( ToEnt( hStartEntity ), vecCenter, flRadius ) ); + } + + HSCRIPT FindByTarget( HSCRIPT hStartEntity, const char *szName ) + { + return ToHScript( gEntList.FindEntityByTarget( ToEnt( hStartEntity ), szName ) ); + } + + HSCRIPT FindByModel( HSCRIPT hStartEntity, const char *szModelName ) + { + return ToHScript( gEntList.FindEntityByModel( ToEnt( hStartEntity ), szModelName ) ); + } + + HSCRIPT FindByNameNearest( const char *szName, const Vector &vecSrc, float flRadius ) + { + return ToHScript( gEntList.FindEntityByNameNearest( szName, vecSrc, flRadius ) ); + } + + HSCRIPT FindByNameWithin( HSCRIPT hStartEntity, const char *szName, const Vector &vecSrc, float flRadius ) + { + return ToHScript( gEntList.FindEntityByNameWithin( ToEnt( hStartEntity ), szName, vecSrc, flRadius ) ); + } + + HSCRIPT FindByClassnameNearest( const char *szName, const Vector &vecSrc, float flRadius ) + { + return ToHScript( gEntList.FindEntityByClassnameNearest( szName, vecSrc, flRadius ) ); + } + + HSCRIPT FindByClassnameWithin( HSCRIPT hStartEntity , const char *szName, const Vector &vecSrc, float flRadius ) + { + return ToHScript( gEntList.FindEntityByClassnameWithin( ToEnt( hStartEntity ), szName, vecSrc, flRadius ) ); + } +#ifdef MAPBASE_VSCRIPT + HSCRIPT FindByClassnameWithinBox( HSCRIPT hStartEntity , const char *szName, const Vector &vecMins, const Vector &vecMaxs ) + { + return ToHScript( gEntList.FindEntityByClassnameWithin( ToEnt( hStartEntity ), szName, vecMins, vecMaxs ) ); + } + + HSCRIPT FindByClassNearestFacing( const Vector &origin, const Vector &facing, float threshold, const char *classname ) + { + return ToHScript( gEntList.FindEntityClassNearestFacing( origin, facing, threshold, const_cast(classname) ) ); + } +#endif +private: +} g_ScriptEntityIterator; + +BEGIN_SCRIPTDESC_ROOT_NAMED( CScriptEntityIterator, "CEntities", SCRIPT_SINGLETON "The global list of entities" ) +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC( GetLocalPlayer, "Get local player or listen server host" ) +#endif + DEFINE_SCRIPTFUNC( First, "Begin an iteration over the list of entities" ) + DEFINE_SCRIPTFUNC( Next, "Continue an iteration over the list of entities, providing reference to a previously found entity" ) + DEFINE_SCRIPTFUNC( CreateByClassname, "Creates an entity by classname" ) + DEFINE_SCRIPTFUNC( FindByClassname, "Find entities by class name. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) + DEFINE_SCRIPTFUNC( FindByName, "Find entities by name. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) + DEFINE_SCRIPTFUNC( FindInSphere, "Find entities within a radius. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) + DEFINE_SCRIPTFUNC( FindByTarget, "Find entities by targetname. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) + DEFINE_SCRIPTFUNC( FindByModel, "Find entities by model name. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) + DEFINE_SCRIPTFUNC( FindByNameNearest, "Find entities by name nearest to a point." ) + DEFINE_SCRIPTFUNC( FindByNameWithin, "Find entities by name within a radius. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) + DEFINE_SCRIPTFUNC( FindByClassnameNearest, "Find entities by class name nearest to a point." ) + DEFINE_SCRIPTFUNC( FindByClassnameWithin, "Find entities by class name within a radius. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) +#ifdef MAPBASE_VSCRIPT + DEFINE_SCRIPTFUNC( FindByClassnameWithinBox, "Find entities by class name within an AABB. Pass 'null' to start an iteration, or reference to a previously found entity to continue a search" ) + DEFINE_SCRIPTFUNC( FindByClassNearestFacing, "Find the nearest entity along the facing direction from the given origin within the angular threshold with the given classname." ) +#endif +END_SCRIPTDESC(); + +#ifndef MAPBASE_VSCRIPT // Mapbase adds this to the base library so that CScriptKeyValues can be accessed anywhere, like VBSP. +// ---------------------------------------------------------------------------- +// KeyValues access - CBaseEntity::ScriptGetKeyFromModel returns root KeyValues +// ---------------------------------------------------------------------------- + +BEGIN_SCRIPTDESC_ROOT( CScriptKeyValues, "Wrapper class over KeyValues instance" ) + DEFINE_SCRIPT_CONSTRUCTOR() + DEFINE_SCRIPTFUNC_NAMED( ScriptFindKey, "FindKey", "Given a KeyValues object and a key name, find a KeyValues object associated with the key name" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptGetFirstSubKey, "GetFirstSubKey", "Given a KeyValues object, return the first sub key object" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptGetNextKey, "GetNextKey", "Given a KeyValues object, return the next key object in a sub key group" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptGetKeyValueInt, "GetKeyInt", "Given a KeyValues object and a key name, return associated integer value" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptGetKeyValueFloat, "GetKeyFloat", "Given a KeyValues object and a key name, return associated float value" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptGetKeyValueBool, "GetKeyBool", "Given a KeyValues object and a key name, return associated bool value" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptGetKeyValueString, "GetKeyString", "Given a KeyValues object and a key name, return associated string value" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptIsKeyValueEmpty, "IsKeyEmpty", "Given a KeyValues object and a key name, return true if key name has no value" ); + DEFINE_SCRIPTFUNC_NAMED( ScriptReleaseKeyValues, "ReleaseKeyValues", "Given a root KeyValues object, release its contents" ); +END_SCRIPTDESC(); + +HSCRIPT CScriptKeyValues::ScriptFindKey( const char *pszName ) +{ + KeyValues *pKeyValues = m_pKeyValues->FindKey(pszName); + if ( pKeyValues == NULL ) + return NULL; + + CScriptKeyValues *pScriptKey = new CScriptKeyValues( pKeyValues ); + + // UNDONE: who calls ReleaseInstance on this?? + HSCRIPT hScriptInstance = g_pScriptVM->RegisterInstance( pScriptKey ); + return hScriptInstance; +} + +HSCRIPT CScriptKeyValues::ScriptGetFirstSubKey( void ) +{ + KeyValues *pKeyValues = m_pKeyValues->GetFirstSubKey(); + if ( pKeyValues == NULL ) + return NULL; + + CScriptKeyValues *pScriptKey = new CScriptKeyValues( pKeyValues ); + + // UNDONE: who calls ReleaseInstance on this?? + HSCRIPT hScriptInstance = g_pScriptVM->RegisterInstance( pScriptKey ); + return hScriptInstance; +} + +HSCRIPT CScriptKeyValues::ScriptGetNextKey( void ) +{ + KeyValues *pKeyValues = m_pKeyValues->GetNextKey(); + if ( pKeyValues == NULL ) + return NULL; + + CScriptKeyValues *pScriptKey = new CScriptKeyValues( pKeyValues ); + + // UNDONE: who calls ReleaseInstance on this?? + HSCRIPT hScriptInstance = g_pScriptVM->RegisterInstance( pScriptKey ); + return hScriptInstance; +} + +int CScriptKeyValues::ScriptGetKeyValueInt( const char *pszName ) +{ + int i = m_pKeyValues->GetInt( pszName ); + return i; +} + +float CScriptKeyValues::ScriptGetKeyValueFloat( const char *pszName ) +{ + float f = m_pKeyValues->GetFloat( pszName ); + return f; +} + +const char *CScriptKeyValues::ScriptGetKeyValueString( const char *pszName ) +{ + const char *psz = m_pKeyValues->GetString( pszName ); + return psz; +} + +bool CScriptKeyValues::ScriptIsKeyValueEmpty( const char *pszName ) +{ + bool b = m_pKeyValues->IsEmpty( pszName ); + return b; +} + +bool CScriptKeyValues::ScriptGetKeyValueBool( const char *pszName ) +{ + bool b = m_pKeyValues->GetBool( pszName ); + return b; +} + +void CScriptKeyValues::ScriptReleaseKeyValues( ) +{ + m_pKeyValues->deleteThis(); + m_pKeyValues = NULL; +} + + +// constructors +CScriptKeyValues::CScriptKeyValues( KeyValues *pKeyValues = NULL ) +{ + m_pKeyValues = pKeyValues; +} + +// destructor +CScriptKeyValues::~CScriptKeyValues( ) +{ + if (m_pKeyValues) + { + m_pKeyValues->deleteThis(); + } + m_pKeyValues = NULL; +} +#endif + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +static float Time() +{ + return gpGlobals->curtime; +} + +static float FrameTime() +{ + return gpGlobals->frametime; +} + +#ifdef MAPBASE_VSCRIPT +static int MaxPlayers() +{ + return gpGlobals->maxClients; +} + +static int GetLoadType() +{ + return gpGlobals->eLoadType; +} +#endif + +static void SendToConsole( const char *pszCommand ) +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayerOrListenServerHost(); + if ( !pPlayer ) + { +#ifdef MAPBASE + CGMsg( 1, CON_GROUP_VSCRIPT, "Cannot execute \"%s\", no player\n", pszCommand ); +#else + DevMsg ("Cannot execute \"%s\", no player\n", pszCommand ); +#endif + return; + } + + engine->ClientCommand( pPlayer->edict(), pszCommand ); +} + +static void SendToConsoleServer( const char *pszCommand ) +{ + // TODO: whitelist for multiplayer + engine->ServerCommand( UTIL_VarArgs("%s\n", pszCommand) ); +} + +static const char *GetMapName() +{ + return STRING( gpGlobals->mapname ); +} + +static const char *DoUniqueString( const char *pszBase ) +{ + static char szBuf[512]; + g_pScriptVM->GenerateUniqueKey( pszBase, szBuf, ARRAYSIZE(szBuf) ); + return szBuf; +} + +#ifdef MAPBASE_VSCRIPT +static int DoEntFire( const char *pszTarget, const char *pszAction, const char *pszValue, float delay, HSCRIPT hActivator, HSCRIPT hCaller ) +#else +static void DoEntFire( const char *pszTarget, const char *pszAction, const char *pszValue, float delay, HSCRIPT hActivator, HSCRIPT hCaller ) +#endif +{ + const char *target = "", *action = "Use"; + variant_t value; + + target = STRING( AllocPooledString( pszTarget ) ); + + // Don't allow them to run anything on a point_servercommand unless they're the host player. Otherwise they can ent_fire + // and run any command on the server. Admittedly, they can only do the ent_fire if sv_cheats is on, but + // people complained about users resetting the rcon password if the server briefly turned on cheats like this: + // give point_servercommand + // ent_fire point_servercommand command "rcon_password mynewpassword" + if ( gpGlobals->maxClients > 1 && V_stricmp( target, "point_servercommand" ) == 0 ) + { +#ifdef MAPBASE_VSCRIPT + return 0; +#else + return; +#endif + } + + if ( *pszAction ) + { + action = STRING( AllocPooledString( pszAction ) ); + } + if ( *pszValue ) + { + value.SetString( AllocPooledString( pszValue ) ); + } + if ( delay < 0 ) + { + delay = 0; + } + +#ifdef MAPBASE_VSCRIPT + return +#endif + g_EventQueue.AddEvent( target, action, value, delay, ToEnt(hActivator), ToEnt(hCaller) ); +} + + +bool DoIncludeScript( const char *pszScript, HSCRIPT hScope ) +{ + if ( !VScriptRunScript( pszScript, hScope, true ) ) + { + g_pScriptVM->RaiseException( CFmtStr( "Failed to include script \"%s\"", ( pszScript ) ? pszScript : "unknown" ) ); + return false; + } + return true; +} + +HSCRIPT CreateProp( const char *pszEntityName, const Vector &vOrigin, const char *pszModelName, int iAnim ) +{ + CBaseAnimating *pBaseEntity = (CBaseAnimating *)CreateEntityByName( pszEntityName ); + pBaseEntity->SetAbsOrigin( vOrigin ); + pBaseEntity->SetModel( pszModelName ); + pBaseEntity->SetPlaybackRate( 1.0f ); + + int iSequence = pBaseEntity->SelectWeightedSequence( (Activity)iAnim ); + + if ( iSequence != -1 ) + { + pBaseEntity->SetSequence( iSequence ); + } + + return ToHScript( pBaseEntity ); +} + +//-------------------------------------------------------------------------------------------------- +// Use an entity's script instance to add an entity IO event (used for firing events on unnamed entities from vscript) +//-------------------------------------------------------------------------------------------------- +#ifdef MAPBASE_VSCRIPT +static int DoEntFireByInstanceHandle( HSCRIPT hTarget, const char *pszAction, const char *pszValue, float delay, HSCRIPT hActivator, HSCRIPT hCaller ) +#else +static void DoEntFireByInstanceHandle( HSCRIPT hTarget, const char *pszAction, const char *pszValue, float delay, HSCRIPT hActivator, HSCRIPT hCaller ) +#endif +{ + const char *action = "Use"; + variant_t value; + + if ( *pszAction ) + { + action = STRING( AllocPooledString( pszAction ) ); + } + if ( *pszValue ) + { + value.SetString( AllocPooledString( pszValue ) ); + } + if ( delay < 0 ) + { + delay = 0; + } + + CBaseEntity* pTarget = ToEnt(hTarget); + + if ( !pTarget ) + { + CGWarning( 0, CON_GROUP_VSCRIPT, "VScript error: DoEntFire was passed an invalid entity instance.\n" ); +#ifdef MAPBASE_VSCRIPT + return 0; +#else + return; +#endif + } + +#ifdef MAPBASE_VSCRIPT + return +#endif + g_EventQueue.AddEvent( pTarget, action, value, delay, ToEnt(hActivator), ToEnt(hCaller) ); +} + +static float ScriptTraceLine( const Vector &vecStart, const Vector &vecEnd, HSCRIPT entIgnore ) +{ + // UTIL_TraceLine( vecAbsStart, vecAbsEnd, MASK_BLOCKLOS, pLooker, COLLISION_GROUP_NONE, ptr ); + trace_t tr; + CBaseEntity *pLooker = ToEnt(entIgnore); + UTIL_TraceLine( vecStart, vecEnd, MASK_NPCWORLDSTATIC, pLooker, COLLISION_GROUP_NONE, &tr); + if (tr.fractionleftsolid && tr.startsolid) + { + return 1.0 - tr.fractionleftsolid; + } + else + { + return tr.fraction; + } +} + +#ifdef MAPBASE_VSCRIPT +static bool CancelEntityIOEvent( int event ) +{ + return g_EventQueue.RemoveEvent(event); +} + +static float GetEntityIOEventTimeLeft( int event ) +{ + return g_EventQueue.GetTimeLeft(event); +} +#endif // MAPBASE_VSCRIPT + +bool VScriptServerInit() +{ + VMPROF_START + + if( scriptmanager != NULL ) + { + ScriptLanguage_t scriptLanguage = SL_DEFAULT; + + char const *pszScriptLanguage; +#ifdef MAPBASE_VSCRIPT + if (GetWorldEntity()->GetScriptLanguage() != SL_NONE) + { + // Allow world entity to override script language + scriptLanguage = GetWorldEntity()->GetScriptLanguage(); + + // Less than SL_NONE means the script language should literally be none + if (scriptLanguage < SL_NONE) + scriptLanguage = SL_NONE; + } + else +#endif + if ( CommandLine()->CheckParm( "-scriptlang", &pszScriptLanguage ) ) + { + if( !Q_stricmp(pszScriptLanguage, "gamemonkey") ) + { + scriptLanguage = SL_GAMEMONKEY; + } + else if( !Q_stricmp(pszScriptLanguage, "squirrel") ) + { + scriptLanguage = SL_SQUIRREL; + } + else if( !Q_stricmp(pszScriptLanguage, "python") ) + { + scriptLanguage = SL_PYTHON; + } +#ifdef MAPBASE_VSCRIPT + else if( !Q_stricmp(pszScriptLanguage, "lua") ) + { + scriptLanguage = SL_LUA; + } +#endif + else + { + CGWarning( 1, CON_GROUP_VSCRIPT, "-server_script does not recognize a language named '%s'. virtual machine did NOT start.\n", pszScriptLanguage ); + scriptLanguage = SL_NONE; + } + + } + if( scriptLanguage != SL_NONE ) + { + if ( g_pScriptVM == NULL ) + g_pScriptVM = scriptmanager->CreateVM( scriptLanguage ); + + if( g_pScriptVM ) + { +#ifdef MAPBASE_VSCRIPT + CGMsg( 0, CON_GROUP_VSCRIPT, "VSCRIPT SERVER: Started VScript virtual machine using script language '%s'\n", g_pScriptVM->GetLanguageName() ); +#else + Log( "VSCRIPT: Started VScript virtual machine using script language '%s'\n", g_pScriptVM->GetLanguageName() ); +#endif + +#ifdef MAPBASE_VSCRIPT + // MULTIPLAYER + // ScriptRegisterFunctionNamed( g_pScriptVM, UTIL_PlayerByIndex, "GetPlayerByIndex", "PlayerInstanceFromIndex" ); + // ScriptRegisterFunctionNamed( g_pScriptVM, UTIL_PlayerByUserId, "GetPlayerByUserId", "GetPlayerFromUserID" ); + // ScriptRegisterFunctionNamed( g_pScriptVM, UTIL_PlayerByName, "GetPlayerByName", "" ); + // ScriptRegisterFunctionNamed( g_pScriptVM, ScriptGetPlayerByNetworkID, "GetPlayerByNetworkID", "" ); + + ScriptRegisterFunctionNamed( g_pScriptVM, UTIL_ShowMessageAll, "ShowMessage", "Print a hud message on all clients" ); +#else + ScriptRegisterFunctionNamed( g_pScriptVM, UTIL_ShowMessageAll, "ShowMessage", "Print a hud message on all clients" ); +#endif + + ScriptRegisterFunction( g_pScriptVM, SendToConsole, "Send a string to the console as a command" ); + ScriptRegisterFunction( g_pScriptVM, GetMapName, "Get the name of the map."); + ScriptRegisterFunctionNamed( g_pScriptVM, ScriptTraceLine, "TraceLine", "given 2 points & ent to ignore, return fraction along line that hits world or models" ); + + ScriptRegisterFunction( g_pScriptVM, Time, "Get the current server time" ); + ScriptRegisterFunction( g_pScriptVM, FrameTime, "Get the time spent on the server in the last frame" ); +#ifdef MAPBASE_VSCRIPT + ScriptRegisterFunction( g_pScriptVM, SendToConsoleServer, "Send a string to the server console as a command" ); + ScriptRegisterFunction( g_pScriptVM, MaxPlayers, "Get the maximum number of players allowed on this server" ); + ScriptRegisterFunction( g_pScriptVM, GetLoadType, "Get the way the current game was loaded (corresponds to the MapLoad enum)" ); + ScriptRegisterFunction( g_pScriptVM, DoEntFire, SCRIPT_ALIAS( "EntFire", "Generate an entity i/o event" ) ); + ScriptRegisterFunction( g_pScriptVM, DoEntFireByInstanceHandle, SCRIPT_ALIAS( "EntFireByHandle", "Generate an entity i/o event. First parameter is an entity instance." ) ); + // ScriptRegisterFunction( g_pScriptVM, IsValidEntity, "Returns true if the entity is valid." ); + + ScriptRegisterFunction( g_pScriptVM, CancelEntityIOEvent, "Remove entity I/O event." ); + ScriptRegisterFunction( g_pScriptVM, GetEntityIOEventTimeLeft, "Get time left on entity I/O event." ); +#else + ScriptRegisterFunction( g_pScriptVM, DoEntFire, SCRIPT_ALIAS( "EntFire", "Generate and entity i/o event" ) ); + ScriptRegisterFunctionNamed( g_pScriptVM, DoEntFireByInstanceHandle, "EntFireByHandle", "Generate and entity i/o event. First parameter is an entity instance." ); +#endif + ScriptRegisterFunction( g_pScriptVM, DoUniqueString, SCRIPT_ALIAS( "UniqueString", "Generate a string guaranteed to be unique across the life of the script VM, with an optional root string. Useful for adding data to tables when not sure what keys are already in use in that table." ) ); + ScriptRegisterFunctionNamed( g_pScriptVM, ScriptCreateSceneEntity, "CreateSceneEntity", "Create a scene entity to play the specified scene." ); +#ifndef MAPBASE_VSCRIPT + ScriptRegisterFunctionNamed( g_pScriptVM, NDebugOverlay::Box, "DebugDrawBox", "Draw a debug overlay box" ); + ScriptRegisterFunctionNamed( g_pScriptVM, NDebugOverlay::Line, "DebugDrawLine", "Draw a debug overlay box" ); +#endif + ScriptRegisterFunction( g_pScriptVM, DoIncludeScript, "Execute a script (internal)" ); + ScriptRegisterFunction( g_pScriptVM, CreateProp, "Create a physics prop" ); + + if ( GameRules() ) + { + GameRules()->RegisterScriptFunctions(); + } + + g_pScriptVM->RegisterInstance( &g_ScriptEntityIterator, "Entities" ); + + +#ifdef MAPBASE_VSCRIPT + g_pScriptVM->RegisterAllClasses(); + g_pScriptVM->RegisterAllEnums(); + + IGameSystem::RegisterVScriptAllSystems(); + + RegisterSharedScriptConstants(); + RegisterSharedScriptFunctions(); +#endif + + if (scriptLanguage == SL_SQUIRREL) + { + g_pScriptVM->Run( g_Script_vscript_server ); + } + + VScriptRunScript( "vscript_server", true ); + VScriptRunScript( "mapspawn", false ); + +#ifdef MAPBASE_VSCRIPT + // Since the world entity spawns before VScript is initted, RunVScripts() is called before the VM has started, so no scripts are run. + // This gets around that by calling the same function right after the VM is initted. + GetWorldEntity()->RunVScripts(); +#endif + + VMPROF_SHOW( pszScriptLanguage, "virtual machine startup" ); + + return true; + } + else + { + CGWarning( 1, CON_GROUP_VSCRIPT, "VM Did not start!\n" ); + } + } +#ifdef MAPBASE_VSCRIPT + else + { + CGMsg( 0, CON_GROUP_VSCRIPT, "VSCRIPT SERVER: Not starting because language is set to 'none'\n" ); + } +#endif + } + else + { + CGMsg( 0, CON_GROUP_VSCRIPT, "\nVSCRIPT: Scripting is disabled.\n" ); + } + g_pScriptVM = NULL; + return false; +} + +void VScriptServerTerm() +{ + if( g_pScriptVM != NULL ) + { + if( g_pScriptVM ) + { + scriptmanager->DestroyVM( g_pScriptVM ); + g_pScriptVM = NULL; + } + } +} + + +bool VScriptServerReplaceClosures( const char *pszScriptName, HSCRIPT hScope, bool bWarnMissing ) +{ + if ( !g_pScriptVM ) + { + return false; + } + + HSCRIPT hReplaceClosuresFunc = g_pScriptVM->LookupFunction( "__ReplaceClosures" ); + if ( !hReplaceClosuresFunc ) + { + return false; + } + HSCRIPT hNewScript = VScriptCompileScript( pszScriptName, bWarnMissing ); + if ( !hNewScript ) + { + return false; + } + + g_pScriptVM->Call( hReplaceClosuresFunc, NULL, true, NULL, hNewScript, hScope ); + return true; +} + +CON_COMMAND( script_reload_code, "Execute a vscript file, replacing existing functions with the functions in the run script" ) +{ + if ( !*args[1] ) + { + CGWarning( 0, CON_GROUP_VSCRIPT, "No script specified\n" ); + return; + } + + if ( !g_pScriptVM ) + { + CGWarning( 0, CON_GROUP_VSCRIPT, "Scripting disabled or no server running\n" ); + return; + } + + VScriptServerReplaceClosures( args[1], NULL, true ); +} + +CON_COMMAND( script_reload_entity_code, "Execute all of this entity's VScripts, replacing existing functions with the functions in the run scripts" ) +{ + extern CBaseEntity *GetNextCommandEntity( CBasePlayer *pPlayer, const char *name, CBaseEntity *ent ); + + const char *pszTarget = ""; + if ( *args[1] ) + { + pszTarget = args[1]; + } + + if ( !g_pScriptVM ) + { + CGWarning( 0, CON_GROUP_VSCRIPT, "Scripting disabled or no server running\n" ); + return; + } + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( !pPlayer ) + return; + + CBaseEntity *pEntity = NULL; + while ( (pEntity = GetNextCommandEntity( pPlayer, pszTarget, pEntity )) != NULL ) + { + if ( pEntity->m_ScriptScope.IsInitialized() && pEntity->m_iszVScripts != NULL_STRING ) + { + char szScriptsList[255]; + Q_strcpy( szScriptsList, STRING(pEntity->m_iszVScripts) ); + CUtlStringList szScripts; + V_SplitString( szScriptsList, " ", szScripts); + + for( int i = 0 ; i < szScripts.Count() ; i++ ) + { + VScriptServerReplaceClosures( szScripts[i], pEntity->m_ScriptScope, true ); + } + } + } +} + +CON_COMMAND( script_reload_think, "Execute an activation script, replacing existing functions with the functions in the run script" ) +{ + extern CBaseEntity *GetNextCommandEntity( CBasePlayer *pPlayer, const char *name, CBaseEntity *ent ); + + const char *pszTarget = ""; + if ( *args[1] ) + { + pszTarget = args[1]; + } + + if ( !g_pScriptVM ) + { + CGWarning( 0, CON_GROUP_VSCRIPT, "Scripting disabled or no server running\n" ); + return; + } + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + if ( !pPlayer ) + return; + + CBaseEntity *pEntity = NULL; + while ( (pEntity = GetNextCommandEntity( pPlayer, pszTarget, pEntity )) != NULL ) + { + if ( pEntity->m_ScriptScope.IsInitialized() && pEntity->m_iszScriptThinkFunction != NULL_STRING ) + { + VScriptServerReplaceClosures( STRING(pEntity->m_iszScriptThinkFunction), pEntity->m_ScriptScope, true ); + } + } +} + +class CVScriptGameSystem : public CAutoGameSystemPerFrame +{ +public: + // Inherited from IAutoServerSystem + virtual void LevelInitPreEntity( void ) + { + m_bAllowEntityCreationInScripts = true; + VScriptServerInit(); + } + + virtual void LevelInitPostEntity( void ) + { + m_bAllowEntityCreationInScripts = false; + } + + virtual void LevelShutdownPostEntity( void ) + { +#ifdef MAPBASE_VSCRIPT + g_ScriptNetMsg->LevelShutdownPreVM(); +#endif + VScriptServerTerm(); + } + + virtual void FrameUpdatePostEntityThink() + { + if ( g_pScriptVM ) + g_pScriptVM->Frame( gpGlobals->frametime ); + } + + bool m_bAllowEntityCreationInScripts; +}; + +CVScriptGameSystem g_VScriptGameSystem; + +#ifdef MAPBASE_VSCRIPT +ConVar script_allow_entity_creation_midgame( "script_allow_entity_creation_midgame", "1", FCVAR_NOT_CONNECTED, "Allows VScript files to create entities mid-game, as opposed to only creating entities on startup." ); +#endif + +bool IsEntityCreationAllowedInScripts( void ) +{ +#ifdef MAPBASE_VSCRIPT + if (script_allow_entity_creation_midgame.GetBool()) + return true; +#endif + + return g_VScriptGameSystem.m_bAllowEntityCreationInScripts; +} diff --git a/sp/src/game/server/vscript_server.h b/sp/src/game/server/vscript_server.h new file mode 100644 index 00000000..2808f3eb --- /dev/null +++ b/sp/src/game/server/vscript_server.h @@ -0,0 +1,47 @@ +//========== Copyright © 2008, Valve Corporation, All rights reserved. ======== +// +// Purpose: +// +//============================================================================= + +#ifndef VSCRIPT_SERVER_H +#define VSCRIPT_SERVER_H + +#include "vscript/ivscript.h" +#include "tier1/KeyValues.h" +#include "vscript_shared.h" + +#if defined( _WIN32 ) +#pragma once +#endif + +bool VScriptServerReplaceClosures( const char *pszScriptName, HSCRIPT hScope, bool bWarnMissing = false ); + +// Only allow scripts to create entities during map initialization +bool IsEntityCreationAllowedInScripts( void ); + +#ifndef MAPBASE_VSCRIPT // Mapbase adds this to the base library so that CScriptKeyValues can be accessed anywhere, like VBSP. +// ---------------------------------------------------------------------------- +// KeyValues access +// ---------------------------------------------------------------------------- +class CScriptKeyValues +{ +public: + CScriptKeyValues( KeyValues *pKeyValues ); + ~CScriptKeyValues( ); + + HSCRIPT ScriptFindKey( const char *pszName ); + HSCRIPT ScriptGetFirstSubKey( void ); + HSCRIPT ScriptGetNextKey( void ); + int ScriptGetKeyValueInt( const char *pszName ); + float ScriptGetKeyValueFloat( const char *pszName ); + const char *ScriptGetKeyValueString( const char *pszName ); + bool ScriptIsKeyValueEmpty( const char *pszName ); + bool ScriptGetKeyValueBool( const char *pszName ); + void ScriptReleaseKeyValues( ); + + KeyValues *m_pKeyValues; // actual KeyValue entity +}; +#endif + +#endif // VSCRIPT_SERVER_H diff --git a/sp/src/game/server/vscript_server.nut b/sp/src/game/server/vscript_server.nut new file mode 100644 index 00000000..deeacf5d --- /dev/null +++ b/sp/src/game/server/vscript_server.nut @@ -0,0 +1,167 @@ +static char g_Script_vscript_server[] = R"vscript( +//========== Copyright © 2008, Valve Corporation, All rights reserved. ======== +// +// Purpose: +// +//============================================================================= + +local DoEntFire = DoEntFire +local DoEntFireByInstanceHandle = DoEntFireByInstanceHandle +local DoDispatchParticleEffect = DoDispatchParticleEffect +local DoUniqueString = DoUniqueString + +function UniqueString( string = "" ) +{ + return DoUniqueString( "" + string ); +} + +function EntFire( target, action, value = null, delay = 0.0, activator = null, caller = null ) +{ + if ( !value ) + { + value = ""; + } + + if ( "self" in this ) + { + if ( !caller ) + { + caller = self; + } + + if ( !activator ) + { + activator = self; + } + } + + return DoEntFire( "" + target, "" + action, "" + value, delay, activator, caller ); +} + +function EntFireByHandle( target, action, value = null, delay = 0.0, activator = null, caller = null ) +{ + if ( !value ) + { + value = ""; + } + + if ( "self" in this ) + { + if ( !caller ) + { + caller = self; + } + + if ( !activator ) + { + activator = self; + } + } + + return DoEntFireByInstanceHandle( target, "" + action, "" + value, delay, activator, caller ); +} + +function DispatchParticleEffect( particleName, origin, angles, entity = null ) +{ + DoDispatchParticleEffect( particleName, origin, angles, entity ); +} + +__Documentation.RegisterHelp( "CConvars::GetClientConvarValue", "CConvars::GetClientConvarValue(string, int)", "Returns the convar value for the entindex as a string. Only works with client convars with the FCVAR_USERINFO flag." ); + +function __ReplaceClosures( script, scope ) +{ + if ( !scope ) + { + scope = getroottable(); + } + + local tempParent = { getroottable = function() { return null; } }; + local temp = { runscript = script }; + temp.set_delegate(tempParent); + + temp.runscript() + foreach( key,val in temp ) + { + if ( typeof(val) == "function" && key != "runscript" ) + { + printl( " Replacing " + key ); + scope[key] <- val; + } + } +} + +local __OutputsPattern = regexp("^On.*Output$"); + +function ConnectOutputs( table ) +{ + local nCharsToStrip = 6; + foreach( key, val in table ) + { + if ( typeof( val ) == "function" && __OutputsPattern.match( key ) ) + { + //printl(key.slice( 0, nCharsToStrip ) ); + table.self.ConnectOutput( key.slice( 0, key.len() - nCharsToStrip ), key ); + } + } +} + +function IncludeScript( name, scope = null ) +{ + if ( !scope ) + { + scope = this; + } + return ::DoIncludeScript( name, scope ); +} + +//--------------------------------------------------------- +// Text dump this scope's contents to the console. +//--------------------------------------------------------- +function __DumpScope( depth, table ) +{ + local indent=function( count ) + { + local i; + for( i = 0 ; i < count ; i++ ) + { + print(" "); + } + } + + foreach(key, value in table) + { + indent(depth); + print( key ); + switch (type(value)) + { + case "table": + print("(TABLE)\n"); + indent(depth); + print("{\n"); + __DumpScope( depth + 1, value); + indent(depth); + print("}"); + break; + case "array": + print("(ARRAY)\n"); + indent(depth); + print("[\n") + __DumpScope( depth + 1, value); + indent(depth); + print("]"); + break; + case "string": + print(" = \""); + print(value); + print("\""); + break; + default: + print(" = "); + print(value); + break; + } + print("\n"); + } +} + +)vscript"; \ No newline at end of file diff --git a/sp/src/game/server/waterbullet.cpp b/sp/src/game/server/waterbullet.cpp new file mode 100644 index 00000000..d5e726f3 --- /dev/null +++ b/sp/src/game/server/waterbullet.cpp @@ -0,0 +1,107 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: An effect for a single bullet passing through a body of water. +// The slug quickly decelerates, leaving a trail of bubbles behind it. +// +// TODO: make clientside +// +//=============================================================================// +#include "cbase.h" +#include "waterbullet.h" +#include "ndebugoverlay.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define WATERBULLET_INITIAL_SPEED 1000.0 +#define WATERBULLET_STOP_TIME 0.5 // how long it takes a bullet in water to come to a stop! + +#define WATERBULLET_DECAY ( WATERBULLET_INITIAL_SPEED / WATERBULLET_STOP_TIME ) + +BEGIN_DATADESC( CWaterBullet ) + + // Function Pointers + DEFINE_FUNCTION( Touch ), + DEFINE_FUNCTION( BulletThink ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( waterbullet, CWaterBullet ); + +IMPLEMENT_SERVERCLASS_ST( CWaterBullet, DT_WaterBullet ) +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWaterBullet::Precache() +{ + PrecacheModel( "models/weapons/w_bullet.mdl" ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWaterBullet::Spawn( const Vector &vecOrigin, const Vector &vecDir ) +{ + Precache(); + + SetSolid( SOLID_BBOX ); + SetModel( "models/weapons/w_bullet.mdl" ); + UTIL_SetSize( this, vec3_origin, vec3_origin ); + + SetMoveType( MOVETYPE_FLY ); + + SetGravity( 0.0 ); + + QAngle angles; + SetAbsOrigin( vecOrigin ); + + SetAbsVelocity( vecDir * 1500.0f ); + VectorAngles( GetAbsVelocity(), angles ); + SetAbsAngles( angles ); + + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + SetTouch( &CWaterBullet::Touch ); + + SetThink( &CWaterBullet::BulletThink ); + SetNextThink( gpGlobals->curtime ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CWaterBullet::BulletThink() +{ + //NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() - GetAbsVelocity() * 0.1, 255, 255, 255, false, 1 ); + SetNextThink( gpGlobals->curtime + 0.05 ); + +/* + QAngle angles = GetAbsAngles(); + angles.x += random->RandomInt( -6, 6 ); + angles.y += random->RandomInt( -6, 6 ); + SetAbsAngles( angles ); +*/ + + Vector forward; + AngleVectors( GetAbsAngles(), &forward ); + SetAbsVelocity( forward * 1500.0f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWaterBullet::Touch( CBaseEntity *pOther ) +{ + Vector vecDir = GetAbsVelocity(); + float speed = VectorNormalize( vecDir ); + + Vector vecStart = GetAbsOrigin() - ( vecDir * 8 ); + Vector vecEnd = GetAbsOrigin() + ( vecDir * speed ); + + trace_t tr; + UTIL_TraceLine( vecStart, vecEnd, MASK_SHOT, NULL, &tr ); + UTIL_ImpactTrace( &tr, DMG_BULLET ); + + UTIL_Remove( this ); +} diff --git a/sp/src/game/server/waterbullet.h b/sp/src/game/server/waterbullet.h new file mode 100644 index 00000000..5fc8f9e1 --- /dev/null +++ b/sp/src/game/server/waterbullet.h @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Entity that simulates bullets that are underwater. +// +//=============================================================================// + +#ifndef WEAPON_WATERBULLET_H +#define WEAPON_WATERBULLET_H +#ifdef _WIN32 +#pragma once +#endif + +#define WATER_BULLET_BUBBLES_PER_INCH 0.05f + +//========================================================= +//========================================================= +class CWaterBullet : public CBaseAnimating +{ + DECLARE_CLASS( CWaterBullet, CBaseAnimating ); + +public: + void Precache(); + void Spawn( const Vector &vecOrigin, const Vector &vecDir ); + void Touch( CBaseEntity *pOther ); + void BulletThink(); + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); +}; + +#endif // WEAPON_WATERBULLET_H diff --git a/sp/src/game/server/wcedit.cpp b/sp/src/game/server/wcedit.cpp new file mode 100644 index 00000000..5a454e6b --- /dev/null +++ b/sp/src/game/server/wcedit.cpp @@ -0,0 +1,853 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Namespace for functions having to do with WC Edit mode +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "mathlib/mathlib.h" +#include "player.h" +#include "wcedit.h" +#include "ai_network.h" +#include "ai_initutils.h" +#include "ai_hull.h" +#include "ai_link.h" +#include "ai_node.h" +#include "ai_dynamiclink.h" +#include "ai_networkmanager.h" +#include "ndebugoverlay.h" +#include "editor_sendcommand.h" +#include "movevars_shared.h" +#include "model_types.h" +// UNDONE: Reduce some dependency here! +#include "physics_prop_ragdoll.h" +#include "items.h" +#include "utlsymbol.h" +#include "physobj.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern CAI_Node* FindPickerAINode( CBasePlayer* pPlayer, NodeType_e nNodeType ); +extern CAI_Link* FindPickerAILink( CBasePlayer* pPlayer ); +extern float GetFloorZ(const Vector &origin); + +//----------------------------------------------------------------------------- +// Purpose: Make sure the version of the map in WC is the same as the map +// that's being edited +// Input : +// Output : +//----------------------------------------------------------------------------- +bool NWCEdit::IsWCVersionValid(void) +{ + int status = Editor_CheckVersion(STRING(gpGlobals->mapname), gpGlobals->mapversion, false); + if (!status) + { + return true; + } + else if (status == Editor_NotRunning) + { + Msg("\nAborting map_edit\nWorldcraft not running...\n\n"); + UTIL_CenterPrintAll( "Worldcraft not running..." ); + engine->ServerCommand("disconnect\n"); + } + else + { + Msg("\nAborting map_edit\nWC/Engine map versions different...\n\n"); + UTIL_CenterPrintAll( "WC/Engine map versions different..." ); + engine->ServerCommand("disconnect\n"); + } + return false; +} + +//------------------------------------------------------------------------------ +// Purpose : Figure out placement position of air nodes form where player is +// looking. Keep distance from player constant but adjust height +// based on viewing angle +// Input : +// Output : +//------------------------------------------------------------------------------ +Vector NWCEdit::AirNodePlacementPosition( void ) +{ + CBasePlayer* pPlayer = UTIL_PlayerByIndex(CBaseEntity::m_nDebugPlayer); + + if (!pPlayer) + { + return vec3_origin; + } + + Vector pForward; + pPlayer->EyeVectors( &pForward ); + + Vector floorVec = pForward; + floorVec.z = 0; + VectorNormalize( floorVec ); + VectorNormalize( pForward ); + + float cosAngle = DotProduct(floorVec,pForward); + + float lookDist = g_pAINetworkManager->GetEditOps()->m_flAirEditDistance/cosAngle; + Vector lookPos = pPlayer->EyePosition()+pForward * lookDist; + + return lookPos; +} + +//----------------------------------------------------------------------------- +// Purpose: For create nodes in wc edit mode +// Input : +// Output : +//----------------------------------------------------------------------------- +void NWCEdit::CreateAINode( CBasePlayer *pPlayer ) +{ + // ------------------------------------------------------------- + // Check that WC is running with the right map version + // ------------------------------------------------------------- + if ( !IsWCVersionValid() || !pPlayer ) + return; + + pPlayer->AddSolidFlags( FSOLID_NOT_SOLID ); + + int hullType = g_pAINetworkManager->GetEditOps()->m_iHullDrawNum; + + // ----------------------------------- + // Get position of node to create + // ----------------------------------- + Vector vNewNodePos = vec3_origin; + bool bPositionValid = false; + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + vNewNodePos = NWCEdit::AirNodePlacementPosition(); + + // Make sure we can see the node + trace_t tr; + UTIL_TraceLine(pPlayer->EyePosition(), vNewNodePos, MASK_NPCSOLID_BRUSHONLY, pPlayer, COLLISION_GROUP_NONE, &tr); + if (tr.fraction == 1.0) + { + bPositionValid = true; + } + } + else + { + // Place node by where the player is looking + Vector forward; + pPlayer->EyeVectors( &forward ); + Vector startTrace = pPlayer->EyePosition(); + Vector endTrace = pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH; + trace_t tr; + UTIL_TraceLine(startTrace,endTrace,MASK_NPCSOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0) + { + // Raise the end position up off the floor, place the node and drop him down + tr.endpos.z += 48; + vNewNodePos = tr.endpos; + bPositionValid = true; + } + } + + // ------------------------------------------------------------------------------- + // Now check that this is a valid location for the new node bu using test hull + // ------------------------------------------------------------------------------- + if (bPositionValid) + { + CBaseEntity *testHull = (CBaseEntity*)CAI_TestHull::GetTestHull(); + + // Set the size of the test hull + UTIL_SetSize(testHull, NAI_Hull::Mins(hullType), NAI_Hull::Maxs(hullType)); + + // Set origin of test hull + testHull->SetLocalOrigin( vNewNodePos ); + + // ----------------------------------------------------------------------- + // If a ground node, drop to floor and make sure can stand at test postion + // ----------------------------------------------------------------------- + if (!g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + UTIL_DropToFloor( testHull, MASK_NPCSOLID ); + vNewNodePos = testHull->GetAbsOrigin(); + CTraceFilterSimple traceFilter( testHull, COLLISION_GROUP_NONE ); + if (!UTIL_CheckBottom(testHull, &traceFilter, sv_stepsize.GetFloat())) + { + CAI_TestHull::ReturnTestHull(); + bPositionValid = false; + goto DoneCreate; + } + } + + // ----------------------------------------------------------------------- + // Make sure hull fits at location by seeing if it can move up a fraction + // ----------------------------------------------------------------------- + Vector vUpBit = testHull->GetAbsOrigin(); + vUpBit.z += 1; + trace_t tr; + UTIL_TraceHull( testHull->GetAbsOrigin(), vUpBit, NAI_Hull::Mins(hullType), + NAI_Hull::Maxs(hullType), MASK_NPCSOLID, testHull, COLLISION_GROUP_NONE, &tr ); + if (tr.startsolid || tr.fraction != 1.0) + { + CAI_TestHull::ReturnTestHull(); + bPositionValid = false; + goto DoneCreate; + } + + // <> Round position till DS fixed WC bug + testHull->SetLocalOrigin( Vector( floor(testHull->GetAbsOrigin().x), + floor(testHull->GetAbsOrigin().y ), floor(testHull->GetAbsOrigin().z) ) ); + + // --------------------------------------- + // Send new node to WC + // --------------------------------------- + int status; + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + status = Editor_CreateNode("info_node_air", g_pAINetworkManager->GetEditOps()->m_nNextWCIndex, testHull->GetLocalOrigin().x, testHull->GetLocalOrigin().y, testHull->GetLocalOrigin().z, false); + } + else + { + // Create slightly higher in WC so it can be dropped when its loaded again + Vector origin = testHull->GetLocalOrigin(); + origin.z += 24.0; + testHull->SetLocalOrigin( origin ); + status = Editor_CreateNode("info_node", g_pAINetworkManager->GetEditOps()->m_nNextWCIndex, testHull->GetLocalOrigin().x, testHull->GetLocalOrigin().y, testHull->GetLocalOrigin().z, false); + } + if (status == Editor_BadCommand) + { + Msg( "Worldcraft failed on creation...\n" ); + CAI_TestHull::ReturnTestHull(); + } + else if (status == Editor_OK) + { + // ----------------------- + // Create a new ai node + // ----------------------- + CNodeEnt *pNodeEnt; + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + pNodeEnt = (CNodeEnt*)CreateEntityByName("info_node_air"); + } + else + { + pNodeEnt = (CNodeEnt*)CreateEntityByName("info_node"); + } + + // Note this is a new entity being created as part of wc editing + pNodeEnt->SetLocalOrigin( testHull->GetLocalOrigin() ); + CAI_TestHull::ReturnTestHull(); + + pNodeEnt->m_NodeData.nWCNodeID = g_pAINetworkManager->GetEditOps()->m_nNextWCIndex; + + pNodeEnt->m_debugOverlays |= OVERLAY_WC_CHANGE_ENTITY; + pNodeEnt->Spawn(); + } + } + +DoneCreate: + // ---------------------------------------------------------- + // Flash a red box as a warning that the hull won't fit here + // ---------------------------------------------------------- + if (!bPositionValid) + { + NDebugOverlay::Box(vNewNodePos, NAI_Hull::Mins(hullType), NAI_Hull::Maxs(hullType), 255,0,0,0,0.1); + } + + // Restore player collidability + pPlayer->SetSolid( SOLID_BBOX ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : +// Output : +//----------------------------------------------------------------------------- +void NWCEdit::UndoDestroyAINode(void) +{ + // ------------------------------------------------------------- + // Check that WC is running with the right map version + // ------------------------------------------------------------- + if (!IsWCVersionValid()) + { + return; + } + + if (g_pAINetworkManager->GetEditOps()->m_pLastDeletedNode) + { + Vector nodePos = g_pAINetworkManager->GetEditOps()->m_pLastDeletedNode->GetOrigin(); + + int status; + int nOldWCID = g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[g_pAINetworkManager->GetEditOps()->m_pLastDeletedNode->GetId()]; + + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + status = Editor_CreateNode("info_node_air", nOldWCID, nodePos.x, nodePos.y, nodePos.z, false); + } + else + { + status = Editor_CreateNode("info_node", nOldWCID, nodePos.x, nodePos.y, nodePos.z, false); + } + if (status == Editor_BadCommand) + { + Msg( "Worldcraft failed on creation...\n" ); + } + else if (status == Editor_OK) + { + g_pAINetworkManager->GetEditOps()->m_pLastDeletedNode->SetType( NODE_GROUND ); + //@ tofo g_pAINetworkManager->GetEditOps()->m_pLastDeletedNode->m_pNetwork->BuildNetworkGraph(); + g_pAINetworkManager->BuildNetworkGraph(); + g_pAINetworkManager->GetEditOps()->m_pLastDeletedNode = NULL; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: For destroying nodes in wc edit mode +// Input : +// Output : +//----------------------------------------------------------------------------- +void NWCEdit::DestroyAINode( CBasePlayer *pPlayer ) +{ + // ------------------------------------------------------------- + // Check that WC is running with the right map version + // ------------------------------------------------------------- + if (!IsWCVersionValid()) + { + return; + } + + if (!pPlayer) + { + return; + } + + NodeType_e nNodeType = NODE_GROUND; + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + nNodeType = NODE_AIR; + } + + CAI_Node* pAINode = FindPickerAINode(pPlayer, nNodeType); + if (pAINode) + { + int status = Editor_DeleteNode(g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[pAINode->GetId()], false); + + if (status == Editor_BadCommand) + { + Msg( "Worldcraft failed on deletion...\n" ); + } + else if (status == Editor_OK) + { + // Mark this node as deleted and changed + pAINode->SetType( NODE_DELETED ); + pAINode->m_eNodeInfo |= bits_NODE_WC_CHANGED; + + // Note that network needs to be rebuild + g_pAINetworkManager->GetEditOps()->SetRebuildFlags(); + g_pAINetworkManager->GetEditOps()->m_pLastDeletedNode = pAINode; + + // Now go through at delete any dynamic links that were attached to this node + for (int link = 0; link < pAINode->NumLinks(); link++) + { + int nSrcID = pAINode->GetLinkByIndex(link)->m_iSrcID; + int nDstID = pAINode->GetLinkByIndex(link)->m_iDestID; + if (CAI_DynamicLink::GetDynamicLink(nSrcID, nDstID)) + { + int nWCSrcID = g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[nSrcID]; + int nWCDstID = g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[nDstID]; + int status = Editor_DeleteNodeLink(nWCSrcID, nWCDstID); + + if (status == Editor_BadCommand) + { + Msg( "Worldcraft failed on node link deletion...\n" ); + } + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: For restroring links in WC edit mode. This actually means +// destroying links in WC that have been marked as +// Input : +// Output : +//----------------------------------------------------------------------------- +void NWCEdit::CreateAILink( CBasePlayer* pPlayer ) +{ + // ------------------------------------------------------------- + // Check that WC is running with the right map version + // ------------------------------------------------------------- + if (!IsWCVersionValid()) + { + return; + } + + CAI_Link* pAILink = FindPickerAILink(pPlayer); + if (pAILink && (pAILink->m_LinkInfo & bits_LINK_OFF)) + { + int nWCSrcID = g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[pAILink->m_iSrcID]; + int nWCDstID = g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[pAILink->m_iDestID]; + int status = Editor_DeleteNodeLink(nWCSrcID, nWCDstID, false); + + if (status == Editor_BadCommand) + { + Msg( "Worldcraft failed on node link creation...\n" ); + } + else if (status == Editor_OK) + { + // Don't actually destroy the dynamic link while editing. Just mark the link + pAILink->m_LinkInfo &= ~bits_LINK_OFF; + + CAI_DynamicLink* pDynamicLink = CAI_DynamicLink::GetDynamicLink(pAILink->m_iSrcID, pAILink->m_iDestID); + UTIL_Remove(pDynamicLink); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: For destroying links in wc edit mode. Actually have to create +// a link in WC that is marked as off +// Input : +// Output : +//----------------------------------------------------------------------------- +void NWCEdit::DestroyAILink( CBasePlayer *pPlayer ) +{ + // ------------------------------------------------------------- + // Check that WC is running with the right map version + // ------------------------------------------------------------- + if (!IsWCVersionValid()) + { + return; + } + + CAI_Link* pAILink = FindPickerAILink(pPlayer); + if (pAILink) + { + int nWCSrcID = g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[pAILink->m_iSrcID]; + int nWCDstID = g_pAINetworkManager->GetEditOps()->m_pNodeIndexTable[pAILink->m_iDestID]; + int status = Editor_CreateNodeLink(nWCSrcID, nWCDstID, false); + + if (status == Editor_BadCommand) + { + Msg( "Worldcraft failed on node link creation...\n" ); + } + else if (status == Editor_OK) + { + // Create dynamic link and mark the link + CAI_DynamicLink* pNewLink = (CAI_DynamicLink*)CreateEntityByName("info_node_link" );; + pNewLink->m_nSrcID = pAILink->m_iSrcID; + pNewLink->m_nDestID = pAILink->m_iDestID; + pNewLink->m_nLinkState = LINK_OFF; + pAILink->m_LinkInfo |= bits_LINK_OFF; + } + } +} + +Vector *g_EntityPositions = NULL; +QAngle *g_EntityOrientations = NULL; +string_t *g_EntityClassnames = NULL; + +#ifdef MAPBASE // VDC Memory Leak Fixes +class GlobalCleanUp : public CAutoGameSystem +{ + void Shutdown() + { + delete [] g_EntityPositions; + delete [] g_EntityOrientations; + delete [] g_EntityClassnames; + delete this; + } +}; +#endif + +//----------------------------------------------------------------------------- +// Purpose: Saves the entity's position for future communication with Hammer +//----------------------------------------------------------------------------- +void NWCEdit::RememberEntityPosition( CBaseEntity *pEntity ) +{ + if ( !(pEntity->ObjectCaps() & FCAP_WCEDIT_POSITION) ) + return; + + if ( !g_EntityPositions ) + { +#ifdef MAPBASE // VDC Memory Leak Fixes + new GlobalCleanUp(); +#endif + g_EntityPositions = new Vector[NUM_ENT_ENTRIES]; + g_EntityOrientations = new QAngle[NUM_ENT_ENTRIES]; + // have to save these too because some entities change the classname on spawn (e.g. prop_physics_override, physics_prop) + g_EntityClassnames = new string_t[NUM_ENT_ENTRIES]; + } + int index = pEntity->entindex(); + g_EntityPositions[index] = pEntity->GetAbsOrigin(); + g_EntityOrientations[index] = pEntity->GetAbsAngles(); + g_EntityClassnames[index] = pEntity->m_iClassname; +} + +//----------------------------------------------------------------------------- +// Purpose: Sends Hammer an update to the current position +//----------------------------------------------------------------------------- +void NWCEdit::UpdateEntityPosition( CBaseEntity *pEntity ) +{ + const Vector &newPos = pEntity->GetAbsOrigin(); + const QAngle &newAng = pEntity->GetAbsAngles(); + + DevMsg( 1, "%s\n origin %f %f %f\n angles %f %f %f\n", pEntity->GetClassname(), newPos.x, newPos.y, newPos.z, newAng.x, newAng.y, newAng.z ); + if ( Ragdoll_IsPropRagdoll(pEntity) ) + { + char tmp[2048]; + Ragdoll_GetAngleOverrideString( tmp, sizeof(tmp), pEntity ); + DevMsg( 1, "pose: %s\n", tmp ); + } + + if ( !(pEntity->ObjectCaps() & FCAP_WCEDIT_POSITION) ) + return; + + // can't do this unless in edit mode + if ( !engine->IsInEditMode() ) + return; + + int entIndex = pEntity->entindex(); + Vector pos = g_EntityPositions[entIndex]; + EditorSendResult_t result = Editor_BadCommand; + const char *pClassname = STRING(g_EntityClassnames[entIndex]); + + if ( pEntity->GetModel() && modelinfo->GetModelType(pEntity->GetModel()) == mod_brush ) + { + QAngle xformAngles; + RotationDelta( g_EntityOrientations[entIndex], newAng, &xformAngles ); + if ( xformAngles.Length() > 1e-4 ) + { + result = Editor_RotateEntity( pClassname, pos.x, pos.y, pos.z, xformAngles ); + } + else + { + // don't call through for an identity rotation, may just increase error + result = Editor_OK; + } + } + else + { + if ( Ragdoll_IsPropRagdoll(pEntity) ) + { + char tmp[2048]; + Ragdoll_GetAngleOverrideString( tmp, sizeof(tmp), pEntity ); + result = Editor_SetKeyValue( pClassname, pos.x, pos.y, pos.z, "angleOverride", tmp ); + if ( result != Editor_OK ) + goto error; + } + result = Editor_SetKeyValue( pClassname, pos.x, pos.y, pos.z, "angles", CFmtStr("%f %f %f", newAng.x, newAng.y, newAng.z) ); + } + if ( result != Editor_OK ) + goto error; + + result = Editor_SetKeyValue( pClassname, pos.x, pos.y, pos.z, "origin", CFmtStr("%f %f %f", newPos.x, newPos.y, newPos.z) ); + if ( result != Editor_OK ) + goto error; + + NDebugOverlay::EntityBounds(pEntity, 0, 255, 0, 0 ,5); + // save the update + RememberEntityPosition( pEntity ); + return; + +error: + NDebugOverlay::EntityBounds(pEntity, 255, 0, 0, 0 ,5); +} + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_WC_Create( void ) +{ + // Only allowed in wc_edit_mode + if (engine->IsInEditMode()) + { + CBaseEntity::m_nDebugPlayer = UTIL_GetCommandClientIndex(); + + if (g_pAINetworkManager->GetEditOps()->m_bLinkEditMode) + { + NWCEdit::CreateAILink(UTIL_GetCommandClient()); + } + else + { + NWCEdit::CreateAINode(UTIL_GetCommandClient()); + } + } +} +static ConCommand wc_create("wc_create", CC_WC_Create, "When in WC edit mode, creates a node where the player is looking if a node is allowed at that location for the currently selected hull size (see ai_next_hull)", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_WC_Destroy( void ) +{ + // Only allowed in wc_edit_mode + if (engine->IsInEditMode()) + { + CBaseEntity::m_nDebugPlayer = UTIL_GetCommandClientIndex(); + + // UNDONE: For now just deal with info_nodes + //CBaseEntity* pEntity = FindEntity( pEdict, ""); - use when generalize this to any class + //int status = Editor_DeleteEntity("info_node", pEdict->origin.x, pEdict->origin.y, pEdict->origin.z, false); + + if (g_pAINetworkManager->GetEditOps()->m_bLinkEditMode) + { + NWCEdit::DestroyAILink(UTIL_GetCommandClient()); + } + else + { + NWCEdit::DestroyAINode(UTIL_GetCommandClient()); + } + } +} +static ConCommand wc_destroy("wc_destroy", CC_WC_Destroy, "When in WC edit mode, destroys the node that the player is nearest to looking at. (The node will be highlighted by a red box).", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_WC_DestroyUndo( void ) +{ + // Only allowed in wc_edit_mode + if (engine->IsInEditMode()) + { + CBaseEntity::m_nDebugPlayer = UTIL_GetCommandClientIndex(); + + NWCEdit::UndoDestroyAINode(); + } +} +static ConCommand wc_destroy_undo("wc_destroy_undo", CC_WC_DestroyUndo, "When in WC edit mode restores the last deleted node", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_WC_AirNodeEdit( void ) +{ + // Only allowed in wc_edit_mode + if (engine->IsInEditMode()) + { + // Toggle air edit mode state + if (g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + g_pAINetworkManager->GetEditOps()->m_bAirEditMode = false; + } + else + { + g_pAINetworkManager->GetEditOps()->m_bAirEditMode = true; + } + } +} +static ConCommand wc_air_node_edit("wc_air_node_edit", CC_WC_AirNodeEdit, "When in WC edit mode, toggles laying down or air nodes instead of ground nodes", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_WC_AirNodeEditFurther( void ) +{ + // Only allowed in wc_edit_mode + if (engine->IsInEditMode() && g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + g_pAINetworkManager->GetEditOps()->m_flAirEditDistance += 10.0; + } +} +static ConCommand wc_air_edit_further("wc_air_edit_further", CC_WC_AirNodeEditFurther, "When in WC edit mode and editing air nodes, moves position of air node crosshair and placement location further away from player", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_WC_AirNodeEditNearer( void ) +{ + // Only allowed in wc_edit_mode + if (engine->IsInEditMode() && g_pAINetworkManager->GetEditOps()->m_bAirEditMode) + { + g_pAINetworkManager->GetEditOps()->m_flAirEditDistance -= 10.0; + } +} +static ConCommand wc_air_edit_nearer("wc_air_edit_nearer", CC_WC_AirNodeEditNearer, "When in WC edit mode and editing air nodes, moves position of air node crosshair and placement location nearer to from player", FCVAR_CHEAT); + +//------------------------------------------------------------------------------ +// Purpose : +// Input : +// Output : +//------------------------------------------------------------------------------ +void CC_WC_LinkEdit( void ) +{ + // Only allowed in wc_edit_mode + if (engine->IsInEditMode()) + { + // Toggle air edit mode state + if (g_pAINetworkManager->GetEditOps()->m_bLinkEditMode) + { + g_pAINetworkManager->GetEditOps()->m_bLinkEditMode = false; + } + // Don't allow link mode if graph outdated + else if (!(g_pAINetworkManager->GetEditOps()->m_debugNetOverlays & bits_debugNeedRebuild)) + { + g_pAINetworkManager->GetEditOps()->m_bLinkEditMode = true; + } + } +} +static ConCommand wc_link_edit("wc_link_edit", CC_WC_LinkEdit, 0, FCVAR_CHEAT); + + +/// This is an entity used by the hammer_update_safe_entities command. It allows designers +/// to specify objects that should be ignored. It stores an array of sixteen strings +/// which may correspond to names. Designers may ignore more than sixteen objects by +/// placing more than one of these in a level. +class CWC_UpdateIgnoreList : public CBaseEntity +{ +public: + DECLARE_CLASS( CWC_UpdateIgnoreList, CBaseEntity ); + + enum { MAX_IGNORELIST_NAMES = 16 }; ///< the number of names in the array below + + inline const string_t &GetName( int x ) const { return m_nIgnoredEntityNames[x]; } + +protected: + // the list of names to ignore + string_t m_nIgnoredEntityNames[MAX_IGNORELIST_NAMES]; + +public: + DECLARE_DATADESC(); +}; + + +LINK_ENTITY_TO_CLASS( hammer_updateignorelist, CWC_UpdateIgnoreList ); + +BEGIN_DATADESC( CWC_UpdateIgnoreList ) + + // Be still, classcheck! + //DEFINE_FIELD( m_nIgnoredEntityNames, FIELD_STRING, MAX_IGNORELIST_NAMES ), + + DEFINE_KEYFIELD( m_nIgnoredEntityNames[0], FIELD_STRING, "IgnoredName01" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[1], FIELD_STRING, "IgnoredName02" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[2], FIELD_STRING, "IgnoredName03" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[3], FIELD_STRING, "IgnoredName04" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[4], FIELD_STRING, "IgnoredName05" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[5], FIELD_STRING, "IgnoredName06" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[6], FIELD_STRING, "IgnoredName07" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[7], FIELD_STRING, "IgnoredName08" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[8], FIELD_STRING, "IgnoredName09" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[9], FIELD_STRING, "IgnoredName10" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[10], FIELD_STRING, "IgnoredName11" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[11], FIELD_STRING, "IgnoredName12" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[12], FIELD_STRING, "IgnoredName13" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[13], FIELD_STRING, "IgnoredName14" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[14], FIELD_STRING, "IgnoredName15" ), + DEFINE_KEYFIELD( m_nIgnoredEntityNames[15], FIELD_STRING, "IgnoredName16" ), + +END_DATADESC() + + + +CON_COMMAND( hammer_update_entity, "Updates the entity's position/angles when in edit mode" ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() < 2 ) + { + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + UTIL_TraceLine(pPlayer->EyePosition(), pPlayer->EyePosition() + forward * MAX_COORD_RANGE, + MASK_SHOT_HULL|CONTENTS_GRATE|CONTENTS_DEBRIS, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.DidHit() && !tr.DidHitWorld() ) + { + NWCEdit::UpdateEntityPosition( tr.m_pEnt ); + } + } + else + { + CBaseEntity *pEnt = NULL; + while ((pEnt = gEntList.FindEntityGeneric( pEnt, args[1] ) ) != NULL) + { + NWCEdit::UpdateEntityPosition( pEnt ); + } + } +} + +CON_COMMAND( hammer_update_safe_entities, "Updates entities in the map that can safely be updated (don't have parents or are affected by constraints). Also excludes entities mentioned in any hammer_updateignorelist objects in this map." ) +{ + int iCount = 0; + CBaseEntity *pEnt = NULL; + + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + Msg("\n====================================================\nPerforming Safe Entity Update\n" ); + + // first look for any exclusion objects -- these are entities that list specific things to be ignored. + // All the names that are inside them, we store into a hash table (here implemented through a local + // CUtlSymbolTable) + + CUtlSymbolTable ignoredNames(16,32,true); // grow 16 strings at a time. Case insensitive. + while ( (pEnt = gEntList.FindEntityByClassname( pEnt, "hammer_updateignorelist" )) != NULL ) + { + // for each name in each of those strings, add it to the symbol table. + CWC_UpdateIgnoreList *piglist = static_cast(pEnt); + for (int ii = 0 ; ii < CWC_UpdateIgnoreList::MAX_IGNORELIST_NAMES ; ++ii ) + { + if (!!piglist->GetName(ii)) // if not null + { // add to symtab + ignoredNames.AddString(piglist->GetName(ii).ToCStr()); + } + } + } + + if ( ignoredNames.GetNumStrings() > 0 ) + { + Msg( "Ignoring %d specified targetnames.\n", ignoredNames.GetNumStrings() ); + } + + + // now iterate through everything in the world + for ( pEnt = gEntList.FirstEnt(); pEnt != NULL; pEnt = gEntList.NextEnt(pEnt) ) + { + if ( !(pEnt->ObjectCaps() & FCAP_WCEDIT_POSITION) ) + continue; + + // If we have a parent, or any children, we're not safe to update + if ( pEnt->GetMoveParent() || pEnt->FirstMoveChild() ) + continue; + + IPhysicsObject *pPhysics = pEnt->VPhysicsGetObject(); + if ( !pPhysics ) + continue; + // If we are affected by any constraints, we're not safe to update + if ( pPhysics->IsAttachedToConstraint( Ragdoll_IsPropRagdoll(pEnt) ) ) + continue; + // Motion disabled? + if ( !pPhysics->IsMoveable() ) + continue; + + // ignore brush models (per bug 61318) + if ( dynamic_cast(pEnt) ) + continue; + + // explicitly excluded by designer? + if ( ignoredNames.Find(pEnt->GetEntityName().ToCStr()).IsValid() ) + continue; + + NWCEdit::UpdateEntityPosition( pEnt ); + iCount++; + } + + Msg("Updated %d entities.\n", iCount); +} diff --git a/sp/src/game/server/wcedit.h b/sp/src/game/server/wcedit.h new file mode 100644 index 00000000..a169f00c --- /dev/null +++ b/sp/src/game/server/wcedit.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Namespace for functions having to do with WC Edit mode +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef WCEDIT_H +#define WCEDIT_H +#pragma once + +class CBaseEntity; + +//============================================================================= +// >> NWCEdit +//============================================================================= +namespace NWCEdit +{ + Vector AirNodePlacementPosition( void ); + bool IsWCVersionValid(void); + void CreateAINode( CBasePlayer *pPlayer ); + void DestroyAINode( CBasePlayer *pPlayer ); + void CreateAILink( CBasePlayer *pPlayer ); + void DestroyAILink( CBasePlayer *pPlayer ); + void UndoDestroyAINode(void); + void RememberEntityPosition( CBaseEntity *pEntity ); + void UpdateEntityPosition( CBaseEntity *pEntity ); +}; + +#endif // WCEDIT_H \ No newline at end of file diff --git a/sp/src/game/server/weapon_cubemap.cpp b/sp/src/game/server/weapon_cubemap.cpp new file mode 100644 index 00000000..212023e3 --- /dev/null +++ b/sp/src/game/server/weapon_cubemap.cpp @@ -0,0 +1,48 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CWeaponCubemap : public CBaseCombatWeapon +{ +public: + + DECLARE_CLASS( CWeaponCubemap, CBaseCombatWeapon ); + + void Precache( void ); + + bool HasAnyAmmo( void ) { return true; } + + void Spawn( void ); + + DECLARE_SERVERCLASS(); +}; + +LINK_ENTITY_TO_CLASS( weapon_cubemap, CWeaponCubemap ); + +IMPLEMENT_SERVERCLASS_ST( CWeaponCubemap, DT_WeaponCubemap ) +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponCubemap::Precache( void ) +{ + BaseClass::Precache(); +} + +void CWeaponCubemap::Spawn( void ) +{ + BaseClass::Spawn(); + + //Hack to fix the cubemap weapon not being held by the player. + //Problem is the model has huge bounds so the new pickup code that checks if the player can see the model fails cause half the entity's bounds are inside the ground. + //Since this is just a dev tool I made this quick hack so level designers can use it again asap. - Adrian + UTIL_SetSize( this, Vector( -16, -16, -16 ), Vector( 16, 16, 16 ) ); +} diff --git a/sp/src/game/server/weight_button.cpp b/sp/src/game/server/weight_button.cpp new file mode 100644 index 00000000..fc3f103e --- /dev/null +++ b/sp/src/game/server/weight_button.cpp @@ -0,0 +1,102 @@ + +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: 'Button' which activates after a specified amount of weight is touching it. +// +//=============================================================================// + +#include "cbase.h" + +class CWeightButton : public CBaseEntity +{ +public: + + DECLARE_DATADESC(); + DECLARE_CLASS( CWeightButton, CBaseEntity ); + + void Spawn( void ); + bool CreateVPhysics( void ); + + COutputEvent m_OnPressed; // After threshold weight has been reached + COutputEvent m_OnReleased; // After weight has been removed to go below weight threshold + + float m_fStressToActivate; // Amount of weight required to activate + bool m_bHasBeenPressed; // Once the button has been pressed, fire one + // output until the weight is reduced below the threshold + + void TriggerThink ( void ); + +}; + +LINK_ENTITY_TO_CLASS( func_weight_button, CWeightButton ); + +BEGIN_DATADESC( CWeightButton ) + + DEFINE_KEYFIELD( m_fStressToActivate, FIELD_FLOAT, "WeightToActivate" ), + DEFINE_FIELD( m_bHasBeenPressed, FIELD_BOOLEAN ), + + DEFINE_OUTPUT( m_OnPressed, "OnPressed" ), + DEFINE_OUTPUT( m_OnReleased, "OnReleased" ), + + DEFINE_THINKFUNC( TriggerThink ), + +END_DATADESC() + + +void CWeightButton::Spawn() +{ + BaseClass::Spawn(); + + // Convert movedir from angles to a vector + SetMoveType( MOVETYPE_VPHYSICS ); + SetSolid( SOLID_VPHYSICS ); + SetModel( STRING( GetModelName() ) ); + CreateVPhysics(); + SetThink( &CWeightButton::TriggerThink ); + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); + m_bHasBeenPressed = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Create VPhysics collision for this entity +//----------------------------------------------------------------------------- +bool CWeightButton::CreateVPhysics() +{ + VPhysicsInitShadow( false, false ); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Every second, check total stress and fire an output if we have reached +// our threshold. If the stress is relieved below our threshold, fire a different output. +//----------------------------------------------------------------------------- +void CWeightButton::TriggerThink( void ) +{ + vphysics_objectstress_t vpobj_StressOut; + IPhysicsObject* pMyPhysics = VPhysicsGetObject(); + + if ( !pMyPhysics ) + { + SetNextThink( TICK_NEVER_THINK ); + return; + } + + float fStress = CalculateObjectStress( pMyPhysics, this, &vpobj_StressOut ); + +// fStress = vpobj_StressOut.receivedStress; + + if ( fStress > m_fStressToActivate && !m_bHasBeenPressed ) + { + m_OnPressed.FireOutput( this, this ); + m_bHasBeenPressed = true; + } + else if ( fStress < m_fStressToActivate && m_bHasBeenPressed ) + { + m_OnReleased.FireOutput( this, this ); + m_bHasBeenPressed = false; + } + + // think every tick + SetNextThink( gpGlobals->curtime + TICK_INTERVAL ); +} diff --git a/sp/src/game/server/worker_scientist.h b/sp/src/game/server/worker_scientist.h new file mode 100644 index 00000000..db4f9928 --- /dev/null +++ b/sp/src/game/server/worker_scientist.h @@ -0,0 +1 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// diff --git a/sp/src/game/server/world.cpp b/sp/src/game/server/world.cpp new file mode 100644 index 00000000..2e31c1d6 --- /dev/null +++ b/sp/src/game/server/world.cpp @@ -0,0 +1,780 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Precaches and defs for entities and other data that must always be available. +// +// $NoKeywords: $ +//===========================================================================// + +#include "cbase.h" +#include "soundent.h" +#include "client.h" +#include "decals.h" +#include "EnvMessage.h" +#include "player.h" +#include "gamerules.h" +#include "teamplay_gamerules.h" +#include "physics.h" +#include "isaverestore.h" +#include "activitylist.h" +#include "eventlist.h" +#include "eventqueue.h" +#include "ai_network.h" +#include "ai_schedule.h" +#include "ai_networkmanager.h" +#include "ai_utils.h" +#include "basetempentity.h" +#include "world.h" +#include "mempool.h" +#include "igamesystem.h" +#include "engine/IEngineSound.h" +#include "globals.h" +#include "engine/IStaticPropMgr.h" +#include "particle_parse.h" +#include "globalstate.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern CBaseEntity *g_pLastSpawn; +void InitBodyQue(void); +extern void W_Precache(void); +extern void ActivityList_Free( void ); +extern CUtlMemoryPool g_EntityListPool; + +#define SF_DECAL_NOTINDEATHMATCH 2048 + +class CDecal : public CPointEntity +{ +public: + DECLARE_CLASS( CDecal, CPointEntity ); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + // Need to apply static decals here to get them into the signon buffer for the server appropriately + virtual void Activate(); + + void TriggerDecal( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +public: + int m_nTexture; + bool m_bLowPriority; + +private: + + void StaticDecal( void ); +}; + +BEGIN_DATADESC( CDecal ) + + DEFINE_FIELD( m_nTexture, FIELD_INTEGER ), + DEFINE_KEYFIELD( m_bLowPriority, FIELD_BOOLEAN, "LowPriority" ), // Don't mark as FDECAL_PERMANENT so not save/restored and will be reused on the client preferentially + + // Function pointers + DEFINE_FUNCTION( StaticDecal ), + DEFINE_FUNCTION( TriggerDecal ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( infodecal, CDecal ); + +// UNDONE: These won't get sent to joining players in multi-player +void CDecal::Spawn( void ) +{ + if ( m_nTexture < 0 || + (gpGlobals->deathmatch && HasSpawnFlags( SF_DECAL_NOTINDEATHMATCH )) ) + { + UTIL_Remove( this ); + return; + } +} + +void CDecal::Activate() +{ + BaseClass::Activate(); + + if ( !GetEntityName() ) + { + StaticDecal(); + } + else + { + // if there IS a targetname, the decal sprays itself on when it is triggered. + SetThink ( &CDecal::SUB_DoNothing ); + SetUse(&CDecal::TriggerDecal); + } +} + +void CDecal::TriggerDecal ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // this is set up as a USE function for info_decals that have targetnames, so that the + // decal doesn't get applied until it is fired. (usually by a scripted sequence) + trace_t trace; + int entityIndex; + + UTIL_TraceLine( GetAbsOrigin() - Vector(5,5,5), GetAbsOrigin() + Vector(5,5,5), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &trace ); + + entityIndex = trace.m_pEnt ? trace.m_pEnt->entindex() : 0; + + CBroadcastRecipientFilter filter; + + te->BSPDecal( filter, 0.0, + &GetAbsOrigin(), entityIndex, m_nTexture ); + + SetThink( &CDecal::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +void CDecal::InputActivate( inputdata_t &inputdata ) +{ + TriggerDecal( inputdata.pActivator, inputdata.pCaller, USE_ON, 0 ); +} + + +void CDecal::StaticDecal( void ) +{ + class CTraceFilterValidForDecal : public CTraceFilterSimple + { + public: + CTraceFilterValidForDecal(const IHandleEntity *passentity, int collisionGroup ) + : CTraceFilterSimple( passentity, collisionGroup ) + { + } + + virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) + { + static const char *ppszIgnoredClasses[] = + { + "weapon_*", + "item_*", + "prop_ragdoll", + "prop_dynamic", + "prop_static", + "prop_physics", + "npc_bullseye", // Tracker 15335 + }; + + CBaseEntity *pEntity = EntityFromEntityHandle( pServerEntity ); + + // Tracker 15335: Never impact decals against entities which are not rendering, either. + if ( pEntity->IsEffectActive( EF_NODRAW ) ) + return false; + + for ( int i = 0; i < ARRAYSIZE(ppszIgnoredClasses); i++ ) + { + if ( pEntity->ClassMatches( ppszIgnoredClasses[i] ) ) + return false; + } + + + return CTraceFilterSimple::ShouldHitEntity( pServerEntity, contentsMask ); + } + }; + + trace_t trace; + CTraceFilterValidForDecal traceFilter( this, COLLISION_GROUP_NONE ); + int entityIndex, modelIndex = 0; + + Vector position = GetAbsOrigin(); + UTIL_TraceLine( position - Vector(5,5,5), position + Vector(5,5,5), MASK_SOLID, &traceFilter, &trace ); + + bool canDraw = true; + + entityIndex = trace.m_pEnt ? (short)trace.m_pEnt->entindex() : 0; + if ( entityIndex ) + { + CBaseEntity *ent = trace.m_pEnt; + if ( ent ) + { + modelIndex = ent->GetModelIndex(); + VectorITransform( GetAbsOrigin(), ent->EntityToWorldTransform(), position ); + + canDraw = ( modelIndex != 0 ); + if ( !canDraw ) + { + Warning( "Suppressed StaticDecal which would have hit entity %i (class:%s, name:%s) with modelindex = 0\n", + ent->entindex(), + ent->GetClassname(), + STRING( ent->GetEntityName() ) ); + } + } + } + + if ( canDraw ) + { + engine->StaticDecal( position, m_nTexture, entityIndex, modelIndex, m_bLowPriority ); + } + + SUB_Remove(); +} + + +bool CDecal::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "texture")) + { + // FIXME: should decals all be preloaded? + m_nTexture = UTIL_PrecacheDecal( szValue, true ); + + // Found + if (m_nTexture >= 0 ) + return true; + Warning( "Can't find decal %s\n", szValue ); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Projects a decal against a prop +//----------------------------------------------------------------------------- +class CProjectedDecal : public CPointEntity +{ +public: + DECLARE_CLASS( CProjectedDecal, CPointEntity ); + + void Spawn( void ); + bool KeyValue( const char *szKeyName, const char *szValue ); + + // Need to apply static decals here to get them into the signon buffer for the server appropriately + virtual void Activate(); + + void TriggerDecal( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + // Input handlers. + void InputActivate( inputdata_t &inputdata ); + + DECLARE_DATADESC(); + +public: + int m_nTexture; + float m_flDistance; + +private: + void ProjectDecal( CRecipientFilter& filter ); + + void StaticDecal( void ); +}; + +BEGIN_DATADESC( CProjectedDecal ) + + DEFINE_FIELD( m_nTexture, FIELD_INTEGER ), + + DEFINE_KEYFIELD( m_flDistance, FIELD_FLOAT, "Distance" ), + + // Function pointers + DEFINE_FUNCTION( StaticDecal ), + DEFINE_FUNCTION( TriggerDecal ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( info_projecteddecal, CProjectedDecal ); + +// UNDONE: These won't get sent to joining players in multi-player +void CProjectedDecal::Spawn( void ) +{ + if ( m_nTexture < 0 || + (gpGlobals->deathmatch && HasSpawnFlags( SF_DECAL_NOTINDEATHMATCH )) ) + { + UTIL_Remove( this ); + return; + } +} + +void CProjectedDecal::Activate() +{ + BaseClass::Activate(); + + if ( !GetEntityName() ) + { + StaticDecal(); + } + else + { + // if there IS a targetname, the decal sprays itself on when it is triggered. + SetThink ( &CProjectedDecal::SUB_DoNothing ); + SetUse(&CProjectedDecal::TriggerDecal); + } +} + +void CProjectedDecal::InputActivate( inputdata_t &inputdata ) +{ + TriggerDecal( inputdata.pActivator, inputdata.pCaller, USE_ON, 0 ); +} + +void CProjectedDecal::ProjectDecal( CRecipientFilter& filter ) +{ + te->ProjectDecal( filter, 0.0, + &GetAbsOrigin(), &GetAbsAngles(), m_flDistance, m_nTexture ); +} + +void CProjectedDecal::TriggerDecal ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBroadcastRecipientFilter filter; + + ProjectDecal( filter ); + + SetThink( &CProjectedDecal::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +void CProjectedDecal::StaticDecal( void ) +{ + CBroadcastRecipientFilter initFilter; + initFilter.MakeInitMessage(); + + ProjectDecal( initFilter ); + + SUB_Remove(); +} + + +bool CProjectedDecal::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "texture")) + { + // FIXME: should decals all be preloaded? + m_nTexture = UTIL_PrecacheDecal( szValue, true ); + + // Found + if (m_nTexture >= 0 ) + return true; + Warning( "Can't find decal %s\n", szValue ); + } + else + { + return BaseClass::KeyValue( szKeyName, szValue ); + } + + return true; +} + +//======================= +// CWorld +// +// This spawns first when each level begins. +//======================= +LINK_ENTITY_TO_CLASS( worldspawn, CWorld ); + +BEGIN_DATADESC( CWorld ) + + DEFINE_FIELD( m_flWaveHeight, FIELD_FLOAT ), + + // keyvalues are parsed from map, but not saved/loaded + DEFINE_KEYFIELD( m_iszChapterTitle, FIELD_STRING, "chaptertitle" ), +#ifdef MAPBASE + DEFINE_KEYFIELD( m_bChapterTitleNoMessage, FIELD_BOOLEAN, "chaptertitlenomessage" ), +#endif + DEFINE_KEYFIELD( m_bStartDark, FIELD_BOOLEAN, "startdark" ), + DEFINE_KEYFIELD( m_bDisplayTitle, FIELD_BOOLEAN, "gametitle" ), + DEFINE_FIELD( m_WorldMins, FIELD_VECTOR ), + DEFINE_FIELD( m_WorldMaxs, FIELD_VECTOR ), +#ifdef _X360 + DEFINE_KEYFIELD( m_flMaxOccludeeArea, FIELD_FLOAT, "maxoccludeearea_x360" ), + DEFINE_KEYFIELD( m_flMinOccluderArea, FIELD_FLOAT, "minoccluderarea_x360" ), +#else + DEFINE_KEYFIELD( m_flMaxOccludeeArea, FIELD_FLOAT, "maxoccludeearea" ), + DEFINE_KEYFIELD( m_flMinOccluderArea, FIELD_FLOAT, "minoccluderarea" ), +#endif + DEFINE_KEYFIELD( m_flMaxPropScreenSpaceWidth, FIELD_FLOAT, "maxpropscreenwidth" ), + DEFINE_KEYFIELD( m_flMinPropScreenSpaceWidth, FIELD_FLOAT, "minpropscreenwidth" ), + DEFINE_KEYFIELD( m_iszDetailSpriteMaterial, FIELD_STRING, "detailmaterial" ), +#ifdef MAPBASE_VSCRIPT + DEFINE_KEYFIELD( m_iScriptLanguage, FIELD_INTEGER, "vscriptlanguage" ), + //DEFINE_KEYFIELD( m_iScriptLanguageClient, FIELD_INTEGER, "vscriptlanguage_client" ), +#endif + DEFINE_KEYFIELD( m_bColdWorld, FIELD_BOOLEAN, "coldworld" ), + +#ifdef MAPBASE + DEFINE_INPUTFUNC( FIELD_STRING, "SetChapterTitle", InputSetChapterTitle ), +#endif + +END_DATADESC() + + +// SendTable stuff. +IMPLEMENT_SERVERCLASS_ST(CWorld, DT_WORLD) + SendPropFloat (SENDINFO(m_flWaveHeight), 8, SPROP_ROUNDUP, 0.0f, 8.0f), + SendPropVector (SENDINFO(m_WorldMins), -1, SPROP_COORD), + SendPropVector (SENDINFO(m_WorldMaxs), -1, SPROP_COORD), + SendPropInt (SENDINFO(m_bStartDark), 1, SPROP_UNSIGNED ), + SendPropFloat (SENDINFO(m_flMaxOccludeeArea), 0, SPROP_NOSCALE ), + SendPropFloat (SENDINFO(m_flMinOccluderArea), 0, SPROP_NOSCALE ), + SendPropFloat (SENDINFO(m_flMaxPropScreenSpaceWidth), 0, SPROP_NOSCALE ), + SendPropFloat (SENDINFO(m_flMinPropScreenSpaceWidth), 0, SPROP_NOSCALE ), + SendPropStringT (SENDINFO(m_iszDetailSpriteMaterial) ), + SendPropInt (SENDINFO(m_bColdWorld), 1, SPROP_UNSIGNED ), +#ifdef MAPBASE + SendPropStringT (SENDINFO(m_iszChapterTitle) ), +#endif +END_SEND_TABLE() + +// +// Just to ignore the "wad" field. +// +bool CWorld::KeyValue( const char *szKeyName, const char *szValue ) +{ + if ( FStrEq(szKeyName, "skyname") ) + { + // Sent over net now. + ConVarRef skyname( "sv_skyname" ); + skyname.SetValue( szValue ); + } + else if ( FStrEq(szKeyName, "newunit") ) + { + // Single player only. Clear save directory if set + if ( atoi(szValue) ) + { + extern void Game_SetOneWayTransition(); + Game_SetOneWayTransition(); + } + } + else if ( FStrEq(szKeyName, "world_mins") ) + { + Vector vec; + sscanf( szValue, "%f %f %f", &vec.x, &vec.y, &vec.z ); + m_WorldMins = vec; + } + else if ( FStrEq(szKeyName, "world_maxs") ) + { + Vector vec; + sscanf( szValue, "%f %f %f", &vec.x, &vec.y, &vec.z ); + m_WorldMaxs = vec; + } + else + return BaseClass::KeyValue( szKeyName, szValue ); + + return true; +} + + +extern bool g_fGameOver; +CWorld *g_WorldEntity = NULL; + +CWorld* GetWorldEntity() +{ + return g_WorldEntity; +} + +CWorld::CWorld( ) +{ + AddEFlags( EFL_NO_AUTO_EDICT_ATTACH | EFL_KEEP_ON_RECREATE_ENTITIES ); + NetworkProp()->AttachEdict( INDEXENT(RequiredEdictIndex()) ); + ActivityList_Init(); + EventList_Init(); + + SetSolid( SOLID_BSP ); + SetMoveType( MOVETYPE_NONE ); + +#ifdef MAPBASE_VSCRIPT + m_iScriptLanguage = SL_NONE; + //m_iScriptLanguageClient = -2; +#endif + + ConVarRef("mat_slopescaledepthbias_shadowmap").SetValue("16"); + ConVarRef("mat_depthbias_shadowmap").SetValue("0.0001"); + + m_bColdWorld = false; +} + +CWorld::~CWorld( ) +{ + EventList_Free(); + ActivityList_Free(); + if ( g_pGameRules ) + { + g_pGameRules->LevelShutdown(); + delete g_pGameRules; + g_pGameRules = NULL; + } + g_WorldEntity = NULL; +} + + +//------------------------------------------------------------------------------ +// Purpose : Add a decal to the world +// Input : +// Output : +//------------------------------------------------------------------------------ +void CWorld::DecalTrace( trace_t *pTrace, char const *decalName) +{ + int index = decalsystem->GetDecalIndexForName( decalName ); + if ( index < 0 ) + return; + + CBroadcastRecipientFilter filter; + if ( pTrace->hitbox != 0 ) + { + te->Decal( filter, 0.0f, &pTrace->endpos, &pTrace->startpos, 0, pTrace->hitbox, index ); + } + else + { + te->WorldDecal( filter, 0.0, &pTrace->endpos, index ); + } +} + +void CWorld::RegisterSharedActivities( void ) +{ + ActivityList_RegisterSharedActivities(); +} + +void CWorld::RegisterSharedEvents( void ) +{ + EventList_RegisterSharedEvents(); +} + + +void CWorld::Spawn( void ) +{ + SetLocalOrigin( vec3_origin ); + SetLocalAngles( vec3_angle ); + // NOTE: SHOULD NEVER BE ANYTHING OTHER THAN 1!!! + SetModelIndex( 1 ); + // world model + SetModelName( AllocPooledString( modelinfo->GetModelName( GetModel() ) ) ); + AddFlag( FL_WORLDBRUSH ); + + g_EventQueue.Init(); + Precache( ); + GlobalEntity_Add( "is_console", STRING(gpGlobals->mapname), ( IsConsole() ) ? GLOBAL_ON : GLOBAL_OFF ); + GlobalEntity_Add( "is_pc", STRING(gpGlobals->mapname), ( !IsConsole() ) ? GLOBAL_ON : GLOBAL_OFF ); +} + +static const char *g_DefaultLightstyles[] = +{ + // 0 normal + "m", + // 1 FLICKER (first variety) + "mmnmmommommnonmmonqnmmo", + // 2 SLOW STRONG PULSE + "abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba", + // 3 CANDLE (first variety) + "mmmmmaaaaammmmmaaaaaabcdefgabcdefg", + // 4 FAST STROBE + "mamamamamama", + // 5 GENTLE PULSE 1 + "jklmnopqrstuvwxyzyxwvutsrqponmlkj", + // 6 FLICKER (second variety) + "nmonqnmomnmomomno", + // 7 CANDLE (second variety) + "mmmaaaabcdefgmmmmaaaammmaamm", + // 8 CANDLE (third variety) + "mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa", + // 9 SLOW STROBE (fourth variety) + "aaaaaaaazzzzzzzz", + // 10 FLUORESCENT FLICKER + "mmamammmmammamamaaamammma", + // 11 SLOW PULSE NOT FADE TO BLACK + "abcdefghijklmnopqrrqponmlkjihgfedcba", + // 12 UNDERWATER LIGHT MUTATION + // this light only distorts the lightmap - no contribution + // is made to the brightness of affected surfaces + "mmnnmmnnnmmnn", +}; + + +const char *GetDefaultLightstyleString( int styleIndex ) +{ + if ( styleIndex < ARRAYSIZE(g_DefaultLightstyles) ) + { + return g_DefaultLightstyles[styleIndex]; + } + return "m"; +} + +void CWorld::Precache( void ) +{ + g_WorldEntity = this; + g_fGameOver = false; + g_pLastSpawn = NULL; + + ConVarRef stepsize( "sv_stepsize" ); + stepsize.SetValue( 18 ); + + ConVarRef roomtype( "room_type" ); + roomtype.SetValue( 0 ); + + // Set up game rules + Assert( !g_pGameRules ); + if (g_pGameRules) + { + delete g_pGameRules; + } + + InstallGameRules(); + Assert( g_pGameRules ); + g_pGameRules->Init(); + + CSoundEnt::InitSoundEnt(); + + // Only allow precaching between LevelInitPreEntity and PostEntity + CBaseEntity::SetAllowPrecache( true ); + IGameSystem::LevelInitPreEntityAllSystems( STRING( GetModelName() ) ); + + // Create the player resource + g_pGameRules->CreateStandardEntities(); + + // UNDONE: Make most of these things server systems or precache_registers + // ================================================= + // Activities + // ================================================= + ActivityList_Free(); + RegisterSharedActivities(); + + EventList_Free(); + RegisterSharedEvents(); + + InitBodyQue(); +// init sentence group playback stuff from sentences.txt. +// ok to call this multiple times, calls after first are ignored. + + SENTENCEG_Init(); + + // Precache standard particle systems + PrecacheStandardParticleSystems( ); + +// the area based ambient sounds MUST be the first precache_sounds + +// player precaches + W_Precache (); // get weapon precaches + ClientPrecache(); + g_pGameRules->Precache(); + // precache all temp ent stuff + CBaseTempEntity::PrecacheTempEnts(); + + g_Language.SetValue( LANGUAGE_ENGLISH ); // TODO use VGUI to get current language + + if ( g_Language.GetInt() == LANGUAGE_GERMAN ) + { + PrecacheModel( "models/germangibs.mdl" ); + } + else + { + PrecacheModel( "models/gibs/hgibs.mdl" ); + } + + PrecacheScriptSound( "BaseEntity.EnterWater" ); + PrecacheScriptSound( "BaseEntity.ExitWater" ); + +// +// Setup light animation tables. 'a' is total darkness, 'z' is maxbright. +// + for ( int i = 0; i < ARRAYSIZE(g_DefaultLightstyles); i++ ) + { + engine->LightStyle( i, GetDefaultLightstyleString(i) ); + } + + // styles 32-62 are assigned by the light program for switchable lights + + // 63 testing + engine->LightStyle(63, "a"); + + // ================================================= + // Load and Init AI Networks + // ================================================= + CAI_NetworkManager::InitializeAINetworks(); + // ================================================= + // Load and Init AI Schedules + // ================================================= + g_AI_SchedulesManager.LoadAllSchedules(); + // ================================================= + // Initialize NPC Relationships + // ================================================= + g_pGameRules->InitDefaultAIRelationships(); + CBaseCombatCharacter::InitInteractionSystem(); + + // Call all registered precachers. + CPrecacheRegister::Precache(); + +#ifdef MAPBASE + if ( m_iszChapterTitle.Get() != NULL_STRING && !m_bChapterTitleNoMessage ) + { + DevMsg( 2, "Chapter title: %s\n", STRING(m_iszChapterTitle.Get()) ); + CMessage *pMessage = (CMessage *)CBaseEntity::Create( "env_message", vec3_origin, vec3_angle, NULL ); + if ( pMessage ) + { + pMessage->SetMessage( m_iszChapterTitle.Get() ); + m_iszChapterTitle.Set( NULL_STRING ); + + // send the message entity a play message command, delayed by 1 second + pMessage->AddSpawnFlags( SF_MESSAGE_ONCE ); + pMessage->SetThink( &CMessage::SUB_CallUseToggle ); + pMessage->SetNextThink( gpGlobals->curtime + 1.0f ); + } + } +#else + if ( m_iszChapterTitle != NULL_STRING ) + { + DevMsg( 2, "Chapter title: %s\n", STRING(m_iszChapterTitle) ); + CMessage *pMessage = (CMessage *)CBaseEntity::Create( "env_message", vec3_origin, vec3_angle, NULL ); + if ( pMessage ) + { + pMessage->SetMessage( m_iszChapterTitle ); + m_iszChapterTitle = NULL_STRING; + + // send the message entity a play message command, delayed by 1 second + pMessage->AddSpawnFlags( SF_MESSAGE_ONCE ); + pMessage->SetThink( &CMessage::SUB_CallUseToggle ); + pMessage->SetNextThink( gpGlobals->curtime + 1.0f ); + } + } +#endif + + g_iszFuncBrushClassname = AllocPooledString("func_brush"); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : float +//----------------------------------------------------------------------------- +float GetRealTime() +{ + return engine->Time(); +} + + +bool CWorld::GetDisplayTitle() const +{ + return m_bDisplayTitle; +} + +bool CWorld::GetStartDark() const +{ + return m_bStartDark; +} + +void CWorld::SetDisplayTitle( bool display ) +{ + m_bDisplayTitle = display; +} + +void CWorld::SetStartDark( bool startdark ) +{ + m_bStartDark = startdark; +} + +bool CWorld::IsColdWorld( void ) +{ + return m_bColdWorld; +} + +#ifdef MAPBASE +void CWorld::InputSetChapterTitle( inputdata_t &inputdata ) +{ + m_iszChapterTitle.Set( inputdata.value.StringID() ); +} +#endif diff --git a/sp/src/game/server/world.h b/sp/src/game/server/world.h new file mode 100644 index 00000000..5d75a64b --- /dev/null +++ b/sp/src/game/server/world.h @@ -0,0 +1,107 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: The worldspawn entity. This spawns first when each level begins. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef WORLD_H +#define WORLD_H +#ifdef _WIN32 +#pragma once +#endif + + +class CWorld : public CBaseEntity +{ +public: + DECLARE_CLASS( CWorld, CBaseEntity ); + + CWorld(); + ~CWorld(); + + DECLARE_SERVERCLASS(); + + virtual int RequiredEdictIndex( void ) { return 0; } // the world always needs to be in slot 0 + + static void RegisterSharedActivities( void ); + static void RegisterSharedEvents( void ); + virtual void Spawn( void ); + virtual void Precache( void ); + virtual bool KeyValue( const char *szKeyName, const char *szValue ); + virtual void DecalTrace( trace_t *pTrace, char const *decalName ); + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) {} + virtual void VPhysicsFriction( IPhysicsObject *pObject, float energy, int surfaceProps, int surfacePropsHit ) {} + + inline void GetWorldBounds( Vector &vecMins, Vector &vecMaxs ) + { + VectorCopy( m_WorldMins, vecMins ); + VectorCopy( m_WorldMaxs, vecMaxs ); + } + + inline float GetWaveHeight() const + { + return (float)m_flWaveHeight; + } + + bool GetDisplayTitle() const; + bool GetStartDark() const; + + void SetDisplayTitle( bool display ); + void SetStartDark( bool startdark ); + + bool IsColdWorld( void ); + +#ifdef MAPBASE + inline const char *GetChapterTitle() + { + return STRING(m_iszChapterTitle.Get()); + } + + void InputSetChapterTitle( inputdata_t &inputdata ); +#endif + +#ifdef MAPBASE_VSCRIPT + ScriptLanguage_t GetScriptLanguage() { return (ScriptLanguage_t)(m_iScriptLanguage); } +#endif + +private: + DECLARE_DATADESC(); + +#ifdef MAPBASE + // Now needs to show up on the client for RPC + CNetworkVar( string_t, m_iszChapterTitle ); + + // Suppresses m_iszChapterTitle's env_message creation, + // allowing it to only be used for saves and RPC + bool m_bChapterTitleNoMessage; +#else + string_t m_iszChapterTitle; +#endif + + CNetworkVar( float, m_flWaveHeight ); + CNetworkVector( m_WorldMins ); + CNetworkVector( m_WorldMaxs ); + CNetworkVar( float, m_flMaxOccludeeArea ); + CNetworkVar( float, m_flMinOccluderArea ); + CNetworkVar( float, m_flMinPropScreenSpaceWidth ); + CNetworkVar( float, m_flMaxPropScreenSpaceWidth ); + CNetworkVar( string_t, m_iszDetailSpriteMaterial ); + +#ifdef MAPBASE_VSCRIPT + int m_iScriptLanguage; + //CNetworkVar( int, m_iScriptLanguageClient ); // Now entirely on client +#endif + + // start flags + CNetworkVar( bool, m_bStartDark ); + CNetworkVar( bool, m_bColdWorld ); + bool m_bDisplayTitle; +}; + + +CWorld* GetWorldEntity(); +extern const char *GetDefaultLightstyleString( int styleIndex ); + + +#endif // WORLD_H